I've spent the last few weeks working for a client who brought me in for my Ruby on Rails experience, but I ended up working almost exclusively in JavaScript. Doing complicated things with JavaScript used to be annoying and painful, but with the newer debugging tools for Firefox and cool things like the Prototype library, it's a lot more fun.
I don't want to give away too much about the project, but I ended up building a diagramming system that let users add and remove elements in a graphical hierarchy and see the relationships between them. All of this was done entirely in the browser - no going back to the server and its friendly relational database and Ruby code - and it turned out quite nicely. The diagram could get quite large, though, so we decided that you should be able to scroll around in it a bit like how Google Maps works.
I looked around the web for quite a while and couldn't find any information on how to do this. So, to help anyone else out there who may want to do a similar thing, I thought I'd share what I figured out. Actually, this isn't exactly how I did it in my recent project, since I used a more object-oriented approach and leaned heavily on the awesome Prototype library; this version is smaller and only uses plain JavaScript. Also, this just shows a simple picture, not a whole dynamically-generated tree hierarchy thing.
You can see a scrolling example here, and "View Source" to see all of the code involved.
The heart of this is two nested elements on the page - one is a smaller framing element with the important "overflow: hidden;" style attribute, and another larger element inside it with "position: absolute;". For this example, I'm putting the style information directly into the HTML instead of breaking it out into a style sheet - it's usually a good idea to break style information out, but for JavaScript to access and change the positioning information, it needs to be specified in the div description itself - this took a while to figure out!
In this case, the framing window is a div called "main-window" and the internal part that scrolls is another div called "scrolling-section". Here it contains a picture as a background image, but it could contain other divs or a table or text or whatever. I set a starting offset for the "top" and "left" values so we see the middle of the picture, not the top corner.
You'll also notice there are handlers for onmousedown, onmouseup and onmousemove - those are the other key to how this all works: whenever you click on an image, the script starts tracking your mouse position, and as you move your mouse around it changes the left and top offsets of the inner div to match. When you unclick, it stops tracking. The rest of the page is the JavaScript code that does all of this magic.
To start with, we set up a few global variables for the page - these keep track of where you started dragging around, where your mouse is, whether the mouse button is being held down, and where the limits are for the scrolling:
When the user clicks down anywhere inside the inner div, it calls the "setClickPos" function:
Here we set a number of the global variables for our starting information. The click event itself holds the coordinates that were clicked, and we store these in "clickposx" and "clickposy". We also get the width and height of the framing div and the current div so we can calculate the bottom and right limits for our scrolling (the top and left limits are of course 0). We then set the flag that says whether the mouse is down or not. The mouseup function is really simple: "ismousedown = false;" - it just lets the page know that we've finished dragging.
Position information usually comes in as a string like this: "250px" - thankfully JavaScript's parseInt() function removes non-numeric characters from a string before trying to convert it into a number. I had built a complicated parsing function before I realized that. Dynamic languages are fun!
The tricky part is in the moveTarget function:
This function could be made a bit shorter, but I like to break out my calculations into separate lines, since it makes it easier to follow and easier to debug: if you're doing everything in one line, and there's an error in that line, it's trickier to figure out where the problem is. The first thing we do is check our flag to see if the mouse is down or not. If the mouse is down, then we figure out the current mouse position and subtract the original click position from it to find the distance that we've dragged since we clicked. We then figure out the left and top offsets we'll need to apply to the div. Before we do that, though, we check to make sure we're not dragging out of bounds - this looks uglier than it actually is, since the offset positions are negative.
Then we call the simple "moveToPosition" function:
Here we change the position back to a string with the appropriate "px" suffix.
The larger inside div can contain text or other positioned divs with content - be careful with image tags, since many browsers interpret clicking and dragging on a foreground image to be a drag-and-drop action for the image itself, and that usually comes first before the JavaScript can get the message to start scrolling. Text can also get funny, since the browser may try to select the text while you're dragging. If you want to use an image, making it a css background image (like here) seems to work pretty nicely.
While I was working on this, I would often stop and just move the diagram around a bunch with the mouse and watch it scrolling - it just looked so cool! Now you can do it to.