Thursday, January 18, 2007

SWFUpload Revisited and Revised

Some discussion about my SWFUpload FORM solution for ASP.Net rekindled my interest in the script. I also re-read my post about SWFUpload and realized that it was a bit incomplete in its explaination. So I've done some more work and I hope this is more helpful.

I also realize that I'm just posting code. You'll have to get the original SWFUpload and make sure all your file names and paths are correct/updated before this example code will work.

The Problem

SWFUpload does not provide a way for having more than one upload object per page. This is probably fine for most cases. However, if you want to upload an image and a word document (with different file size constraints or other settings) you need to have more than one upload object on the page.

So, I've revised the SWFUpload script. I had to restructure the design a bit (SWFUpload was built as a "Static" object, I needed it to work as an "Instance" object).

The new script operates almost like the original. However, these changes make it so you can't just throw a script tag in the body and instantiate the upload object. If you try that IE will have a fit. So you need to attach some code to the window.onload event (or any event that gets executed after the page is finished loading).

Operational Notes/Change log

My installation of Internet Explorer is broken. The SWFObject flash detection does not work for me. I've updated the SWFUpload script so you can set the "flash_version" setting to 0 (zero) bypassing the version detection. I'm not sure what will happen if Flash is not installed.

I've replaced the Debug output with a "Debug Console". The old debug doesn't work now that we have to wait until the page is loaded. The Debug Console looks for a FORM tag, adds a TextArea and puts the output there. If it can't find a FORM then it creates one and adds it to the body.

I've added a setting that allows you to tell SWFUpload where to add the Flash objects. ASP.Net requires that the Flash be added outside the FORM tags.

I've added a setting that indicates where to place the A tag (link) that causes the File Dialog to open.

I've updated the default Error Handler so it writes to the "Debug Console" instead of putting up alerts.

I de-obfuscated a bunch of the SWFObject code. So, now it's easier to read, but makes for a larger file.


The Script


/**
* mmSWFUpload 0.7.1 Revision 2 by Jacob Roberts, January 2007, linebyline.blogspot.com
*
* + Changed from a Static Class to an Instance (changed code/class structure)
* + Added "flash_version" setting. When set to zero the version check is skipped
* + Added Debug Console. The Instance class can't do document.write.
* = De-obfuscated SWFObject a bit
* - Removed standalone mode.
* + Added "ui_target" setting. When non-blank the link is added.
* + Added "flash_target" setting. When blank the flash is appended to the <body> tag
* = This fixes ASP.Net not allowing the flash to be added to the Form
* + Added error checking to the callSWF method
*
* mmSWFUpload 0.7: Flash upload dialog - http://profandesign.se/swfupload/
*
* SWFUpload is (c) 2006 Lars Huring and Mammon Media and is released under the MIT License:
* http://www.opensource.org/licenses/mit-license.php
*
* VERSION HISTORY
* 0.5 - First release
*
* 0.6 - 2006-11-24
* - Got rid of flash overlay
* - SWF size reduced to 840b
* - CSS-only styling of button
* - Add upload to links etc.
*
* 0.7 - 2006-11-27
* - Added filesize param and check in SWF
*
* 0.7.1 - 2006-12-01
* - Added link_mode param for standalone links
* if set to "standalone", createElement("a") won't run.
* - Added link_text param if css isn't needed.
* - Renamed cssClass to css_class for consistency
*
*/

/* Constructor */
function mmSWFUpload(settings) {
// Remove background flicker in IE
try {
document.execCommand('BackgroundImageCache', false, true);
} catch(e) {}

this.movieName = "mmSWFUpload_" + mmSWFUpload.movieCount++;

this.init(settings);
this.doDebug = this.getSetting("debug");

this.movieElement = this.loadFlash();
if (this.movieElement == null) {
if (this.doDebug) Console.Writeln("Could not load flash.");
return;
}

this.loadUI();

if (this.doDebug) {
this.debug();
}

}

/* Static thingies */
mmSWFUpload.movieCount = 0;

// Default error handling.
mmSWFUpload.handleErrors = function(errcode, file, msg) {

switch(errcode) {

case -10: // HTTP error
Console.Writeln("Error Code: HTTP Error, File name: " + file.name + ", Message: " + msg);
break;

case -20: // No backend file specified
Console.Writeln("Error Code: No backend file, File name: " + file.name + ", Message: " + msg);
break;

case -30: // IOError
Console.Writeln("Error Code: IO Error, File name: " + file.name + ", Message: " + msg);
break;

case -40: // Security error
Console.Writeln("Error Code: Security Error, File name: " + file.name + ", Message: " + msg);
break;

case -50: // Filesize too big
Console.Writeln("Error Code: File too big, File name: " + file.name + ", File size: " + file.size + ", Message: " + msg);
break;

}

};

/* Instance Thingies */
// Ensure that all the object settings are set or get a default value
mmSWFUpload.prototype.init = function(settings) {
this.settings = [];

this.addSetting("ui_target", settings["ui_target"], "");
this.addSetting("upload_backend", settings["upload_backend"], "");
this.addSetting("upload_start_callback", settings["upload_start_callback"], "");
this.addSetting("upload_complete_callback", settings["upload_complete_callback"], "");
this.addSetting("upload_progress_callback", settings["upload_progress_callback"], "");
this.addSetting("upload_cancel_callback", settings["upload_cancel_callback"], "");
this.addSetting("upload_error_callback", settings["upload_error_callback"], "mmSWFUpload.handleErrors");
this.addSetting("upload_queue_complete_callback", settings["upload_queue_complete_callback"], "");
this.addSetting("allowed_filetypes", settings["allowed_filetypes"], "*.gif;*.jpg;*.png");
this.addSetting("allowed_filesize", settings["allowed_filesize"], "1000");
this.addSetting("debug", settings["debug"], false);
this.addSetting("flash_path", settings["flash_path"], "upload.swf");

this.addSetting("flash_target", settings["flash_target"], "");
this.addSetting("flash_width", settings["flash_width"], "1px");
this.addSetting("flash_height", settings["flash_height"], "1px");
this.addSetting("flash_version", settings["flash_version"], "8")
this.addSetting("flash_color", settings["flash_color"], "#000000");

this.addSetting("link_css_class", settings["link_css_class"], "SWFUploadLink")
this.addSetting("link_text", settings["link_text"], "Upload File");

};

mmSWFUpload.prototype.loadFlash = function() {
var movieElement = null;

// Create SWFObject
var so = new SWFObject(this.getSetting("flash_path"), this.movieName, this.getSetting("flash_width"), this.getSetting("flash_height"), this.getSetting("flash_version"), this.getSetting("flash_color"));

// If we have the right version of flash load the flash
//if (deconcept.SWFObjectUtil.getPlayerVersion(so.getAttribute("version")).major >= this.getSetting("flash_version")) {

so.addParam("wmode", "transparent");
so.addParam("menu", "false");

// Add all settings to flash
so.addVariable("uploadBackend", this.getSetting("upload_backend"));
so.addVariable("uploadStartCallback", this.getSetting("upload_start_callback"));
so.addVariable("uploadProgressCallback", this.getSetting("upload_progress_callback"));
so.addVariable("uploadCompleteCallback", this.getSetting("upload_complete_callback"));
so.addVariable("uploadCancelCallback", this.getSetting("upload_cancel_callback"));
so.addVariable("uploadErrorCallback", this.getSetting("upload_error_callback"));
so.addVariable("allowedFiletypes", this.getSetting("allowed_filetypes"));
so.addVariable("allowedFilesize", this.getSetting("allowed_filesize"));
so.addVariable("uploadQueueCompleteCallback", this.getSetting("upload_queue_complete_callback"));

// Output the flash

so.write(this.getSetting("flash_target"));

movieElement = document.getElementById(this.movieName);

//}

return movieElement;
};

mmSWFUpload.prototype.loadUI = function() {
if(this.getSetting("target") != "") {
var self = this;

// Create link element
var link = document.createElement("a");
link.href = "#";
link.onclick = function () { self.callSWF(); return false; };
link.className = this.getSetting("link_css_class");
link.innerHTML = this.getSetting("link_text");


// Add the link to the target
var target = document.getElementById(this.getSetting("ui_target"))
if (typeof(target) != "undefined" && target && target.innerHTML) {
target.innerHTML = ""; // Clear all the children. This is the "bad way" but I really don't want to walk the childNodes array
target.appendChild(link);
}
}
};

// Make sure that we get a few default values
mmSWFUpload.prototype.addSetting = function(name, value, default_value) {
if (typeof(value) == "undefined" || value == null) {
this.settings[name] = default_value;
} else {
this.settings[name] = value;
}

return this.settings[name];
};

mmSWFUpload.prototype.getSetting = function(name) {
if (typeof(this.settings[name]) == "undefined") {
return null;
} else {
return this.settings[name];
}
};


mmSWFUpload.prototype.callSWF = function() {
if (this.movieElement != null) {
try {
this.movieElement.uploadImage();
}
catch (e) {
if (this.doDebug) {
Console.Writeln("Could not call uploadImage");
}
}
} else {
if (this.doDebug) {
Console.Writeln("Could not find Flash element");
}
}
};

mmSWFUpload.prototype.debug = function() {
var debug_message = "----- DEBUG OUTPUT ----\n";

debug_message += "ID: " + this.movieElement.id + "\n";

// It's bad to use the for..in with an associative array, but oh well
for (var key in this.settings) {
debug_message += key + ": " + this.settings[key] + "\n";
}
/*debug_message += "Flash Target: " + this.getSetting("flash_target") + "\n";
debug_message += "UI Target: " + this.getSetting("ui_target") + "\n";
debug_message += "Upload start callback: " + this.getSetting("upload_start_callback") + "\n";
debug_message += "Upload progress callback: " + this.getSetting("upload_progress_callback") + "\n";
debug_message += "Upload complete callback: " + this.getSetting("upload_complete_callback") + "\n";
debug_message += "Upload filetypes: " + this.getSetting("allowed_filetypes") + "\n";
debug_message += "Max filesize: " + this.getSetting("allowed_filesize") + "kb \n";
debug_message += "Link text: " + this.getSetting("link_text") + "\n";
debug_message += "CSS class: " + this.getSetting("link_css_class") + "\n";
debug_message += "Upload backend file: " + this.getSetting("upload_backend") + "\n";
debug_message += "Upload error callback: " + this.getSetting("upload_error_callback") + "\n";
debug_message += "Upload cancel callback: " + this.getSetting("upload_cancel_callback") + "\n";
debug_message += "Upload queue complete callback: " + this.getSetting("upload_queue_complete_callback") + "\n";*/
debug_message += "----- DEBUG OUTPUT END ----\n";
debug_message += "\n";

Console.Writeln(debug_message);
};

if (typeof Console == "undefined") {
var Console = new Object();
}

Console.Writeln = function(value) {
var console = document.getElementById("mmSWFUpload_Console");

if (!console) {
var documentForm = document.getElementsByTagName("form")[0];

if (!documentForm) {
documentForm = document.createElement("form");
document.getElementsByTagName("body")[0].appendChild(documentForm);
}

console = document.createElement("textarea");
console.id = "mmSWFUpload_Console";
console.style.width = "500px";
console.style.height = "350px";
documentForm.appendChild(console);
}

console.value += value + "\n";

console.scrollTop = console.scrollHeight - console.clientHeight;
}



/**
* SWFObject v1.4: Flash Player detection and embed - http://blog.deconcept.com/swfobject/
*
* SWFObject is (c) 2006 Geoff Stearns and is released under the MIT License:
* http://www.opensource.org/licenses/mit-license.php
*
* **SWFObject is the SWF embed script formerly known as FlashObject. The name was changed for
* legal reasons.
*/
if (typeof deconcept == "undefined") {
var deconcept = new Object();
}
if (typeof deconcept.util == "undefined") {
deconcept.util = new Object();
}
if (typeof deconcept.SWFObjectUtil == "undefined") {
deconcept.SWFObjectUtil=new Object();
}

deconcept.SWFObject = function(flash_path, id, width, height, minimum_major_version, background_color, _7, default_quality, _9, _a, _b) {
if (!document.createElement || !document.getElementById) {
return;
}

this.DETECT_KEY = _b ? _b : "detectflash";

this.skipDetect = deconcept.util.getRequestParameter(this.DETECT_KEY) == "0" ? true : false;

this.params = new Object();
this.variables = new Object();
this.attributes = new Array();

if (flash_path) {
this.setAttribute("swf", flash_path);
}

if (id) {
this.setAttribute("id", id);
}

if (width) {
this.setAttribute("width", width);
}

if (height) {
this.setAttribute("height", height);
}

if (minimum_major_version) {
this.setAttribute("version", new deconcept.PlayerVersion(minimum_major_version.toString().split(".")));
} else {
this.setAttribute("version", new deconcept.PlayerVersion([0,0,0]));
}

this.installedVer = deconcept.SWFObjectUtil.getPlayerVersion(this.getAttribute("version"), _7);

if (background_color) {
this.addParam("bgcolor", background_color);
}

var quality = default_quality ? default_quality : "high";

this.addParam("quality", quality);
this.setAttribute("useExpressInstall", _7);
this.setAttribute("doExpressInstall", false);
var _d = (_9) ? _9 : window.location;
this.setAttribute("xiRedirectUrl", _d);
this.setAttribute("redirectUrl", "");

if (_a) {
this.setAttribute("redirectUrl", _a);
}
};

deconcept.SWFObject.prototype = {
setAttribute : function(attribute_name, value) {
this.attributes[attribute_name] = value;
},

getAttribute : function(attribute_name) {
return this.attributes[attribute_name];
},

addParam: function(name, value){
this.params[name] = value;
},

getParams: function() {
return this.params;
},

addVariable: function(name, value) {
this.variables[name] = value;
},

getVariable: function(name) {
return this.variables[name];
},

getVariables: function(){
return this.variables;
},

getVariablePairs: function() {
var name_value_pairs = new Array();
var variables = this.getVariables();

for (var key in variables) {
name_value_pairs.push(key + "=" + variables[key]);
}

return name_value_pairs;
},

getSWFHTML : function() {
var html = "";

// Create Mozilla Embed HTML
if (navigator.plugins && navigator.mimeTypes &amp;& navigator.mimeTypes.length) {
if (this.getAttribute("doExpressInstall")) {
this.addVariable("MMplayerType","PlugIn");
}

// Build the basic embed html
html = '<embed type="application/x-shockwave-flash" src="' + this.getAttribute("swf") + '" width="' + this.getAttribute("width") + '" height="' + this.getAttribute("height") + '"';
html += ' id="' + this.getAttribute("id") + '" name="' + this.getAttribute("id") + '" ';

// Add all our parameters
var params = this.getParams();
for (var key in params) {
html += [key] + '="' + params[key] + '" ';
}

// Add the flash variables, this is passed as a query string (e.g. name=value&name=value) via the "flashvars" attribute
var variable_pairs = this.getVariablePairs().join("&amp;");
if (variable_pairs.length > 0) {
html += 'flashvars="' + variable_pairs + '"';
}

html += "/>";

// Create IE Object HTML
} else {

if (this.getAttribute("doExpressInstall")) {
this.addVariable("MMplayerType","ActiveX");
}

// Build the basic Object tag
html = '<object id="' + this.getAttribute("id") + '" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="' + this.getAttribute("width") + '" height="' + this.getAttribute("height") + '">';
html += '<param name="movie" value="' + this.getAttribute("swf") + '" />';

// Add our parameters
var params = this.getParams();
for(var key in params) {
html += '<param name="' + key + '" value="' + params[key] + '" />';
}

// Add the flash variables. This is passed as a query string (e.g. name=value&name=value) via the "flashvars" param
var variable_pairs = this.getVariablePairs().join("&amp;");
if (variable_pairs.length > 0) {
html += '<param name="flashvars" value="' + variable_pairs + '" />';
}

html += "</object>";
}

return html;
},

write : function(flash_target) {

if (this.getAttribute("useExpressInstall")) {

var min_version_for_express_install = new deconcept.PlayerVersion([6,0,65]);

if (this.installedVer.versionIsValid(min_version_for_express_install) && !this.installedVer.versionIsValid(this.getAttribute("version"))) {
this.setAttribute("doExpressInstall", true);
this.addVariable("MMredirectURL", escape(this.getAttribute("xiRedirectUrl")));
document.title = document.title.slice(0,47) + " - Flash Player Installation";
this.addVariable("MMdoctitle", document.title);
}
}

if (this.skipDetect || this.getAttribute("doExpressInstall") || this.installedVer.versionIsValid(this.getAttribute("version"))) {
// Attempt to load the flash appended to the body

// NOTE: this part will cause IE to throw an error if not called after the page is finished loading
var container = document.createElement("div");
container.style.width = "0px";
container.style.height = "0px";
container.style.position = "absolute";
container.style.top = "0px";
container.style.left = "0px";

var target_element;
if (flash_target != "") {
target_element = document.getElementById(flash_target);
}
if (typeof(target_element) == "undefined") {
target_element = document.getElementsByTagName("body")[0];
}
if (typeof(target_element) == "undefined") {
return false;
}

target_element.appendChild(container);

container.innerHTML = this.getSWFHTML();

return true;
} else {
if (this.getAttribute("redirectUrl") != "") {
document.location.replace(this.getAttribute("redirectUrl"));
}
}

return false;
}
};

deconcept.SWFObjectUtil.getPlayerVersion = function(_23,_24) {
var player_version = new deconcept.PlayerVersion([0,0,0]);

// Mozilla Detect
if (navigator.plugins && navigator.mimeTypes.length) {
var swf_plugin = navigator.plugins["Shockwave Flash"];
if (swf_plugin && swf_plugin.description) {
player_version = new deconcept.PlayerVersion(swf_plugin.description.replace(/([a-z]|[A-Z]|\s)+/,"").replace(/(\s+r|\s+b[0-9]+)/,".").split("."));
}

// IE Detect
} else {
try {
var axo=new ActiveXObject("ShockwaveFlash.ShockwaveFlash");
for(var i=3; axo != null; i++) {
axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash." + i);
player_version = new deconcept.PlayerVersion([i,0,0]);}
} catch (e){ }

if (_23 && player_version.major > _23.major) { return player_version; }

if (!_23 || ((_23.minor != 0 || _23.rev != 0) && player_version.major == _23.major) || player_version.major != 6 || _24) {
try{
player_version = new deconcept.PlayerVersion(axo.GetVariable("$version").split(" ")[1].split(","));
}
catch (e) { }
}
}

return player_version;
};

deconcept.PlayerVersion = function(version_array) {
this.major = parseInt(version_array[0]) != null ? parseInt(version_array[0]) : 0;
this.minor = parseInt(version_array[1]) || 0;
this.rev = parseInt(version_array[2]) || 0;
};

deconcept.PlayerVersion.prototype.versionIsValid = function(fv) {
if (this.major < fv.major) { return false; }
if (this.major > fv.major) { return true; }
if (this.minor < fv.minor) { return false; }
if (this.minor > fv.minor) { return true; }
if (this.rev < fv.rev) {return false; }

return true;
};

deconcept.util = {
getRequestParameter : function(parameter_name) {
var query_string = document.location.search || document.location.hash;
if (query_string) {
var parameter_pair = query_string.indexOf(parameter_name + "=");
var parameter_value_end_index = (query_string.indexOf("&", parameter_pair) > -1) ? query_string.indexOf("&", parameter_pair) : query_string.length;
if (query_string.length > 1 && parameter_pair > -1) {
return query_string.substring(query_string.indexOf("=", parameter_pair) + 1, parameter_value_end_index);
}
}

return "";
}
};

if (Array.prototype.push == null) {
Array.prototype.push = function(_2f) {
this[this.length] = _2f;
return this.length;
};
}
var getQueryParamValue = deconcept.util.getRequestParameter;
var FlashObject = deconcept.SWFObject; // for backwards compatibility
var SWFObject = deconcept.SWFObject;




Example HTML



<!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" >
<head>
<title>SWFUpload ASP.Net Revision</title>
<script type="text/javascript" src="jscripts/SWFUpload/mmSWFUploadR2.js"></script>
<script type="text/javascript">
uploadStart = function(fileObj) {
try {
Console.Writeln("Upload started");
} catch (e) { Console.Writeln("Error displaying file upload start information"); }

}

uploadProgress = function(fileObj, bytesLoaded) {

try {
var percent = Math.ceil((bytesLoaded / fileObj.size) * 100)
Console.Writeln("Upload Progress: " + fileObj.name + " " + percent);
} catch (e) { Console.Writeln("Error displaying file progress"); }
}

uploadComplete = function(fileObj) {
try {
Console.Writeln("Upload Complete: " + fileObj.name);
} catch (e) { Console.Writeln("Error displaying file upload complete information"); }
}

uploadQueueComplete = function(fileObj) {
try {
Console.Writeln("Queue Done");
} catch (e) { Console.Writeln("Error displaying queue complete information"); }
}

uploadCancel = function() {
try {
Console.Writeln("Pressed Cancel");
} catch (e) { Console.Writeln("Error displaying file cancel information"); }
}
</script>

<style type="text/css">

a.SWFUploadLink {
font-weight: bold;
color: blue;
text-decoration: none;
}

a.SWFUploadLink:hover {
text-decoration: underline;
}

</style>



</head>
<body>
<div id="flashTarget"></div>
<form action="" onsubmit="return false;">

<div id="wrapper">

<div id="SWFUpload">
Upload a PNG: <input type="file" name="upload" />
</div>

<div id="SWFUpload2">
Upload a GIF: <input type="file" name="upload" />
</div>


<input type="submit" value="Upload" onclick="javascript:alert('disabled...'); return false;" />



<script type="text/javascript">
var upload1;
var upload2;
var da_onload = window.onload;
window.onload = function() {
if (typeof(da_onload) == "function") {
da_onload();
}

upload1 = new mmSWFUpload({
debug : true,
flash_version: 8,
upload_backend : "../../upload.aspx",
ui_target : "SWFUpload",
flash_target : "flashTarget",
link_text : "Select File",
//link_css_class : "myCustomClass",
allowed_filesize : "400",
allowed_filetypes : "*.png",
upload_start_callback : 'uploadStart',
upload_progress_callback : 'uploadProgress',
upload_complete_callback : 'uploadComplete',
// upload_error_callback : 'uploadError',
upload_cancel_callback : 'uploadCancel',
upload_queue_complete_callback : 'uploadQueueComplete'
});

upload2 = new mmSWFUpload({
debug : true,
flash_version: 8,
upload_backend : "../../upload.aspx",
ui_target : "SWFUpload2",
flash_target : "flashTarget",
link_text : "Select File",
//link_css_class : "myCustomClass",
allowed_filesize : "400",
allowed_filetypes : "*.png",
upload_start_callback : 'uploadStart',
upload_progress_callback : 'uploadProgress',
upload_complete_callback : 'uploadComplete',
// upload_error_callback : 'uploadError',
upload_cancel_callback : 'uploadCancel',
upload_queue_complete_callback : 'uploadQueueComplete'
});
}


</script>


</div>
</form>

</body>
</html>



Usage Tips

Here are some tips on using this thing:
  • Refer to the script's init method to see all the settings and their defaults
  • Any settings you don't include will automatically get the default value
  • Any callbacks you don't specify won't be called
  • Setting flash_version to 0 (zero) disables the flash version detection
  • You can use the Debug Console for your own output, if you want
  • The upload_backend setting should have a comlpete URL or be relative to the SWF file (not the Javascript or the HTML file)
  • If you don't specifiy a ui_target setting (or the target is not found) then the Upload link is not added.
  • You can call up the File Dialog yourself using the callSWF() method on your SWFUpload instance objects.
  • If you don't specify a flash_target setting the script adds the flash to the body (which will be outside any FORMs).
  • In your events the this object won't be your SWFUpload instance object.
  • You can't attach the callbacks like you do Javascript events. The callback string has to be the full string you would use if you were calling the callback from Javascript. P.S. Callbacks are just Javascript functions you've declared somewhere. See the example callbacks to see what values are passed in when they are called.
  • SWFUpload doesn't seem to work if you are viewing the page as a local file (file://). You need to put it on a web server (http://)
  • SWFUpload will probably return an error if your upload_backend is not a script file (if you submit to an html/htm file IIS will return a HTTP 405 error. Submit to a script file (php, aspx, etc).
  • Make sure you upload_backend file is working normally before using it with SWFUpload. You will get IOErrors or HTTP errors.
  • For ASP.Net developers you'll want to make sure your maxRequestLength and executionTimeout are large enough.

So, there you go. Again, Good Luck!

3 comments:

  1. Anonymous2:07 PM

    This code does not work. A friendly advice: don't spend time on it.

    ReplyDelete
  2. Anonymous8:42 AM

    wish i read this comment earlier

    ReplyDelete
  3. Anonymous12:46 PM

    The code works just fine for me.

    ReplyDelete