/**********************************
* File: nr_gadgets.js
* Desc: Novel Racers -- Gadget Code
* Auth: Kevin Machin.
**********************************/

/*jsl:import cb.js      */
/*jsl:import exlinks.js */
/*jsl:import nr_data.js */

// Loading
cbOnLoad (cbNrGadgets);
var gsCbNrIdPfx = "CbNrBg"; // Gadget identifier prefix
var gsCbNrClass = "nrbg";   // Gadget class name

var goCbNrDate = new Date ();

//////////////////////////////////////////
// Func: cbNrGadgets
// Desc: Initialise Novel Racers' gadgets.
//////////////////////////////////////////

function cbNrGadgets ()
{
    var iGadgets = 0, aoGadget = new Array;
    var i, oGadget, oG, oContainer, sMessage, oMessage, oMark, sError = "";

    try
    {
        // Set up gadget array
        gadgetAdd ("Quick",    cbNrQuick,    "Quick page links");
        gadgetAdd ("Wrl",      cbNrWrl,      "Writing Resource Links");
        gadgetAdd ("Race",     cbNrRace,     "The Race " + goCbNrDate.getFullYear ());
        gadgetAdd ("List",     cbNrList,     "Who we are");
        gadgetAdd ("Rota",     cbNrRota,     "Coffee rota");
        gadgetAdd ("Coffee",   cbNrCoffee,   "Coffee breaks");
        gadgetAdd ("Accomp08", null,         "Accomplishments 2008");
        gadgetAdd ("Accomp07", null,         "Accomplishments 2007");
        gadgetAdd ("Feedback", cbNrFeedback, "Gadgets feedback");

        // Mark gadgets as loading
        for (i = 0; i < iGadgets; i++)
        {
            oGadget = aoGadget[i];
            oContainer = oGadget.oContainer;
            if (oContainer !== null)
            {
                // Set class and attach loading message
                oMessage = cbTag ("p", "Loading...");
                oMessage.id = oGadget.sContainer + "Message";
                oMessage.style.color = "DarkGreen";
                oContainer.className = gsCbNrClass;
                oContainer.appendChild (oMessage);

                // Create bookmark
                oMark = cbTag ("a", "(&uarr;)");
                oMark.name = oMark.id = oGadget.sContainer + "Mark";
                oMark.href = "#";
                oMark.title = "Go back to the top of the page...";

                // Find the widget heading
                oG = oContainer.parentNode; // Widget container section
                do
                {
                    oG = oG.previousSibling;
                }
                while ((oG !== null) && (oG.nodeType != 1));
                sMessage = " ";
                if (oGadget.sContainer === (gsCbNrIdPfx + "Race"))
                {
                    sMessage += goCbNrDate.getFullYear () + " ";
                }
                oMessage = document.createTextNode (sMessage);
                oG.appendChild (oMessage);
                oG.appendChild (oMark);
            }
        }
        
        // Now load the gadgets
        for (i = 0; i < iGadgets; i++)
        {
            oGadget = aoGadget[i];
            oContainer = oGadget.oContainer;
            
            // Attempt the load
            try
            {
                if (oContainer !== null)
                {
                    if (oGadget.pfnLoader !== null)
                    {
                        oG = oGadget.pfnLoader (oGadget);
                        if (oG !== null)
                        {
                            // Anchor the gadget onto the page
                            oContainer.appendChild (oG);
                        }
                    }

                    // Add bookmark reference to "Quick Links" gadget
                    oG = aoGadget[0];
                    if ((i > 0) && (oG.oContainer !== null))
                    {
                        oMark = cbTag ("a", oGadget.sDesc);
                        oMark.href = "#" + oGadget.sContainer + "Mark";
                        cbNrQuick (oG, oMark);
                    }
                }
            }
            catch (e)
            {
                sError += "Error with gadget '" + oGadget.sContainer + "'.<br />";
                if (oGadget.oContainer !== null)
                {
                    cbErrMsg ("Failed to load gadget!", e, null, oGadget.sContainer);
                }
            }
            
            // Clear message
            if (oContainer !== null)
            {
                oMessage = document.getElementById (oGadget.sContainer + "Message");
                if (oMessage !== null)
                {
                    oContainer.removeChild (oMessage);
                }
            }
        }

        // Report any errors
        if (sError !== "")
        {
            throw (sError);
        }
    }
    catch (e)
    {
        cbErrMsg ("Failed to load one or more gadgets!", e);
    }

    //////////////////////////////////////////
    // Priv: gadgetAdd
    // Desc: Add a new gadget to the array.
    // Args: psId -- Identifier (sans prefix).
    //       pfnL -- Loader function.
    //       psDe -- Description.
    //////////////////////////////////////////

    function gadgetAdd (psId, pfnL, psDe)
    {
        // Create new gadget and append it to the array
        oGadget = new gadget (psId, pfnL, psDe);
        aoGadget[iGadgets] = oGadget; iGadgets++;
        
        // Constructor
        function gadget (psC, pfn, psD)
        {
            this.sContainer = gsCbNrIdPfx + psC;
            this.oContainer = document.getElementById (this.sContainer);
            this.pfnLoader  = pfn;
            this.sDesc      = psD;
        }
        // End of gadget (constructor)
    }
    // End of gadgetAdd
}
// End of cbNrGadgets

////////////////////////////////////////
// Func: cbNrQuick
// Desc: Quick links to other gadgets.
// Args: oInfo -- Gadget information.
//       oAdd  -- Gadget to add link to.
// Retn: Gadget object reference.
////////////////////////////////////////

function cbNrQuick (oInfo)
{
    var oAdd, oGadget, oItem;

    // Create or reference list
    if (arguments.length > 1)
    {
        oAdd = arguments[1];
        oGadget = oInfo.oContainer.getElementsByTagName ("ul");
        oGadget = oGadget[0];
        oItem = cbTag ("li", oAdd);
        oItem.className = gsCbNrClass;
        oGadget.appendChild (oItem);
    }
    else
    {
        oGadget = cbTag ("ul");
        oGadget.className = gsCbNrClass;
    }

    return (oGadget);
}
// End of cbNrQuick

/////////////////////////////////////
// Func: cbNrWrl
// Desc: Writing Resource Links.
// Args: oInfo -- Gadget information.
// Retn: Gadget object reference.
/////////////////////////////////////

function cbNrWrl (oInfo)
{
    var oDB, aoExLink, oGadget, oList, iL, sCat, oLink, oItem, oGroup, oTable;

    // Identifier prefix
    var sIdPrefix = "Wrl";

    // Initialise database
    oDB = new cbExLinks ();
    aoExLink = oDB.aoLink;
    
    // Create gadget section
    oGadget = cbTag ("form");
    oGadget.action = "#";
    oGadget.className = gsCbNrClass;
    oGadget.style.height = "15em";
    
    // Drop-down list
    oList = cbTag ("select");
    oList.id = sIdPrefix + "List";
    oList.title = "Select a link from the drop-down list...";
    oList.onclick  = wrlSelect;
    oList.onkeyup  = wrlSelect;
    oList.onchange = wrlSelect;
    oGadget.appendChild (oList);

    // Prompt
    oItem = cbTag ("option", "Use drop-down list to select...");
    oItem.value = "-1";
    oList.appendChild (oItem);

    // Display table
    oTable = cbTable ();
    oTable.className = gsCbNrClass;
    oTable.style.width = "100%";
    oTable.style.marginBottom = "2px";
    tableRow ("Cat",   "Cat",    "Category of link.");
    tableRow ("Title", "Title",  "Title of page or article.");
    tableRow ("Auth",  "Author", "Author or owner of web site or blog.");
    tableRow ("Desc",  "Desc",   "Description and comments.");

    // Link
    oItem = tableRow ("Link", "Link", "Click and use clipboard to copy...");
    oLink = cbTag ("input");
    oLink.type = "text";
    oLink.readOnly = "readonly";
    oLink.id = sIdPrefix + "Url";
    oLink.onfocus = urlSelect;
    oLink.onclick = urlSelect;
    oLink.onkeyup = urlSelect;
    oItem.appendChild (oLink);

    // Add table to gadget
    oGadget.appendChild (oTable);

    // Table button
    oItem = cbTag ("input");
    oItem.type = "button";
    oItem.id = sIdPrefix + "Full";
    oItem.value = "Full Table...";
    oItem.title = "Click to display a full table of links in a separate window...";
    oItem.onclick = wrlWindow;
    //oItem.className = gsCbNrClass;
    oGadget.appendChild (oItem);

    // Iterate over links
    for (iL = 0, sCat = ""; iL < oDB.iCountLinks; iL++)
    {
        // Only interested in resources
        oLink = aoExLink[iL];
        if (oLink.sType == "Resource")
        {
            // New category?
            if (oLink.sCat != sCat)
            {
                // Create a new group and add it to the list
                sCat = oLink.sCat;
                oGroup = cbTag ("optgroup");
                oGroup.label = sCat;
                oList.appendChild (oGroup);
            }

            // Add new item to current group
            oItem = cbTag ("option", oLink.sName);
            oItem.value = String (iL);
            oGroup.appendChild (oItem);
        }
    }

    // Append a final items for credits
    oGroup = cbTag ("optgroup");
    oGroup.label = "Credits";
    oItem = cbTag ("option", "Code by Captain Black");
    oItem.value = "-1";
    oGroup.appendChild (oItem);
    oItem = cbTag ("option", "Most links by Calistro");
    oItem.value = "-1";
    oGroup.appendChild (oItem);
    oList.appendChild (oGroup);

    // Attach gadget to container (early, so element identifiers can be found)
    oInfo.oContainer.appendChild (oGadget);

    // Display initial information in the table
    wrlShow (-1);

    /////////////////////////////////////////////
    // Priv: tableRow
    // Desc: Create a new display table row.
    // Args: psPrompt -- Prompt text.
    //       psId     -- Identifier.
    //       psHover  -- Text for mouse hovering.
    // Retn: Last cell reference.
    /////////////////////////////////////////////

    function tableRow (psPrompt, psId, psHover)
    {
        var oCell;

        oTable.row ();
        oCell = oTable.cell ("th", psPrompt + ":");
        oCell.style.textAlign = "Right";
        oCell.style.width = "3em";
        oCell = oTable.cell ();
        oCell.id = sIdPrefix + psId;
        oCell.title = psHover;

        return (oCell);
    }
    // End of tableRow

    //////////////////////////////////////
    // Desc: Populate display elements.
    // Args: iL -- Item number to display.
    //////////////////////////////////////

    function wrlShow (iL)
    {
        var sText, oLink, oE, sLink, sURL, sTitle, oA;
        
        if (iL < 0)
        {
            // Calistro's details
            sURL = "http://writing-about-writing.blogspot.com";
            sLink = "<a title=\"Visit Calistro's blog...\" target=\"_blank\" ";
            sLink += "href=\"" + sURL + "\">Calistro</a>";
            sText =  "Thanks to " + sLink;
            sText += " and others for many of the links. <br />";
            sText += "Please report any issues to Captain Black.";

            // Captain's details
            sTitle = "Writing Resource Links";
            sURL = "http://kevinmachin.pwp.blueyonder.co.uk";

            oLink = oDB.add (sTitle, "Captain Black", "Resource", "Gadget Information", sURL, sText);
        }
        else
        {
            oLink = aoExLink[iL];
        }

        // Fill in category
        sText = "<b>" + oLink.sCat + "</b>";
        oE = document.getElementById (sIdPrefix + "Cat");
        oE.innerHTML = sText;

        // Fill in title
        sTitle =  "title=" + oLink.sName;
        sTitle += ": " + oLink.sDesc;
        oA = cbTag ("a", oLink.sName, "target=_blank", sTitle);
        oA.href = oLink.sURL;
        oE = document.getElementById (sIdPrefix + "Title");
        oE.innerHTML = "";
        oE.appendChild (oA);

        // Fill in author
        oE = document.getElementById (sIdPrefix + "Author");
        oE.innerHTML = oLink.sAuthor;

        // Fill in description
        if (oLink.oDesc === null)
        {
            sText = "";
        }
        else
        {
            sText = oLink.oDesc.innerHTML;
        }
        oE = document.getElementById (sIdPrefix + "Desc");
        oE.innerHTML = sText;

        // Fill in URL
        oE = document.getElementById (sIdPrefix + "Url");
        oE.value = oLink.sURL;
    }
    // End of wrlShow

    //////////////////////////////////
    // Priv: wrlSelect
    // Desc: Display selected details.
    // Args: oEvent -- Event details.
    //////////////////////////////////

    function wrlSelect (oEvent)
    {
        var iL, oList, oItem;

        try
        {
            // See which item is selected
            oEvent = cbEvent (oEvent);
            oList = document.getElementById (sIdPrefix + "List");

            // Remove initial entry if it's just the help instruction
            oItem = oList.options[0];
            if (oItem.value < 0)
            {
                oList.remove (0);
            }

            // Display details
            iL = parseInt (oList.value, 10);
            wrlShow (iL);
        }
        catch (e)
        {
            cbErrMsg ("Failed to display list item!", e, null, "WrlDesc");
        }
    }
    // End of wrlSelect

    ///////////////////////////////////////////////
    // Priv: urlSelect
    // Desc: Select all of the text in the URL box.
    // Args: oEvent -- Event details.
    ///////////////////////////////////////////////

    function urlSelect (oEvent)
    {
        var oBox;
        oEvent = cbEvent (oEvent);
        oBox = oEvent.target;
        oBox.select ();
    }
    // End of urlSelect

    /////////////////////////////
    // Priv: wrlWindow
    // Desc: Generate full table.
    /////////////////////////////
    
    function wrlWindow ()
    {
        var oW, sSite;

        // Create a new window for the table
        sSite = "http://www.kevinmachin.pwp.blueyonder.co.uk/";
        oW = window.open (sSite + "Resources/WRL/wrl_table.htm", "_blank");
    }
    // End of wrlWindow
    
    return (null);
}
// End of cbNrWrl

//////////////////////////////////////////
// Func: cbNrRace
// Desc: Render the race gadget.
// Args: oInfo   -- Gadget information.
//       iWidth  -- Max width for display.
//////////////////////////////////////////

function cbNrRace (oInfo)
{
    var oGadget, aoEntry;

    // Handle parameters
    var iWidth = (arguments.length > 1) ? arguments[1] : 218;

    // Meter bar sizes
    var iBarW = 50, iBarH = 8;

    // Some "constants"
    var sGadgetId = gsCbNrIdPfx + "Race";   // Gadget identifier
    var sCellId   = gsCbNrIdPfx + "Cell_";  // Cell identifier prefix

    // Display modes
    var sDisplayMode = "Bars", iDisplayYear = goCbNrDate.getFullYear ();

    // Set intro message
    setIntro ();
    
    // Create gadget
    oGadget = cbTag ("div");

    // Get information from the database
    readData (goCbNrDate.getFullYear ());

    // Build and display the controls and table
    controlsBuild ();
    tableBuild ();
    oInfo.oContainer.appendChild (oGadget);
    tableFill ();

    ///////////////////////////////////////////////////////
    // Func: setIntro
    // Desc: Set up the Captain's intro and e-mail address.
    ///////////////////////////////////////////////////////

    function setIntro ()
    {
        var s, oE, oB;

        // Create introduction paragraph
        s = "To add or update your race entry details, please ";
        oE = cbTag ("p", s);
        oE.className = gsCbNrClass;
        oE.style.marginTop = "0";
        oB = cbTag ("input", "type=button");
        oB.value = "send e-mail";
        oB.title = "Send an e-mail to the Captain...";
        oB.onclick = eMail;
        oE.appendChild (oB);
        s = document.createTextNode (" to Captain Black.");
        oE.appendChild (s);

        // Attach paragraph
        oInfo.oContainer.appendChild (oE);

        ///////////////////////////////////////////////////////////////
        // Priv: eMail
        // Desc: Create an e-mail for users to send their race details.
        // Args: oEvent -- Event details.
        ///////////////////////////////////////////////////////////////

        function eMail (oEvent)
        {
            var sMsg;

            // Form message text
            sMsg =  "Please fill in your race entry details below...%0A%0A";
            sMsg += "             Name of author:%20%0A";
            sMsg += "              Title of book:%20%0A";
            sMsg += "         Current word count:%20%0A";
            sMsg += "Expected maximum word count:%20%0A";
            sMsg += "  Finish date (if finished):%20%0A%0A";

            // Create message
            cbEMail ("Novel Race Details", sMsg);
        }
        // End of eMail
    }
    // End of setIntro

    //////////////////////////////////////////////
    // Func: readData
    // Desc: Get data from the supplied data page.
    // Args: iYear -- Year of data to retrieve.
    //////////////////////////////////////////////

    function readData (iYear)
    {
        var iD, iA, iY, aoE, oE;

        // Create database instance
        var oDB = new nrDatabase ("entries");

        // Sift through, selecting only the year specified and works still in progress
        aoEntry = new Array;
        aoE = oDB.aoEntry;
        for (iD = iA = 0; iD < oDB.iCountEntries; iD++)
        {
            oE = aoE[iD];
            iY = oE.oDate.getFullYear ();
            if ((iY === iYear) || (iY === 3000)) // 3000 means "WiP"
            {
                aoEntry[iA] = oE; iA++;
            }
        }

        // Sort data into order
        aoEntry.sort (compare);

        // Priv: Comparison function for sortation
        function compare (oA, oB)
        {
            var iResult;

            // Completion dates
            iResult = oA.oDate - oB.oDate;
            if (iResult === 0)
            {
                // Percentage completed
                iResult = oB.iPercent - oA.iPercent;
            }

            return (iResult);
        }
    }

    ///////////////////////////////////////////
    // Priv: controlsBuild
    // Desc: Build the controls for the gadget.
    ///////////////////////////////////////////

    function controlsBuild ()
    {
        var oBox, oList, oItem, iY, sHover;

        // Create control box
        oBox = cbTag ("form");
        oBox.className = gsCbNrClass;
        oBox.style.marginTop = "1em";
        oBox.action = "#";
        sHover = "Select which race year you want to display...";
        oItem = cbTag ("p", "Race year: ");
        oItem.title = sHover;
        oItem.className = gsCbNrClass;
        oItem.style.margin = "0 0 0.5em 0";
        oItem.style.textAlign = "Center";
        oBox.appendChild (oItem);

        // Drop-down list to select year of interest
        oList = cbTag ("select");
        oList.value = "-1";
        oList.id = sGadgetId + "Year";
        oList.title = sHover;
        oList.onclick  = handleYear;
        oList.onchange = handleYear;
        oList.onkeyup  = handleYear;
        oItem.appendChild (oList);

        // Populate list
        for (iY = goCbNrDate.getFullYear (); iY >= 2009; iY--)
        {
            oItem = cbTag ("option", iY);
            oItem.value = iY;
            oList.appendChild (oItem); 
        }

        // Radio buttons for author/title
        radio (oBox, "Bars", "Bars",  "Display progress bars and percentages.",   true);
        radio (oBox, "Wrds", "Words", "Display word counts and expected maxima.", false);
        radio (oBox, "Date", "Dates", "Display finishing dates.",                 false);

        // Attach control box to gadget
        oGadget.appendChild (oBox);

        /////////////////////////////////////////////////
        // Priv: radio
        // Desc: Create a radio button.
        // Args: oContainer -- Parent element.
        //       psId       -- Button's ID.
        //       psLabel    -- Label text.
        //       psTitle    -- Text for hover-over title.
        //       bChecked   -- Buttons status.
        /////////////////////////////////////////////////

        function radio (oContainer, psId, psLabel, psTitle, bChecked)
        {
            var oRadio, oLabel;

            // Build the radio button
            oRadio = cbTag ("input", "type=radio");
            oRadio.className = gsCbNrClass;
            oRadio.id = sGadgetId + psId;
            oRadio.name = sGadgetId + "Mode";
            oRadio.title = psTitle;
            oRadio.checked = bChecked;
            oRadio.onchange = handleRadio;
            oRadio.onclick  = handleRadio;
            oRadio.style.margin = "0 0 0 1em";

            // Build label
            oLabel = cbTag ("label", psLabel, "for=" + sGadgetId + psId);
            oLabel.className = gsCbNrClass;
            oLabel.title = psTitle;
            oLabel.style.margin = "0 0 0 0.2em";

            // Put radio button and label into the container
            oContainer.appendChild (oRadio);
            oContainer.appendChild (oLabel);
        }
        // End of radio
    }
    // End of controlsBuild

    /////////////////////////////////////////
    // Priv: tableBuild
    // Desc: Build the table of race entries.
    /////////////////////////////////////////

    function tableBuild ()
    {
        var oEntry, oE, oT, oC, i, s;

        // Does the table already exist?
        s = sGadgetId + "Table";
        oT = document.getElementById (s);
        if (oT !== null)
        {
            // Destroy existing table
            oE = oT.parentNode;
            oE.removeChild (oT);
        }

        // Build table with data
        oT = cbTable ();
        oT.className = gsCbNrClass;
        oT.style.margin = "0.5em 0 0 0";
        oT.id = s;

        // Heading row
        oT.row ();
        oC = oT.cell ("th", "Racer/Book");
        cellAttr (oC, "", "");
        oC = oT.cell ("th");
        oC.colSpan = "2";
        cellAttr (oC, "0", "");

        // Warn if no data were found
        if (aoEntry.length === 0)
        {
            cbErrMsg ("Warning - No data found!", "", null, sGadgetId);
        }

        // Iterate over data items but just create empty cells for the right hand side
        for (i = 0; i < aoEntry.length; i++)
        {
            // New row
            oT.row ();

            // Cells for racer's details
            oEntry = aoEntry[i];
            oC = cellEntry (oT, i + 1, oEntry);
            cellAttr (oC, "", "");

            // Percentage
            oC = oT.cell ();
            cellAttr (oC, "P_" + i, "int");

            // Progress bar
            oE = cbBar (oEntry.iVal, oEntry.iMax, iBarW, iBarH);
            oC = oT.cell ();
            cellAttr (oC, "B_" + i, "int");
        }

        // Attach table
        oGadget.appendChild (oT);

        //////////////////////////////////////////////////
        // Priv: cellEntry
        // Desc: Generate racer name/book entry.
        // Args: oTable -- Table reference.
        //       iPosn  -- Position in race. Zero if none.
        //       oEntry -- Race entry.
        // Retn: Cell reference.
        //////////////////////////////////////////////////

        function cellEntry (oTable, iPosn, oEntry)
        {
            var oCell, sHover, sText, oRacer, oTag;

            // Get racer and book title details
            oRacer = oEntry.oRacer;
            if (oRacer === null)
            {
                sText = "{" + oEntry.sCode + "}";
            }
            else
            {
                sText = oRacer.asNickName[0];
            }
            sText += ": " + oEntry.sTitle;

            // Create hover-over effect
            sHover = "title=" + iPosn + ". " + sText;

            // Work-around for IE8 bug where max-width and overflow CSS settings don't work.
            // TODO: Revisit when/if the bug is resolved by Microsoft.
            if (sText.length >= 30)
            {
                sText = sText.slice (0, 27) + "..";
            }

            // Create cell
            oTag = cbTag ("span", sText, sHover);
            oCell = oTable.cell (oTag);

            return (oCell);
        }
        // End of cellEntry

        ///////////////////////////////////////
        // Priv: cellAttr
        // Desc: Set a table cell's attributes.
        // Args: oCell   -- Cell to alter.
        //       psId    -- Identifier.
        //       psClass -- Class suffix.
        ///////////////////////////////////////

        function cellAttr (oCell, psId, psClass)
        {
            // Specific attributes
            if (psId === "")
            {
                oCell.style.whiteSpace = "nowrap";
                oCell.style.overflow = "hidden";
                oCell.style.maxWidth = "132px";
                oCell.style.width    = "132px";
            }
            else
            {
                oCell.id = sCellId + psId;
                if (psId.slice (0, 1) === "B")
                {
                    oCell.style.verticalAlign = "Middle";
                }
            }

            if ((psId === "") || (psId === "0"))
            {
                oCell.style.fontFamily = "Tahoma, Sans-Serif";
                oCell.style.textAlign = "Left";
            }
            else
            {
                oCell.style.fontFamily = "\"Courier New\", Monospace";
                oCell.style.textAlign = "Right";
            }

            // Common attributes
            oCell.className = gsCbNrClass + psClass;
            oCell.style.fontSize = "7pt";
        }
        // End of cellAttr
    }
    // End of tableBuild

    /////////////////////////////////////
    // Priv: tableFill
    // Desc: Fill in the table with data.
    /////////////////////////////////////

    function tableFill ()
    {
        var oP, oB, sVal, i, oEntry, oBar, oDate, sDate, s;

        // Fill in the heading row
        oP = document.getElementById (sCellId + "0");
        switch (sDisplayMode)
        {
        case "Bars":
            sVal = "Progress";
            break;
        case "Wrds":
            sVal = "Words/Max";
            break;
        case "Date":
            sVal = "Finish Date";
            break;
        default:
            sVal = "Error!";
            break;
        }
        oP.innerHTML = sVal;

        // Now fill in the other rows
        for (i = 0; i < aoEntry.length; i++)
        {
            oEntry = aoEntry[i];
            oP = document.getElementById (sCellId + "P_" + i);
            oB = document.getElementById (sCellId + "B_" + i);
            switch (sDisplayMode)
            {
            case "Bars":
                oP.innerHTML = oEntry.iPercent + "%";
                oBar = cbBar (oEntry.iVal, oEntry.iMax, iBarW, iBarH);
                oB.innerHTML = "";
                oB.appendChild (oBar);
                break;
            case "Wrds":
                oP.innerHTML = oEntry.sVal;
                oB.innerHTML = oEntry.sMax;
                break;
            case "Date":
                oDate = oEntry.oDate;
                if (oDate.getFullYear () >= 3000)
                {
                    oP.innerHTML = "";
                    oB.innerHTML = "WiP";
                }
                else
                {
                    s = String (oDate.getDate ());
                    if (s.length < 2)
                    {
                        s = "0" + s;
                    }
                    sDate = s + "/";
                    s = String (oDate.getMonth () + 1);
                    if (s.length < 2)
                    {
                        s = "0" + s;
                    }
                    sDate += s + "/";
                    s = String (oDate.getFullYear ());
                    sDate += s.slice (2);
                    oP.innerHTML = sDate;
                    oB.innerHTML = "";
                }
                break;
            default:
                oP.innerHTML = "Error!";
                oB.innerHTML = "Error!";
                break;
            }
        }
    }
    // End of tableFill

    //////////////////////////////////////////////
    // Priv: handleYear
    // Desc: Event handler for race year selector.
    // Args: oEvent -- Event details.
    //////////////////////////////////////////////

    function handleYear (oEvent)
    {
        try
        {
            oEvent = cbEvent (oEvent);
            processYear (oEvent);
        }
        catch (e)
        {
            cbErrMsg ("Failed to select race year!", e, null, sGadgetId);
        }

        // Handler
        function processYear (oEvent)
        {
            var oL, sVal, iY;

            // Find list and get the value
            oL = document.getElementById (sGadgetId + "Year");
            sVal = oL.value;
            iY = parseInt (sVal, 10);

            // Rebuild table with new year if necessary
            if (iY !== iDisplayYear)
            {
                iDisplayYear = iY;
                readData (iDisplayYear);
                tableBuild ();
                tableFill ();
            }
        }
        // End of processYear
    }
    // End of handleYear

    /////////////////////////////////////////
    // Priv: handleRadio
    // Desc: Event handler for radio buttons.
    // Args: oEvent -- Event details.
    /////////////////////////////////////////

    function handleRadio (oEvent)
    {
        try
        {
            oEvent = cbEvent (oEvent);
            processRadio (oEvent);
        }
        catch (e)
        {
            cbErrMsg ("Failed to update race table!", e, null, sGadgetId);
        }

        // Handler
        function processRadio (oEvent)
        {
            var i, sRad, oRad, sMode = "";
            var asRad = new Array ("Bars", "Wrds", "Date");

            // See which radio button is selected
            for (i = 0; i < asRad.length; i++)
            {
                sRad = asRad[i];
                oRad = document.getElementById (sGadgetId + sRad);
                if (oRad.checked)
                {
                    sMode = sRad;
                    break;
                }
            }

            // Refill the table if necessary
            if (sMode !== sDisplayMode)
            {
                sDisplayMode = sMode;
                tableFill ();
            }
        }
        // End of processRadio
    }
    // End of handleRadio
    
    // Return null as the gadget has already been attached
    return (null);
}
// End of cbNrRace

/////////////////////////////////////////
// Func: cbNrList
// Desc: Create the list of Novel Racers.
// Args: oInfo -- Gadget information.
// Retn: Gadget object reference.
/////////////////////////////////////////

function cbNrList (oInfo)
{
    var oGadget = null, bCompact = true;
    var sGadgetId = "CbNrBgList";
    var oContainer = oInfo.oContainer;

    // Create the options controls
    controls ();

    // Display the list
    list ();

    /////////////////////////////////////
    // Priv: controls
    // Desc: Create the options controls.
    /////////////////////////////////////

    function controls ()
    {
        var oE, oP;

        // Create paragraph to hold controls
        oP = cbTag ("p");
        oP.className = gsCbNrClass;
        oP.style.margin = "0 0 0 1em";
        oP.title = "Toggle between compact display and one-per-line display.";

        // Checkbox for compact/expanded display
        oE = cbTag ("input", "type=checkbox");
        oE.id = sGadgetId + "Compact";
        oE.name = sGadgetId + "_Compact";
        oE.checked = bCompact;
        oE.onchange = compactEvent;
        oE.onclick  = compactEvent;
        oP.appendChild (oE);

        // Label for checkbox
        oE = cbTag ("label", "for=" + sGadgetId + "Compact", "Compact");
        oE.style.marginLeft = "0.2em";
        oP.appendChild (oE);

        // Attach paragraph to anchor point
        oContainer.appendChild (oP);
    }
    // End of controls

    /////////////////////////
    // Priv: list
    // Desc: Render the list.
    /////////////////////////

    function list ()
    {
        var oDB, oSec, oE, i, iSec, oR, sCat;
        var aoCat = new Array, aoSec = new Array, aoRacer;

        // Create database instance
        oDB = new nrDatabase ();
        aoRacer = oDB.aoRacer;

        // (Re)create gadget element
        kill ();
        oGadget = cbTag ("div");

        // Set up elements for the different categories
        aoCat[0] = new category ("c", "Current Racers", oDB.iCountCurrent);
        aoCat[1] = new category ("w", "Waiting List",   oDB.iCountWaiting);
        aoCat[2] = new category ("s", "On Sabbatical",  oDB.iCountSabbatical);
        aoCat[3] = new category ("a", "Alumni",         oDB.iCountAlumni);

        // Category headings
        for (i = 0; i < aoCat.length; i++)
        {
            // Any members in this category?
            if (aoCat[i].iCount > 0)
            {
                // Heading
                oE = cbTag ("h4", aoCat[i].sTitle);
                oE.className = gsCbNrClass;
                oGadget.appendChild (oE);

                // Section to receive list
                oSec = cbTag ("p");
                oSec.className = gsCbNrClass;

                // Save and attach
                aoSec[i] = oSec;
                oGadget.appendChild (oSec);
            }
        }

        // Sort racers into alphabetical order
        aoRacer.length = oDB.iCountRacers; // Lop off "developer"
        aoRacer.sort (compare);

        // Iterate over all racers
        for (i = 0; i < oDB.iCountRacers; i++)
        {
            // Get racer details
            oR = aoRacer[i];

            // Which list?
            sCat = oR.cStatus.toLowerCase ();
            oSec = null;
            for (iSec = 0; iSec < aoCat.length; iSec++)
            {
                if (sCat === aoCat[iSec].cCode)
                {
                    oSec = aoSec[iSec];
                    break;
                }
            }

            // Found section?
            // If not then racer is not in one of the given categories!
            if (oSec !== null)
            {
                // Add separator
                if (oSec.childNodes.length > 0)
                {
                    if (bCompact)
                    {
                        oE = document.createTextNode (", ");
                    }
                    else
                    {
                        oE = cbTag ("br");
                    }
                    oSec.appendChild (oE);
                }

                // Add racer to list
                if (sCat.slice (0, 1) === "w")
                {
                    oE = document.createTextNode (oR.sCode + ". ");
                    oSec.appendChild (oE);
                }
                oSec.appendChild (oR.nickLink ());
            }
        }

        // Attach gadget to anchor point
        oContainer.appendChild (oGadget);

        /////////////////////////////////////////////////
        // Priv: category
        // Desc: Contructor for categories.
        // Args: psCode -- Code character.
        //       psTitle -- Title for heading.
        //       iCount  -- Number of racers in category.
        // Retn: Object reference.
        /////////////////////////////////////////////////

        function category (psCode, psTitle, iCount)
        {
            this.cCode  = psCode;
            this.sTitle = psTitle + " (" + iCount + ")";
            this.iCount = iCount;
        }
        // End of category

        //////////////////////////////////////////////////////
        // Priv: compare
        // Desc: Racer comparison for sortation.
        // Args: Two racer objects to compare.
        // Retn: Positive or negative according to sort order.
        //////////////////////////////////////////////////////

        function compare (oA, oB)
        {
            var iResult = 0, cStatusA, cStatusB;

            // Compare status
            cStatusA = oA.cStatus.toLowerCase ();
            cStatusB = oB.cStatus.toLowerCase ();
            iResult = cbStrComp (cStatusA, cStatusB);
            if (iResult === 0)
            {
                if (cStatusA == "w")
                {
                    iResult =  parseInt (oA.sCode, 10);
                    iResult -= parseInt (oB.sCode, 10);
                }
            }
            if (iResult === 0)
            {
                iResult = cbStrComp (oA.asNickName[0], oB.asNickName[0]);
            }

            return (iResult);
        }
        // End of compare
    }
    // End of list

    /////////////////////////////
    // Priv: kill
    // Desc: Get rid of the list.
    /////////////////////////////

    function kill ()
    {
        // Remove gadget from anchor point
        if (oGadget !== null)
        {
            oContainer.removeChild (oGadget);
            oGadget = null;
        }
    }
    // End of kill

    ///////////////////////////////////////////////////
    // Priv: compactEvent
    // Desc: Event handler for compact display changes.
    ///////////////////////////////////////////////////

    function compactEvent ()
    {
        // See if we want compact display or single line per racer
        var oE = document.getElementById (sGadgetId + "Compact");
        bCompact = oE.checked;

        // Redisplay
        list ();
    }
    // End of compactEvent

    return (oGadget);
}
// End of cbNrList

//////////////////////////////////////////
// Func: cbNrRota
// Desc: Novel Racers' Coffee Rota gadget.
// Args: oInfo -- Gadget information.
// Retn: Gadget object reference.
//////////////////////////////////////////

function cbNrRota (oInfo)
{
    var oGadget, oDB, aoRota = new Array;
    var oToday = new Date ();

    var sGadgetId = gsCbNrIdPfx + "Rota";

    // Load
    readData ();
    render ();

    ////////////////////////////////////
    // Priv: readData
    // Desc: Read data from NR database.
    ////////////////////////////////////

    function readData ()
    {
        var i, iC, iP, iN, iR = 0, aoP;

        // Load database
        oDB = new nrDatabase ("coffee");
        aoP = oDB.aoCoffee;
        iN = oDB.iCountCoffeePost;

        // Include the last few posted coffee breaks
        iC = 4;
        iP = iN - iC;
        if (iP < 0)
        {
            iP = 0;
        }
        for (i = 0; i < iC; i++, iP++, iR++)
        {
            aoRota[iR] = aoP[iP];
        }

        // Add the rota items
        iC = iN + oDB.iCountCoffeeRota;
        for (i = iN; i < iC; i++, iR++)
        {
            aoRota[iR] = aoP[i];
        }
    }
    // End of readData

    ///////////////////////////
    // Priv: render
    // Desc: Render the gadget.
    ///////////////////////////

    function render ()
    {
        var i, oT, oC, oP, oD, oH, tH;

        // Create a table for the rota
        oT = cbTable ();
        oT.className = gsCbNrClass;

        // Table heading
        oT.row ();
        oC = oT.cell ("th", "Date");
        oC = oT.cell ("th", "Host");

        // Render the data into the table
        for (i = 0; i < aoRota.length; i++)
        {
            // Get post and date
            oP = aoRota[i];
            oD = oP.oDate;

            // Date
            oT.row ();
            oC = cell (oP.sDate);
            oC.className = gsCbNrClass + "int";
            oC.style.textAlign = "Left";
            if (oD < oToday)
            {
                // Faded colour for days gone by
                oC.style.color = "#D0D0D0";
            }
            else if (cbDebugMask () && (oD.getDay () !== 5))
            {
                // Alert color for non-Fridays
                oC.style.color = "Red";
                oC.title = "Please note that this is not a Friday.";
            }

            // Host
            oH = oP.oHostRacer;
            if (oH === null)
            {
                tH = oP.sTitle;
                if (tH === "")
                {
                    tH = "{TBA}";
                }
            }
            else
            {
                tH = oH.nickLink ();
            }
            oC = cell (tH);
        }

        // Add warning if there are too few on the rota
        if (oDB.iCountCoffeeRota < 5)
        {
            oT.row ();
            oC = oT.cell ("Help! We need volunteers.");
            oC.className = gsCbNrClass;
            oC.colSpan = 2;
            oC.style.color = "Purple";
            oC.style.fontWeight = "Bold";
            oC.style.textDecoration = "Blink";
            oC.style.textAlign = "Center";
        }

        // set gadget
        oGadget = oT;

        //////////////////////////////////////
        // Priv: cell
        // Desc: Create a table cell.
        // Args: tData   -- Data to put in cell.
        //       psClass -- Class suffix.
        // Retn: Cell reference.
        //////////////////////////////////////

        function cell (tData)
        {
            var oCell, sClass = gsCbNrClass;
            var psClass = (arguments.length > 1) ? arguments[1] : "";

            // Create cell
            oCell = oT.cell (tData);

            // Set class and styles
            if (psClass !== "")
            {
                sClass += psClass;
            }
            oCell.className = sClass;
            //oCell.style.fontSize = "80%";

            return (oCell);
        }
        // End of cell
    }
    // End of render
    
    return (oGadget);
}
// End of cbNrRota

/////////////////////////////////////
// Func: cbNrCoffee
// Desc: Coffee breaks gadget.
// Args: oInfo -- Gadget information.
// Retn: Gadget object reference.
/////////////////////////////////////

function cbNrCoffee (oInfo)
{
    var oGadget, oDB, oList, oTable, i;
    var sIdPrefix = gsCbNrIdPfx + "Coffee";

    // Gadget version numbers
    var sGadgetVer = "1.07";

    // Names of the months
    var asMonth = new Array ("January","February","March","April","May","June","July","August","September","October","November","December");

    // Create the gadget
    oGadget = cbTag ("form");
    oGadget.action = "#";
    oGadget.className = gsCbNrClass;
    oGadget.style.height = "12em";

    // Create drop-down list
    oList = cbTag ("select");
    oList.id = sIdPrefix + "Select";
    oList.title = "Select a previous coffee break from the drop-down list...";
    oList.onkeyup  = coffeeSelect;
    oList.onchange = coffeeSelect;
    oGadget.appendChild (oList);

    // Table for display
    oTable = cbTable ();
    oTable.className = gsCbNrClass;
    oTable.style.width = "100%";
    oTable.style.marginBottom = "2px";

    // Rows
    tableRow ("Date",    "Date of coffee break posting.");
    tableRow ("Author",  "Coffee break host.");
    tableRow ("Title",   "Title of coffee break posting.");
    tableRow ("Subject", "Brief description of coffee break subject.");
    oGadget.appendChild (oTable);

    // Buttons
    button ("Table", "Click to display a full table of coffee break postings, in a separate window...");
    button ("Graph", "Click to display a graph table of coffee break hosts, in a separate window...");

    // Initialise the database
    oDB = new nrDatabase ("coffee");

    // Populate the drop-down list
    coffeeList ();

    // Attach the gadget to the page (early, so that population can occur)
    oInfo.oContainer.appendChild (oGadget);

    // Populate table with topmost list entry
    i = oDB.iCountCoffeePost;
    if (i > 0)
    {
        i--;
    }
    coffeePopulate (i);

    //////////////////////////////////////////////////
    // Priv: tableRow
    // Desc: Add a row to the display table.
    // Args: psText  -- Text for prompt and identifier.
    //       psTitle -- Text for "hover over".
    //////////////////////////////////////////////////

    function tableRow (psText, psTitle)
    {
        var oCell;

        oTable.row ();
        oCell = oTable.cell ("th", psText + ":");
        oCell.style.textAlign = "Right";
        oCell.style.width = oCell.style.maxWidth = "42px";
        oCell.style.fontSize = "7pt";
        oCell = oTable.cell ();
        oCell.id = sIdPrefix + psText;
        oCell.title = psTitle;
        oCell.style.fontSize = "7pt";
    }
    // End of tableRow

    ////////////////////////////////////////////
    // Priv: button
    // Desc: Create a pushbutton.
    // Args: psText  -- Text to go inside button.
    //       psTitle -- "Hover" text.
    ////////////////////////////////////////////

    function button (psText, psTitle)
    {
        var oButton;

        oButton = cbTag ("input");
        oButton.type = "button";
        oButton.id = sIdPrefix + psText;
        oButton.value = psText + "...";
        oButton.title = psTitle;
        oButton.onclick = coffeeWindow;
        oButton.style.marginLeft = "2px";
        oGadget.appendChild (oButton);
    }
    // End of button

    ///////////////////////////////////////////
    // Priv: coffeeList
    // Desc: Fill in the drop-down list values.
    ///////////////////////////////////////////

    function coffeeList ()
    {
        var oGroup, oItem, i, iP, iC, oPost, iMonth;

        // Iterate over postings
        //oList = document.getElementById (sIdPrefix + "Select");
        iC = oDB.iCountCoffeePost;
        for (i = 0, iP = iC, iMonth = -1; i < iC; i++)
        {
        	iP--;
            oPost = oDB.aoCoffee[iP];
            if (oPost.oDate.getMonth () != iMonth)
            {
                iMonth = oPost.oDate.getMonth ();
                oGroup = document.createElement ("optgroup");
                oGroup.label = asMonth[iMonth] + " " + oPost.oDate.getFullYear ();
                oList.appendChild (oGroup);
            }

            // Add list item
            oItem = document.createElement ("option");
            oItem.value = String (iP);
            oItem.innerHTML = oPost.sDate + " " + oPost.sTitle;
            oGroup.appendChild (oItem);
        }

        // Terminate data list
        if (oDB.iCountCoffeePost > 0)
        {
            oGroup = document.createElement ("optgroup");
            oGroup.label = "Information";
            oList.appendChild (oGroup);

            oItem = document.createElement ("option");
            oItem.value = "-1";
            oItem.innerHTML = "Gadget version: " + sGadgetVer;
            oGroup.appendChild (oItem);

            oItem = document.createElement ("option");
            oItem.value = "-1";
            oItem.innerHTML = "Database version: " + oDB.sVersionCode;
            oGroup.appendChild (oItem);

            oItem = document.createElement ("option");
            oItem.value = "-1";
            oItem.innerHTML = "Data updated: " + oDB.sVersionData;
            oGroup.appendChild (oItem);
        }
        else
        {
            // Warn user about lack of data
            oItem = oList.options[0];
            oItem.innerHTML = "No data available!";
        }
    }
    // End of coffeeList

    ///////////////////////////
    // Priv: coffeePopulate
    // Desc: Fill in fields.
    // Args: iP -- Post number.
    ///////////////////////////

    function coffeePopulate (iP)
    {
        var oP, oE, oH;

        // Fill in date
        oP = oDB.aoCoffee[iP];
        if (oP === null)
        {
            return;
        }
        oE = document.getElementById (sIdPrefix + "Date");
        oE.innerHTML = oP.sDate + " (#" + oP.iNumber + ")";

        // Fill in author
        oE = document.getElementById (sIdPrefix + "Author");
        oH = oP.oHostRacer;
        if (oH === null)
        {
            oE.innerHTML = "{Unknown}";
        }
        else
        {
            oE.innerHTML = "";
            oE.appendChild (oH.nickLink ());
        }

        // Fill in title
        oE = document.getElementById (sIdPrefix + "Title");
        oE.innerHTML = "";
        oE.appendChild (oP.oLink);

        // Fill in subject
        oE = document.getElementById (sIdPrefix + "Subject");
        oE.innerHTML = oP.sSubject;
    }
    // End of coffeePopulate

    //////////////////////////////////////////
    // Priv: coffeeSelect
    // Desc: Event handler for drop-down list.
    // Args: oEvent -- Event details.
    //////////////////////////////////////////

    function coffeeSelect (oEvent)
    {
        var oE, iP;

        // Which one was selected?
        oE = document.getElementById (sIdPrefix + "Select");
        if (oE !== null)
        {
            iP = parseInt (oE.value, 10);

            // Fill in
            coffeePopulate (iP);
        }
    }
    // End of coffeeSelect

    ///////////////////////////////////
    // Priv: coffeeWindow
    // Desc: Event handler for buttons.
    // Args: oEvent -- Event details.
    ///////////////////////////////////

    function coffeeWindow (oEvent)
    {
        var sId, i, sUrl;

        // Get identifier of button
        oEvent = cbEvent (oEvent);
        sId = oEvent.target.id;
        i = sIdPrefix.length;
        sId = sId.slice (i);

        // Open window according to which button was clicked
        sUrl =  "http://www.kevinmachin.pwp.blueyonder.co.uk/Resources/NRP/nrc_";
        sUrl += sId.toLowerCase () + ".htm";
        window.open (sUrl, "_blank");
    }
    // End of coffeeWindow

    return (null);
}
// End of cbNrCoffee

////////////////////////////////////////////////
// Func: cbNrFeedback
// Desc: Gadget to allow users to send feedback.
// Args: oInfo -- Gadget information.
// Retn: Gadget object reference.
////////////////////////////////////////////////

function cbNrFeedback (oInfo)
{
    var oForm, oPage, oButton;

    // Create a form for the controls
    oForm = cbTag ("form", "action=#");

    // Create button
    oButton = cbTag ("input", "type=button");
    oButton.value = "Send feedback...";
    oButton.title = "Click to send a feedback e-mail to Captain Black...";
    oButton.onclick = feedBack;
    oButton.style.margin = "1em 0 0 1em";
    oForm.appendChild (oButton);

    // Attach form to page
    oPage = oInfo.oContainer;
    oPage.innerHTML = "";
    //oPage.appendChild (oForm);

    //////////////////////////////////
    // Priv: feedBack
    // Desc: Create a feedback e-mail.
    // Args: oEvent -- Event details.
    //////////////////////////////////

    function feedBack (oEvent)
    {
        var sMsg, oN;

        // Issue/feedback details
        sMsg =  "Issue/Feedback Details%0A";
        sMsg += "----------------------%0A";
        sMsg += "Please enter the details of your feedback below. ";
        sMsg += "If it's regarding a problem issue, then please include the following:%0A%0A";
        sMsg += "1. Description of the issue.%0A";
        sMsg += "2. Steps needed to reproduce the issue.%0A";
        sMsg += "3. Expected behaviour and results.%0A";
        sMsg += "4. Actual behaviour and results.%0A%0A";

        sMsg += "Your feedback here...%0A%0A";

        sMsg += "Please leave the following system information intact, ";
        sMsg += "which can be useful for diagnosing browser-specific issues.%0A%0A";

        // System information
        sMsg += "System Information%0A";
        sMsg += "------------------%0A";
        oN = navigator;
        sMsg += "   Browser: " + oN.appName + " (" + oN.appCodeName + ") " + oN.appVersion + "%0A";
        sMsg += "  Platform: " + oN.platform + "%0A";
        sMsg += "User agent: " + oN.userAgent + "%0A";
        sMsg += "   Cookies: " + (oN.cookieEnabled   ? "Enabled" : "Disabled") + "%0A";
        sMsg += "      Java: " + (oN.javaEnabled ()  ? "Enabled" : "Disabled") + "%0A";
      //sMsg += "Data taint: " + (oN.taintEnabled () ? "Enabled" : "Disabled") + "%0A";

        // Generate the e-mail
        cbEMail ("Novel Racers gadget feedback", sMsg);
    }
    // End of feedBack

    // Gadget already attached, so return null
    return (oForm);
}
// End of cbNrFeedback
/* End of nr_gadgets.js */
