[ Team LiB ] Previous Section Next Section

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.

Figure 13-3. A custom scrolling container
figs/jsdc_1303.gif

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.

    [ Team LiB ] Previous Section Next Section