13.12 Scrolling div Content
NN 7, IE 5
13.12.1 Problem
You want to let users scroll up
and down through content located in a separate positioned viewable
area on the page, without resorting to system (overflow) scrollbars.
13.12.2 Solution
This solution requires some HTML elements that are used as both
scrollable content containers and the buttons that control the
scrolling. You can see the HTML portion in Example 13-5 of the Discussion. You then use the
scrollButtons.js library, shown in Example 13-6 of the Discussion, as the script basis for
controlling scrollable regions on your page.
Your HTML page needs to link in and initialize two JavaScript
libraries: DHTMLAPI.js from Recipe 13.3 and
scrollButtons.js. Initializations should go in
the onload event handler of the body:
<body onload="initDHTMLAPI( ); initScrollers( )">
The initScrollers(
) function invokes a function that is
not necessarily part of the scrollButtons.js
library because it specifies HTML details of each instance of a
scrollable region on the page. For example, the following
initScrollers( ) function creates a JavaScript
object that governs the scrolling activity for one region:
function initScrollers( ) {
scrollBars[0] = new scrollBar("outerWrapper0", "innerWrapper0", "lineup0",
"linedown0");
scrollBars[0].initScroll( );
}
13.12.3 Discussion
The vital HTML portion of this recipe is shown in
Example 13-5. The scrolling region consists of a
series of nested div elements. The content
container is a pair of nested containers. The outer wrapper defines
the rectangular boundaries of the viewport through which the content
is visible, while the second positioned container,
innerWrapper0, holds the actual content to scroll.
The trailing number of the IDs in this example helps illustrate that
you can have multiple scrolling regions on the same page, and they
will not collide as long as you use unique IDs for the components.
Example 13-5. HTML scrolling region and controller
<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>
<img id="lineup0" class="lineup" src="scrollUp.gif" height="16" width="16"
alt="Scroll Up" style="position:absolute; top:10px; left:112px" />
<img id="linedown0" class="linedown" src="scrollDn.gif" height="16" width="16"
alt="Scroll Down" style="position:absolute; top:128px; left:112px" />
</div>
Buttons that perform the scrolling (vertical scrolling in this case)
are simple img elements. The arrow
img elements are absolute-positioned within the
context of the outermost div element
(pseudoWindow0). If some other scripting needs to
move the outermost div element, the buttons keep
their positions relative to the whole set of components. Figure 13-3 illustrates the look of the scrolling
pseudowindow assembly based on the HTML of Example 13-5.
The job of the associated script library,
scrollButtons.js (shown in Example 13-6), is to slide the inner wrapper element up or
down. Its content is clipped by the outer wrapper element, whose
overflow style property is set to
hidden.
Example 13-6. The scrollButtons.js library
// Global variables
var scrollEngaged = false;
var scrollInterval;
var scrollBars = new Array( );
// Read 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 "";
}
// Abstract object constructor function
function scrollBar(ownerID, ownerContentID, upID, dnID) {
this.ownerID = ownerID;
this.ownerContentID = ownerContentID;
this.index = scrollBars.length;
this.upButton = document.getElementById(upID);
this.dnButton = document.getElementById(dnID);
this.upButton.index = this.index;
this.dnButton.index = this.index;
this.ownerHeight = parseInt(getElementStyle(this.ownerID, "height", "height"));
this.contentElem = document.getElementById(ownerContentID);
this.contentFontSize = parseInt(getElementStyle(this.ownerContentID,
"fontSize", "font-size"));
this.contentScrollHeight = (this.contentElem.scrollHeight) ?
this.contentElem.scrollHeight : this.contentElem.offsetHeight;
this.initScroll = initScroll;
}
// Assign event handlers to actual scroll buttons
function initScroll( ) {
this.upButton.onmousedown = handleScrollClick;
this.upButton.onmouseup = handleScrollStop;
this.upButton.oncontextmenu = blockEvent;
this.dnButton.onmousedown = handleScrollClick;
this.dnButton.onmouseup = handleScrollStop;
this.dnButton.oncontextmenu = blockEvent;
var isIEMac = (navigator.appName.indexOf("Explorer") != -1 &&
navigator.userAgent.indexOf("Mac") != -1);
if (!isIEMac) {
document.getElementById("innerWrapper0").style.overflow = "hidden";
}
}
/**************************
Event Handler Functions
***************************/
// Turn off scrolling
function handleScrollStop( ) {
scrollEngaged = false;
}
// Block contextmenu for Mac (holding down mouse button)
function blockEvent(evt) {
evt = (evt) ? evt : event;
evt.cancelBubble = true;
return false;
}
// Initiate scrolling the content per the clicked button (up or down)
function handleScrollClick(evt) {
var fontSize;
evt = (evt) ? evt : event;
var target = (evt.target) ? evt.target : evt.srcElement;
var index = target.index;
fontSize = scrollBars[index].contentFontSize;
fontSize = (target.className = = "lineup") ? fontSize : -fontSize;
scrollEngaged = true;
// do single click scroll
scrollBy(index, parseInt(fontSize));
// trigger click-and-hold scrolling
scrollInterval = setInterval("scrollBy(" + index + ", " +
parseInt(fontSize) + ")", 100);
evt.cancelBubble = true;
return false;
}
// Perform actual scroll singly or repeatedly (through setInterval( ))
function scrollBy(index, px) {
var scroller = scrollBars[index];
var elem = document.getElementById(scroller.ownerContentID);
var top = parseInt(elem.style.top);
var scrollHeight = parseInt(scroller.contentScrollHeight);
var height = scroller.ownerHeight;
if (scrollEngaged && top + px >= -scrollHeight + height && top + px <= 0) {
shiftBy(elem, 0, px);
} else {
clearInterval(scrollInterval);
}
}
The library employs an object-oriented approach by creating an
abstract object that holds information about a pair of scroll buttons
and the content containers. This simplifies an implementation that
employs multiple scrolling boxes. At the start of the library, a few
global variables are defined that preserve the collection of scroller
objects and important state values during scrolling. Also included is
the getElementStyle( ) function from Recipe 11.12,
which other functions call on.
The scrollBar( ) constructor function for
the scroller objects receives four string parameters: the IDs for the
outer wrapper, the inner wrapper, and the two scroll buttons. The
purpose of this constructor is to perform some one-time calculations
and initializations per scroller, facilitating the click-and-hold
scroll action 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
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.
Each scroller object has its own initScroll(
) method, right after the scroller object
is created (this happens inside the page's
initScrollers( ) function, which runs at load
time). The function defined for this method assigns event handlers to
the button images, including one that prevents a click-and-hold
action from displaying the context menu (on the Macintosh).
Next come a group of 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. At the heart of this application is the
handleScrollClick( ) event handler, which takes
care of scrolling in both directions. Scrolling for this example is
line-by-line, so the content's font size is the
approximation used to determine the scroll jump size. The event
targets are img elements, each of which is
assigned an index value property corresponding to the scroller
object's array position. Further identifying each
button is the class attribute, which categorizes a
button as either an up or down (by one line) action button. Scrolling
the content upward requires subtracting the height of one line from
the current vertical position of the element. One immediate call to
the scrollBy( ) function comes within the function
to let the buttons react instantaneously to a quick click. After
that, the scrollBy( ) function is invoked every
100 milliseconds until other conditions (releasing the mouse button)
turn off scrolling.
Adjusting the position of the inner content wrapper is the job of the
scrollBy( ) function. It receives as parameters
both the index number of the scroller object and the number of pixels
to increment the vertical position. If the content is not scrolled
completely to the top or bottom, the DHTML API shiftBy(
) function moves the element along the vertical axis the
number of pixels instructed by the calling function. But if the
scrolling has reached an end point, the interval timer is turned off,
and further holding of the mouse button over the image scrolls no
more in the current direction.
The user interface possibilities for this kind of scrolling view port
are endless. The code in this recipe can be adapted to a multitude of
scroll controller buttons, whether they are images, hyperlinks, image
maps, or widgets constructed out of div elements
and text. It's just a question of assigning the
desired event handlers to the hot spots and making sure that those
spots have index properties associated with
scrollBar objects (as shown in the
scrollBar( ) constructor function).
At the same time, however, some designer choices can be disastrous.
Using mouse rollover events to trigger the scroll may not be a good
idea, despite its practice in some sites. Autoscrolling can also be
frustrating if the content is important because good autoscrolling
needs to be smoother (a scroll size of only one or two pixels), yet
the time it takes to scroll through the content and start over can be
frustratingly long for impatient visitors.
Choosing an object-oriented approach to the application is not as
arbitrary as it might seem. The core, frequently repeated routines
(especially the scrollBy( ) function invoked at
short time intervals), rely on several properties of the content
container and its outer wrapper. Some of those properties must be
accessed (ultimately) through the getElementStyle(
) function, which must perform a fair amount of processing
to do the job right. It is inefficient to invoke that function over
and over while the interval is firing away. The values that
don't change once the wrapper elements exist (such
as dimensions) should be obtained only once. Preserving those values
in an object representing the scroller simply makes good programming
sense. As a by-product, the scrollBar object lets
us preserve additional one-time calculated values throughout the
entire session. Moreover, we can limit the button event handlers so
that they are not active until the page is loaded. Premature clicking
of the buttons causes no errors because the events
aren't yet bound to the elements.
More about this application could be generalized, rather than
governed by fixed-style sheet values for positioning. You can see an
example of this (and the additional complexity it brings to the code)
in Recipe 13.13, which produces a more fully loaded vertical
scrollbar that controls the same kind of content.
13.12.4 See Also
Recipe 13.13 for a more complex scrollbar; Recipe 3.8 for creating a
custom object; Recipe 11.12 for reading default style sheet property
values as they apply to a rendered element.
|