Showing posts with label JavaScript. Show all posts
Showing posts with label JavaScript. Show all posts

Thursday, May 14, 2009

AJAX.Net

In the past (and in the present) I have used a custom built Ajax library for making my Ajax calls and handling the responses. It's simple, supports plain text, XML and JSON, and handles multiple requests, cancellations and errors properly.

In the less distant past I began using ASP.Net Update Panels and AJAX.NET libraries. In some cases they are convenient and easy. But I'm finding in most cases they are buggy, extremely limited and implementation doesn't really fit well with my style.

Now...most likely all my AJAX.Net difficulties arise from my limited experience with the library and my failure to familiarize myself properly with all the ins and outs of implementing it in my sites.

Anyhow, given my experience I recommend against using AJAX.Net and update panels and recommend sticking with a client side library like those found in jQuery or MooTools.

Here is a list of problems that make implementing and using basic AJAX.Net Update Panels difficult:

  • Updates are too heavy. The view state has to go back and forth. Even just to update one word on the page.
  • Inflexible when you move outside the box. For example, I only want to trigger an update panel update if there are 4 or more characters typed in a box. Now I have to implement my own client side event handlers and trick AJAX.Net in to making the update. If I have to do all that anyways what savings is AJAX.Net giving me?
  • Out of order responses. I often see out of order responses. Faster executing updates return before slower executing updates regardless of the order I called them in. Most often I see this when the application decides it has to restart and the longer executing first call often returns after the second call.
  • Poor error handling. I haven't found a way to gracefully handle errors. I can catch some but it doesn't seem to catch them all.
  • Dies after 1 error. If I have 1 AJAX.Net error all AJAX.Net stops responding and no panels will update until the page is refreshed.
  • Updates die for no good reason. I have a text field that triggers an update after some keystrokes. But if the user presses tab to move to the next field between the time when the update starts and when it returns then the update hangs. Monitoring the application shows that it does process the request but the client side just ignores it. I guess it has ADD.
  • Loss of state. Since an update panel nukes the contents of a div and replaces with what it just got back from the server I lose some UI state. If I've done any client side tweaking that dissapears but most often I just lose focus on the control I just changed. This is a nuisance for those that navigate the form with the tab key.
So, needless to say, I've gone back to my custom Ajax JavaScript library. I never have any of the problems described above. Some things are a little more work but the reliability is worth it.

To be fair here are the issues with using my own Ajax library:
  • Everything is difficult (not that difficult) because even simple updates require full programming unlike UpdatePanels which just need the markup and event handler.
  • Manual management. I have to remember to cancel outstanding requests so things don't get out of order and I have to handle updating the page with the response. It's just more work.
  • Non-standard. Good luck future developer who gets to maintain my project. I've coded it well but it's all custom so your experience with standard libraries won't help you here.
  • Full post backs. I'm a little lazy and so I just create a separate ajax.aspx page that handles my ajax requests and makes the expected response. It's not very ASP.Net-y and is probably pretty heavy since I'm doing a complete page request. In fact, this method takes me outside all of ASP.Net's help. I don't have access to controls or events. I have to pull data from Request.Form process it and respond. I sometimes end up with duplicate code and duplicate markup (especially if I'm being lazy).

Even given those issues I still like my library because of the fine grained control it gives me.

Friday, November 30, 2007

SWFUpload v2.0

SWFUpload v2.0 has been "pre-released" at www.swfupload.org

Pre-released means the Flash Source, Flash SWF, and JavaScript are available but that the demos are not yet updated and documentation is not done.

What's New
  • Flash 8 and Flash 9 versions
    • Flash 9 version includes support for retrieving server data and sending additional POST values
  • Many bug fixes and Flash Player bug work-arounds
  • Plug-ins
    • v1.0.2 compatibility plugin
    • Queue handling plugin
    • Cookie plugin
    • and more...
  • New Official Version

SWFUpload v2.0 is the new "official" version. v1.0.2 is being retired and development on the Revisions has now ended. There will be no Revision 7 final release.

SWFUpload v2.0 is the best SWFUpload release to date. I recommend all SWFUpload users upgrade to it.

Development continues and we have a few outstanding issues. Please visit the site to let us know what you think, get help, and report bugs.

Happy Uploading!

Wednesday, October 31, 2007

JavaScript Date Object Tip

Quicktip: Nov. 31, 2007 == Dec. 1, 2007

Background
I had to repair a JavaScript based calendar popup tool today. For some reason November was not being displayed correctly. It had the correct number of days but November 1st was on Saturday, not Thursday.

This issue only manifested itself when the current date is the 31st day of the month and only caused display problem for months with less than 31 days.

What is going on?
This issue turned out to be related to an odd behavior of the JavaScript date object so I thought I'd share.

Let's go over the "offending" code:

var today = new Date(); // new Date(2007, 9, 31);
This creates a new date object with Oct. 31, 2007 (which is today)

today.setMonth(10);
This sets the month to November

alert(today);
This shows an alert box with Dec 1, 2007

But wait!! Didn't we set the month to November? Well, we did, but November only has 30 days. October had 31 days. So I guess we set the date to be Nov. 31. You could say that Nov. 31 is the same as Dec. 1. You might not say that but JavaScript does.


Solution(s):
1. Set the day of the month before setting the month.
2. Create a new date object using appropriate parameters: new Date(2007, 10, 1);

Tuesday, October 23, 2007

SWFUpload Revision 7 beta 3

I decided I'd better get beta 3 out there. I've been too busy to test it and the demos as well as I'd have liked to so please forgive anything I've neglected. The updated demos and source have been posted in the usual place.

Changes
  • Fixed the Flash Version Detection. SWFUpload "should" properly detect Flash Player v9.0.28.
  • Fixed a left-over event call when cancelling an upload.
  • Fixed out of order parameters when calling uploadError.
  • Changed the showUI function and ui_function setting to swfUploadLoaded/swfupload_loaded_handler. This is more in line with other event callbacks.
  • Updated internal flashReady event so it is called like the other events (with setTimeout).
  • Added an internal event queue which fixes out of order events in Safari (out of order events were caused by using setTimeout to call them, just as I feared).

New Known Issues

If an upload is cancelled after the progress has reached 100% but before the upload script has finished processing then SWFUpload blocks until the upload script has completed. uploadComplete and fileComplete are still called along with uploadError. I'm still working out the best way to handle this situation (and have received some very good ideas in the comments).

To avoid this issue don't use the cancel feature or track your file progress in the JavaScript and prevent cancel calls to 100% still processing files. The issue will be fixed before the final 7.0 release.

You can test this by performing a long running process in your upload script. In PHP I tested by adding a sleep(30) statement. This causes SWFUpload to progress to 100% and then pause for 30 seconds while the upload script sleeps.

Question

I'm considering removing the ui_container and degraded_container settings. This would remove the last UI modifying bits in SWFUpload. A hands-off approach to the UI has been one of SWFUpload's design philosophies. I wondered what everyone else thinks about that.

Rather than remove them completely, however, I'd keep a default swfUploadLoaded handler that uses ui_container and degraded_container which could be stored in the customSettings object. That would keep a simple graceful degradation feature built in while better separating SWFUpload from the UI.

Monday, October 08, 2007

SWFUpload Revision 7.0 beta 2

Update: Revision 6.2 & 7 require Flash Player 9.0.28 or higher. However they fail to do proper checking to ensure that the necessary minimum Flash Player version is present. I will release an update to the R6 and R7 lines shortly.

Revision 7 beta 2

SWFUpload Revision 7.0 beta 2 is out!. I even updated the demos this time. It's all posted at the usual place: http://swfupload.praxion.co.za/. Many thanks again to our Host!

Don't forget to visit the Official SWFUpload site to show your support and to get some support.

Why Beta?

There is a bug in Flash Player that affects SWFUpload severely. It is more apparent in R7 because of the change in the Event design. I really like the new event design so I'm working hard to keep it. I'm not sure that this version is stable or how compatible it is with browsers on different operating systems. So it'll be Beta for a little while.

Changes in R7b2 from R7b1
  • I worked around an encoding bug in Flash Player where the Server Data or a file name or any string that contains a backslash (\) or a quotation mark (") in the wrong place will cause a JavaScript error which causes events to abort. Stupid Flash Player. See source for details.
  • I moved the ERROR_CODE_* constants in to "sub" objects so it's more clear what group the error belongs to (queue errors/upload errors). I changed all the error code numbers too. You didn't use any magic numbers, right?
  • Added the UPLOAD_STOPPED upload error which can supplement for an uploadStopped event.
  • Updated Flash calls and event calls with setTimeout to avoid limitations in Flash's ExternalInterface api. Stupid Flash. I haven't had any problems but watch out for concurrency and execution order issues.
  • Added a customSettings object to the SWFUpload instance so you can just set your own variables on an instance rather than using addSetting and getSetting (you can still use addSetting/getSetting if you want).
  • Changed the FILE_NOT_FOUND error code to FILE_ID_NOT_FOUND which is more precise.
  • Changed the parameter order on some events so the file object is always first in an effort to be more consistent.
Known Issues
  • You can run in to problems if, from JavaScript, you call in to Flash; which then calls back to JavaScript; who then tries to call back in to Flash. That final call in to Flash will fail. This is most apparent when calling startUpload() which calls back to uploadStart. If you try to call in to Flash from here, calling getStats() for instance, you may get an error or just a silent failure. I've work for 6 full days trying to understand and get around this. I've done pretty well by wrapping all the calls I could in setTimeouts but you can still force the bug so be careful. Stupid Flash.
  • Setting the file_post_name does not seem to work in the Linux Flash Player. It's an unnecessary feature anyway so don't use it unless you really have to (and you shouldn't have to). Thanks to Shadow Walker for the Linux bug reports.
  • You have to return something from your upload_url script or uploadComplete will never be called and your uploads will hang. I sometimes call echo ' ';
Where's the docs?

If anyone wants to write up some HTML format docs that'd be great. Otherwise I'll get to it eventually. In the meanwhile it's a good idea to skim through all the SWFUpload posts (the really old ones are still relevant). I've tried to include lots of comments in the code and between all the demos you can see how to do just about everything. Still, some docs would be nice.

Wednesday, September 19, 2007

SWFUpload Revision 7.0 Beta

I have almost completed work on Revision 7.0. I think the coding on the SWF and JS files is done. But I have not had time to convert the demos. I decided to post a "Beta" since I don't know when I'll have time to come back to the demos and documentation.

The R7 beta will be available at http://swfupload.praxion.co.za/

R7 Notes

The API has change significantly for R7 (that's why is R7 and not R65). You won't be able to drop this in over SWFUpload v1.0.2 or any of the other Revisions.

This version uses a different event model which introduced some issues with the Flash to JavaScript and JavaScript to Flash calls. I think I've resolved them but I'm sure someone will figure out a way to bring the bug up again. The long of the short of it is that when Flash calls to JavaScript if that JavaScript thread calls back in to Flash then Flash misbehaves. In order to work around this I use a setTimeout (the Timer class in ActionScript) to make calls from Flash to JavaScript. This effectively makes the call on a separate thread and works around the problem. The only place that this is not done is the call to the uploadStart event since uploadStart is required to return true or false. If you make any calls in to the Flash from uploadStart then you run the risk of triggering this issue.

Several pieces of functionality will appear to have been removed from this version. Almost everything that has been removed is still doable using the new events. There are comments in the code that explain some things and once the demos are complete hopefully you will be able to see how everything works.


Changes
  • Removed the settings: begin_upload_on_queue, validate_files, and use_server_data_event.
  • Changed the setting: upload_target_url to upload_url.
  • The events called are as follows and are listed in their approximate order:
    • fileDialogStart: Called from Flash immediately before the File Dialog is displayed.
    • fileQueued: Called once for each file that is added to the queue.
    • fileQueueError: Called once for each file that fails to be added to the queue.
    • fileDialogComplete: Called after all selected files have been processed.
    • uploadStart: Called for each file in the queue immediately before it is uploaded. Return true to continue the upload. Return false to skip the upload.
    • uploadProgress: Called periodically to provide upload status for the current file.
    • uploadError: Called when an error occurs on an upload or if an upload is cancelled.
    • uploadComplete: Called when an upload finishes successfully.
    • fileComplete: Called after an upload and SWFUpload is ready to allow another upload to begin. This is called even if an upload is cancelled or has an error.
  • Removed the queueComplete event and the cancelQueue command.
  • Enhanced the cancelUpload command so if it is not passed a File ID then the current upload or first file in the queue is cancelled.
  • File cancelling behavior has changed. If an in-progress upload is cancelled the uploadError event is fired and the fileComplete event is fired. If the file is not currently uploading then only the uploadError event is called.
  • Added a GetStats command. This returns an object that contains the number of successful uploads, the number of files remaining in the queue (excluding the current upload, if any), the number of queue errors and the number of upload errors.
  • Added a file_status property to the File Object.
  • Added a single file browser. This allows the user to only select a single file for upload.
Some of the Command names, Event names, and Error code constants have changed. See the source code for the updated names.

Tuesday, July 03, 2007

SWFUpload Revision 6 Release

Update 7-5-2007

A couple issues were found in Revision 6. The major one was another race condition that could occur in IE6. Hopefully these race condition bugs have finally been squashed. A few other issues were found but were not noticeable since they kind of canceled each other out.

I've posted updated Demos and Source code. I'm not updating the version number so if you downloaded a copy before today I recommend that you download the updated copy.

SWFUpload Revision 6 Release

I am pleased to announce the release of Revision 6 of SWFUpload. This version has been moved forward to ActionScript 3.0 and Flash Player 9 which gives us some power features what everyone has been waiting for.

What's new:
  • Code moved to ActionScript 3.0.
  • Flash Player 9 required.
  • Added ability to send custom POST values.
  • Added a fileValidation event that allows each file to be validated before upload.
  • The fileComplete event receives the output from the server.
  • File type (extension) is checked again in code since the file browser can be bypassed.
  • The file post variable name (aka Filedata) can now be changed.
  • Fixed a bug caused by canceling single files.
  • Removed the cookie sending feature. The post_params setting can be used.
  • Removed query_params. Use post_params instead.
  • Fixed another race condition that could prevent SWFUpload from loading on a page refresh.
  • Updated PHP and ASP.Net demos.
How to use it

See the demos and download Revision 6 at our generously hosted site (thanks again!). I have had some trouble with browsers/proxies caching some of the old JavaScript files. If it doesn't work make sure your caches are cleared.

My next project is to produce documentation for SWFUpload. I don't plan any new features (unless Flash Player 10 fixes/adds cool features) except bug fixes.

Don't forget to show your support to the original SWFUpload developers. We're grateful for their innovation and hard work

Friday, May 04, 2007

HTML <button> tag

I am frequently amazed at the new "old things" I discover as I continue in web development. For example, I'm coming around to using the H tags and the P tag for markup. I've always used spans, divs, and brs.

My most recent "discovery" is the button tag. This little jewel has its problems but it gives me some of the flexibility that an input type="button" just doesn't provide.

See for yourself:



The <button> tag accepts HTML content rather than a simple text value. As far as I know (I don't know much) you can put most anything in there.

It is important to remember that <button> has a type attribute that determines whether it behaves as a submit, reset, or simple button.

Some caveats:

IE does not handle form submission of <button>s correctly.
  • Rather than submitting the value (as specified in the value attribute) it submits the inner HTML of the tag.
  • IE submits all the <button> tags; not just the button that was clicked. This makes it so the server-side script is unable to tell which button was clicked.

Work-arounds:
  • Set the type attribute to button and use JavaScript. If you can't use JavaScript then I'm afraid you may not have much success with the <button> tag (especially if you want to use more than one <button> tag in a form)
  • Button Value: Use the JavaScript onclick event to populate a hidden field with the value from the button. In your server-side scripts you can read out values from the hidden fields and ignore the buttons.
  • Which Button Clicked: Use the JavaScript onclick event to populate a hidden field with a value that identifies which button was clicked. On the server-side use the hidden field to determine action rather than the button.
ASP.Net already uses similar methods for its PostBacks and so these work-arounds aren't necessary. Just remember to set the type attribute to button or you will get two postbacks when a button is clicked. Bind click events in ASP.Net by using the onserverclick event.

Thursday, May 03, 2007

SWFUpload Revision 5.2 Release

Edit: If you downloaded R5.2 before May 4th you should download again. I made some minor fixes that I missed:
  • Zero file size limit fixed to mean unlimited
  • Default flash width/height set to 1px to work better in Linux (also default color set to #FFFFFF since it is visible in Linux.
/Edit

Alright. I've completed another overhaul and feature creep cycle in the SWFUpload Revisions. I'm going to have to take a break for awhile. It's time to get back to work (real work I mean).

Revision 5.2 includes updates to several features, improves the internal processing of the files and takes care of some bugs that I missed because of insufficient testing.

Changes
  • Out of Order file uploads now possible.
  • Per file upload parameters (these are added to the upload target query string. These are also passed to JavaScript with the fileObj).
  • Dynamic modification of cookie and custom parameters (fixed so cookies aren't wiped out when updating other settings).
  • Added a Queue Limit. E.g. Use if you only want users to queue and upload one file at a time you can set the queue limit to 1.
  • Added a QueueStopped event (change from throwing a FileQueued event when the upload was stopped).
  • Fixed the various limits so setting them to zero means unlimited.
  • Added a ui_function setting that allows you to specify the function to call instead of _showUI. This gives you the power to do whatever you want with the UI after SWFUpload has completed loading without having to modify the SWFUpload javascript files.
  • Created a "Features" demo that lets you tweak (almost) all the settings in SWFUpload R5.2 and test it without having to fiddle in the code.
  • ActionScript modifications:
    • Changed from using an ever growing array to track each queued file to a FIFO queue. This simplifies the code in a lot of ways.
    • Wrapped the FileReference object in a custom FileItem object which allows per file parameters and better abstraction.

The Revisions website has been update: http://swfupload.praxion.co.za/index.php

Another big thanks to Tarkheena for providing the great hosting!

Additional Notes

I've been having all kinds of trouble with UTF-8 encoding and byte headers. I tried to get the encoding consistent but may have missed some files. Incorrect encoding has caused issues ranging from PHP errors to JavaScript files not loading. Let me know if there are any problems.

There was a report that SWFUpload R5 (and probably R5.2) does not work under Firefox 1.5 in Linux. I plan to test and hopefully fix this soon.

I tested R5.2 in Ubuntu 6.10 on Firefox 2.0. I fixed a couple small issues that prevented SWFUpload from loading (see Edit above). I noted that it seems like FileProgress events are not called very often. Also, Stopping or Cancelling causes a crash. If you are targeting Linux users you should include JavaScript that detects the Linux Flash Player and prevents these actions. I don't plan to add this kind of detection to my code.

I can create a browser crash in both IE and Firefox on Windows XP SP2. While I have the File Dialog open, I force a page change (reload or move to a different url). I happened on this when I was using <input type="submit" onclick="suo.Browse()">. The File Dialog would open but then the form would submit and change pages. The browser would crash as soon as I clicked Open or Cancel.

Tuesday, April 24, 2007

SWFUpload R5 Release!

I'm happy to announce the release of SWFUpload R5 (my heavily modified version of the original SWFUpload).

I'm also pleased to announce that Tarkheena has graciously provided hosting for the SWFUpload R5 demos and downloads.

And a final thanks to Lars and the SWFUpload team. They have created a great upload control and it's been great expanding on their work.

SWFUpload R5 New Features and Changes
  • Tested and Fixed the automatic cookie passing feature. It works now.
  • Switched to PHP for the demo format.
  • Switched from Flash Studio 8 to FlashDevelop. This is an Open Source Flash IDE that works great. I seem to have been able to work around the bugs found when SWFUpload v1.0.2 moved to FlashDevelop. This move allows anyone to modify the SWF file. Not just people who can afford Adobe's Flash Studio.
  • Greatly improved the callback system. You no longer pass in a string with the function name to the settings. Now you pass in the actual Function reference. This gets rid of the flashReady_callback handler, which always felt wrong, and makes more advanced things possible with multiple instances. This also allow the this object to refer to the SWFUpload instance inside the Event Handlers.
  • Renamed just about everything to make the settings and functions more consistent and clear. Replaced the -XX error codes with names and other such niceties.
  • File Selecting is now subject to the FileUploadLimit. E.g., if the upload limit is 10 and the user attempts to queue 11 files then an error is thrown. Or if the file has queued 10 files and attempts to queue 1 more then an error is thrown.
  • Returns a sensible error when Zero Byte files are uploaded.
  • You can change the UploadTargetURL at anytime using the SetUploadTargetURL function.
  • Improved the SWF to Javascript debugging.
  • Improved the encoding of strings.
Demos and Download

You can try out the demos and download the source at http://swfupload.praxion.co.za/index.php

Please note: there is a 2MB upload limit on this server regardless of what SWFUpload allows. I'll see if I can get this fixed soon

Monday, April 02, 2007

SWFUpload R4 Update Release

I've fixed a couple of issues and added some features to SWFUpload Revision 4.

  • Cleaned up the code. Removed unused code. Added more error checking. Added more comments. (Both Javascript and ActionScript)
  • Removed create_ui feature that would add a link to queue up files. The feature was too incomplete to bother with. You should build your own UI and use the ui_target, degraded_target and the callback handlers to display updates.
  • Added the upload_backend_cookies setting to help with a Windows Flash/Firefox bug. It accepts a cookie name (or an array of cookie names) that will be added to the upload_backend url (if the cookie is found). This assists with passing ASP.Net Auth tickets or PHP Session IDs. Why you would want to do this is described on the SWFUpload forums. *This feature is untested.
  • Implemented a workaround for an IE bug where a Flash Movie placed inside a FORM cannot make callbacks to JavaScript (source). You can now place the flash_target in the form if you want. You might still have problems adding a Flash Movie to a form if don't use the Window.Onload event.
  • Renamed the class to SWFUpload from mmSWFUpload.
  • Did some more sprucing up of the demos.
Feel free to download SWFUpload R4.1.

I would really appreciate feedback on issues, bugs, and any other questions or successes you might have.

If anyone would like to host the demos please let me know. I've given up on free hosts and am not currently in a position to purchase hosting.

Monday, March 26, 2007

SWFUploadR4 release and SWFUpload v1.0.2

SWFUpload v1.0.2

The official SWFUpload has been updated to version 1.0.2. If you check it out you'll see some familiar code. I was happy to submit my R2 and R3 code to Lars and it seems they liked it enough to incorporate my code design and concepts in to SWFUpload.

It would have been nice to get a little nod for the contribution.

The new 1.0.2 code is very clean and C#-ish. I haven't tried it out but it sure looks pretty.

SWFUpload R4

In the meanwhile, I've updated my Revision of SWFUpload. See this post for the latest release. Sorry to have to use such a crappy download service, but, --what you pay for is what you get--.

One other note before I continue: I get some comments that sound like people are only reading one post about the script. I recommend that you read through all the SWFUpload posts. There are several tips and explanations that will help you get things up and running.

Also, please don't post problems you have with R4 to the official SWFUpload forums. The SWFUpload team cannot be expected to support my unofficial changes and it will only cause headache and confusion to have R4 issues mixed in at the v1.0.2 forum.

Revision 4 includes:
  • Debug messages from the SWF File when debugging is enabled in the JavaScript.
  • New "Stop Upload" feature. This stops the current uploads and allows you to restart them again without canceling anything. Unfortunately Flash doesn't support pausing an upload, but given the nature of HTTP this is understandable.
  • The QueueComplete event only calls back if one or more files were successfully uploaded.
  • Fixed the bug where canceling the first queued upload starts the other uploads.
  • Added url encoding of the uploadBackend string so you can pass more than one value to the upload back-end script. (I didn't get around to testing this)
  • The consistent misspelling of Canceled.
  • The ActionScript code was cleaned up, commented, improved error handling, debugging, revised to avoid self-calling loops, etc.
I also updated the Demos a little to include buttons to demonstrate some more of the functionality. I also added some bling to the MultiUpload demo.

One Bug

I found one bug that I could not work around during my testing. When you attempt to upload a Window's shortcut file SWFUpload normally reports an IO Error. If you use the "Stop Upload" function while a shortcut is being uploaded then you cannot start the upload again. Flash just doesn't start the upload again and doesn't throw any errors or raise any event listeners. This shouldn't affect most users as it is somewhat unusual to upload Window's shortcuts anyhow.

Tuesday, January 30, 2007

SWFUpload: The Saga Continues...

I've had a little feedback on SWFUpload. And you might want to thumb through the comments for a few hints (there aren't that many).

Macs (they always gotta be different)

It seems on Macs that if your 'upload_backend' does not return any data that the UploadComplete event will never be called. This seems to happen for any browser which indicates that is probably an issue in the Flash for Mac implementation. So, instead of just having a blank page for your 'upload_backend' just throw in some text like, Thank you. Upload Complete. That, reportedly, will solve the problem.

IOError (specifically, Firefox for Windows)

If you get an IOError then you may not be able to upload any more files. The report says the Open File dialog will no longer open. For me the Open File dialog does appear and the UploadStart event gets called but then I never get any UploadProgress callbacks.

Solution: Handle this some way in your error handler. You could alert the user to reload the page. Or use Javascript to remove the offending Flash movie and reload it then let the user know that she needs to try again.

Better Solution: Fix the upload.swf. I realized that the logic in my changes to the Flash file was not very good. When an error occurred upload.swf would just stop processing the queue. Then when you tried to add more files it just tried to upload the erred out file again. So, I've fixed that. There still seems to be occasions when upload.swf just stops responding and you can't get the Open File dialog to come up. You may still have to rely on the "reload the movie" solution discussed above. This still feels like a deeper flash issue that may be part of the immaturity of the file upload features of Flash itself.

Firefox, Cookies and Sessions

I've finally started implemented SWFUpload in one of my own projects. It's been quite the challenge. It turns out that in Firefox for Windows that when SWFUpload uploads the file it sends the Cookies from Internet Explorer. So that means the session set up by ASP.Net (the session_id is stored in a cookie) for Firefox is not re-established when SWFUpload makes the upload. This is a problem I can't fix. I have had to change the way I link the form that my user is filling out and the files she upload since they're submitted separately and I can't rely on any session values.

Another Feature

I also added an upload limit feature. You set the upload limit when instantiating the SWFUpload object and, even if the user queues up more files, the movie won't upload any more files than the limit. I added a new error code (-60) to report back on files rejected because the upload limit has been reached.

Isn't implementation grand!

Wednesday, January 24, 2007

SWFUpload R3 Update

I've finished my final additions to SWFUpload. The "delayed" upload works fine. It is a bit of extra work, especially if you try to carefully take care of errors.

I've updated the Demos and sent a copy back to the original authors. They like the features I've added. Maybe some of them will end up in the official release.

It's been fun. Now it's time to get back to work.


Tuesday, January 23, 2007

SWFUpload Revision 3 Demo

I've completed the major work on my third revision of SWFUpload. I've thoroughly taken the plunge and have made several modifications to the SWFUpload flash file. That, in turn, required several changes to the Javascript file.

I’ve added the following features:
  • SWFObject has been removed in favor of my Flash Feature Detection technique.
  • File Objects now include a unique ID. This is useful for working with the DOM.
  • It is now possible to cancel an upload-in-progress.
  • It is now possible to cancel all the files queued for upload (but not yet uploaded).

I have updated my demo to show off the new features. Again, I don't have a host. Contact me if you want a copy. It's based around asp.net but is easily adapted to any server technology:


Other ideas

I’d like to add a feature that allows you to remove queued files before the upload starts. Now that I have unique file IDs this should be pretty easy.


Trained the wrong way, but still trained

It occurred to me that SWFUpload does not quite fit in with the normal HTTP file upload format that we (and our users) are all used to. Usually we select our files and then probably have to fill in other parts of a form. Once completed we submit the entire form together. SWFUpload is breaking this up. Users might have a tendancy to click the Submit button before their uploads have finished.

I appreciate the benefit of starting uploads immediately. Users can finish the form while the upload is going and it'll all be more efficient. However, that’s not how users have been trained to use websites. They have been trained to complete a form (without looking at its labels). Then you pressing submit to back the message that says you did it all wrong. You trying again (and again) and finally getting it submitted.

With that in mind I think I am going to add one final feature. It will allow you to specify whether or not the queued-up files should be uploaded immediately or should wait until a command to upload is given. Now the user can queue up their files and fill out the form. The Submit button will kick off the SWFUpload file upload (the user still gets the great benefit of watching the file progress and getting feedback about the file size). The uploadQueueComplete event handler is called when the upload finishes and it submits the form. The page can watch for any SWFUpload errors and cancel the submit if necessary.

This is more in keeping with the expected user experience.

I'll keep ya'll posted. Yep, I mean both of you.

Flash Feature Detection

I've continued to work on my SWFUpload revision. I've tested the graceful degradation a bit more and it is slowly improving. I've tested it with Flash 2, 4, 6, and 8 in Internet Explorer 6. Of course, it only works in Flash Player 8 or above but it correctly degrades for early players.

Flash Player Feature Detection
I've also started on a Revision 3 which changes the entire method of Flash Version Detection. I have removed the SWFObject code (which performs the Flash version detection). I'm experimenting with a detection method similar to browser Object Detection. Instead of looking at the flash version number (as reported by the browser) I have the player check for key features, such as, Javascript calls to Flash, Flash calls to Javascript and the whole File Upload bit, which only work in since Flash Player 8.

I do this by having Javascript first add the flash movie to the page. It does not attempt any Flash version checking. If Flash is installed and working in the browser the movie will load. If a Flash Player older than version 8 is installed it will not be able to play the movie properly and the SWFUpload script stops.

However, if Flash Player 8 or above is installed the movie calls a Javascript function which alerts the SWFUpload script that it can continue loading. SWFUpload continues by loading the UI components so the user can begin uploading files.

New SWFUpload features

I didn't think I'd go so far in this project as to begin tweaking the Flash files. But now that I have I am going to begin looking in to adding Cancel Upload and Cancel Queue features.

It's been an interesting excursion so far but I think I've just about spent enough time on it. Hopefully I'll have what I want wrapped up pretty quick.

Thursday, January 18, 2007

SWFUpload Revision 2 Demo

Here I go again. After posting my changes to the SWFUpload script I decided I better throw the script up on a server and make sure it really works. Well, it didn't quite work. I've updated the post a bit so hopefully everything is peachy.

I built an ASP.Net demo that shows off my changes. Unfortunately, I don't have anywhere to host it or to post the files for download. So, if you want a copy of the demo files leave contact info in the comments.

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!

Friday, November 10, 2006

Textarea Cursor Position in Internet Explorer

Background...
I'm building a content management type website and wanted to include a nice WYSIWYG editor. Unfortunately all the editors I tested are either too unpredictable and erratic, cost a fortune, or are not customizable enough for my needs (out of the box anyways).

In the end I just didn't want to deal with the awful (x)html that they produce. I also didn't want to deal with my end users, whom have never heard of HTML and won't want to "View Source" to fix the HTML when the WYSIWYG fails.

So, I decided to build my own. After a smidgen of research and testing I decided not to build my own. Now I'm going for a Wiki style code editor with a toolbar. With this I can generate clean consistent simple markup. It's not WYSIWYG but at least it will be consistent and predictable.

I quickly found that I'd need to be able to retrieve and adjust the current text selection in the textarea control. So, after not finding any satisfactory solutions via Google I've come up with my own.

Textarea Cursor Position in IE

First off I'd like to give credit to the sites that provided good tips, suggestions, and solutions.
  • parentNode - The best written and closest solution I found.
  • Mishoo - A good idea but the comments were more helpful
  • Scott Van Vliet - Also a very interesting approach. The comments were also helpful
  • Thanks to WikkaWiki which has a good solution for their text editor.
  • MSDN Library
Summary of the Implementation

My solution works like this:
  1. Make 2 TextRanges.
    1. The first TextRange selects from the beginning of the textarea to the beginning of the selected text.
    2. The second TextRange is a duplicate of the selection.
  2. Restore missing new line characters in both TextRanges
    1. The TextRange.text property right trims all the \r\n characters from the selection. We must restore the new lines to calculate the correct cursor position.
  3. Calculate the start position which is the untrimmed length of the 1st TextRange.text property.
  4. Calculate the end position which is the untrimmed length of the 2nd TextRange.text property plus the start position.
  5. Calculate the complete untrimmed selection using textarea.value.substring(startPoint, endPoint).
Further Explanation and Repetition

Internet Explorer makes this whole process very difficult. The biggest issues are:
  • It treats \r\n as one character when working with the TextRanges
  • It trims \r\n from the end of the TextRange.text so you don't know the real selection length.
My solution discovers the correct index of the start and end positions of the selection which can't be done by just using the TextRange.text.length. Once you have the positions you can use textarea.value.substring(start,end) to get the actual selected text rather than the trimmed TextRange.text property.

All the string methods (substring, indexOf, length, etc.) treat \r\n as 2 characters, however, the TextRange methods (moveStart, moveEnd, etc.) treat \r\n as 1 character. So just moving the TextRange until we reach the beginning and counting how many times we moved will be off by exactly the number of new lines passed along the way.

A Word About Changing the Selection

The TextRange.text \r\n trim issue also causes problems when attempting to change the selection's position. You need to count the newlines between the beginning and the target index and then subtract count from the target index. Then you can use this adjusted index with the TextRange.moveStart and TextRange.moveEnd methods to get the correct selection.

The Code

In my code I use 3 TextRanges (instead of 2 as described above) so that I can re-assemble the entire string and compare it to the original textarea.value. I do this to make sure I performed the untrimming correctly.

You will also notice that I only use a single do..while loop to untrim all the TextRanges. I could have done this with 3 separate loops. Even better, I probably should move the untrim bit to a separate function. I purposely made these design decisions but it could be done differently.

Also note that this bit of code was only tested in IE 6.0. Please don't complain because it doesn't work in Opera, Firefox, Safari or Lynx.


var textarea = document.getElementById("myTextArea");
textarea.focus();
var selection_range = document.selection.createRange().duplicate();

if (selection_range.parentElement() == textarea) {    // Check that the selection is actually in our textarea
  // Create three ranges, one containing all the text before the selection,
  // one containing all the text in the selection (this already exists), and one containing all
  // the text after the selection.
  var before_range = document.body.createTextRange();
  before_range.moveToElementText(textarea);                    // Selects all the text
  before_range.setEndPoint("EndToStart", selection_range);     // Moves the end where we need it

  var after_range = document.body.createTextRange();
  after_range.moveToElementText(textarea);                     // Selects all the text
  after_range.setEndPoint("StartToEnd", selection_range);      // Moves the start where we need it

  var before_finished = false, selection_finished = false, after_finished = false;
  var before_text, untrimmed_before_text, selection_text, untrimmed_selection_text, after_text, untrimmed_after_text;

  // Load the text values we need to compare
  before_text = untrimmed_before_text = before_range.text;
  selection_text = untrimmed_selection_text = selection_range.text;
  after_text = untrimmed_after_text = after_range.text;

  // Check each range for trimmed newlines by shrinking the range by 1 character and seeing
  // if the text property has changed.  If it has not changed then we know that IE has trimmed
  // a \r\n from the end.
  do {
    if (!before_finished) {
      if (before_range.compareEndPoints("StartToEnd", before_range) == 0) {
        before_finished = true;
      } else {
        before_range.moveEnd("character", -1)
        if (before_range.text == before_text) {
          untrimmed_before_text += "\r\n";
        } else {
          before_finished = true;
        }
      }
    }
    if (!selection_finished) {
      if (selection_range.compareEndPoints("StartToEnd", selection_range) == 0) {
        selection_finished = true;
      } else {
        selection_range.moveEnd("character", -1)
        if (selection_range.text == selection_text) {
          untrimmed_selection_text += "\r\n";
        } else {
          selection_finished = true;
        }
      }
    }
    if (!after_finished) {
      if (after_range.compareEndPoints("StartToEnd", after_range) == 0) {
        after_finished = true;
      } else {
        after_range.moveEnd("character", -1)
        if (after_range.text == after_text) {
          untrimmed_after_text += "\r\n";
        } else {
          after_finished = true;
        }
      }
    }

  } while ((!before_finished || !selection_finished || !after_finished));

  // Untrimmed success test to make sure our results match what is actually in the textarea
  // This can be removed once you're confident it's working correctly
  var untrimmed_text = untrimmed_before_text + untrimmed_selection_text + untrimmed_after_text;
  var untrimmed_successful = false;
  if (textarea.value == untrimmed_text) {
    untrimmed_successful = true;
  }
  // ** END Untrimmed success test

  var startPoint = untrimmed_before_text.length;
  var endPoint = startPoint + untrimmed_selection_text.length;
  var selected_text = untrimmed_selection_text;

  alert("Start Index: " + startPoint + "\nEnd Index: " + endPoint + "\nSelected Text\n'" + selected_text + "'");
}

Wednesday, October 11, 2006

Javascript Closures

Editors Note: I had this all edited and fixed up and then something funny happened and I lost it all. So you're getting the unedited version. I'm not doing it again and nobody reads this blog yet anyhow.

Javascript can be a frustrating language. At first it seems like BASIC with a C syntax. But quickly odd things start happening. They aren't really odd but they aren't what you expect. In order to do my part to save the world from odd things I present some thoughts on Javascript Closures. I recommend reading Javascript Closures by Richard Cornford.

Many of the odd things in Javascript begin to manifest themselves when you begin embedding functions inside other functions. This happens a lot when you dynamically assign event handlers to objects on a web page. These thoughts are mostly in that context.

Closures

Closures have to do with the way Javascript supports scoping in nested functions. A nested function has access to the scope of its parent functions. However, the scope of the parent functions can change after the nested function has been declared and before the nested function is actually called and executed.

It's common to assume that the nested function gets the parent scope when it is declared. But this is not the case. The parent scope is checked when the nested function is called. This also means the parent scope continues to exist after the parent function has ended.

Examples:
function parent() {
var parentVariable = "Parent Variable Before";

function child(childVariable) {
var childVariable2 = "Child Variable 2";
alert(parentVariable);
alert(childVariable);
alert(childVairable2);
}

child("Child Variable");

parentVariable = "Parent Variable After";

child("Child Vairable");
}


This example shows that the function child checks the parent function scope when it is called, and so alerts "Parent Variable After" at the second call. A common mistake is to assume that the child function will always alert "Parent Variable Before" since it was declared when parentVariable was set to that value.

This misconception causes trouble when trying to set onclick events in a for loop. The code usually looks something like this:

for (var i=1; i <= 10; i++) {
var new_element = document.createElement("div");
new_element.onclick = function (e) { alert(i); };
new_element.innerHTML = i;
document.appendChild(new_element);
}


In this example 10 divs are created. Each is numbered from 1 to 10. When you click each on you would expect the div to alert with its number. It does not. The alert displays 10 on all the divs.

This behavior makes sense if you think about the scope of the parent function and when the child (the onclick) function is being called versus when it is being declared. The child function does not use the parent scope when it is declared. It hasn't actually been executed yet. By the time it is called the parent scope (and the variable i has changed.

By the time you click a div and the onclick is called the i variable in the parent scope is set to 10 because the loop has finished and stopped at 10. If you add the line below after the for loop then each click on a div will display "Hello World". So, understanding the parent scope what did you expect? It's using the parent scope at the time of the click, not the scope when the onclick function was declared.

i = "Hello World";



Here is another example. You should be able to predict the behavior without running the script.

var my_function;
for (var i=1; i <= 10; i++) {
if (i == 5) {
my_function = function() { alert(i); };
}
}

my_function();


So, without running the script, will the call to my_function alert with a 5 or a 10? If you said 5 then shame on you. You'd better start again.

A Solution

It's not uncommon to want to assign event functions in a loop (like the onclick div example). But the obvious code example above doesn't do what you might expect (although it does do the right/correct thing). So, in order to make it work you need to either create a scope that's not going to change between the time you declare the inline function and the time you call the inline function or you could declare a variable that is not going to change in the parent scope. You can't use i because it's going to change each time you loop.

I use the the "create a scope" method. The only way I know to create a new scope in javascript is by calling in to a function.

(in Javascript loops do not have their own scope. Their variables are elevated to the existing scope so you can't use any kind of nested for. That is why i == 10 and not undefined in our div example above. Even though the i was declared in the for-loop, Javascript promotes it to the function's scope and it still exists after the loop has ended).

function SetOnClick(value) {
// A new scope is created when this function is called
return function(e) { alert(value); };
// Once the function exits value can never be changed again.
// So when this inline function is called we can be assured that value will still be the same
}


for (var i=1; i<=10; i++) {
var new_element = document.createElement("div");
new_element.onclick = SetOnClick(i); // Now this onclick gets the inline function returned by SetOnClick.
// Calling this function creates the new scope we need
new_element.innerHTML = i;
document.appendChild(new_element);
}