How to build a Pan & Zoom Plugin Using the Revealing Module Pattern

In one of our recent projects, we had to devise a solution for the virtual visitors of a large shopping mall, which has an overwhelming diversity of businesses, brands, activities and services. What we needed was a surface map that would interact with the user, so that they could search for points of interest, be taken there and shown key information on the fly. The source serving the map had to be dynamic and thus would be changed frequently and updated as a ‘dumb’ SVG file; the live map would have to adapt to the changes.

If you face a similar challenge, this article takes you through the steps of building a basic JavaScript plugin for interacting with an element which allows zooming and panning using your mouse. We will be using the Revealing Module design pattern (RMP) to write our plugin. (check this out if you want to read more on RMP).

Step 0 – Basic Prerequisites

Let’s call our plugin Moveit and get the HTML & CSS out of the way.

We need a fresh HTML document, in which to put 3 things: a button, a container that can be anything (in our case a <figure>) and a target that can also be anything (in our case an <img>):

<!DOCTYPE html> 
<html lang="en"> 
 <head> 
  <title>Moveit</title> 
   <link rel="stylesheet" href="style.css"> 
 </head> 
 <body> 
  <button>Reset</button> 
  <figure class="box"> 
   <img src="funny.jpg" alt="Funny Whale Shark"> 
  </figure> 
 </body> 
</html>

Now the CSS, to style our box so that we can see our context better, and the button:

.box { 
 width: 50%; margin: 10px auto; 
 padding: 50px 0; 
 border: 1px solid #dedede; 
 overflow: hidden; 
 text-align: 
 center; 
 background-color: #eee; 
} 
button { 
 display: block; 
 margin: 0 auto; 
 padding: 10px; 
 background-color: #eee; 
 border: 1px solid #ccc; 
}

Step 1 – Initial Settings and Targets

We start our plugin by assigning a self-invoking function to an aptly named variable in which to hold all our logic, and expose only what we need. Let’s create a moveit.js file and start writing:

var moveit = (function(){ 
  // code here 
})();

Now we want to build and hold our initial settings; we do this using an object literal. We will be using CSS3 transformations to manipulate our target, so we need to establish max and min values for the scale (zooming) and a step (how much we zoom at a time), to decide if transitions are allowed and choose the transition string. We then update moveit.js inside our function with:

var settings = { 
     zoomMin: 0.5, 
     zoomMax: 2, 
     zoomStep: 0.2, // How much one step zooms 
     transitionAllowed: true, 
     transitionString: 'transform 0.4s ease-out' 
};

We also need to hook our target and our button (which we will use to reset the target). Here I decided to go with HTML5 data-attributes for selectors, since I consider them easier to read in the code and less intrusive. Depending on your needs, you might want to go for classes or ids. So we first update our HTML, adding the data-attributes. I went with “data-mi-target” for the target and “data-mi-reset” for the button:

<button data-mi-reset data-mpz-reset>Reset</button> 
<figure class="box"> 
 <img data-mi-target src="funny.jpg" alt="Funny Whale Shark"> 
</figure>

We’re not interested in attribute values, we just want them present. Now let’s select them – update moveit.js, bellow the “settings” variable, like this:

var targetElement = document.querySelector( '[data-mi-target]' ), 
     resetTrigger = document.querySelector( '[data-mi-reset]' );

So far, so good. Before we move on, though, let’s also include our new script in the HTML code, just before the tag:

<script src="moveit.js"></script>

Step 2 – Reference Points and Apply/Get Methods

We will be moving things around, so we need to get some reference points for positions and transformations. Add the following code to moveit.js:

var ref = { // Reference points, null at first
     x: null, // X Axis 
     y: null // Y Axis 
  }; 
var transforms = { // Transforms state – CSS default values 
     scale: 1, 
     translateX: 0, 
     translateY: 0 
 }; 
var intialTransforms = JSON.stringify( transforms ); // Making a copy of the initial state for later

Cool. Now let’s build methods that allow us to override default settings, see the settings object and apply transformations. In moveit.js we write:

var applySettings = function ( newSettings ) { // We want to pass in an object literal for settings 
    if (typeof newSettings === 'object') { // We check our argument is an object 
       Object.keys( newSettings ).forEach( function ( option ) { // Now get new values in a forEach loop 
          settings[ option ] = newSettings[ option ]; 
       } ); 
  } else console.log('Wrong settings type!'); 
}; 
var getSettings = function () { // a simple method to double-check settings, handy for debugging 
     console.log( settings ); 
};

Awesome! Now we need a method to call when we have some transform values and we want to apply them. We do that by going over the “transforms” object after updating it (we will write this later) and translating the objects pairs into CSS3 transforms. Add to moveit.js:

var applyTransforms = function () { 
    targetElement.style.transform = Object.keys( transforms ).map( function ( t ) { 
      return t + '(' + transforms[ t ] + ')'; 
    } ).join( ' ' ); // we loop over “transforms” and build a string which we will pass to the style.transform property 
 };

Step 3 – Zooming, Moving and Reset

It’s time for the good bits. First is zooming – we’ll build a function which will be called with a mouse scroll event, determine the scrolling direction using the events “deltaY” property and apply the transformation using our defined “settings.zoomStep” value. We also need to check min and max zoom values. Add to moveit.js:

var zoom = function ( event ) { 
     event.preventDefault(); // We don’t want scrolling to happen, we just need the scroll direction 

     if ( settings.transitionAllowed ) { // Checking to see if transitions are allowed 
targetElement.style.transition = settings.transitionString; 
     } 

     if ( event.deltaY < 0 && transforms.scale < settings.zoomMax ) { // Negative deltaY means we scrolledup, so we need to zoom in 
transforms.scale += settings.zoomStep; // Add one step of zoom for every scroll 
applyTransforms(); // Apply transforms using new values 

     } else if ( event.deltaY > 0 && transforms.scale > settings.zoomMin ) { // Same as before but with positive deltaY, means we zoom out transforms.scale -= settings.zoomStep; applyTransforms(); 
     } 
};

Ok, let’s tackle moving next. We need multiple methods for this, so let’s start with a function which will take reference points at mouse click. Add to moveit.js:

var mouseDown = function ( event ) { 
    var cx = event.pageX, // Get X coordinate of the click 
        cy = event.pageY; // Get Y coordinate of the click 

    targetElement.style.transition = 'none'; // Remove any present transitions, we don’t want them here 
     ref.x = cx - parseInt( transforms.translateX ) * transforms.scale; // Use the event position to set reference points and 
     ref.y = cy - parseInt( transforms.translateY ) * transforms.scale; // factor in existing transforms (translation and scale) 
     window.addEventListener( 'mousemove', move ); // Now listen for movement 

};

Now we need to create the “move” function, which we attached to the “mousemove” event earlier. This function will track our mouse movement (while holding down the click) and translate it into transformations. Add to moveit.js:

var move = function ( event ) { 
    event.preventDefault(); 
    var cx = event.pageX, 
     cy = event.pageY; // We track our position on screen 

    targetElement.style.pointerEvents = 'none'; // The element we’re moving might react to clicks, so we need to cancel that while moving 
    transforms.translateX = ( cx - ref.x ) / transforms.scale + 'px'; // Calculate axis movement by subtracting the reference positions from 
    transforms.translateY = ( cy - ref.y ) / transforms.scale + 'px'; // our current positions 

    applyTransforms(); // Apply transforms using new values 
 };

Next, we need a function to restore reaction to our element and remove the listener which calls our “move” function. Add to moveit.js:

var mouseUp = function () { 
    targetElement.style.pointerEvents = 'auto'; // Restore pointer events, so the element can react again 
    window.removeEventListener( 'mousemove', move ); // Remove movement listener 
 };

Voila! We have movement. Of course, nothing actually happens yet, because we’re not listening for events, so don’t panic. Next, we need a simple function that will reset our transforms. Add to moveit.js:

var reset = function () {
     if ( settings.transitionAllowed ) { // Transition check
        targetElement.style.transition = settings.transitionString;
     }
     transforms = JSON.parse( intialTransforms ); // Remember that copy of transforms we made at the beginning ?
     applyTransforms();
};

Goody! We’re almost done. All that’s left is listening for events and exposing a couple of our methods, so that we can call them from the outside when needed. First, we check if all the elements we need are present and if they are we add in their listeners. Add to moveit.js:

if ( targetElement ) { // Check if a target exists 
      targetElement.addEventListener( 'wheel', zoom ); // Listen for mouseweheel, and call zoom function 
      targetElement.addEventListener( 'mousedown', mouseDown ); // Listen for mouse down, and call mouseDown function 
     window.addEventListener( 'mouseup', mouseUp ); // Listen for mouse up, and call mouseUp function } 
if ( resetTrigger ) { // Check if a reset button exists 
   resetTrigger.addEventListener( 'click', reset ); 
}

Awesome! Now, if everything was done right, we can move and interact with our element. Lastly, we expose a couple of methods, specifically applySettings snd getSettings, so we can override and check default settings if we need to. Add to moveit.js:

return { 
  applySettings: applySettings, // we’re actually returning references to our methods 
  getSettings: getSettings 
  };

That’s all. Congrats’! You just wrote a plugin to pan and zoom stuff, using the Revealing Module Pattern. If you wish, you can download the whole source here.

This is a very basic starting point and from here on, there’s endless room to expand. If you like, you can check out our GitHub where we’re playing around with it; we added some stuff, and are experimenting with ways to accommodate touchscreens. I hope this was helpful and/or fun for you. Thanks for reading, see you next time!

Author Blog Front-end Developer Antonio Mihut

Toni

Front end developer and UI designer at Fortech, a believer in the harmony between form and function and eternally fascinated with the art of turning coffee into software.

 

Share on FacebookTweet about this on TwitterShare on LinkedInShare on Google+