Javascript Tutorial - Trackbar Component

Skill

Javascript Tutorial - Trackbar Component

Posted in:

So continuing on our current kick of making random UI components in javascript and html, today we are going to look at making a trackbar. The look and functionality is based off of the Windows common controls trackbar - although it is missing some of the features (such as tickmarks). Perhaps I'll add those in a future tutorial. As a side note, this tutorial uses a chunk of code developed in a previous tutorial, Draggable Elements. If you have not read that tutorial previously, you should take a look at it.

So here we have a trackbar, which you can slide around at will. The trackbar value texbox will display the current value of the trackbar, and if you enter in a number, the trackbar pointer will move to the correct place. The trackbar width field lets you, as you might expect, set the width of the trackbar (down to a minimum width of 15 pixels). The min and max textboxes set the minimum and maximum values for the trackbar - the code uses these values to scale the position of the pointer to a value within the max-min range (as you would expect from a regular trackbar).


Trackbar Value: Trackbar Width:
Trackbar Min: Trackbar Max:

So today, we are going to take a different approach then usual. Instead of diving straight into the trackbar code, itself, we are going to take a look at the code to actually add and use the trackbar first. This is because I took the tact of "containerizing" the trackbar code this time (unlike many of the previous javascript UI tutorials). What this means is that the code to add and use the trackbar is really simple:

var trackbar = new Trackbar(-100, 100, 500, trackBarChange);
var holder = document.getElementById("trackbarHolder");
holder.appendChild(trackbar.GetContainer());
trackbar.StartListening();

Above is the simplest way to create and add this trackbar component to an html page. First, we create the trackbar, which takes 4 arguments. The first two are the minimum and maximum values for the trackbar. The third is the width of the trackbar, and the fourth is the callback function that you want to get called when the trackbar's value changes. In the second line, we add the trackbar to the document - in this case, there is a div somewhere called trackbarHolder, and we are appending the trackbar to it. Now, the trackbar object itself is not a html element, so it is not something we can add directly to the page. But the trackbar object can return the html element that represents the trackbar - that is what the GetContainer() call does. Finally, we tell the trackbar to start listening - i.e., the user can now click and move the pointer on the trackbar.

Later on, we will take a look at the exact code to replicate the example above - but really, it is not much more than the code just shown. So now that we have an idea of how to use it, lets take a look at the external appearance of the trackbar object:

function Trackbar(min, max, width, callback)
{
  this.GetContainer = function()

  this.GetCurrentValue = function()

  this.GetMaxValue = function()

  this.GetMinValue = function()
 
  this.GetWidth = function()
   
  this.SetCurrentValue = function(value)

  this.SetMaxValue = function(value)
 
  this.SetMinValue = function(value)
 
  this.SetWidth = function(value)
 
  this.SetCallback = function(newCallback)
 
  this.StartListening = function()
 
  this.StopListening = function()
}

The 'get' functions are extremely simple, as well as the SetCallback function, so I'm just going to throw them all out right here:

this.GetContainer = function()
{ return _container; }

this.GetCurrentValue = function()
{ return _currentValue; }

this.GetMaxValue = function()
{ return _maximumVal; }
 
this.GetMinValue = function()
{ return _minimumVal; }
 
this.GetWidth = function()
{ return _width; }

this.SetCallback = function(newCallback)
{ _callback = newCallback; }

Not particularly surprising - they all just reference private member variables and return their values (except for SetCallback, which sets a private variable). So now lets take a look at some of the more complex 'setters':

this.SetMaxValue = function(value)
{
   value = parseFloat(value);
   if(isNaN(value))
     value = 1;
   _maximumVal = value;
     
  UpdatePointerPos();
}
 
this.SetMinValue = function(value)
{
   value = parseFloat(value);
   if(isNaN(value))
     value = 0;
   _minimumVal = value;
   
  UpdatePointerPos();
}

Here we have the code for setting the maximum and minimum values of the trackbar. Pretty simple, we parse the value passed in as a float, and if it ends up being not a number, we assume that the value is 0. Then we set the appropriate private member variable to that new value. Finally, we call the function UpdatePointerPos. What does this function do, you ask? Well, take a look and see:

function UpdatePointerPos()
{
  if(_currentValue < _minimumVal)
    _currentValue = _minimumVal;
  if(_currentValue > _maximumVal)
    _currentValue = _maximumVal;
 
  if(_maximumVal != _minimumVal)
  {
    _pointer.style.left = (((_currentValue -  _minimumVal)
      / (_maximumVal - _minimumVal)) * _width - 4) + 'px';
  }
  else
  {
    _pointer.style.left = '0px';
  }
}

This is a private function inside of the trackbar object, and is used to calculate the appropriate position of the pointer. First, we constrain the stored current value, in case the minimum or maximum value has changed, and the old current value now lies outside of those bounds. If the min and max value are equal, we just set the pointer position to 0, because it doesn't really matter where the pointer is in that case. Otherwise, we calculate the ratio of how far along the trackbar the pointer should be, and then multiply that by the width to get the pixel position of the center of the pointer. Then we subtract 4 (the pointer offset, half the width of the pointer), since we are setting the position of the pointer's left-most edge. This function is also called when the function SetCurrentValue is called:

this.SetCurrentValue = function(value)
{
  value = parseFloat(value);
  if(isNaN(value))
    value = 0;
  _currentValue = value;
   
  UpdatePointerPos();
}

Very similar to SetMinValue and SetMaxValue, the only difference being that this function changes the _currentValue variable.

The final 'set' function, SetWidth, is a little bit more complicated:

this.SetWidth = function(value)
{
  value = parseInt(value);
  if(isNaN(value))
    value = 100;
  if(value < 15)
    value = 15;
   
  _width = value;
   
  _bar.style.width = (_width - 3) + 'px';
  _rightCap.style.left = (_width-2) + 'px';
  _pointerUB.X = _width-4;
  _container.style.width = _width + 'px';

  UpdatePointerPos();
}

Starts off the same as the other setters, although this time we parse to an integer, not to a floating point value. If it is now a number, we assume that the desired with is actualy 100 pixels, and if the value is less than 15, we set the value to 15. We the set the variable _width, and go about updating a number of things.

So here we have a bunch of private variables that we have not run across yet. First, there is _bar. This is a div that represents the main part of the trackbar line, and is always supposed to be _width-3 pixels long (which is what we are setting the width to in this function). Next, there is the _rightCap. This is the div at the right end of the trackbar, and is always 2 pixels wide. But when the width changes, we need to set its correct position, which is _width-2 pixels.

Then we have _pointerUB. This is a position object (if you don't know what a position object is, you should go read Draggable Elements), and represents the upper bound on the trackbar pointer's position. This always needs to be _width-4, so that the pointer can only move to the point where the center of the pointer is at the right edge of the trackbar.

The final thing set here is the width of the trackbar container - and that width is always identical to the value of _width. And of course, we call UpdatePointerPos, to move the pointer to the correct spot now that the trackbar's width has changed.

Now that youve seen all those private variables, you are probably wondering how they get initialized. Wonder no more, and take a look at the code below (which is all the initialization code for the trackbar object):

function Trackbar(min, max, width, callback)
{
  var _minimumVal = min;
  var _maximumVal = max;
  var _width = width;
  var _callback = callback;
  var _currentValue =  (0.5 * (_maximumVal - _minimumVal)) + _minimumVal;
 
  var _container = document.createElement("DIV");
  _container.style.position = 'relative';
  _container.style.height = '17px';
  _container.style.width = _width + 'px';
  _container.style.fontSize = '1px';
 
  var _leftCap = document.createElement("DIV");
  _leftCap.style.backgroundImage = 'url(leftCap_1x4.jpg)';
  _leftCap.style.position = 'absolute';
  _leftCap.style.height = '4px';
  _leftCap.style.width = '1px';
  _leftCap.style.left = '0px';
  _leftCap.style.top = '7px';
 
  var _bar = document.createElement("DIV");
  _bar.style.backgroundImage = 'url(repeater_1x4.jpg)';
  _bar.style.position = 'absolute';
  _bar.style.height = '4px';
  _bar.style.width = (_width - 3) + 'px';
  _bar.style.left = '1px';
  _bar.style.top = '7px';
 
  var _rightCap = document.createElement("DIV");
  _rightCap.style.backgroundImage = 'url(rightCap_2x4.jpg)';
  _rightCap.style.position = 'absolute';
  _rightCap.style.height = '4px';
  _rightCap.style.width = '2px';
  _rightCap.style.left = (_width-2) + 'px';
  _rightCap.style.top = '7px';
 
  var _pointer = document.createElement("DIV");
  _pointer.style.backgroundImage = 'url(pointer_9x17.gif)';
  _pointer.style.position = 'absolute';
  _pointer.style.height = '17px';
  _pointer.style.width = '9px';
  _pointer.style.top = '0px';
 
  _container.appendChild(_leftCap);
  _container.appendChild(_bar);
  _container.appendChild(_rightCap);
  _container.appendChild(_pointer);
 
  var _pointerLB = new Position(-4, 0);
  var _pointerUB = new Position(_width-4, 0);
 
  var _pointerDrag = new dragObject(_pointer, _container,
       _pointerLB, _pointerUB, OnDragBegin, OnDrag, null, true);
 
  UpdatePointerPos();
}

The large majority of that code is creating the html elements that make up the trackbar. There is the _container, the _leftCap, the _bar, the _rightCap, and the _pointer. They all need their width/height/top/left values set, and most get background images. The reason for having the left and the right cap is that the images on the left and the right are slightly different than the repeater that makes up the majority of the bar - they literally cap the bar. Once we have created all these elements, we append them to the container.

The top couple lines are just setting some member variables to the values passed in, except for _currentValue. For that, we calculate the intial value by figuring out the halfway point between the given minimum and maximum values. Down at the bottom, we set up the upper and lower bounds for the pointer, taking into account the width of the pointer and the fact that we want the center of the pointer to reach both edges of the trackbar.

Next, we create a dragObject for the pointer. We pass in the _container as the handle, which means that clicking anywhere on the trackbar will start a pointer drag operation. We pass in the lower and upper bounds, as well as callback functions for when a drag begins and when the pointer is moved during a drag. We don't need a callback for when a drag ends, so we pass in null. Finally, we pass in true as the last argument, because we don't want the dragObject to start listening for dragging yet. If you would like more information on how the dragObject works, you can read the Draggable Elements tutorial.

And the final thing we do during initialization is move the pointer to its initial correct position.

Ok, so we introduced two more functions that have not yet been defined in the previous code - OnDragBegin and OnDrag. Time to define them:

function OnDragBegin(eventObj, element)
{
  var pos = getMousePos(eventObj);

  var target = getEventTarget(eventObj);
  if(target == _pointer)
    pos.X += parseInt(_pointer.style.left);
  else if(target == _rightCap)
    pos.X += _width-1;

  pos.X -= 4;

  pos = pos.Bound(_pointerLB, _pointerUB);

  pos.Apply(_pointer);

  OnDrag(pos);
}
 
function OnDrag(newPos, element)
{
  newPos.X += 4;
  _currentValue = Math.round(1000 * (((newPos.X/_width) *
       (_maximumVal - _minimumVal)) + _minimumVal))/1000;
  if(_callback != null)
    _callback(_currentValue);
}

The goal of the first function is to move the pointer to where the user clicked before dragging begins. Here we are using some more functions from that draggable elements tutorial (have I linked to it enough yet? :P ), such as getMosePosition. It does what you might expect - it returns a Position object with the current mouse position. Now, this position is relative to the element clicked on, and we want the position relative to the container. So if the user clicked on the pointer itself, we add the current position of the pointer to the value, and if it was the _rightCap clicked on, we add _width-1. Then, right after that, we subtract 4 (half the pointer width) so that this value represents the left edge of the pointer. We then bound the value by the upper and lower bounds of the pointer, and apply the new position to the pointer. Finally, we call OnDrag with the new position, because the code in OnDrag will calculate the new _currentValue.

The function OnDrag gets called everytime the pointer is moved during a drag. The new position of the pointer is passed in every time. First, we modify that position by adding 4 (to get the center of the pointer), and then we calculate the new _currentValue by using the ratio of where the pointer is compared to the width of the trackbar. The weird thing done here is the multiplication and division by 1000. This is because I want the return value rounded to three decimal places, but the javascript round function does not take a 'precision' value. So instead, I multiply by 1000, round the value, and then divide by 1000. Now, if there is a callback function to call, we call it with the new current value as the argument.

And finally, the last two functions for the trackbar object:

this.StartListening = function()
{ _pointerDrag.StartListening(); }
 
this.StopListening = function()
{ _pointerDrag.StopListening(); }

These two functions just turn on and off the drag listening. As you saw way far above, you need to call StartListening when you want to start using the trackbar. The reason that the trackbar doesn't start of listening is that the drag object can't attach the events appropriately until the trackbar is actually added to the document. So don't call StartListening before you add the trackbar container to the document somewhere, because otherwise the trackbar won't work.

And here we go, all the trackbar code in one big chunk:

function Trackbar(min, max, width, callback)
{
  var _minimumVal = min;
  var _maximumVal = max;
  var _width = width;
  var _callback = callback;
  var _currentValue =  (0.5 * (_maximumVal - _minimumVal)) + _minimumVal;
 





  var _container = document.createElement("DIV");
  _container.style.position = 'relative';
  _container.style.height = '17px';
  _container.style.width = _width + 'px';
  _container.style.fontSize = '1px';
 
  var _leftCap = document.createElement("DIV");
  _leftCap.style.backgroundImage = 'url(leftCap_1x4.jpg)';
  _leftCap.style.position = 'absolute';
  _leftCap.style.height = '4px';
  _leftCap.style.width = '1px';
  _leftCap.style.left = '0px';
  _leftCap.style.top = '7px';
 
  var _bar = document.createElement("DIV");
  _bar.style.backgroundImage = 'url(repeater_1x4.jpg)';
  _bar.style.position = 'absolute';
  _bar.style.height = '4px';
  _bar.style.width = (_width - 3) + 'px';
  _bar.style.left = '1px';
  _bar.style.top = '7px';
 
  var _rightCap = document.createElement("DIV");
  _rightCap.style.backgroundImage = 'url(rightCap_2x4.jpg)';
  _rightCap.style.position = 'absolute';
  _rightCap.style.height = '4px';
  _rightCap.style.width = '2px';
  _rightCap.style.left = (_width-2) + 'px';
  _rightCap.style.top = '7px';
 
  var _pointer = document.createElement("DIV");
  _pointer.style.backgroundImage = 'url(pointer_9x17.gif)';
  _pointer.style.position = 'absolute';
  _pointer.style.height = '17px';
  _pointer.style.width = '9px';
  _pointer.style.top = '0px';
 
  _container.appendChild(_leftCap);
  _container.appendChild(_bar);
  _container.appendChild(_rightCap);
  _container.appendChild(_pointer);
 
  var _pointerLB = new Position(-4, 0);
  var _pointerUB = new Position(_width-4, 0);
 
  var _pointerDrag = new dragObject(_pointer, _container,
       _pointerLB, _pointerUB, OnDragBegin, OnDrag, null, true);
 
  UpdatePointerPos();
 
  function OnDragBegin(eventObj, element)
  {
    var pos = getMousePos(eventObj);

    var target = getEventTarget(eventObj);
    if(target == _pointer)
      pos.X += parseInt(_pointer.style.left);
    else if(target == _rightCap)
      pos.X += _width-1;

    pos.X -= 4;

    pos = pos.Bound(_pointerLB, _pointerUB);

    pos.Apply(_pointer);

    OnDrag(pos);
  }
 
  function OnDrag(newPos, element)
  {
    newPos.X += 4;
    _currentValue = Math.round(1000 * (((newPos.X/_width) *
        (_maximumVal - _minimumVal)) + _minimumVal))/1000;
    if(_callback != null)
      _callback(_currentValue);
  }
 
  function UpdatePointerPos()
  {
    if(_currentValue < _minimumVal)
      _currentValue = _minimumVal;
    if(_currentValue > _maximumVal)
      _currentValue = _maximumVal;
 
    if(_maximumVal != _minimumVal)
    {
      _pointer.style.left = (((_currentValue -  _minimumVal)
        / (_maximumVal - _minimumVal)) * _width - 4) + 'px';
    }
    else
    {
      _pointer.style.left = '0px';
    }
  }
 
  this.GetMaxValue = function()
  { return _maximumVal; }
 
  this.GetMinValue = function()
  { return _minimumVal; }
 
  this.GetCurrentValue = function()
  { return _currentValue; }
 
  this.GetWidth = function()
  { return _width; }
   
  this.SetMaxValue = function(value)
  {
     value = parseFloat(value);
     if(isNaN(value))
       value = 1;
     _maximumVal = value;
     
    UpdatePointerPos();
  }
 
  this.SetMinValue = function(value)
  {
     value = parseFloat(value);
     if(isNaN(value))
       value = 0;
     _minimumVal = value;
     
    UpdatePointerPos();
  }
 
  this.SetCurrentValue = function(value)
  {
    value = parseFloat(value);
    if(isNaN(value))
      value = 0;
    _currentValue = value;
   
    UpdatePointerPos();
  }
 
  this.SetWidth = function(value)
  {
    value = parseInt(value);
    if(isNaN(value))
      value = 100;
    if(value < 15)
      value = 15;
   
    _width = value;
   
    _bar.style.width = (_width - 3) + 'px';
    _rightCap.style.left = (_width-2) + 'px';
    _pointerUB.X = _width-4;
    _container.style.width = _width + 'px';
    UpdatePointerPos();
  }
 
  this.GetContainer = function()
  { return _container; }
 
  this.SetCallback = function(newCallback)
  { _callback = newCallback; }
 
  this.StartListening = function()
  { _pointerDrag.StartListening(); }
 
  this.StopListening = function()

  { _pointerDrag.StopListening(); }
}

So I promised to go over the code used for the example above. Well, it isn't much more complicated than the very simple code for just adding the trackbar, but here you go:

<div style="position:relative;height:200px;width:530px;" id="trackbarHolder">

<table cellspacing="5">
<tr>
  <td>Trackbar Value:</td>
  <td>
    <input id="trackVal" type="text" onchange="trackBoxChange();" value="0" />
  </td>
  <td>Trackbar Width:</td>
  <td>
    <input id="trackWidth" type="text" onchange="trackBoxWidthChange();" value="200" />
  </td>
</tr>
<tr>
  <td>Trackbar Min:</td>
  <td>
    <input id="trackMin" type="text" onchange="trackBoxMinChange();" value="-100" />
  </td>
  <td>Trackbar Max:</td>
  <td>
    <input id="trackMax" type="text" onchange="trackBoxMaxChange();" value="100" />
  </td>
</tr>
</table>
</div>

Not much there, just a div called "trackbarHolder" holding a table with some text input fields. So now for the javascript:

var trackbar = new Trackbar(-100, 100, 500, trackBarChange);

trackbar.GetContainer().style.position = 'absolute';
trackbar.GetContainer().style.top = '150px';
trackbar.GetContainer().style.left = '15px';

var holder = document.getElementById("trackbarHolder");
holder.appendChild(trackbar.GetContainer());
trackbar.StartListening();

document.getElementById("trackVal").value = trackbar.GetCurrentValue();
document.getElementById("trackWidth").value = trackbar.GetWidth();
document.getElementById("trackMax").value = trackbar.GetMaxValue();
document.getElementById("trackMin").value = trackbar.GetMinValue();

So there is a little bit more code here than above, but that is mostly because we are setting the position of the trackbar. Aslo, we are getting the trackbar values for min, max, width, and current value, and setting the contents of the textboxes to them.

Next we need to define the callback function we passed into the trackbar:

function trackBarChange(val)
{
  document.getElementById("trackVal").value = val;
}

Simple, eh?

And finally, the functions that get called when the contents of the textboxes change:

function trackBoxChange()
{
  trackbar.SetCurrentValue(document.getElementById("trackVal").value);
  document.getElementById("trackVal").value = trackbar.GetCurrentValue();
}

function trackBoxWidthChange()
{
  trackbar.SetWidth(document.getElementById("trackWidth").value);
  document.getElementById("trackWidth").value = trackbar.GetWidth();
}

function trackBoxMinChange()
{
  trackbar.SetMinValue(document.getElementById("trackMin").value);
  document.getElementById("trackVal").value = trackbar.GetCurrentValue();
  document.getElementById("trackMin").value = trackbar.GetMinValue();
}

function trackBoxMaxChange()
{
  trackbar.SetMaxValue(document.getElementById("trackMax").value);
  document.getElementById("trackVal").value = trackbar.GetCurrentValue();
  document.getElementById("trackMax").value = trackbar.GetMaxValue();
}

Pretty much, in each one of these, we first update trackbar with the value changed by the textbox. Then, because the trackbar will not necessarily use the value given (say we passed in a width of less than 15), we ask the trackbar for the value and update the textbox. And there you go!

So if you would like to play around or use this trackbar, feel free to download the code from the links below. And, as usual, any questions or comments are welcome.

Charles
11/30/2007 - 13:11

I get a javascript error: Line 137, char: 3, Error: 'pointerOffset' is undefined

I suppose the above error is why I can't drag the trackbar with my mouse.

Otherwise the code looks pretty neat.

reply

The Tallest
11/30/2007 - 16:46

Whoops! Apparently when we put the resizeable comment box on the blog, I forgot to make sure that the code didn't conflict with some of our older draggable element code. Its fixed now - sorry about that.

reply

Kyle S
01/21/2008 - 14:40

Your trackbar code seems to conflict with other elements on the page causing a terrible flickering in Netscape 7.1. (I use IE7, but cross-compatibility is a huge concern.)

reply

Zhouqm27
08/29/2008 - 08:15

excellent! I use it in my project!

reply

Add Comment

Put code snippets inside language tags:
[language] [/language]

Examples:
[javascript] [/javascript]
[actionscript] [/actionscript]
[csharp] [/csharp]

See here for supported languages.

Javascript must be enabled to submit anonymous comments - or you can login.

Sponsors