13.13 Creating a Custom Scrollbar
NN 7, IE 5
13.13.1 Problem
You want to let users scroll
through a separate block of content within the page via a scrollbar
containing line and page regions, as well as a draggable scrollbar
thumb.
13.13.2 Solution
This solution requires numerous HTML elements that are used as both
scrollable content containers and a simulated scrollbar. You can see
the HTML portion in Example 13-7 of the Discussion.
You then use the scrollBars.js library, shown in
Example 13-8 of the Discussion, as the script basis
for both generating and controlling customized scrollbars on your
page.
Your HTML page needs to link in and initialize two JavaScript
libraries: DHTMLAPI.js from Recipe 13.3 and
scrollBars.js. Initializations should go in the
onload event handler of the body:
<body onload="initDHTMLAPI( ); initScrollbars( )">
The initScrolbars(
) function invokes a function that is
not necessarily part of the scrollBars.js
library because it specifies HTML details of each custom scrollbar on
the page. For example, the following initScrollbars(
) function both creates a JavaScript object that governs
the scrollbar associated with a fixed set of HTML elements, and
creates the HTML pieces for the visible scrollbar:
function initScrollbars( ) {
scrollBars[0] = new scrollBar("pseudoWindow", "outerWrapper", "innerWrapper");
scrollBars[0].appendScroll( );
}
...
<body onload="initDHTMLAPI( ); initScrollbars( )">
13.13.3 Discussion
This solution is an extension of Recipe 13.12, but with far more
complex issues involving the dragging of the scrollbar thumb and
synchronizing the scroll of the document with the thumb location. It
also employs dynamic creation of the scrollbar components (consisting
of images and styled div elements) so that precise
positioning isn't necessary: the positioning of
elements depends on the specified dimensions of the content container
and its various style sheet settings (borders, padding, and the
like). Figure 13-4 shows the effect created by this
solution.
Two previous recipes play important roles in this solution. First is
the DHTML API of Recipe 13.3. Second is the element dragging code
from Recipe 13.11. One of the functions, dragIt(
), is tailored to this scrollbar application, so all of the
dragging functions are embedded within the
scrollBars.js file.
The solution begins with the HTML for the pseudowindow container and
its scrollable content, shown in Example 13-7.
Missing is the HTML for the scrollbars themselves because they are
generated by code later.
Example 13-7. HTML scrollbar region awaiting scripted scrollbars
<div id="pseudoWindow0" style="position:absolute; top:350px; left:400px">
<div id="outerWrapper0" style="position:absolute; top:0px; left:0px;
height:150px; width:100px; overflow:hidden; border-top:4px solid #666666;
border-left:4px solid #666666; border-right:4px solid #cccccc;
border-bottom:4px solid #cccccc; background-color:#ffffff">
<div id="innerWrapper0" style="position:absolute; top:0px; left:0px; padding:5px;
font:10px Arial, Helvetica, sans-serif">
<p style="margin-top:0em"> Lorem ipsum dolor sit amet, consectetaur ...</p>
...
</div>
</div>
</div>
Example 13-8 shows the extensive
scrollBars.js library. It is divided into four
sections: Scrollbar Creation, Event Handler Functions, Scrollbar
Tracking, and Element Dragging (for the scrollbar thumb).
Example 13-8. The scrollBars.js library
/***********************
Scrollbar Creation
************************/
// Global variables
var scrollEngaged = false;
var scrollInterval;
var scrollBars = new Array( );
// Utility to retrieve effective style property
function getElementStyle(elemID, IEStyleAttr, CSSStyleAttr) {
var elem = document.getElementById(elemID);
if (elem.currentStyle) {
return elem.currentStyle[IEStyleAttr];
} else if (window.getComputedStyle) {
var compStyle = window.getComputedStyle(elem, "");
return compStyle.getPropertyValue(CSSStyleAttr);
}
return "";
}
// Scrollbar constructor function
function scrollBar(rootID, ownerID, ownerContentID) {
this.rootID = rootID;
this.ownerID = ownerID;
this.ownerContentID = ownerContentID;
this.index = scrollBars.length;
// one-time evaluations for use by other scroll bar manipulations
this.rootElem = document.getElementById(rootID);
this.ownerElem = document.getElementById(ownerID);
this.contentElem = document.getElementById(ownerContentID);
this.ownerHeight = parseInt(getElementStyle(ownerID, "height", "height"));
this.ownerWidth = parseInt(getElementStyle(ownerID, "width", "width"));
this.ownerBorder = parseInt(getElementStyle(ownerID, "borderTopWidth",
"border-top-width")) * 2;
this.contentHeight = Math.abs(parseInt(this.contentElem.style.top));
this.contentWidth = this.contentElem.offsetWidth;
this.contentFontSize = parseInt(getElementStyle(this.ownerContentID,
"fontSize", "font-size"));
this.contentScrollHeight = this.contentElem.scrollHeight;
// create quirks object whose default (CSS-compatible) values
// are zero; pertinent values for quirks mode filled in later
this.quirks = {on:false, ownerBorder:0, scrollBorder:0, contentPadding:0};
if (navigator.appName = = "Microsoft Internet Explorer" &&
navigator.userAgent.indexOf("Win") != -1 &&
(typeof document.compatMode = = "undefined" ||
document.compatMode = = "BackCompat")) {
this.quirks.on = true;
this.quirks.ownerBorder = this.ownerBorder;
this.quirks.contentPadding = parseInt(getElementStyle(ownerContentID,
"padding", "padding"));
}
// determined at scrollbar initialization time
this.scrollWrapper = null;
this.upButton = null;
this.dnButton = null;
this.thumb = null;
this.buttonLength = 0;
this.thumbLength = 0;
this.scrollWrapperLength = 0
this.dragZone = {left:0, top:0, right:0, bottom:0}
// build a physical scrollbar for the root div
this.appendScroll = appendScrollBar;
}
// Create scrollbar elements and append to the "pseudo-window"
function appendScrollBar( ) {
// button and thumb image sizes (programmer customizable)
var imgH = 16;
var imgW = 16;
var thumbH = 27;
// "up" arrow, needed first to help size scrollWrapper
var lineup = document.createElement("img");
lineup.id = "lineup" + (scrollBars.length - 1);
lineup.className = "lineup";
lineup.index = this.index;
lineup.src="scrollUp.gif";
lineup.height = imgH;
lineup.width = imgW;
lineup.alt = "Scroll Up";
lineup.style.position = "absolute";
lineup.style.top = "0px";
lineup.style.left = "0px";
// scrollWrapper defines "page" region color and 3-D borders
var wrapper = document.createElement("div");
wrapper.id = "scrollWrapper" + (scrollBars.length - 1);
wrapper.className = "scrollWrapper";
wrapper.index = this.index;
wrapper.style.position = "absolute";
wrapper.style.visibility = "hidden";
wrapper.style.top = "0px";
wrapper.style.left = this.ownerWidth + this.ownerBorder -
this.quirks.ownerBorder + "px";
wrapper.style.borderTop = "2px solid #666666";
wrapper.style.borderLeft = "2px solid #666666";
wrapper.style.borderRight= "2px solid #cccccc";
wrapper.style.borderBottom= "2px solid #cccccc";
wrapper.style.backgroundColor = "#999999";
if (this.quirks.on) {
this.quirks.scrollBorder = 2;
}
wrapper.style.width = lineup.width + (this.quirks.scrollBorder * 2) + "px";
wrapper.style.height = this.ownerHeight + (this.ownerBorder - 4) -
(this.quirks.scrollBorder * 2) + "px";
// "down" arrow
var linedn = document.createElement("img");
linedn.id = "linedown" + (scrollBars.length - 1);
linedn.className = "linedown";
linedn.index = this.index;
linedn.src="scrollDn.gif";
linedn.height = imgH;
linedn.width = imgW;
linedn.alt = "Scroll Down";
linedn.style.position = "absolute";
linedn.style.top = parseInt(this.ownerHeight) + (this.ownerBorder - 4) -
(this.quirks.ownerBorder) - linedn.height + "px";
linedn.style.left = "0px";
// fixed-size draggable thumb
var thumb = document.createElement("img");
thumb.id = "thumb" + (scrollBars.length - 1);
thumb.index = this.index;
thumb.src="thumb.gif";
thumb.height = thumbH;
thumb.width = imgW;
thumb.alt = "Scroll Dragger";
thumb.style.position = "absolute";
thumb.style.top = lineup.height + "px";
thumb.style.width = imgW + "px";
thumb.style.height = thumbH + "px";
thumb.style.left = "0px";
// fill in scrollBar object properties from rendered elements
this.upButton = wrapper.appendChild(lineup);
this.thumb = wrapper.appendChild(thumb);
this.dnButton = wrapper.appendChild(linedn);
this.scrollWrapper = this.rootElem.appendChild(wrapper);
this.buttonLength = imgH;
this.thumbLength = thumbH;
this.scrollWrapperLength = parseInt(getElementStyle(this.scrollWrapper.id,
"height", "height"));
this.dragZone.left = 0;
this.dragZone.top = this.buttonLength;
this.dragZone.right = this.buttonLength;
this.dragZone.bottom = this.scrollWrapperLength - this.buttonLength -
(this.quirks.scrollBorder * 2)
// all events processed by scrollWrapper element
this.scrollWrapper.onmousedown = handleScrollClick;
this.scrollWrapper.onmouseup = handleScrollStop;
this.scrollWrapper.oncontextmenu = blockEvent;
this.scrollWrapper.ondrag = blockEvent;
// OK to show
this.scrollWrapper.style.visibility = "visible";
}
/***************************
Event Handler Functions
****************************/
// onmouse up handler
function handleScrollStop( ) {
scrollEngaged = false;
}
// Prevent Mac context menu while holding down mouse button
function blockEvent(evt) {
evt = (evt) ? evt : event;
evt.cancelBubble = true;
return false;
}
// click event handler
function handleScrollClick(evt) {
var fontSize, contentHeight;
evt = (evt) ? evt : event;
var target = (evt.target) ? evt.target : evt.srcElement;
target = (target.nodeType = = 3) ? target.parentNode : target;
var index = target.index;
fontSize = scrollBars[index].contentFontSize;
switch (target.className) {
case "lineup" :
scrollEngaged = true;
scrollBy(index, parseInt(fontSize));
scrollInterval = setInterval("scrollBy(" + index + ", " +
parseInt(fontSize) + ")", 100);
evt.cancelBubble = true;
return false;
break;
case "linedown" :
scrollEngaged = true;
scrollBy(index, -(parseInt(fontSize)));
scrollInterval = setInterval("scrollBy(" + index + ", -" +
parseInt(fontSize) + ")", 100);
evt.cancelBubble = true;
return false;
break;
case "scrollWrapper" :
scrollEngaged = true;
var evtY = (evt.offsetY) ? evt.offsetY : ((evt.layerY) ? evt.layerY : -1);
if (evtY >= 0) {
var pageSize = scrollBars[index].ownerHeight - fontSize;
var thumbElemStyle = scrollBars[index].thumb.style;
// set value negative to push document upward
if (evtY > (parseInt(thumbElemStyle.top) +
scrollBars[index].thumbLength)) {
pageSize = -pageSize;
}
scrollBy(index, pageSize);
scrollInterval = setInterval("scrollBy(" + index + ", " +
pageSize + ")", 100);
evt.cancelBubble = true;
return false;
}
}
return false;
}
// Activate scroll of inner content
function scrollBy(index, px) {
var scroller = scrollBars[index];
var elem = document.getElementById(scroller.ownerContentID);
var top = parseInt(elem.style.top);
var scrollHeight = parseInt(elem.scrollHeight);
var height = scroller.ownerHeight;
if (scrollEngaged && top + px >= -scrollHeight + height && top + px <= 0) {
shiftBy(elem, 0, px);
updateThumb(index);
} else if (top + px < -scrollHeight + height) {
shiftTo(elem, 0, -scrollHeight + height - scroller.quirks.contentPadding);
updateThumb(index);
clearInterval(scrollInterval);
} else if (top + px > 0) {
shiftTo(elem, 0, 0);
updateThumb(index);
clearInterval(scrollInterval);
} else {
clearInterval(scrollInterval);
}
}
/**********************
Scrollbar Tracking
***********************/
// Position thumb after scrolling by arrow/page region
function updateThumb(index) {
var scroll = scrollBars[index];
var barLength = scroll.scrollWrapperLength - (scroll.quirks.scrollBorder * 2);
var buttonLength = scroll.buttonLength;
barLength -= buttonLength * 2;
var docElem = scroll.contentElem;
var docTop = Math.abs(parseInt(docElem.style.top));
var scrollFactor = docTop/(scroll.contentScrollHeight - scroll.ownerHeight);
shiftTo(scroll.thumb, 0, Math.round((barLength - scroll.thumbLength) *
scrollFactor) + buttonLength);
}
// Position content per thumb location
function updateScroll( ) {
var index = selectedObj.index;
var scroller = scrollBars[index];
var barLength = scroller.scrollWrapperLength - (scroller.quirks.scrollBorder * 2);
var buttonLength = scroller.buttonLength;
var thumbLength = scroller.thumbLength;
var wellTop = buttonLength;
var wellBottom = barLength - buttonLength - thumbLength;
var wellSize = wellBottom - wellTop;
var thumbTop = parseInt(getElementStyle(scroller.thumb.id, "top", "top"));
var scrollFactor = (thumbTop - buttonLength)/wellSize;
var docElem = scroller.contentElem;
var docTop = Math.abs(parseInt(docElem.style.top));
var scrollHeight = scroller.contentScrollHeight;
var height = scroller.ownerHeight;
shiftTo(scroller.ownerContentID, 0, -(Math.round((scrollHeight - height) *
scrollFactor)));
}
/*******************
Element Dragging
********************/
// Global holds reference to selected element
var selectedObj;
// Globals hold location of click relative to element
var offsetX, offsetY;
var zone = {left:0, top:16, right:16, bottom:88};
// Set global reference to element being engaged and dragged
function setSelectedElem(evt) {
var target = (evt.target) ? evt.target : evt.srcElement;
target = (target.nodeType && target.nodeType = = 3) ? target.parentNode : target;
var divID = (target.id.indexOf("thumb") != -1) ? target.id : "";
if (divID) {
if (document.layers) {
selectedObj = document.layers[divID];
} else if (document.all) {
selectedObj = document.all(divID);
} else if (document.getElementById) {
selectedObj = document.getElementById(divID);
}
setZIndex(selectedObj, 100);
return;
}
selectedObj = null;
return;
}
// Drag thumb only within scrollbar region
function dragIt(evt) {
evt = (evt) ? evt : event;
var x, y, width, height;
if (selectedObj) {
if (evt.pageX) {
x = evt.pageX - offsetX;
y = evt.pageY - offsetY;
} else if (evt.clientX || evt.clientY) {
x = evt.clientX - offsetX;
y = evt.clientY - offsetY;
}
var index = selectedObj.index;
var scroller = scrollBars[index];
var zone = scroller.dragZone;
width = scroller.thumb.width;
height = scroller.thumb.height;
x = (x < zone.left) ? zone.left : ((x + width > zone.right) ?
zone.right - width : x);
y = (y < zone.top) ? zone.top : ((y + height > zone.bottom) ?
zone.bottom - height : y);
shiftTo(selectedObj, x, y);
updateScroll( );
evt.cancelBubble = true;
return false;
}
}
// Turn selected element on and set cursor offsets
function engage(evt) {
evt = (evt) ? evt : event;
setSelectedElem(evt);
if (selectedObj) {
if (document.body && document.body.setCapture) {
// engage event capture in IE/Win
document.body.setCapture();
}
if (evt.pageX) {
offsetX = evt.pageX - ((typeof selectedObj.offsetLeft != "undefined") ?
selectedObj.offsetLeft : selectedObj.left);
offsetY = evt.pageY - ((selectedObj.offsetTop) ?
selectedObj.offsetTop : selectedObj.top);
} else if (typeof evt.clientX != "undefined") {
offsetX = evt.clientX - ((selectedObj.offsetLeft) ?
selectedObj.offsetLeft : 0);
offsetY = evt.clientY - ((selectedObj.offsetTop) ?
selectedObj.offsetTop : 0);
}
return false;
}
}
// Turn selected element off
function release(evt) {
if (selectedObj) {
setZIndex(selectedObj, 0);
if (document.body && document.body.releaseCapture) {
// stop event capture in IE/Win
document.body.releaseCapture();
}
selectedObj = null;
}
}
// Assign event handlers used by both Navigator and IE
function initDrag( ) {
if (document.layers) {
// turn on event capture for these events in NN4 event model
document.captureEvents(Event.MOUSEDOWN | Event.MOUSEMOVE | Event.MOUSEUP);
return;
} else if (document.body & document.body.addEventListener) {
// turn on event capture for these events in W3C DOM event model
document.addEventListener("mousedown", engage, true);
document.addEventListener("mousemove", dragIt, true);
document.addEventListener("mouseup", release, true);
return;
}
document.onmousedown = engage;
document.onmousemove = dragIt;
document.onmouseup = release;
return;
}
The code begins by defining some scrollbar and scroll action global
variables. A supporting function, getElementStyle(
) from Recipe 11.12, is defined for use of the scrollbar
creation routines later.
The scrollBar( ) constructor function for
the scrollbar objects receives three string parameters: the IDs for
the div that holds the content and scrollbar
(informally referred to here as the root container), the
content's outer wrapper div (the
content div's owner), and the
content inner wrapper (the owner content). The purpose of this
constructor is to perform some one-time calculations and
initializations per scrollbar (multiple scrollbars per page are
allowed), facilitating several possible scrollbar actions later on.
To help the buttons' event handlers know which set
of scrollers is operating, an index value, corresponding to the
position within the scrollBars array, is assigned
to the index properties of the two button
elements. The scrollBars.length value represents
the numeric index of the scrollBars item being
generated because the scrollBars array has not yet
been assigned the finished object, meaning that the array length is
one less than it will be after the object finishes its construction.
Numerous properties of each scrollBar object
don't receive their active values until the function
that creates the physical scrollbar executes. There is also a section
in the constructor that concerns itself with browsers operating in
quirks (i.e., non-CSS- compliant) mode, such as IE 5 and 5.5. When
element dimensions affect element positioning, factors such as
borders and padding are treated very differently in quirks and
CSS-compatibility modes. Another property of this object,
dragZone, is eventually used to guide the dragging
of the thumb image to keep it restricted to the space within the
scrollbar.
The next function, appendScrollBar(
), is a monster. It could be easily
broken into multiple pieces, but the structure is simple enough to
follow whole, as it assembles the DOM objects for the physical
scrollbar. As relevant values become available, they are assigned to
the abstract scrollBar object's
properties created in the constructor function. The physical
scrollbar consists of one scroll wrapper (which also serves as the
background grey region for the scrollbar) and three
img elements: clickable line-up and line-down
buttons and the draggable thumb. Mouse-related event handlers are
assigned to the scrollbar wrapper to process events from any of the
components within the scrollbar. The scrollbar is initially created
invisibly, and then shown at the end to overcome a rendering bug in
IE/Windows that otherwise positions the scrollbar errantly.
The next section of the library contains the event handler functions.
All that handleScrollStop(
) does is turn off the flag that other
functions use to permit repeated scrolling, while
blockEvent( ) stops the
oncontextmenu event from carrying out its default
action on the Macintosh. Event processing for clicks on the arrow
images or in the page-up and page-down regions of the scrollbar is
managed by the handleScrollClick(
) function. The function provides three
branches for calculating the distance that scrolling is to jump in
response to the click. Negative values move the content document
upward. The trickiest part of this function is calculating whether
the click on the scroll wrapper is above or below the thumb to reach
the appropriate scroll direction (in the
"scrollWrapper" case).
Compared to the simple scroll buttons of Recipe 13.12, this
recipe's version of the scrollBy(
) function has more to worry about. When the user clicks an
arrow or page area of the scrollbar, not only must the document
scroll, but the thumb image must also move into a position that
corresponds to the scrolled percentage of the document. Thus, after
each invocation of the DHTML API's shiftBy(
) or shiftTo( ) function, the
updateThumb( ) function gets a call. Notice that
there are two extra branches of the scrollBy( )
function. They take care of the cases when the user clicks on the
page regions and there is less than a full page to go. The result of
the two extra branches forces the scroll (and thumb) to the top or
bottom of the range, depending on direction.
The third section of the library is devoted to keeping the scrolled
content and thumb position synchronized with each other. To keep the
thumb image in sync with the scrolled position of the document, the
updateThumb( ) function calculates the proportion of
document scrolled upward and applies that proportion to the position
of the thumb element within the scroll wrapper element (offset by the
up button image). Conversely, the updateScroll( )
function adjusts the scrolled position of the content to represent
the same proportion as the location of the thumb along the scrollbar
while the user drags the thumb. The known value (after some
calculations involving the current size of the scrollbar and related
images) is the proportion of the thumb along the area between scroll
buttons (the well). That factor is applied to the scroll
characteristics of the content document. As the user slides the thumb
up or down, the document scrolls in real time.
The last library code section contains functions needed for dragging
the thumb. The basics of element dragging in Recipe 13.11 carry over
to the scrollbar thumb-dragging operation here. Although we use the
same event handler assignments and three primary functions
(engage( ), dragIt( ), and
release( )), a couple of items are modified to
work specifically within this specialized scrollbar environment. The
setSelectedElem(
) function (invoked by engage(
)) is modified slightly to respond to elements whose IDs
contain the string "thumb".
Although the scrollbar doesn't work with Version 4
browsers, the dragging library code that supports those browsers is
left in place.
The biggest modifications apply to the dragIt( )
function. Shown in boldface in Example 13-8, these
changes deal primarily with restricting the drag of the thumb image
within the vertical travel of the scrollbar, and preventing the thumb
from exceeding the area between the scroll buttons. Values
controlling the boundaries are set in the zone
global object variable. If you use different scrollbar designs and
sizes, you'll need to modify these object properties
to fit your elements. The revised dragIt( )
function also invokes the updateScroll( )
function, which synchronizes the scroll of the content with the
position of the thumb.
To create the scrollbar and prepare it for user interaction,
initialize the process by calling the two key functions: the
scrollBar( ) constructor function (passing IDs of
the three hardwired HTML components shown at the beginning of this
solution), and the appendScroll( ) function.
Activate these functions from the onload event
handler after initializing the DHTML API.
The design of the scrollbar shown in Figure 13-4 is
very traditional. You're not limited to that style
by any means. You might, for example, elect to eliminate the buttons,
and include only a highly stylized slider to control scrolling. Or
you could get more platform-specific, and include art that more
closely resembles the user's operating system.
Native scrollbars look very different in the Windows
9x, Windows XP, Mac OS 9, and Mac OS X
environments.
There is another factor to consider: Mac OS 9 and later displays
scrollbar buttons together at the bottom of the scrollbar, rather
than split to the top and bottom. Not that Mac users
wouldn't know how to operate the split button kind
of scrollbar, but the split design may not feel natural to users who
are accustomed to the newer scrollbar interface. The code in the
solution could be modified to produce a scrollbar with the buttons
together. This change impacts a lot of things, particularly the
positioning of the thumb, but it is possible to branch your code (or
perhaps load the scrollbar as separate external
.js libraries) for the main operating systems.
Horizontal scrolling is not addressed in this recipe. If you need to
scroll horizontally, you need to make several modifications to the
code. Look first to all invocations of the scrollBy(
), shiftBy( ), and shiftTo(
) functions, which need to swap their parameters so that
the y axis values are zero, and the x axis values are the ones that
change. Dimensions of key elements, such as the scroll wrapper and
content holders, need to focus on their widths, rather than heights.
Fortunately, all of the technicalities of working in quirks mode
apply directly to horizontal measures as well as to vertical
measures, so you won't have to delve deeply in those
parts of the code.
This scrollbar recipe is among the most code-intensive applications
in this book. Yet it builds upon foundations from other recipes
without reinventing infrastructure wheels (especially the DHTML- and
element-dragging APIs). It also demonstrates that, at least for
modern browsers, you can accomplish quite a lot from the user
interface realm, even in the otherwise ordinary published document
model.
13.13.4 See Also
Recipe 13.3 for the vital DHTML API library; Recipe 13.11 for the
element-dragging routines; Recipe 13.12 for scrolling only with
buttons rather than a complete scrollbar; Recipe 11.12 for details on
the getElementStyle( ) utility function.
|