Main Page |
13.13 Creating a Custom ScrollbarNN 7, IE 5 13.13.1 ProblemYou 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 SolutionThis 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 DiscussionThis 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. Figure 13-4. Scripted scrollbars for a div elementTwo 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 AlsoRecipe 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. |
Main Page |