Thursday, March 01, 2007

Simple draggable HTML using prototype.js

I know there are plenty of JavaScript packages out there to allow you to create draggable elements on your HTML page, but to me, most of them appear to be overly complicated for a simple function and are often part of a larger framework that you may not need. The solution I'm providing today does depends on the prototype.js package (I'm using version 1.5 currently) which allows the code size to stay smaller. Here's how to make any HTML element draggable in 5 easy steps.

  1. Add the class of draggable to whatever you want to move. Actually, you can use any class identifier you want, but draggable is what I'm looking for in my code.
  2. Give your draggable element a position style (position: absolute; or position: relative;) and an initial top and left value. Generally, you'd want to put this in your styles in an external CSS with the draggable class, but the top and left values need to be set as an attribute in the HTML. Hint: setting the position to relative with the top and left values set to 0px will allow it to start located where it normally would be in the browser. You'll probably want to give it a higher z-index and solid background-color as well, but that's up to you.
  3. Include the 3 drag event handling functions I'll describe below in your code somewhere
  4. Register all your draggable elements to the event listeners.
  5. Have fun dragging window elements around.

The event handlers

Before you can use the following functions, you need to define some variables to hold some information during the drag. If you make the subsequent functions part of another containing object, you can simply prepend all occurrences of these variables in the functions with this. and they will be stored in the containing object instead.
var dragObj;
var cursorStartX;
var cursorStartY;
var elementStartX;
var elementStartY;
var dragGofn;
var dragStopfn;
startDrag
startDrag will initialize a mouse drag event. It captures the initial location of the mouse when beginning the drag in order to calculate movement later. It also captures the current coordinates of the dragged element for correct positioning as the location of the mouse are most likely not the origin of the element being dragged. Finally, it associates the additional event observers to the element to capture the mouse movement (onmousemove) and release of the mouse button (onmouseup).
function startDrag(event) { 
    dragObj = Event.element(event);
    // if the target of the event is not a "draggable" element, then search
    // through it's parents for an element that is draggable.
    // if we can't find a draggable element before reaching the root document
    // element, then bail out
    while (!Element.hasClassName(dragObj,"draggable")) {
         dragObj = $(dragObj).up('.draggable');
    }
    cursorStartX = Event.pointerX(event);
    cursorStartY = Event.pointerY(event);
    elementStartX = parseInt(dragObj.style.left, 10);
    elementStartY = parseInt(dragObj.style.top, 10);
    
    Event.observe(dragObj, 'mousemove', dragGofn);
    Event.observe(dragObj, 'mouseup', dragStopfn);
}
dragGo
Once the drag event has been initiated by the startDrag function, the dragGo function will update the location of the element being dragged so that it moves around the screen with the mouse.
function dragGo(event) { 

    var x, y;
    x = Event.pointerX(event);
    y = Event.pointerY(event);

    // move the target object
    dragObj.style.left = (x - cursorStartX + elementStartX) + "px";
    dragObj.style.top = (y - cursorStartY + elementStartY) + "px";
    
    Event.stop(event);
}
dragStop
Once the user has release the mouse button, we need to stop observing the mouse movements, so the dragStop function will un-register the event handlers.
function dragStop(event) { 
    // Stop capturing mousemove and mouseup events.
    Event.stopObserving(dragObj, 'mousemove', dragGofn);
    Event.stopObserving(dragObj, 'mouseup', dragStopfn);
    dragObj = null;

    Event.stop(event);
}

Registering the draggable elements

You're almost ready to start moving items on the screen, but first you have to register all your draggable elements to the startDrag function so that they will be notified of the drag events. So, we utilize prototype's element-by-classname selector to find all the draggable elements and associate the event listener to them. Note: in order to un-register the event listeners, you have to have the handle to the event listener you used to register it originally. Thus, we used the variables dragGofn and dragStopfn to register and un-register the event listeners. These variables are defined here during the original event registration.
function registerEvents() {
    dragGofn = dragGo.bindAsEventListener();
    dragStopfn = dragStop.bindAsEventListener();
    
    $$(".draggable").each(function(draggableElem) {
        Event.observe(draggableElem, 'mousedown', startDrag.bindAsEventListener(), true);
    });
    
}
Note: the final true on the Event.observe tells prototype to request capturing instead of bubbling. This is necessary for this function to work in Safari (see Kir's blog (http://kirblog.idetalk.com/2006/06/safari-javascript-problems.html))

Monday, February 26, 2007

Adding a Processing indicator with Prototype.js

Here's a simple method to add a Processing/Loading/Working indicator to indicate when an Ajax request is being processed with prototype.js.

First, define a hidden div (or other preferred HTML element) which will be displayed to indicate that some background processing is going on. Mine happens to look like:


<div id="loading" style="display:none">Loading...
<div style="background-color: white; position: absolute; top: 0px; right: 0px; width: 5px; height: 5px; cursor:pointer;"
onclick="$('loading').hide()"></div>
</div>

In my code, I added another div on the upper right corner which I can click to close this indicator in case something goes wrong with the Ajax event handling which I'll provide in a moment.

I then added a little CSS magic to make it look the way I wanted. In my case, I want the indicator to be on top of everything, centered on the page, with a processing image () on the right side of the text. The background color is white with a double gray border and large, bold font.

#loading {
z-index: 100;
position: absolute;
top: 40%;
left: 40%;
background-image: url("../images/progress-running.gif");
background-repeat: no-repeat;
background-position: 5px;
background-color: white;
padding-left: 25px;
padding-top: 8px;
border-style: double;
border-color: #c0c0c0;
width: 120px;
height: 30px;
font-size: 1.5em;
font-weight: bolder;
}


Ok, so now I have the indicator I want to popup. This can be tailored to your preferences. Now, you simply tell prototype to display it whenever it is processing an Ajax request. This is done by registering some event listeners to the prototype Ajax class.

// register event listeners on the Ajax requests to show/hide the processing indicator
Ajax.Responders.register({
onCreate: function() {
if (Ajax.activeRequestCount === 1) {
$('loading').show();
}
},
onComplete: function() {
if (Ajax.activeRequestCount === 0) {
$('loading').hide();
}
}
});


That's it! Now you should get an indicator whenever you issue an Ajax request that goes away whenever the processing is done.

Friday, February 23, 2007

Cookies and Flash for local storage

In previous posts I provided code for using cookies in JavaScript. I've now enhanced that code to utilize the flash storage mechanism which was described by Christoph Khouri, however his page seems to be down currently, so I'll provide the files you need. Note: this is the same technique used by the Dojo Storage module, without the need for Dojo which adds extra overhead costs. Using flash storage alleviates the 4k size limit of cookies and initially provides 100k of storage seamlessly and can be increased to any size by the user. It also eliminates unnecessary network traffic as all the cookies are transmitted to the server all the time, and makes it harder for prying eyes to see what's being stored as vs browsing the cookies in the browser. In order to use the flash storage, first, download flashStorage.zip which came from Christoph Khouri's site. Next, download my current cookies.js file. Now, include the flashStorage JavaScript and my cookies.js JavaScript in the header of your HTML page:

<script type="text/javascript" src="jscript/flashStorage/swfobject.js">lt;script>
<script type="text/javascript" src="jscript/flashStorage/flashStorage.js"><script>
<script type="text/javascript" src="jscript/cookies.js"></script>
Next, somewhere in the body your page add the following code:
<div style="float: right;" id="storageHolder">
<embed type="application/x-shockwave-flash"
src="jscript/flashStorage/storage.swf" id="storageMovie" name="storageMovie" bgcolor="#ffffff" quality="high"
swliveconnect="true" wmode="transparent" height="137" width="214" />
</div>
<script type="text/javascript">
 var so = new SWFObject("jscript/flashStorage/storage.swf", "storageMovie", "50", "10", "8", "#ffffff");
 so.addParam("swLiveConnect", "true");
 so.addParam("wmode", "transparent");
 so.write("storageHolder");

//Initialize the flash storage by passing the id of the flash movie
Storage.init("storageMovie");
</script>
That's it! Now whenever you create a cookie like Cookies.create("myCookie", "myValue") it will actually store that in the flash storage instead. However, if you don't wish to include the Flash storage component, it will default to store the value in a browser cookie instead. Maybe sometime in the future I'll add detection to see if the Flash plugin is installed in addition to the Storage object and default to standard cookies if Flash isn't installed. Update: I've added a check for the Flash plugin being installed as a pre-requisite to using the Flash storage object in addition to the check for the JavaScript code/object being included. FYI, my check for Flash for those needing to do the same thing is:
var hasFlash = (navigator.plugins &&
              (navigator.plugins["Shockwave Flash"] || navigator.plugins["Shockwave Flash 2.0"])) ||
             (navigator.mimeTypes &&
              navigator.mimeTypes['application/x-shockwave-flash']);

Ajax Cross-Domain Issue

A common solution to the XMLHttpRequest cross-domain issue (aka cross site scripting or XSS), where you can only communicate with the originating domain, is to configure Apache to use either mod_proxy or mod_rewrite for URL rewriting and a pass-through proxy.

With Apache you need to enable the proxy functions (configure --enable-proxy). If you want to handle it all with mod_proxy, then define you proxy settings in httpd.conf such as:


ProxyPass /doExt http://external.com/doAction
ProxyPassReverse /doExt http://external.com/doAction

If you want to use mod_rewrite then turn on mod_rewrite in httpd.conf or .htaccess with RewriteEngine on and define your rewriting rules such as:

RewriteRule ^/doExt$ http://external.com/doAction [P]

Then when you post to your server via /doExt, Apache will pass the request on to the external site.

That's all well and good if you have access to the Apache configuration or the server is already configured with the proper modules. But, when your on a hosted environment, that's not always feasible. Another solution would be to provide a proxy servlet to make the remote request for you and return the result. What I am providing is in Java and PHP, but it could be written you language of choice.

Java proxy


The following servlet class will invoke some external URL on behalf of the original request and return the result from the external request as the result of the original local request. Note: In the following code I hard-coded the real destination. However, it could be determined from a parameter in the original request just as easily.


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.URL;
import java.util.Enumeration;

/**
* This is a simply proxy class in order to bypass AJAX security contraints.
*
*/

public class ServerProxy extends javax.servlet.http.HttpServlet {

private static final long serialVersionUID = 1L;

public void doGet(javax.servlet.http.HttpServletRequest request,
javax.servlet.http.HttpServletResponse response)
throws javax.servlet.ServletException, java.io.IOException {
performTask(request, response);
}

public void doPost(javax.servlet.http.HttpServletRequest request,
javax.servlet.http.HttpServletResponse response)
throws javax.servlet.ServletException, java.io.IOException {
performTask(request, response);
}

public void performTask(javax.servlet.http.HttpServletRequest request,
javax.servlet.http.HttpServletResponse response)
throws javax.servlet.ServletException, java.io.IOException {

// Get the external URL to invoke
String realUrl = "http://www.external.com";

// Copy the request parameters from the original request
Enumeration paramNames = request.getParameterNames();
String paramName;
while (paramNames.hasMoreElements()) {
paramName = (String) paramNames.nextElement();
realUrl = realUrl.concat(paramName + "="
+ request.getParameter(paramName));
if (paramNames.hasMoreElements()) {
realUrl = realUrl.concat("&");
}
}

// real URL will be returning XML data
response.setContentType("text/xml");
PrintWriter out = response.getWriter();

// invoke the real URL and copy the result into the response
// for the original request
URL real = new URL(realUrl);
BufferedReader in = new BufferedReader(
new InputStreamReader(real.openStream()));
String inputLine;
while ((inputLine = in.readLine()) != null)
out.println(inputLine);
in.close();

return;
}

}


PHP proxy


Here is a similar solution using PHP:

$remoteServer = "http://www.external.com";
$path = $_GET[”path”];
$proxyTarget = $remoteServer.$path;
$connection = curl_init($proxyTarget);
curl_setopt($connection, CURLOPT_HEADER, false);
curl_setopt($connection, CURLOPT_RETURNTRANSFER, true);
$data = curl_exec($connection);
curl_close($connection);
header(”Content-Type: text/xml”);
echo $data;

Google Apps Premier Edition, a good thing?

I see on Google's Blog that they are announcing Google Apps Premier Edition which is a collection of the existing Google Apps (Gmail, Google Talk, Google Calendar, Page Creator, and Google Docs & Spreedsheets) which have additional collaboration features, APIs and support for which they will charge $50/yr per user. I certainly think Google's apps are very useful, of high quality, and worthy of compensation and they certainly offer good service with a 99.9 percent uptime service-level agreement in which customers will receive credits for downtime. However, the reason I and so many others use Google apps is because they are free so I hope this is not a sign of things to come. In the blog entry for this announcement they say,

Google Apps also won't forget its roots anytime soon. The Standard and Education Editions will continue to be offered for free...

It's the "anytime soon" clause that causes me apprehension. Does that infer that there is a future plan to charge for their services? I've been migrating to Google more and more, I don't want to go through the trouble of migrating somewhere else.