Journal codefool's Journal: Multi-Column List Box Emulation in Javascript
<!--
Skeletal framework for implementing a multiple column list
box with 'standard' windows selection behavior using
javascript for dhtml.
Comments are sadly lacking - more to come.
Use as you see fit, check back here for updates as I have time.
-->
//
// manage a DHTML scrollable list
//
function scrollList_Column( owner, colId, index )
{
this._owner = owner; // the owning scrolllist
this._index = index;
this._id = colId; // our 'unique' id
}
scrollList_Column.prototype.getId =
function()
{
return this._id;
};
scrollList_Column.prototype.setWidth = function( sWidth ) { this._width = sWidth; return this; }
scrollList_Column.prototype.setStyle = function( sStyle ) { this._style = sStyle; return this; }
scrollList_Column.prototype.setClass = function( sClass ) { this._class = sClass; return this; }
scrollList_Column.prototype.setPrec = function( sPrec ) { this._prec = sPrec; return this; }
scrollList_Column.prototype.setAlign = function( sAlign ) { this._textAlign = sAlign; return this; }
scrollList_Column.prototype.setXmlTag = function( sTag ) { this._xmlTag = sTag; return this; }
scrollList_Column.prototype.setHeading = function( sHead ) { this._heading = sHead; return this; }
scrollList_Column.prototype.getHeading =
function()
{
var sRet = "[Unnamed Column]";
if( this.hasOwnProperty( "_heading" ) )
sRet = this._heading;
else if( this.hasOwnProperty( "_id" ) )
sRet = this._id;
return sRet;
};
//
// create a <TD> element with this column attributes and properties
//
scrollList_Column.prototype.createElement =
function( sData )
{
var oColumn = document.createElement( "TD" );
oColumn.id = this._id;
if( this.hasOwnProperty( "_width" ) ) oColumn.style.width = this._width;
if( this.hasOwnProperty( "_class" ) ) oColumn.className = this._class;
if( this.hasOwnProperty( "_style" ) ) oColumn.styleString = this._style;
if( this.hasOwnProperty( "_textAlign" ) ) oColumn.style.textAlign = this._textAlign;
if( null != sData )
{
var oTextNode = document.createTextNode( sData );
oColumn.appendChild( oTextNode );
}
return oColumn;
};
scrollList_Column.prototype.format =
function( sVal )
{
if( this.hasOwnProperty( "_prec" ) )
sVal = Number( sVal ).toFixed( this._prec );
return sVal;
};
////////////////////////////////////////////////////////////////////////////////
//
// S T A T I C F U N C T I O N S
//
////////////////////////////////////////////////////////////////////////////////
function scrollList_onRowClick()
{
var e = window.event.srcElement;
while( null != e && e.tagName != "TR" )
e = e.parentNode;
if( null != e && null != e._scrollList )
{
e._scrollList.hilite( e );
}
}
function scrollList_onRowDblClick()
{
//
// the only reason to go through this is if there is someone to send
// the info to, since we do nothing with double clicks.
//
var e = window.event.srcElement;
while( null != e && e.tagName != "TR" )
e = e.parentNode;
if( null != e && null != e._scrollList && null != e._scrollList._extRowDblClickHandler )
{
e._scrollList._extRowDblClickHandler( e.rowIndex );
}
}
//
// onSelectStart
//
// This function is used to negate select start events on our owning table. This is so mouse
// navigation works without having the browswer block-select text for us.
//
// This function answers false. That's all it does. Just walk away.
//
function scrollList_onSelectStart()
{
return false;
}
function scrollList_onKeyDown()
{
var bRet = false;
var e = window.event.srcElement;
while( e != null && e.tagName != "TABLE" )
e = e.parentElement;
if( null != e && null != e._scrollList && null != e._scrollList._curRow )
{
var list = e._scrollList;
var ek = event.keyCode;
if( !event.shiftKey )
{
list._hiliteAnchor = null;
}
bRet = true;
switch( ek )
{
case 0x26: // cursor up (VK_UP)
if( null != list._curRow.previousSibling )
list.hilite( list._curRow.previousSibling );
break;
case 0x28: // cursor down (VK_DOWN)
if( null != list._curRow.nextSibling )
list.hilite( list._curRow.nextSibling );
break;
case 0x20: // space (VK_SPACE)
// toggle the entry's selection status.
{
var bSelected = !list._curRow._selected;
list._curRow._selected = bSelected;
list._curRow.className = list.getRowStyle( list._curRow );
list._rowsSelected = bSelected;
}
break;
case 0x2d: // insert (VK_INSERT)
if( null != list._extInsertRowHandler )
list._extInsertRowHandler();
break;
case 0x2e: // delete (VK_DELETE)
list.removeSelectedRows();
break;
case 0x21: // page up (VK_PRIOR)
case 0x22: // page down (VK_NEXT)
if( list._viewDepth != -1 )
{
var scrollValue = list._viewDepth - 1;
if( ek == 0x21 )
scrollValue = -scrollValue;
var rowIdx = list._curRow.rowIndex + scrollValue;
if( rowIdx >= list.rowCount() )
rowIdx = list.rowCount() - 1;
else if( rowIdx < 0 )
rowIdx = 0;
list.hilite( list.getRow( rowIdx ) );
}
else
bRet = false;
break;
case 0x24: // home (VK_HOME)
list.hilite( list.getRow( 0 ) );
break;
case 0x23: // end (VK_END)
list.hilite( list.getRow( list.rowCount() - 1 ) );
break;
default:
bRet = false;
break;
}
//
// if we usurped our table's onKeyDown proc, then call it here
//
if( null != list._tableOnKeyDown )
bRet = list._tableOnKeyDown();
}
return bRet;
}
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
//
// class scrollList
//
// The actual row data is stored in the HTML document itself - the scroll
// list only contains information necessary to manage that piece of the
// document which must be totally eveloped by a single element, such as
// a <div> or <tbody>.
//
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
function scrollList( elemId )
{
this._elem = elemId; // the DHTML element where we live
this._styleClass = new Object(); // container for css style classes
this._cols = new Object(); // container for column descriptions
this._colCnt = 0;
this._curRow = null; // there is no current row
this._hiliteAnchor = null;
this._rowsSelected = null;
this._viewTop = 0; // first visible row in the view
this._viewDepth = -1; // show everything
this._emptyMessage = "[List is empty]";
this._extRemoveRowHandler = null;
this._extEndRemoveHandler = null;
this._extRowSelectHandler = null;
this._extRowDblClickHandler = null;
//
// locate our containing <TABLE> element and setup some magic
//
var e = elemId;
while( null != e && e.tagName != "TABLE" )
e = e.parentElement;
if( null != e )
{
this._table = e; // save our table reference
this._tableOnKeyDown = e.onkeydown; // save our table's onKeyDown handler
e._scrollList = this; // point our table to us
e.onkeydown = scrollList_onKeyDown; // substitute our own handler
e.onselectstart = scrollList_onSelectStart; // nullify mouse select events
}
}
scrollList.prototype.DEFAULT_CLASS_STYLE_ID = "__default__";
scrollList.prototype.EMPTY_ROW_ID = "__emptyrow__";
//
// Associate an external handler for the insert row event.
//
// This handler is called whenever a the user presses the 'insert' key on the scoll list.
// The list itself does nothing, but will evoke this handler if it is defined. The handler
// is expected to manipulate the to physically insert and populate the row where desired.
//
// The handler accepts no arguments, and answers nothing.
//
scrollList.prototype.setInsertRowHandler =
function( h )
{
this._extInsertRowHandler = h;
};
//
// Associate an external handler for the end remove event.
//
// This handler is called whenever a remove operation is completed (and something was
// actually removed). This gives the owning document an opportunity to adjust from
// the rows removed.
//
// The handler accepts no arguments, and answers nothing.
//
scrollList.prototype.setEndRemoveHandler =
function( h )
{
this._extEndRemoveHandler = h;
};
//
// associate an external handler for the remove row event.
//
// The handler accepts a single argument which is the row index of the row being
// removed. The handler can get additional information about the targetted row
// by supplying this index to scrollList.getRow().
//
// The handler MUST answer true if the row may be removed, false otherwise.
//
scrollList.prototype.setRemoveRowHandler =
function( h )
{
this._extRemoveRowHandler = h;
};
//
// Associate an external handler for when a row is selected. This handler is
// called whenever a new row is selected either by the keyboard or mouse.
//
// The handler accepts a single argument which is the row index of the row
// selected. The handler can get additional information about the target row
// by supplying this index to scrollList.getRow();
//
scrollList.prototype.setRowSelectHandler =
function( h )
{
this._extRowSelectHandler = h;
};
//
// associate an external handler for when a user double-clicks a row.
//
// The handler accepts a single argument which is the row index of the row being
// double-clicked. The handler can get additional information about the target row
// by supplying this index to scrollList.getRow();
//
scrollList.prototype.setRowDblClickHandler =
function( h )
{
this._extRowDblClickHandler = h;
};
scrollList.prototype.setViewDepth =
function( iSize )
{
iSize += 0;
if( 0 < iSize )
; // throw - BAD PARAMETER
this._viewDepth = iSize;
return this;
};
scrollList.prototype.getViewDepth =
function()
{
return this._viewDepth;
};
//
// for scrollable lists, scroll the named index into view. Optionaly specify
// whether it should be scrolled to the center of the view. Otherwise, the
// row is scrolled to either the first or last item, depending on whether
// the target row is above or below the view.
//
scrollList.prototype.scrollIntoView =
function( row, bCenter )
{
bCenter = !!bCenter; // assure boolean
row = this.getRow( row );
if( null != row )
{
var trueViewDepth = this._viewDepth - 1;
var viewBottom = this._viewTop + trueViewDepth;
var newTopRow = -1;
if( bCenter )
{
var centerOffset = trueViewDepth / 2;
if( row.rowIndex >= centerOffset )
newTopRow = row.rowIndex - centerOffset;
// else, the row can't be centered cuz there's not enough rows to
// fill the view.
}
// if the row is below the bottom of the view
else if( viewBottom < row.rowIndex )
{
newTopRow = row.rowIndex - trueViewDepth;
}
// if the row is above the top of the view
else if( row.rowIndex < this._viewTop )
{
newTopRow = row.rowIndex;
}
// else the row is already visible
if( newTopRow != -1 )
{
this._viewTop = newTopRow;
this.paint();
}
}
//else
// throw - index out of range
};
scrollList.prototype.hilite =
function( e, force )
{
force = !!force;
//
// determine if its the row element, or a child TD element that originated
// the event
//
while( null != e && "TR" != e.tagName.toUpperCase() )
e = e.parentElement;
if( null != e && ( e != this._curRow || force ) )
{
//
// handle keyboard augments
//
if( null != event )
{
if( !event.shiftKey )
{
this._hiliteAnchor = null;
if( !event.ctrlKey )
this.unselectAll();
}
else if( event.shiftKey && null == this._hiliteAnchor )
this._hiliteAnchor = this._curRow;
}
//
// first, un-hilite the currently hilited row, if there is one
//
var priorRow = this._curRow;
this._curRow = e;
if( null != priorRow )
priorRow.className = this.getRowStyle( priorRow );
//
// see if we have an anchor set by a prev shift operation
//
if( null != this._hiliteAnchor )
{
//
// we need to hilite every node from hiliteAnchor to the current node
//
this.unselectAll();
var lo = ( this._hiliteAnchor.rowIndex < this._curRow.rowIndex )
? this._hiliteAnchor
: this._curRow;
var hi = ( lo == this._hiliteAnchor )
? this._curRow
: this._hiliteAnchor;
for( e = lo; e != hi.nextSibling; e = e.nextSibling )
{
e._selected = true;
e.className = this.getRowStyle( e );
this._rowsSelected = true;
}
}
this._curRow.className = this.getRowStyle( this._curRow );
// now make sure the current row is visible
this.scrollIntoView( this._curRow, false );
if( null != this._extRowSelectHandler )
this._extRowSelectHandler( this._curRow.rowIndex );
}
};
scrollList.prototype.unselectAll =
function()
{
for( var e = this._elem.firstChild; e != null; e = e.nextSibling )
{
e._selected = false;
e.className = this.getRowStyle( e );
}
this._rowsSelected = false;
};
scrollList.prototype.rowCount =
function()
{
var iRet = 0;
if( !this.isEmpty() )
iRet = this._elem.rows.length;
return iRet;
};
scrollList.prototype.columnCount =
function()
{
return this._colCnt;
};
//
// remove selected rows
//
scrollList.prototype.removeSelectedRows =
function()
{
//
// if there are no selected rows, then the current row
// is dead meat ...
//
if( !this._rowsSelected )
{
if( null == this._curRow )
this._curRow = this._elem.rows[ 0 ];
this._curRow._selected = true;
}
var e = this._elem.rows[ this._elem.rows.length - 1 ];
var bSomethingRemoved = false;
while( e != null )
{
var ePrev = e.previousSibling;
if( e._selected )
{
var bRemove = true;
//
// see if our owner wants to be notified that
// we're about to remove a row...
//
if( null != this._extRemoveRowHandler )
bRemove = this._extRemoveRowHandler( e.rowIndex );
if( bRemove )
{
this.removeRow( e );
bSomethingRemoved = true;
}
}
e = ePrev;
}
//
// see if our owner wants to know that we're done removing stuff
//
if( bSomethingRemoved && null != this._extEndRemoveHandler )
this._extEndRemoveHandler();
this.paint();
};
scrollList.prototype.removeRow =
function( oRow )
{
if( oRow === this._curRow )
{
this._curRow = ( this._curRow.nextSibling != null )
? this._curRow.nextSibling
: this._curRow.previousSibling;
}
this._elem.removeChild( oRow );
this.createEmptyRow();
};
//
// addColumn
//
// Creates a new column object with the given id (provided it doesn't already exist),
// and sets its parent and index.
//
// Answer the column created, or null if something goes wrong.
//
scrollList.prototype.addColumn =
function( colId )
{
var newCol = this._cols[ colId ];
if( null == newCol )
{
newCol = new scrollList_Column( this, colId, this._colCnt++ );
this._cols[ colId ] = newCol;
}
return newCol;
};
scrollList.prototype.createColumnDataObject =
function()
{
var oRet = new Array();
for( var idx = 0; idx < this._colCnt; idx++ )
oRet[ idx ] = "";
return oRet;
};
//
// creates a state-oriented style. Essentially, there are four basic styles:
// 'normal' - row is not the current row, and is not selected
// 'hilite' - row is the current row, but is not selected
// 'select' - row is selected, but not the current row
// 'selecthi' - row is selected and is the current row
//
// This breaks down into a 2x2 matrix like so:
//
// | not selected | selected |
// ------------+--------------+----------+
// not current | normal | select |
// ------------+--------------+----------+
// current row | hilite | selecthi |
// ------------+--------------+----------+
//
// So the style strings are stored as an array of [false,true]
//
//
scrollList.prototype.setStyleClass =
function( sState, sNormal, sNormalHilite, sSelect, sSelectHilite )
{
//
// first, make sure we have four styles to choose from.
// sNormal is required, but the others are not
//
if( null != sNormal && sNormal != "" )
{
this._styleClass[ sState ] = new Array();
this._styleClass[ sState ][ false ] = new Array();
this._styleClass[ sState ][ false ][ false ] = sNormal;
this._styleClass[ sState ][ false ][ true ] = sNormalHilite;
this._styleClass[ sState ][ true ] = new Array();
this._styleClass[ sState ][ true ][ false ] = sSelect;
this._styleClass[ sState ][ true ][ true ] = sSelectHilite;
}
};
// provide a shorthand for setting the default style class
scrollList.prototype.setDefaultClassStyle =
function( sNormal, sNormalHilite, sSelect, sSelectHilite )
{
this.setStyleClass( this.DEFAULT_CLASS_STYLE_ID, sNormal, sNormalHilite, sSelect, sSelectHilite );
};
scrollList.prototype.getStateClassStyle =
function( sState, isCurrentRow, isSelected )
{
var sRet = null;
var oStyle = this._styleClass[ sState ];
if( null != oStyle )
sRet = oStyle[ !!isSelected ][ !!isCurrentRow ];
//
// if the style isn't provided, try to use a 'default' style
//
if( null == sRet )
sRet = this.getStateClassStyle( this.DEFAULT_CLASS_STYLE_ID, !!isCurrentRow, !!isSelected );
// if there still isn't any style, use the basic 'default' style
if( null == sRet )
sRet = this.getStateClassStyle( this.DEFAULT_CLASS_STYLE_ID, false, false );
return sRet;
};
scrollList.prototype.getDefaultNormalStyle = function() { return this.getStateClassStyle( this.DEFAULT_CLASS_STYLE_ID, false, false ); };
scrollList.prototype.getDefaultNormalHiliteStyle = function() { return this.getStateClassStyle( this.DEFAULT_CLASS_STYLE_ID, false, true ); };
scrollList.prototype.getDefaultSelectStyle = function() { return this.getStateClassStyle( this.DEFAULT_CLASS_STYLE_ID, true, false ); };
scrollList.prototype.getDefaultSelectHiliteStyle = function() { return this.getStateClassStyle( this.DEFAULT_CLASS_STYLE_ID, true , true ); };
scrollList.prototype.getRowStyle =
function( oRow )
{
var sRet = this.getDefaultNormalStyle();
oRow = this.getRow( oRow );
if( null != oRow )
sRet = this.getStateClassStyle( this.getRowState( oRow ), (this._curRow == oRow ), oRow._selected );
return sRet;
};
scrollList.prototype.paint =
function()
{
//
// make only the rows between _viewTop and _viewTop + _viewDepth - 1 visible
//
var viewBottom;
if( -1 == this._viewDepth )
viewBottom = this.rowCount() - 1;
else
{
viewBottom = this._viewTop + this._viewDepth;
if( viewBottom > this.rowCount() )
viewBottom = this.rowCount();
}
for( var idx = 0; idx < this.rowCount(); idx++ )
{
var thisRow = this._elem.rows[ idx ];
thisRow.style.display = ( this._viewTop <= idx && idx < viewBottom )
? "inline"
: "none";
}
this.hilite( this.getCurrentRow(), true );
};
scrollList.prototype.setHeaderRow =
function( tableElem )
{
var oTableRow = document.createElement( "TR" );
for( var colName in this._cols )
{
var col = this._cols[ colName ];
var oTableCol = document.createElement( "TD" );
var oTextElem = document.createTextNode( col.getHeading() );
oTableCol.appendChild( oTextElem );
oTableRow.appendChild( oTableCol );
}
tableElem.appendChild( oTableRow );
};
scrollList.prototype.addRow =
function()
{
this.removeEmptyRow();
var oTableRow = document.createElement( "TR" );
oTableRow.style.display = "none"; // create invisible
oTableRow.onclick = scrollList_onRowClick;
oTableRow.ondblclick = scrollList_onRowDblClick;
oTableRow._scrollList = this;
oTableRow.csRowState = this.DEFAULT_CLASS_STYLE_ID;
this._elem.appendChild( oTableRow );
return oTableRow;
};
scrollList.prototype.createColumn =
function( colIndex )
{
var oCol = this.getColumn( colIndex );
var oTableCol = oCol.createElement();
oTableCol.setAttribute( "cellId", colIndex );
return oTableCol;
};
//
// answer the TR element for the given row index.
//
// rowIndex can be either the TR element or an index to the TR element.
//
scrollList.prototype.getRow =
function( rowIndex )
{
var oRow = null;
//
// first see if we were passed a reference to an Element
// object that happens to be a TR object.
//
if( typeof rowIndex == "object" && rowIndex.tagName == "TR" )
oRow = rowIndex;
//
// If rowIndex is -1, then answer the "current row". If there is no
// current row, answer the first row.
//
else if( -1 == rowIndex )
{
if( null == this._curRow )
this._curRow = this._elem.rows[ 0 ];
oRow = this._curRow;
}
//
// else, if the index is sane, answer the row object for that index
//
else if( 0 <= rowIndex && rowIndex < this.rowCount() )
oRow = this._elem.rows[ rowIndex ];
return oRow;
};
scrollList.prototype.getRowColumn =
function( oRow, colIndex )
{
var oCol = null;
if( null != this._cols[ colIndex ] ) // in case colIndex is a column name
colIndex = this._cols[ colIndex ]._index;
if( 0 <= colIndex && colIndex < this._colCnt )
{
oRow = this.getRow( oRow );
var thisCol = null;
if( null != oRow )
{
// this is not as simple as just getting the element from the row's cell collection
// because its possible that the cells are being created out of order. So we have to
// loop through the cell entries, looking for a cellid property of the desired index.
// if the index is not found, it is created and inserted into the appropriate spot
//
// however, to possibly save us some work, just peek to see if the cell we want
// is in the right place already...
//
if( colIndex < oRow.cells.length && oRow.cells[ colIndex ].getAttribute( "cellId" ) == colIndex )
oCol = oRow.cells[ colIndex ];
else
{
for( var iColIdx = 0; iColIdx < oRow.cells.length; iColIdx++ )
{
thisCol = oRow.cells[ iColIdx ];
var cellId = thisCol.getAttribute( "cellId" );
if( cellId == colIndex )
{
oCol = thisCol;
break;
}
else if( cellId > colIndex )
// this is our insertion point
break;
thisCol = null;
}
if( null == oCol )
{
// didn't find the column - so create it
oCol = this.createColumn( colIndex );
if( null == thisCol )
// this is the first cell in the row
oRow.appendChild( oCol );
else
oRow.insertBefore( oCol, thisCol );
}
}
}
}
return oCol;
};
scrollList.prototype.setColData =
function( row, colIndex, vColData )
{
var oCol = this.getColumn( colIndex );
var oNewCol = this.getRowColumn( row, colIndex );
if( null != oNewCol )
oNewCol.innerHTML = oCol.format( vColData );
};
scrollList.prototype.getColumnData =
function( row, colIndex )
{
var oCol = this.getRowColumn( row, colIndex );
return oCol.innerHTML;
};
scrollList.prototype.clear =
function()
{
while( this._elem.hasChildNodes() )
this._elem.removeChild( this._elem.firstChild );
this._curRow = null;
this.createEmptyRow();
};
//
// creates a new row from the supplied XML documnt fragment. For this to work,
// the xmlTag properties of each row must have been supplied prior to calling
// this method.
//
// answers the row created
//
scrollList.prototype.createRowFromXML =
function( oXmlDoc )
{
var oRow = this.addRow();
if( null != oRow )
{
for( var colIdx = 0; colIdx < this.columnCount(); colIdx++ )
{
var oCol = this.getColumn( colIdx );
var sColVal = "";
if( null != oCol._xmlTag )
{
var oNode = oXmlDoc.selectSingleNode( ".//" + oCol._xmlTag );
if( null != oNode )
sColVal = oNode.text;
}
this.setColData( oRow, colIdx, sColVal );
}
}
return oRow;
};
scrollList.prototype.getColumn =
function( colIndex )
{
var oRet = this._cols[ colIndex ];
if( null == oRet )
{
for( var colName in this._cols )
{
if( this._cols[ colName ]._index == colIndex )
{
oRet = this._cols[ colName ];
break;
}
}
}
return oRet;
};
scrollList.prototype.swapRows =
function( row1, row2 )
{
if( typeof row1 == "number" )
row1 = this.getRow( row1 );
if( typeof row2 == "number" )
row2 = this.getRow( row2 );
if( null != row1 && null != row2 )
{
row1.swapNode( row2 );
}
};
scrollList.prototype.moveRowUp =
function( row )
{
if( typeof row == "number" )
row = this.getRow( row );
if( null != row )
this.swapRows( row, row.previousSibling );
};
scrollList.prototype.moveRowDown =
function( row )
{
if( typeof row == "number" )
row = this.getRow( row );
if( null != row )
this.swapRows( row, row.previousSibling );
};
//
// answers the row element for the currently hilited row
//
scrollList.prototype.getCurrentRow =
function()
{
return this.getRow( -1 );
}
scrollList.prototype.setCurrentRow =
function( row )
{
row = this.getRow( row );
if( row != null )
{
this.hilite( row );
}
};
//
// sets the state value of a row. This is set as an attribute to the
// row element as csRowState
//
scrollList.prototype.setRowState =
function( row, state )
{
row = this.getRow( row );
if( null != row )
row.csRowState = state;
};
//
// answers the state value of a rows, or empty string if none exist
//
scrollList.prototype.getRowState =
function( row )
{
var sRet = "";
row = this.getRow( row );
if( null != row && null != row.csRowState )
sRet = row.csRowState;
return sRet;
};
//
// answers the selected state for a given row
//
scrollList.prototype.isSelected =
function( row )
{
var bRet = false;
row = this.getRow( row );
if( null != row )
bRet = row._selected;
return bRet;
};
//
// answers the CSS class for the given row based on its
// csRowState and _selected values.
//
scrollList.prototype.getRowStateClass =
function( row )
{
var sClass = "";
row = this.getRow( row );
if( null != row )
{
var sState = this.getRowState( row );
}
return sClass;
};
//
// specifies the string to display when the list is empty
//
scrollList.prototype.setEmptyMessage =
function( sMessage )
{
this._emptyMessage = sMessage;
};
//
// answer true if the list is currently empty. The list is
// empty if the only item in the list is the 'special' empty item
// (which contains the mesage that the list is empty ;-)
//
scrollList.prototype.isEmpty =
function()
{
return ( null != this._elem && this._elem.rows.length == 1 && this._elem.rows[ 0 ].id == this.EMPTY_ROW_ID );
};
scrollList.prototype.createEmptyRow =
function()
{
if( this._elem.rows.length == 0 )
{
var oText = document.createTextNode( this._emptyMessage );
var oCol = document.createElement( "TD" );
var oRow = document.createElement( "TR" );
oCol.colSpan = this._colCnt;
oRow.id = this.EMPTY_ROW_ID;
oRow.className = this.getDefaultNormalStyle();
oCol.appendChild( oText );
oRow.appendChild( oCol );
this._elem.appendChild( oRow );
}
};
scrollList.prototype.removeEmptyRow =
function()
{
if( this.isEmpty() )
{
this._elem.removeChild( this._elem.firstChild );
}
};
Skeletal framework for implementing a multiple column list
box with 'standard' windows selection behavior using
javascript for dhtml.
Comments are sadly lacking - more to come.
Use as you see fit, check back here for updates as I have time.
-->
//
// manage a DHTML scrollable list
//
function scrollList_Column( owner, colId, index )
{
this._owner = owner;
this._index = index;
this._id = colId;
}
scrollList_Column.prototype.getId =
function()
{
return this._id;
};
scrollList_Column.prototype.setWidth = function( sWidth ) { this._width = sWidth; return this; }
scrollList_Column.prototype.setStyle = function( sStyle ) { this._style = sStyle; return this; }
scrollList_Column.prototype.setClass = function( sClass ) { this._class = sClass; return this; }
scrollList_Column.prototype.setPrec = function( sPrec ) { this._prec = sPrec; return this; }
scrollList_Column.prototype.setAlign = function( sAlign ) { this._textAlign = sAlign; return this; }
scrollList_Column.prototype.setXmlTag = function( sTag ) { this._xmlTag = sTag; return this; }
scrollList_Column.prototype.setHeading = function( sHead ) { this._heading = sHead; return this; }
scrollList_Column.prototype.getHeading =
function()
{
var sRet = "[Unnamed Column]";
if( this.hasOwnProperty( "_heading" ) )
sRet = this._heading;
else if( this.hasOwnProperty( "_id" ) )
sRet = this._id;
return sRet;
};
//
// create a <TD> element with this column attributes and properties
//
scrollList_Column.prototype.createElement =
function( sData )
{
var oColumn = document.createElement( "TD" );
oColumn.id = this._id;
if( this.hasOwnProperty( "_width" ) ) oColumn.style.width = this._width;
if( this.hasOwnProperty( "_class" ) ) oColumn.className = this._class;
if( this.hasOwnProperty( "_style" ) ) oColumn.styleString = this._style;
if( this.hasOwnProperty( "_textAlign" ) ) oColumn.style.textAlign = this._textAlign;
if( null != sData )
{
var oTextNode = document.createTextNode( sData );
oColumn.appendChild( oTextNode );
}
return oColumn;
};
scrollList_Column.prototype.format =
function( sVal )
{
if( this.hasOwnProperty( "_prec" ) )
sVal = Number( sVal ).toFixed( this._prec );
return sVal;
};
////////////////////////////////////////////////////////////////////////////////
//
// S T A T I C F U N C T I O N S
//
////////////////////////////////////////////////////////////////////////////////
function scrollList_onRowClick()
{
var e = window.event.srcElement;
while( null != e && e.tagName != "TR" )
e = e.parentNode;
if( null != e && null != e._scrollList )
{
e._scrollList.hilite( e );
}
}
function scrollList_onRowDblClick()
{
var e = window.event.srcElement;
while( null != e && e.tagName != "TR" )
e = e.parentNode;
if( null != e && null != e._scrollList && null != e._scrollList._extRowDblClickHandler )
{
e._scrollList._extRowDblClickHandler( e.rowIndex );
}
}
//
// onSelectStart
//
// This function is used to negate select start events on our owning table. This is so mouse
// navigation works without having the browswer block-select text for us.
//
// This function answers false. That's all it does. Just walk away.
//
function scrollList_onSelectStart()
{
return false;
}
function scrollList_onKeyDown()
{
var bRet = false;
var e = window.event.srcElement;
while( e != null && e.tagName != "TABLE" )
e = e.parentElement;
if( null != e && null != e._scrollList && null != e._scrollList._curRow )
{
var list = e._scrollList;
var ek = event.keyCode;
if( !event.shiftKey )
{
list._hiliteAnchor = null;
}
bRet = true;
switch( ek )
{
case 0x26:
if( null != list._curRow.previousSibling )
list.hilite( list._curRow.previousSibling );
break;
case 0x28:
if( null != list._curRow.nextSibling )
list.hilite( list._curRow.nextSibling );
break;
case 0x20:
{
var bSelected = !list._curRow._selected;
list._curRow._selected = bSelected;
list._curRow.className = list.getRowStyle( list._curRow );
list._rowsSelected = bSelected;
}
break;
case 0x2d:
if( null != list._extInsertRowHandler )
list._extInsertRowHandler();
break;
case 0x2e:
list.removeSelectedRows();
break;
case 0x21:
case 0x22:
if( list._viewDepth != -1 )
{
var scrollValue = list._viewDepth - 1;
if( ek == 0x21 )
scrollValue = -scrollValue;
var rowIdx = list._curRow.rowIndex + scrollValue;
if( rowIdx >= list.rowCount() )
rowIdx = list.rowCount() - 1;
else if( rowIdx < 0 )
rowIdx = 0;
list.hilite( list.getRow( rowIdx ) );
}
else
bRet = false;
break;
case 0x24:
list.hilite( list.getRow( 0 ) );
break;
case 0x23:
list.hilite( list.getRow( list.rowCount() - 1 ) );
break;
default:
bRet = false;
break;
}
if( null != list._tableOnKeyDown )
bRet = list._tableOnKeyDown();
}
return bRet;
}
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
//
// class scrollList
//
// The actual row data is stored in the HTML document itself - the scroll
// list only contains information necessary to manage that piece of the
// document which must be totally eveloped by a single element, such as
// a <div> or <tbody>.
//
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
function scrollList( elemId )
{
this._elem = elemId; // the DHTML element where we live
this._styleClass = new Object(); // container for css style classes
this._cols = new Object(); // container for column descriptions
this._colCnt = 0;
this._curRow = null; // there is no current row
this._hiliteAnchor = null;
this._rowsSelected = null;
this._viewTop = 0;
this._viewDepth = -1; // show everything
this._emptyMessage = "[List is empty]";
this._extRemoveRowHandler = null;
this._extEndRemoveHandler = null;
this._extRowSelectHandler = null;
this._extRowDblClickHandler = null;
var e = elemId;
while( null != e && e.tagName != "TABLE" )
e = e.parentElement;
if( null != e )
{
this._table = e; // save our table reference
this._tableOnKeyDown = e.onkeydown; // save our table's onKeyDown handler
e._scrollList = this;
e.onkeydown = scrollList_onKeyDown;
e.onselectstart = scrollList_onSelectStart;
}
}
scrollList.prototype.DEFAULT_CLASS_STYLE_ID = "__default__";
scrollList.prototype.EMPTY_ROW_ID = "__emptyrow__";
//
// Associate an external handler for the insert row event.
//
// This handler is called whenever a the user presses the 'insert' key on the scoll list.
// The list itself does nothing, but will evoke this handler if it is defined. The handler
// is expected to manipulate the to physically insert and populate the row where desired.
//
// The handler accepts no arguments, and answers nothing.
//
scrollList.prototype.setInsertRowHandler =
function( h )
{
this._extInsertRowHandler = h;
};
//
// Associate an external handler for the end remove event.
//
// This handler is called whenever a remove operation is completed (and something was
// actually removed). This gives the owning document an opportunity to adjust from
// the rows removed.
//
// The handler accepts no arguments, and answers nothing.
//
scrollList.prototype.setEndRemoveHandler =
function( h )
{
this._extEndRemoveHandler = h;
};
//
// associate an external handler for the remove row event.
//
// The handler accepts a single argument which is the row index of the row being
// removed. The handler can get additional information about the targetted row
// by supplying this index to scrollList.getRow().
//
// The handler MUST answer true if the row may be removed, false otherwise.
//
scrollList.prototype.setRemoveRowHandler =
function( h )
{
this._extRemoveRowHandler = h;
};
//
// Associate an external handler for when a row is selected. This handler is
// called whenever a new row is selected either by the keyboard or mouse.
//
// The handler accepts a single argument which is the row index of the row
// selected. The handler can get additional information about the target row
// by supplying this index to scrollList.getRow();
//
scrollList.prototype.setRowSelectHandler =
function( h )
{
this._extRowSelectHandler = h;
};
//
// associate an external handler for when a user double-clicks a row.
//
// The handler accepts a single argument which is the row index of the row being
// double-clicked. The handler can get additional information about the target row
// by supplying this index to scrollList.getRow();
//
scrollList.prototype.setRowDblClickHandler =
function( h )
{
this._extRowDblClickHandler = h;
};
scrollList.prototype.setViewDepth =
function( iSize )
{
iSize += 0;
if( 0 < iSize )
;
this._viewDepth = iSize;
return this;
};
scrollList.prototype.getViewDepth =
function()
{
return this._viewDepth;
};
//
// for scrollable lists, scroll the named index into view. Optionaly specify
// whether it should be scrolled to the center of the view. Otherwise, the
// row is scrolled to either the first or last item, depending on whether
// the target row is above or below the view.
//
scrollList.prototype.scrollIntoView =
function( row, bCenter )
{
bCenter = !!bCenter;
row = this.getRow( row );
if( null != row )
{
var trueViewDepth = this._viewDepth - 1;
var viewBottom = this._viewTop + trueViewDepth;
var newTopRow = -1;
if( bCenter )
{
var centerOffset = trueViewDepth / 2;
if( row.rowIndex >= centerOffset )
newTopRow = row.rowIndex - centerOffset;
}
else if( viewBottom < row.rowIndex )
{
newTopRow = row.rowIndex - trueViewDepth;
}
else if( row.rowIndex < this._viewTop )
{
newTopRow = row.rowIndex;
}
if( newTopRow != -1 )
{
this._viewTop = newTopRow;
this.paint();
}
}
};
scrollList.prototype.hilite =
function( e, force )
{
force = !!force;
while( null != e && "TR" != e.tagName.toUpperCase() )
e = e.parentElement;
if( null != e && ( e != this._curRow || force ) )
{
if( null != event )
{
if( !event.shiftKey )
{
this._hiliteAnchor = null;
if( !event.ctrlKey )
this.unselectAll();
}
else if( event.shiftKey && null == this._hiliteAnchor )
this._hiliteAnchor = this._curRow;
}
var priorRow = this._curRow;
this._curRow = e;
if( null != priorRow )
priorRow.className = this.getRowStyle( priorRow );
if( null != this._hiliteAnchor )
{
this.unselectAll();
var lo = ( this._hiliteAnchor.rowIndex < this._curRow.rowIndex )
? this._hiliteAnchor
: this._curRow;
var hi = ( lo == this._hiliteAnchor )
? this._curRow
: this._hiliteAnchor;
for( e = lo; e != hi.nextSibling; e = e.nextSibling )
{
e._selected = true;
e.className = this.getRowStyle( e );
this._rowsSelected = true;
}
}
this._curRow.className = this.getRowStyle( this._curRow );
this.scrollIntoView( this._curRow, false );
if( null != this._extRowSelectHandler )
this._extRowSelectHandler( this._curRow.rowIndex );
}
};
scrollList.prototype.unselectAll =
function()
{
for( var e = this._elem.firstChild; e != null; e = e.nextSibling )
{
e._selected = false;
e.className = this.getRowStyle( e );
}
this._rowsSelected = false;
};
scrollList.prototype.rowCount =
function()
{
var iRet = 0;
if( !this.isEmpty() )
iRet = this._elem.rows.length;
return iRet;
};
scrollList.prototype.columnCount =
function()
{
return this._colCnt;
};
//
// remove selected rows
//
scrollList.prototype.removeSelectedRows =
function()
{
if( !this._rowsSelected )
{
if( null == this._curRow )
this._curRow = this._elem.rows[ 0 ];
this._curRow._selected = true;
}
var e = this._elem.rows[ this._elem.rows.length - 1 ];
var bSomethingRemoved = false;
while( e != null )
{
var ePrev = e.previousSibling;
if( e._selected )
{
var bRemove = true;
if( null != this._extRemoveRowHandler )
bRemove = this._extRemoveRowHandler( e.rowIndex );
if( bRemove )
{
this.removeRow( e );
bSomethingRemoved = true;
}
}
e = ePrev;
}
if( bSomethingRemoved && null != this._extEndRemoveHandler )
this._extEndRemoveHandler();
this.paint();
};
scrollList.prototype.removeRow =
function( oRow )
{
if( oRow === this._curRow )
{
this._curRow = ( this._curRow.nextSibling != null )
? this._curRow.nextSibling
: this._curRow.previousSibling;
}
this._elem.removeChild( oRow );
this.createEmptyRow();
};
//
// addColumn
//
// Creates a new column object with the given id (provided it doesn't already exist),
// and sets its parent and index.
//
// Answer the column created, or null if something goes wrong.
//
scrollList.prototype.addColumn =
function( colId )
{
var newCol = this._cols[ colId ];
if( null == newCol )
{
newCol = new scrollList_Column( this, colId, this._colCnt++ );
this._cols[ colId ] = newCol;
}
return newCol;
};
scrollList.prototype.createColumnDataObject =
function()
{
var oRet = new Array();
for( var idx = 0; idx < this._colCnt; idx++ )
oRet[ idx ] = "";
return oRet;
};
//
// creates a state-oriented style. Essentially, there are four basic styles:
// 'normal' - row is not the current row, and is not selected
// 'hilite' - row is the current row, but is not selected
// 'select' - row is selected, but not the current row
// 'selecthi' - row is selected and is the current row
//
// This breaks down into a 2x2 matrix like so:
//
// | not selected | selected |
// ------------+--------------+----------+
// not current | normal | select |
// ------------+--------------+----------+
// current row | hilite | selecthi |
// ------------+--------------+----------+
//
// So the style strings are stored as an array of [false,true]
//
//
scrollList.prototype.setStyleClass =
function( sState, sNormal, sNormalHilite, sSelect, sSelectHilite )
{
if( null != sNormal && sNormal != "" )
{
this._styleClass[ sState ] = new Array();
this._styleClass[ sState ][ false ] = new Array();
this._styleClass[ sState ][ false ][ false ] = sNormal;
this._styleClass[ sState ][ false ][ true ] = sNormalHilite;
this._styleClass[ sState ][ true ] = new Array();
this._styleClass[ sState ][ true ][ false ] = sSelect;
this._styleClass[ sState ][ true ][ true ] = sSelectHilite;
}
};
// provide a shorthand for setting the default style class
scrollList.prototype.setDefaultClassStyle =
function( sNormal, sNormalHilite, sSelect, sSelectHilite )
{
this.setStyleClass( this.DEFAULT_CLASS_STYLE_ID, sNormal, sNormalHilite, sSelect, sSelectHilite );
};
scrollList.prototype.getStateClassStyle =
function( sState, isCurrentRow, isSelected )
{
var sRet = null;
var oStyle = this._styleClass[ sState ];
if( null != oStyle )
sRet = oStyle[ !!isSelected ][ !!isCurrentRow ];
if( null == sRet )
sRet = this.getStateClassStyle( this.DEFAULT_CLASS_STYLE_ID, !!isCurrentRow, !!isSelected );
if( null == sRet )
sRet = this.getStateClassStyle( this.DEFAULT_CLASS_STYLE_ID, false, false );
return sRet;
};
scrollList.prototype.getDefaultNormalStyle = function() { return this.getStateClassStyle( this.DEFAULT_CLASS_STYLE_ID, false, false ); };
scrollList.prototype.getDefaultNormalHiliteStyle = function() { return this.getStateClassStyle( this.DEFAULT_CLASS_STYLE_ID, false, true ); };
scrollList.prototype.getDefaultSelectStyle = function() { return this.getStateClassStyle( this.DEFAULT_CLASS_STYLE_ID, true, false ); };
scrollList.prototype.getDefaultSelectHiliteStyle = function() { return this.getStateClassStyle( this.DEFAULT_CLASS_STYLE_ID, true , true ); };
scrollList.prototype.getRowStyle =
function( oRow )
{
var sRet = this.getDefaultNormalStyle();
oRow = this.getRow( oRow );
if( null != oRow )
sRet = this.getStateClassStyle( this.getRowState( oRow ), (this._curRow == oRow ), oRow._selected );
return sRet;
};
scrollList.prototype.paint =
function()
{
var viewBottom;
if( -1 == this._viewDepth )
viewBottom = this.rowCount() - 1;
else
{
viewBottom = this._viewTop + this._viewDepth;
if( viewBottom > this.rowCount() )
viewBottom = this.rowCount();
}
for( var idx = 0; idx < this.rowCount(); idx++ )
{
var thisRow = this._elem.rows[ idx ];
thisRow.style.display = ( this._viewTop <= idx && idx < viewBottom )
? "inline"
: "none";
}
this.hilite( this.getCurrentRow(), true );
};
scrollList.prototype.setHeaderRow =
function( tableElem )
{
var oTableRow = document.createElement( "TR" );
for( var colName in this._cols )
{
var col = this._cols[ colName ];
var oTableCol = document.createElement( "TD" );
var oTextElem = document.createTextNode( col.getHeading() );
oTableCol.appendChild( oTextElem );
oTableRow.appendChild( oTableCol );
}
tableElem.appendChild( oTableRow );
};
scrollList.prototype.addRow =
function()
{
this.removeEmptyRow();
var oTableRow = document.createElement( "TR" );
oTableRow.style.display = "none"; // create invisible
oTableRow.onclick = scrollList_onRowClick;
oTableRow.ondblclick = scrollList_onRowDblClick;
oTableRow._scrollList = this;
oTableRow.csRowState = this.DEFAULT_CLASS_STYLE_ID;
this._elem.appendChild( oTableRow );
return oTableRow;
};
scrollList.prototype.createColumn =
function( colIndex )
{
var oCol = this.getColumn( colIndex );
var oTableCol = oCol.createElement();
oTableCol.setAttribute( "cellId", colIndex );
return oTableCol;
};
//
// answer the TR element for the given row index.
//
// rowIndex can be either the TR element or an index to the TR element.
//
scrollList.prototype.getRow =
function( rowIndex )
{
var oRow = null;
if( typeof rowIndex == "object" && rowIndex.tagName == "TR" )
oRow = rowIndex;
else if( -1 == rowIndex )
{
if( null == this._curRow )
this._curRow = this._elem.rows[ 0 ];
oRow = this._curRow;
}
else if( 0 <= rowIndex && rowIndex < this.rowCount() )
oRow = this._elem.rows[ rowIndex ];
return oRow;
};
scrollList.prototype.getRowColumn =
function( oRow, colIndex )
{
var oCol = null;
if( null != this._cols[ colIndex ] )
colIndex = this._cols[ colIndex ]._index;
if( 0 <= colIndex && colIndex < this._colCnt )
{
oRow = this.getRow( oRow );
var thisCol = null;
if( null != oRow )
{
if( colIndex < oRow.cells.length && oRow.cells[ colIndex ].getAttribute( "cellId" ) == colIndex )
oCol = oRow.cells[ colIndex ];
else
{
for( var iColIdx = 0; iColIdx < oRow.cells.length; iColIdx++ )
{
thisCol = oRow.cells[ iColIdx ];
var cellId = thisCol.getAttribute( "cellId" );
if( cellId == colIndex )
{
oCol = thisCol;
break;
}
else if( cellId > colIndex )
break;
thisCol = null;
}
if( null == oCol )
{
oCol = this.createColumn( colIndex );
if( null == thisCol )
oRow.appendChild( oCol );
else
oRow.insertBefore( oCol, thisCol );
}
}
}
}
return oCol;
};
scrollList.prototype.setColData =
function( row, colIndex, vColData )
{
var oCol = this.getColumn( colIndex );
var oNewCol = this.getRowColumn( row, colIndex );
if( null != oNewCol )
oNewCol.innerHTML = oCol.format( vColData );
};
scrollList.prototype.getColumnData =
function( row, colIndex )
{
var oCol = this.getRowColumn( row, colIndex );
return oCol.innerHTML;
};
scrollList.prototype.clear =
function()
{
while( this._elem.hasChildNodes() )
this._elem.removeChild( this._elem.firstChild );
this._curRow = null;
this.createEmptyRow();
};
//
// creates a new row from the supplied XML documnt fragment. For this to work,
// the xmlTag properties of each row must have been supplied prior to calling
// this method.
//
// answers the row created
//
scrollList.prototype.createRowFromXML =
function( oXmlDoc )
{
var oRow = this.addRow();
if( null != oRow )
{
for( var colIdx = 0; colIdx < this.columnCount(); colIdx++ )
{
var oCol = this.getColumn( colIdx );
var sColVal = "";
if( null != oCol._xmlTag )
{
var oNode = oXmlDoc.selectSingleNode( ".//" + oCol._xmlTag );
if( null != oNode )
sColVal = oNode.text;
}
this.setColData( oRow, colIdx, sColVal );
}
}
return oRow;
};
scrollList.prototype.getColumn =
function( colIndex )
{
var oRet = this._cols[ colIndex ];
if( null == oRet )
{
for( var colName in this._cols )
{
if( this._cols[ colName ]._index == colIndex )
{
oRet = this._cols[ colName ];
break;
}
}
}
return oRet;
};
scrollList.prototype.swapRows =
function( row1, row2 )
{
if( typeof row1 == "number" )
row1 = this.getRow( row1 );
if( typeof row2 == "number" )
row2 = this.getRow( row2 );
if( null != row1 && null != row2 )
{
row1.swapNode( row2 );
}
};
scrollList.prototype.moveRowUp =
function( row )
{
if( typeof row == "number" )
row = this.getRow( row );
if( null != row )
this.swapRows( row, row.previousSibling );
};
scrollList.prototype.moveRowDown =
function( row )
{
if( typeof row == "number" )
row = this.getRow( row );
if( null != row )
this.swapRows( row, row.previousSibling );
};
//
// answers the row element for the currently hilited row
//
scrollList.prototype.getCurrentRow =
function()
{
return this.getRow( -1 );
}
scrollList.prototype.setCurrentRow =
function( row )
{
row = this.getRow( row );
if( row != null )
{
this.hilite( row );
}
};
//
// sets the state value of a row. This is set as an attribute to the
// row element as csRowState
//
scrollList.prototype.setRowState =
function( row, state )
{
row = this.getRow( row );
if( null != row )
row.csRowState = state;
};
//
// answers the state value of a rows, or empty string if none exist
//
scrollList.prototype.getRowState =
function( row )
{
var sRet = "";
row = this.getRow( row );
if( null != row && null != row.csRowState )
sRet = row.csRowState;
return sRet;
};
//
// answers the selected state for a given row
//
scrollList.prototype.isSelected =
function( row )
{
var bRet = false;
row = this.getRow( row );
if( null != row )
bRet = row._selected;
return bRet;
};
//
// answers the CSS class for the given row based on its
// csRowState and _selected values.
//
scrollList.prototype.getRowStateClass =
function( row )
{
var sClass = "";
row = this.getRow( row );
if( null != row )
{
var sState = this.getRowState( row );
}
return sClass;
};
//
// specifies the string to display when the list is empty
//
scrollList.prototype.setEmptyMessage =
function( sMessage )
{
this._emptyMessage = sMessage;
};
//
// answer true if the list is currently empty. The list is
// empty if the only item in the list is the 'special' empty item
// (which contains the mesage that the list is empty
//
scrollList.prototype.isEmpty =
function()
{
return ( null != this._elem && this._elem.rows.length == 1 && this._elem.rows[ 0 ].id == this.EMPTY_ROW_ID );
};
scrollList.prototype.createEmptyRow =
function()
{
if( this._elem.rows.length == 0 )
{
var oText = document.createTextNode( this._emptyMessage );
var oCol = document.createElement( "TD" );
var oRow = document.createElement( "TR" );
oCol.colSpan = this._colCnt;
oRow.id = this.EMPTY_ROW_ID;
oRow.className = this.getDefaultNormalStyle();
oCol.appendChild( oText );
oRow.appendChild( oCol );
this._elem.appendChild( oRow );
}
};
scrollList.prototype.removeEmptyRow =
function()
{
if( this.isEmpty() )
{
this._elem.removeChild( this._elem.firstChild );
}
};