Andrew Burke - Digital Construction since 1994

On Twitter

On Facebook


andrewburke.ca - How to do Google Maps-Style Scrolling Windows with JavaScript and DHTML

 

Posted on: 2007-09-06 07:47:19

Previous: The Young Gods Play Kurt Weill Next: Music, With Occasional City

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!
<div id="main-window" style="width: 450px; height: 450px; overflow: hidden; position: relative;">
  <div id="scrolling-section" 
      style="top: -1000px; left:-1000px; width: 2272px; height: 1704px; position: absolute; background: url(/images/lake.jpg) top left no-repeat;" 
      onmousedown="setClickPos(event, this)" 
      onmouseup="mouseup(event, this)" 
      onmousemove="moveTarget(event, this)">
  </div>
</div>
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:
 // initial setup
  clickposx = 0;
  clickposy = 0;

  scrollstartleft = 0;
  scrollstarttop = 0;

  ismousedown = false;

  maxleft = 0;
  maxtop = 0;

When the user clicks down anywhere inside the inner div, it calls the "setClickPos" function:
  function setClickPos(event, targetDiv) { 
    clickposx = event.screenX;
    clickposy = event.screenY;

    scrollstartleft = parseInt(targetDiv.style.left);
    scrollstarttop = parseInt(targetDiv.style.top);

    var framingDiv = document.getElementById("main-window");

    maxleft = parseInt(targetDiv.style.width) - parseInt(framingDiv.style.width);
    maxtop = parseInt(targetDiv.style.height) - parseInt(framingDiv.style.height);

    ismousedown = true;
  }
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:
  function moveTarget(event, target) {
    if (ismousedown == true) {
      var newposx = event.screenX;
      var newposy = event.screenY;

      var difx = newposx - clickposx;
      var dify = newposy - clickposy;

      var newleft = scrollstartleft + difx;
      if (newleft > 0) { newleft = 0; }
      if (newleft < (0 - maxleft)) { newleft = (0 - maxleft); }

      var newtop = scrollstarttop + dify;
      if (newtop > 0) { newtop = 0; }
      if (newtop < (0 - maxtop)) { newtop = (0 - maxtop); }

      moveToPosition(target, newleft, newtop);
    }
  }
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:
  function moveToPosition(target, x, y) {
    target.style.left = "" + x + "px";
    target.style.top = "" + y + "px";
  }
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.

Previous: The Young Gods Play Kurt Weill Next: Music, With Occasional City

Other Blog Posts:
- Berlin: Museums
- Berlin: Ghosts of the Past, Visions of the Future
- Flâneur in Berlin
- Berlin: Finding the Best Wurst
- Istanbul: Overwhelmed by History in the Hippodrome
- Istanbul: That dolphin-torn, that gong-tormented sea
- Istanbul: The Topkapi Palace and Harem
- Istanbul: Mosques
- Istanbul's Basilica Cistern: Gorgeous, Creepy, Nerdy
- Istanbul: Hagia Sophia
- The Streets of Istanbul - II
- The Streets of Istanbul - I
- Munich Airport: Legoland mit Bier und NapCab
- Heathrow Airport: You Are In A Maze Of Twisty Little Passages, All Different
- Getting Ready to Travel
- Quick Advice on Canadian Indie Music
- My Favourite Roadside Sign
- Well That Explains a Lot...
- Poland: Gear from the Army Museum
- Poland: Warsaw's Palace of Culture and the University Library
- Poland: Warsaw
- Poland: Winged Hussars
- Poland
- What's awesome about Toronto
- Possibly the best sentence in the English language
- QUOTE: We Shouldn't Have Music Anxiety
- Now *that's* Santa Cruz
- Small Town Newspaper Headline Dada
- Great Quote from Seth Godin
- McSweeney's: My Pet Peeves
- Shindig!
- Dresden Dolls / Die Mannequin / Friendly Rich at the Phoenix
- In Store for 2008: Wailing and Gnashing of Teeth?!
- Coffee Updates: Urbana and Far Coast
- Canadian: Walking to Tim Horton's Through a Blizzard
- Lighting as language
- TSOT Ruby/Rails Project Night
- IE is pants, pure and simple
- Passport Canada's Secure Enterprise Software
- DemoCampToronto16
- Faulty By Design
- Buynlarge.com - brilliant!
- Joey Starts at TSOT and Jeff goes 37Signals
- How To Doom Your Own Industry
- It's Sigmoidal, Stupid!
- Quick Update on Secured OS X Mail
- Alpha Geeks and Jedi Hooligans
- Now Fake Steve is Getting Close To Home
- Nice Rant on the Sanctity of Farming
- They Must Have Been Reading This Blog
- Well, so much for Reddit
- Zipcar: My Other Car is a Mini Convertible Named Munster
- XKCD Job Interview
- John C. Dvorak Misses It
- Protecting Your OS X Mail With Encrypted Volumes
- Fake Steve Jobs hits it
- My Favorite Bit From Herodotus
- Enterprise Software - like on the spaceship, right?
- So why, again, are you taking so long?
- That sounds about right for Oberlin
- Music, With Occasional City
- How to do Google Maps-Style Scrolling Windows with JavaScript and DHTML
- The Young Gods Play Kurt Weill
- Want a Rails Job?
- Quote of the Day
- Witty and Vibrant, Sensitive and Cranky
- Facebook, already - geez
- The Bolivarian Republic of Wednesday and Pudge
- Here are the real links for the previous post
- Venezuela: How To Have A Good Party
- It's a PHONE that runs UNIX!
- Congratulations Pat & Chris!
- About Venezuela: Traffic
- Venezuela Stuff Coming Later - But While I'm Recovering...
- My most popular posts are un-published!
- ... that creepy ass botox-phenomenon
- Prototype Library and JavaScript
- Godin LG Hmb - my new guitar
- Safari For Windows - What Apple Missed
- Joel Corrects Himself In Mid-Post
- Yorkville's Summer of Love with Gucci
- Update: Coffee
- RJS / AJAX Highlight Colouring in Rails
- Looking Real Good!
- Post-something Post on Big Bags
- Disclaimer
- Analgesic Code: Backtrack
- Baby Steps With EMACS
- Back in Santa Cruz
- You know, I agree that we should worry about Global Warming...
- Life Tip: Digitize Your Documents
- Nifty OS X Finder Enhancement With Little AppleScripts
- Toronto DemoCamp 12
- Wednesday and Pudge
- Rails Pub Nite - HAML Cake!
- BAAX!
- With a Good Search You Can Be Organized
- Something people seem to have missed in the Steve Jobs Keynote today...
- Analgesic Code: Rails Test Progress
- New Year's Greeting 2006

All Blog Entries

RSS Feed


Back