//
//  (c) Copyright 2004-2010 by Percipient Reverence
//
//       ------------ dBoard.js -------------
//
//  JavaScript Code for dBoard

var uniqueValue = 0;

// Browser independent window position functions from O'Reilly Javascript book,
// page 276.  This appears to be verbatim from the book, found it posted on the
// web.  I've changed the comments over to C++ style.

// Geometry.js: portable functions for querying window and document geometry
//
// This module defines functions for querying window and document geometry.
// 
// getWindowX/Y(): return the position of the window on the screen
// getViewportWidth/Height(): return the size of the browser viewport area
// getDocumentWidth/Height(): return the size of the document.
// getHorizontalScroll(): return the position of the horizontal scrollbar
// getVerticalScroll(): return the position of the vertical scrollbar
//
// Note that there is no portable way to query the overall size of the 
// browser window, so there are no getWindowWidth/Height() functions.
// 
// IMPORTANT: This module must be included in the <body> of a document
//            instead of the <head> of the document.
//
// NOTE: Yet it seems to work just fine here in this Javascript source file.

var Geometry = {};

if (window.screenLeft) { // IE and others
    Geometry.getWindowX = function() { return window.screenLeft; };
    Geometry.getWindowY = function() { return window.screenTop; };
}
else if (window.screenX) { // Firefox and others
    Geometry.getWindowX = function() { return window.screenX; };
    Geometry.getWindowY = function() { return window.screenY; };
}

if (window.innerWidth) { // All browsers but IE
    Geometry.getViewportWidth = function() { return window.innerWidth; };
    Geometry.getViewportHeight = function() { return window.innerHeight; };
    Geometry.getHorizontalScroll = function() { return window.pageXOffset; };
    Geometry.getVerticalScroll = function() { return window.pageYOffset; };
}
else if (document.documentElement && document.documentElement.clientWidth) {
    // These functions are for IE6 when there is a DOCTYPE
    Geometry.getViewportWidth =
        function() { return document.documentElement.clientWidth; };
    Geometry.getViewportHeight = 
        function() { return document.documentElement.clientHeight; };
    Geometry.getHorizontalScroll = 
        function() { return document.documentElement.scrollLeft; };
    Geometry.getVerticalScroll = 
        function() { return document.documentElement.scrollTop; };
}
else // if (document.body && document.body.clientWidth) // Body isn't loaded
     // yet, so this line fails and causes the rest of the file to fail to execute
  {
    // These are for IE4, IE5, and IE6 without a DOCTYPE
    Geometry.getViewportWidth =
        function() { return document.body.clientWidth; };
    Geometry.getViewportHeight =
        function() { return document.body.clientHeight; };
    Geometry.getHorizontalScroll =
        function() { return document.body.scrollLeft; };
    Geometry.getVerticalScroll = 
        function() { return document.body.scrollTop; };
}

// These functions return the size of the document.  They are not window 
// related, but they are useful to have here anyway.
if (document.documentElement && document.documentElement.scrollWidth) {
    Geometry.getDocumentWidth =
        function() { return document.documentElement.scrollWidth; };
    Geometry.getDocumentHeight =
        function() { return document.documentElement.scrollHeight; };
}
else if (document.body.scrollWidth) {
    Geometry.getDocumentWidth =
        function() { return document.body.scrollWidth; };
    Geometry.getDocumentHeight =
        function() { return document.body.scrollHeight; };
}

// Provide a shorter call for fetching elements by id
function elem(idString) {
  return document.getElementById(idString);
}

// Registration of onload event handlers
function evalOnLoad(funcCallString) {
  if (evalOnLoad.loaded) eval(funcCallString);
  else evalOnLoad.funcCallStrings.push(funcCallString);
}

evalOnLoad.funcCallStrings = [];
evalOnLoad.imageRegistrations = [];
evalOnLoad.loaded = false;

evalOnLoad.run = function() {
  if (evalOnLoad.loaded) return;
  for (var i=0; i<evalOnLoad.imageRegistrations.length; i++) {
    initImage(evalOnLoad.imageRegistrations[i][0], evalOnLoad.imageRegistrations[i][1],
              evalOnLoad.imageRegistrations[i][2]);
  }
  for (var i=0; i<evalOnLoad.funcCallStrings.length; i++) {
    try { eval(evalOnLoad.funcCallStrings[i]); }
    catch(e) {
      // Don't do anything for a thrown exception right now
    }
  }
  evalOnLoad.loaded = true;
  delete evalOnLoad.funcCallStrings;
  delete evalOnLoad.run;
  // Initiate the blinking last
  initiateToggleBlink();
}

// Register evalOnLoad.run() to be called when the webpage finishes loading
if (window.addEventListener)
  window.addEventListener("load", evalOnLoad.run, false);
else if (window.attachEvent) window.attachEvent("onload", evalOnLoad.run);
else window.onload = evalOnLoad.run;

// Debug function that returns all attributes of an object in a string
function attrVals(obj) {
  var attributes = new Array();
  for (var attr in obj) {
    if (attr) {
      try {attributes.push(attr + ': ' + obj[attr]); }
      catch(e) {
        // Don't do anything
      }
    }
  }
  return attributes.join(', ');
}

function pad(val) {
  if (val < 10) {
    return '0' + val;
  } else {
    return val;
  }
}
  
// Works for all browsers, necessary because some support the non-standard
// outerHTML property and some don't.
function getOuterHTML(object) {
  if (!object)
    return '';
  if (object.outerHTML)
    return object.outerHTML;
  if (XMLSerializer) {
    var outerHTML = XMLSerializer().serializeToString(object);
    if (outerHTML)
      return outerHTML;
  }
  var element = document.createElement('div');
  element.appendChild(object.cloneNode(true));
  return element.innerHTML;
}

// Add event handlers for submit input elements.  This makes it easy to have
// multiple submit input elements in the same form where each sets its name
// attribute to a different value.  This function adds a new value to the form
// (<input type=hidden>) whose name is 'control' and whose value is the same as
// name's value.  This means that both links and forms set a value named
// 'control' that can be used to direct activity.
//
// Note: The ability to have multiple submits already exists, but there are
// drawbacks.  Each submit must have a different value for its name attribute
// (because Internet Explorer gets confused if multiple tags have the same
// name), so this means you have to check each name to see which one is
// defined.  This is in contrast to the way most URLs are used, usually with
// name/value pairs.  For example, if you wanted to be able to say whether to
// edit or reply in a URL you might have these possibilities:
//
//   message.cgi?control=edit
//   message.cgi?control=reply
//
// Then in the code you'd check the value of the control parameter.  But if
// you're doing this with submits something like this:
//
//   <input type=submit name=edit  value="Edit">
//   <input type=submit name=reply value="Reply">
//
// Then in the code you'd check for whether it is the edit or reply parameter
// that exists.
//
// But if you want to do be able to initiate the same functions from both
// submits and from URLs then this doesn't work.  You have to write different
// code for each approach.  So by defining a value for the control parameter
// that is equal to the value of the submit's name attribute we neatly solve the
// problem.
function addSubmitEventHandlers(obj) {
  var e = obj.getElementsByTagName('input');
  for(var i=0; i<e.length; i+=1) {
    var inputObj = e[i];
    if (inputObj.type == 'submit') {
      var f = function(event) {
        var submitObj;
        if (event) {
          submitObj = event.target;
        } else {
          event = window.event;
          if (event) submitObj = event.srcElement;
        }
        // Try again for IE, sometimes it passes an event object that isn't any
        // good
        if (!submitObj) {
          event = window.event;
          if (event) submitObj = event.srcElement;
        }
        if (submitObj) {
          var formObj = submitObj.form;
          if (formObj) {
            var controlValue = submitObj.name;
            if (controlValue) {
              if (formObj.hiddenObj) {
                formObj.removeChild(formObj.hiddenObj);
                formObj.hiddenObj = null;
              }
              var hiddenObj = document.createElement('input');
              hiddenObj.type = 'hidden';
              hiddenObj.name = 'control';
              hiddenObj.value = controlValue;
              formObj.appendChild(hiddenObj);
              formObj.hiddenObj = hiddenObj;
            }
          }
        }
        return true;
      }
      if (inputObj.addEventListener) {
        inputObj.addEventListener('click', f, true);
      } else {
        if (inputObj.attachEvent) {
          inputObj.attachEvent('onclick', f);
        }
      }
    }
  }
}

// Add above function to the routines to run at load complete
evalOnLoad('addSubmitEventHandlers(document)');

var days = new Array('Sun','Mon','Tue','Wed','Thu','Fri','Sat');
var longDays = new Array('Sunday','Monday','Tuesday','Wednesday',
                         'Thursday','Friday','Saturday');
var months = new Array('Jan','Feb','Mar','Apr','May','Jun',
                       'Jul','Aug','Sep','Oct','Nov','Dec');
var longMonths = new Array('January','February','March','April','May','June',
                           'July','August','September','October','November','December');

function FormatDate(theTime, dateFormat, dateSep, yearDig) {
  var year;
  if (yearDig == 4) {
    year = theTime.getFullYear();
  } else {
    year = (theTime.getFullYear() + '').substr(2);
  }
  var month = theTime.getMonth(); // 0-11
  var dayOfMonth = theTime.getDate(); // 1-31
  var dayOfWeek = theTime.getDay(); // 0-6
  var nameOfDayOfWeek = days[dayOfWeek] + ', ';
  switch(dateFormat) {
  case 'US':
    month = pad(month + 1);
    dayOfMonth = pad(dayOfMonth);
    return month + dateSep + dayOfMonth + dateSep + year;
  case 'USAM':
    month = months[month];
    dayOfMonth = pad(dayOfMonth);
    return month + dateSep + dayOfMonth + dateSep + year;
  case 'USFM':
    month = longMonths[month];
    dayOfMonth = pad(dayOfMonth);
    return month + dayOfMonth + year;
  case 'USADOW':
    month = pad(month + 1);
    dayOfMonth = pad(dayOfMonth);
    return nameOfDayOfWeek + month + dateSep + dayOfMonth + dateSep + year;
  case 'USFDOW':
    month =pad(month + 1);
    dayOfMonth = pad(dayOfMonth);
    nameOfDayOfWeek = longDays[dayOfWeek] + ', ';
    return nameOfDayOfWeek + month + dateSep + dayOfMonth + dateSep + year;
  case 'USAMADOW':
    month = months[month];
    dayOfMonth = pad(dayOfMonth);
    return nameOfDayOfWeek + month + dateSep + dayOfMonth + dateSep + year;
  case 'USAMFDOW':
    month = months[month];
    dayOfMonth = pad(dayOfMonth);
    nameOfDayOfWeek = longDays[dayOfWeek] + ', ';
    return nameOfDayOfWeek + month + dateSep + dayOfMonth + dateSep + year;
  case 'USFMADOW':
    month = longMonths[month];
    dayOfMonth = pad(dayOfMonth);
    return nameOfDayOfWeek + month + ' ' + dayOfMonth + ', ' + year;
  case 'USFMFDOW':
    month = longMonths[month];
    dayOfMonth = pad(dayOfMonth);
    nameOfDayOfWeek = longDays[dayOfWeek] + ', ';
    return nameOfDayOfWeek + month + ' ' + dayOfMonth + ', ' + year;
  case 'E':
    month = pad(month + 1);
    dayOfMonth = pad(dayOfMonth);
    return dayOfMonth + dateSep + month + dateSep + year;
  case 'EAM':
    month = months[month];
    dayOfMonth = pad(dayOfMonth);
    return dayOfMonth + dateSep + month + dateSep + year;
  case 'EFM':
    month = longMonths[month];
    dayOfMonth = pad(dayOfMonth);
    return dayOfMonth + dateSep + month + dateSep + year;
  case 'EADOW':
    month = pad(month + 1);
    dayOfMonth = pad(dayOfMonth);
    return nameOfDayOfWeek + dayOfMonth + dateSep + month + dateSep + year;
  case 'EFDOW':
    month = pad(month + 1);
    dayOfMonth = pad(dayOfMonth);
    nameOfDayOfWeek = longDays[dayOfWeek] + ', ';
    return nameOfDayOfWeek + dayOfMonth + dateSep + month + dateSep + year;
  case 'EAMADOW':
    month = months[month];
    dayOfMonth = pad(dayOfMonth);
    return nameOfDayOfWeek + dayOfMonth + dateSep + month + dateSep + year;
  case 'EAMFDOW':
    month = months[month];
    dayOfMonth = pad(dayOfMonth);
    nameOfDayOfWeek = longDays[dayOfWeek] + ', ';
    return nameOfDayOfWeek + dayOfMonth + dateSep + month + dateSep + year;
  case 'EFMADOW':
    month = longMonths[month];
    dayOfMonth = pad(dayOfMonth);
    return nameOfDayOfWeek + dayOfMonth + dateSep + month + dateSep + year;
  case 'EFMFDOW':
    month = longMonths[month];
    dayOfMonth = pad(dayOfMonth);
    nameOfDayOfWeek = longDays[dayOfWeek] + ', ';
    return nameOfDayOfWeek + dayOfMonth + dateSep + month + dateSep + year;
  case 'I':
    month = pad(month + 1);
    dayOfMonth = pad(dayOfMonth);
    return year + dateSep + month + dateSep + dayOfMonth;
  case 'IAM':
    month = months[month];
    dayOfMonth = pad(dayOfMonth);
    return year + dateSep + month + dateSep + dayOfMonth;
  case 'IFM':
    month = longMonths[month];
    dayOfMonth = pad(dayOfMonth);
    return year + dateSep + month + dateSep + dayOfMonth;
  case 'IADOW':
    month = pad(month + 1);
    dayOfMonth = pad(dayOfMonth);
    return nameOfDayOfWeek + year + dateSep + month + dateSep + dayOfMonth;
  case 'IFDOW':
    month = pad(month + 1);
    dayOfMonth = pad(dayOfMonth);
    nameOfDayOfWeek = longDays[dayOfWeek] + ', ';
    return nameOfDayOfWeek + year + dateSep + month + dateSep + dayOfMonth;
  case 'IAMADOW':
    month = months[month];
    dayOfMonth = pad(dayOfMonth);
    return nameOfDayOfWeek + year + dateSep + month + dateSep + dayOfMonth;
  case 'IAMFDOW':
    month = months[month];
    dayOfMonth = pad(dayOfMonth);
    nameOfDayOfWeek = longDays[dayOfWeek] + ', ';
    return nameOfDayOfWeek + year + dateSep + month + dateSep + dayOfMonth;
  case 'IFMADOW':
    month = longMonths[month];
    dayOfMonth = pad(dayOfMonth);
    return nameOfDayOfWeek + year + dateSep + month + dateSep + dayOfMonth;

  case 'IFMFDOW':
    month = longMonths[month];
    dayOfMonth = pad(dayOfMonth);
    nameOfDayOfWeek = longDays[dayOfWeek] + ', ';
    return nameOfDayOfWeek + year + dateSep + month + dateSep + dayOfMonth;
  }
  return '*** Bad Date Format Requested: ' + dateFormat + ' ***';
}

function FormatTime(theTime, timeFormat) {
  var hour = theTime.getHours();
  var min = theTime.getMinutes();
  var time;
  if (timeFormat == '12') {
    var ampm;
    if (hour < 12) {
      ampm = 'AM';
      if (hour == 0) {
        hour = 12;
      }
    } else {
      ampm = 'PM';
      if (hour > 12) {
        hour -= 12;
      }
    }
    return hour + ':' + pad(min) + ' ' + ampm;
  } else {
    return hour + ':' + pad(min);
  }
}

function getDateTime(utcTime, dateFormat, dateSep, yearDig, timeFormat, blinkingColon) {
  var theTime;
  if (utcTime == 0) {
    theTime = new Date();
  } else {
    theTime = new Date(utcTime);
  }
  var date = FormatDate(theTime, dateFormat, dateSep, yearDig);
  var time = FormatTime(theTime, timeFormat);
  // Disable blinking colon for now.  The blink stops when the time is updated,
  // I have to somehow reinvoke the initiateToggleBlink routine, but have it
  // only apply to this blink so that the intervals for other blink regions
  // aren't reset.  I'll have to break the code in the for loop in that routine
  // out so that I can call it for a single blink region.
  if (false && blinkingColon) {
    var regexp = /:/;
    time = time.replace(regexp, '<dblink>:</dblink>');
  }
  return date + ' ' + time;
}

function printDateTime(utcTime, dateFormat, dateSep, yearDig, timeFormat) {
  document.write(getDateTime(utcTime, dateFormat, dateSep, yearDig, timeFormat, false));
}

function getDate(utcTime, dateFormat, dateSep, yearDig) {
  var theTime = new Date(utcTime);
  var date = FormatDate(theTime, dateFormat, dateSep, yearDig);
  return date.toString();
}

function printDate(utcTime, dateFormat, dateSep, yearDig) {
  document.write(getDate(utcTime, dateFormat, dateSep, yearDig));
}

function PrintRecurringCurrentTime(dateFormat, dateSep, yearDig, timeFormat) {
  var timeDiv = elem('DisplayCurrentDateTime');
  if (timeDiv) {
    var curDate = new Date();
    timeDiv.innerHTML = getDateTime(curDate.getTime(), dateFormat,
                                    dateSep, yearDig, timeFormat, true);
    var millisecondsTillNextUpdate = 60000 - curDate.getSeconds()*1000;
    setTimeout("PrintRecurringCurrentTime('"+dateFormat+"','"+dateSep+"',"+yearDig+",'"+
               timeFormat+"')", millisecondsTillNextUpdate);
  }
}

// Set up <dblink> tags to blink
var defaultBlinkOnTime = 1000;
var defaultBlinkOffTime = 500;

// Disabled because even a very subtle blink is a little jarring to look at.
// What would probably look nice is to smoothly vary the opacity, but that's a
// little more complicated then I have time for right now.

function initiateToggleBlink() {
  // Scan our list of blinking objects and start the blinking going
  var blinkTags = document.getElementsByTagName('dblink');
  for (i=0; i<blinkTags.length; i++) {
    var blinkObject = blinkTags[i];
    blinkObject.id = 'blink_' + i;
    var timeOn = blinkObject.getAttribute('timeon');
    var timeOff = blinkObject.getAttribute('timeoff');
    if (timeOn == null || timeOn <= 0)
      blinkObject.setAttribute('timeon', defaultBlinkOnTime);
    if (timeOff == null || timeOff <= 0)
      blinkObject.setAttribute('timeoff', defaultBlinkOffTime);
    blinkObject.blinkState = false;
    toggleBlink(blinkObject);
  }
}

function toggleBlink(blinkObject) {
  var className;
  var timeout;
  if (blinkObject.blinkState) {
    blinkObject.className = 'blink';
    blinkObject.blinkState = false;
    timeout = blinkObject.getAttribute('timeoff');
  } else {
    blinkObject.className = '';
    blinkObject.blinkState = true;
    timeout = blinkObject.getAttribute('timeon');
  }
  setTimeout("toggleBlink(elem('" + blinkObject.id + "'))", timeout);
}

// For backward compatibility, define printTime() function
function printTime(utcTime, dateFormat, dateSep, yearDig, timeFormat) {
  printDateTime(utcTime, dateFormat, dateSep, yearDig, timeFormat);
}

function suspensionTime(utcTime, dateFormat, dateSep, yearDig, timeFormat) {
  var dateTimeText;
  if (utcTime == 2147483647000) {
    dateTimeText = 'Indefinitely suspended.';
  } else {
    var currentUtcTime = new Date;
    var displacementMilliseconds;
    if (utcTime < currentUtcTime.getTime()) {
      displacementMilliseconds = currentUtcTime.getTime() - utcTime;
    } else {
      displacementMilliseconds = utcTime - currentUtcTime.getTime();
    }
    var hours = parseInt((displacementMilliseconds / 3600000).toString(10));
    var minutes = parseInt(((displacementMilliseconds - (hours*3600000)) / 60000).toString(10));
    var diffText;
    if (hours == 0) {
      diffText = minutes + 'm';
    } else {
      diffText = hours + 'h ' + minutes + 'm';
    }
    if (utcTime <  currentUtcTime) {
      diffText += ' ago';
    } else {
      diffText += ' from now';
    }
    dateTimeText = getDateTime(utcTime, dateFormat, dateSep, yearDig, timeFormat, false) + 
      '<br>' + diffText;
  }
  return dateTimeText;
}

function getCookie(name) {
  var cname = "; " + name + "=";
  var dc = "; " + document.cookie;
  if (dc.length > 0) {
    begin = dc.indexOf(cname);
    if (begin != -1) {
      begin += cname.length;
      end = dc.indexOf(";", begin);
      if (end == -1) end = dc.length;
      return unescape(dc.substring(begin, end));
    }
  }
   return null;
}

// Hardwired to set expires date to a year in the future
function setCookie(name, value, path) {
  var date = new Date();
  date.setFullYear(date.getFullYear() + 1);
  var expires = '; expires=' + date.toGMTString();
  if (path != null) {
    path = '; path=' + path;
  }
  document.cookie = name + '=' + value + expires + path;
}

function OpenIPWin(url) {
  var viewIPWin = open(url, '_blank',
                       'directories=no,resizable=no,height=170,width=430');
}

function AdminModCheck(which, forumNum) {
  return(confirm(which + " this topic?"));
}

function ToggleDisplay(divName) {
  var div = elem(divName);
  if (div.style.display == 'none') {
    div.style.display = 'inline';
  } else {
    div.style.display = 'none';
  }
}

function ToggleHelp(objName) {
  var obj  = elem(objName);
  obj.className;
  obj.className = (obj.className.indexOf('helpoff') >= 0)
    ? obj.className.replace('helpoff', 'helpon')
    : obj.className.replace('helpon', 'helpoff');
}

function ToggleTBody(divName, clickedObject, offname, onname) {
  var div = elem(divName);
  if (div.style.display == 'none') {
    div.style.display = 'table-row-group';
    clickedObject.innerHTML = onname;
    clickedObject.scrollIntoView();
  } else {
    div.style.display = 'none';
    clickedObject.innerHTML = offname;
  }
}

function MakeSameWidth(obj1, obj2) {
  // Make two objects the same width
  if (obj1 && obj2) {
    var obj1Width = parseInt(obj1.offsetWidth, 10);
    var obj2Width = parseInt(obj2.offsetWidth, 10);
    var newWidth = (obj1Width > obj2Width
      ? obj1Width
      : obj2Width) + 'px';
    obj1.style.width = newWidth;
    obj2.style.width = newWidth;
  }
}

function JumpToForum(menu) {
  var index = menu.selectedIndex;
  var value = menu.options[index].value;
  if (value.substring(0,1) == 'c') {
    document.location='dBoard.cgi?c=' + value.slice(1);
  } else {
    document.location='Threads.cgi?control=tf&f=' + value;
  }
}

function ChatRoomObject(codeClause, primaryAlias) {
  var object = '<object ' + codeClause +
    '  codebase="http://client0.sigmachat.com/current/"' +
    '  archive="scclient_en.zip"' +
    '  standby="Connecting to chat room, please wait..."' +
    '  width=700 height=400 MAYSCRIPT>' +
    ' <param name="room" value="113348">' +
    ' <param name="cabbase" value="scclient_en.cab">' +
    ' <param name="autologin" value="yes">' +
    ' <param name="username" value="' + primaryAlias + '">' +
    ' <param name="password" value="chatr0om">' +
    ' This browser does not have a Java Plug-in,' +
    ' or the Plug-in version is not current.<br />' +
    ' <a href="http://java.sun.com/products/plugin/downloads/index.html">' +
    ' Get the latest Java Plug-in here.' +
    ' </a>' +
    ' </object>';
  document.write(object);
}

function AddMemberListArgs(link, includeSearchBox) {
  if (elem('ExcludeAliases').checked) {
    link.href += '&noaliases=1';
  }
  if (includeSearchBox) {
    var searchBox = elem('SearchBox');
    if (searchBox) {
      if (searchBox.value != '') {
        link.href += '&SearchBox=' + searchBox.value;
      }
    }
  }
}

// We need to make resizable the images the user has placed in messages using
// the [img] and [thumb] dBCodes.  For all browsers except Internet Explorer we
// can call the initImage routine when the <img> tag's onLoad event triggers,
// but we need the image's actual width to be available, and IE is inconsistent
// in having this available by the time of this event.  Sometimes it's there,
// sometimes it isn't, who knows why.  So we have to do this in two-step
// fashion.  First we record the arguments for all the images with this
// registerImage routine, then we have the evalOnLoad routine call the initImage
// routine for each image with all its arguments.

function registerImage(image, requestedWidth, maxWidth) {
  evalOnLoad.imageRegistrations.push([image, requestedWidth, maxWidth]);
}

function initImage(image, requestedWidth, maxWidth) {
  // Check the image's actual width against the requested width and the
  // maxWidth.  Set up the cursors according to which is greater than the other.
  var actualWidth = image.width;
  if (requestedWidth == null || requestedWidth <= 0) {
    if (actualWidth <= maxWidth) {
      // Display a static image, turn off detection of the mouse click event
      return;
    } else {
      image.lowWidth = maxWidth;
      image.highWidth = actualWidth;
      image.className = 'magplus';
      image.width = maxWidth;
    }
  } else {
    if (actualWidth < requestedWidth) {
      image.lowWidth = actualWidth;
      image.highWidth = requestedWidth;
      image.className = 'magminus';
      image.width = requestedWidth;
    } else if (actualWidth == requestedWidth) {
      // Display a static image, turn off detection of the mouse click event
      return;
    } else {
      image.lowWidth = requestedWidth;
      image.highWidth = actualWidth;
      image.className = 'magplus';
      image.width = requestedWidth;
    }
  }
  image.onclick = function toggleImageSize() {
    if (image.width <= image.lowWidth) {
      image.className = 'magminus';
      image.width = image.highWidth;
    } else {
      image.className = 'magplus';
      image.width = image.lowWidth;
    }
  }
}

// Implement my own hover boxes - differ from acronyms and alt in that
// they persist instead of disappearing after a few seconds.  This
// particular hover box is for displaying suspensions.  We'll
// implement it just as required and then try to generalize from it to
// a generic hover box object type, modifying this comment, of course,
// to make it seem like I knew what I was doing all along.

var xDisplacementRight = 10;
var xDisplacementLeft = 0;
var yDisplacementTop = 5;
var yDisplacementBottom = 23;

var lastMouseX, lastMouseY;

// The onMouseOver event triggers as soon as the mouse moves over the object,
// and the position of the mouse at that instant is recorded in the event
// object, but the mouse almost always will keep moving before stopping over the
// object.  We trigger calls to this function with the onMouseMove event so that
// we can always know the current mouse position.
function mouseTracker(event) {
  lastMouseX = event.clientX;
  lastMouseY = event.clientY;
}

function topLeft(object) {
  var offsetTop = 0;
  var offsetLeft = 0;
  while (object != null) {
    offsetLeft += object.offsetLeft;
    offsetTop += object.offsetTop;
    object = object.offsetParent;
  }
  return Array(offsetLeft, offsetTop);
}

function CalcOffsets(fixedObject, mobileObject) {
  var mouseX = lastMouseX + Geometry.getHorizontalScroll();
  var mouseY = lastMouseY + Geometry.getVerticalScroll();

  // Check if too close to right
  var hoverLeft = mouseX + xDisplacementLeft;
  if ((hoverLeft + mobileObject.offsetWidth) > document.body.clientWidth) {
    // Go to the left
    hoverLeft = mouseX - mobileObject.offsetWidth + xDisplacementRight;
  }

  // Check if too close to bottom
  var hoverTop = mouseY + yDisplacementBottom;
  if ((hoverTop + mobileObject.offsetHeight) >
      (document.body.clientHeight + document.body.scrollTop)) {
    var objOffsetTop = 0;
    var ascendObj = fixedObject;
    while (ascendObj != null) {
      objOffsetTop += ascendObj.offsetTop;
      ascendObj = ascendObj.offsetParent;
    }
    hoverTop = objOffsetTop - mobileObject.offsetHeight - yDisplacementTop;
  }
  return Array(hoverLeft, hoverTop);
}

// Create the actual hover box if necessary, then display it
function turnHoverBoxOn(object, text) {
  if (object.div != null) {
    // Window box already displayed, do not display the hoverbox.
    return;
  }
  if (!hb.div) {
    // Hoverbox not created yet, go ahead and create it
    hb.div = document.body.appendChild(document.createElement('div'));
    hb.div.style.fontStyle = 'normal';
    hb.div.style.borderWidth = '1px';
    hb.div.style.borderStyle = 'solid';
    hb.div.style.visibility = 'hidden';
    hb.div.style.position = 'absolute';
    hb.div.style.zIndex = 20;
    hb.div.className = 'hb';
  }

  // In case the hoverbox is already being displayed, hide it immediately and
  // cancel the timeout if it is set
  hb.div.style.visibility = 'hidden';
  if (hb.timeoutID != 0) {
    clearTimeout(hb.timeoutID);
    hb.timeoutID = 0;
  }

  hb.div.style.left = '10px';
  hb.div.style.top = '10px';
  hb.div.innerHTML = text;
  hb.div.style.width = 'auto';
  var boxWidth = 400;
  if (boxWidth != 0 && hb.div.offsetWidth > boxWidth) {
    hb.div.style.width = boxWidth + 'px';
  }                       
  var offsets = CalcOffsets(object, hb.div);
  hb.div.style.left = offsets[0] + 'px';
  hb.div.style.top = offsets[1] + 'px';

  hb.div.style.visibility = 'visible';
}

function hoverBox() {
  // The hoverbox
  this.div = null;

  // Other class variables
  this.timeoutID = 0;

  this.on = function(object, text, delay, event) {
    if (!object) {
      return;
    }
    if (delay && delay != 0) {
      this.timeoutID =
        setTimeout(function() {turnHoverBoxOn(object, text);}, delay);
    } else {
      turnHoverBoxOn(object, text);
    }
  }

  this.off = function(delay) {
    if (this.timeoutID != 0) {
      clearTimeout(this.timeoutID);
      this.timeoutID = 0;
    }
    if (!this.div) {
      return;
    }
    if (delay && delay != 0) {
      this.timeoutID = setTimeout("hb.div.style.visibility = 'hidden'", delay);
    } else {
      this.div.style.visibility = 'hidden';
    }
  }
}

// Create a single hoverbox for use throughout the webpage
var hb = new hoverBox();

// When a window box is needed one will be fetched from this array.  A new
// window box will be created if the array is empty.  When a window box is
// closed it will be returned to this array.
var windowBoxes = new Array();

// Window box class - displays a window box of text.  If the window box is
// already being displayed, close it.
function wb(object, text, event) {
  if (object.div != null) {
    object.div.style.visibility = 'hidden';
    windowBoxes[windowBoxes.length] = object.div;
    object.div = null;
    return;
  }
  var div;
  if (windowBoxes.length > 0) {
    div = windowBoxes[windowBoxes.length-1];
    windowBoxes.length--;
  } else {
    // No window box is available, create one now
    div = document.body.appendChild(document.createElement('div'));
    div.innerHTML = '';
    div.style.borderWidth = '1px';
    div.style.borderStyle = 'solid';
    div.style.visibility = 'hidden';
    div.style.padding = '1px';
    div.style.position = 'absolute';
    div.style.zIndex = 10;   // Lower than hover boxes
    div.className = 'wb';
  }

  div.style.left = '0px';
  div.style.top = '0px';
  div.innerHTML = '<div style="text-align:right">(Click to Close)</div>' + text;
  div.style.width = 'auto';
  var boxWidth = 400;
  if (boxWidth != 0 && div.offsetWidth > boxWidth) {
    div.style.width = boxWidth + 'px';
  }                       
  var offsets = CalcOffsets(object, div);
  div.style.left = offsets[0] + 'px';
  div.style.top = offsets[1] + 'px';

  // The "this" object inside the function call is for the div object,
  // not this wb object.

  div.onclick =
    function(e) {
      this.style.visibility = 'hidden';
      windowBoxes[windowBoxes.length] = this;
      object.div = null;
    }

  // Turn off any hoverbox
  hb.off();

  div.style.visibility = 'visible';
  object.div = div;
}

var wbpIdnum = 0;

// Window box prompt - issues a prompt requesting a line of text.  If the window
// box prompt is already being displayed, close it.
function wbp(object, text, event, closeImage) {
  if (object.div != null) {
    object.div.style.visibility = 'hidden';
    windowBoxes[windowBoxes.length] = object.div;
    object.div = null;
    return;
  }
  var div;
  if (windowBoxes.length > 0) {
    div = windowBoxes[windowBoxes.length-1];
    windowBoxes.length--;
  } else {
    // No window box is available, create one now
    div = document.body.appendChild(document.createElement('div'));
    div.innerHTML = '';
    div.style.borderWidth = '1px';
    div.style.borderStyle = 'solid';
    div.style.visibility = 'hidden';
    div.style.padding = '3px';
    div.style.position = 'absolute';
    div.style.zIndex = 10;   // Lower than hover boxes
    div.className = 'wb';
    div.id = 'wbp_' + wbpIdnum++;
  }

  div.owningObject = object;
  div.style.left = '0px';
  div.style.top = '0px';
  div.innerHTML =
    '<div style="text-align:right; margin: 0px 0px 3px 0px; padding: 0px">' +
    '<a href="javascript:void()" onclick="closeWbp(elem(\'' + div.id + '\'))">' +
    ((typeof(closeImage) == 'undefined')
     ? 'Close'
     : '<img src="' + closeImage + '" border=0>') +
    '</a></div><div>' + text + '</div>';
  addSubmitEventHandlers(div);

  div.style.width = 'auto';
  var boxWidth = 400;
  if (boxWidth != 0 && div.offsetWidth > boxWidth) {
    div.style.width = boxWidth + 'px';
  }                       
  var offsets = CalcOffsets(object, div);
  div.style.left = offsets[0] + 'px';
  div.style.top = offsets[1] + 'px';

  // Turn off any hoverbox
  hb.off();

  div.style.visibility = 'visible';
  object.div = div;
  elem('wbp').focus();
}

function closeWbp(div) {
  div.style.visibility = 'hidden';
  windowBoxes[windowBoxes.length] = div;
  div.owningObject.div = null;
}

// This works fine in IE but not in Chrome, so can't use until I figure out what I'm doing
// wrong for Chrome.  Keep this around as experimental for now.
function bounce(object) {
  // Bounce the object every 10 seconds.  A bounce consists of one high bounce,
  // one low bounce, then one very low bounce.
  var TopLeftOffsets = topLeft(object);
  var yIncrement = 50;
  // Initially just bounce it up and down just to make sure I have the hang of
  // this.
  var intervalId = setInterval(changePosition, 500);

  function changePosition() {
    if (object == null)
      return;
    for(var cssprop in animation) {
      object.style.top += yIncrement;
      yIncrement = -yIncrement;
    }
    if (ypos > 150) clearInterval(intervalId);
  }
}

// Dynamic menu global variables
var menuBodyOnTimeoutID = 0;
var menuBodyOffTimeoutID = 0;
var menuBodyOn = null;

function modifyCSSRule() {
  // This is an experimental routine to see how easy it is to modify a specific
  // CSS rule, and to make sure that modifying it causes the change to be
  // reflected on the webpage.  I'll have to pick something fairly global, I'll
  // try changing the font family of the body tag.
  //
  // Wow, does this ever work!  Why are the Javascript books discouraging about
  // using this approach?  For global changes it is far, far better to change a
  // single style sheet object than to find dozens of HTML objects and change
  // their className associations.  This is how I'll turn the ID numbers on and
  // off.
  for (var i=0; i<document.styleSheets.length; i++) {
    var ss = document.styleSheets[i];
    var rules = ss.cssRules ? ss.cssRules : ss.rules;
    for (var j=0; j<rules.length; j++) {
      rule = rules[j];
      var selectorText = rule.selectorText;
      if (!selectorText) continue;
      if (selectorText == "body") {
        rule.style.fontFamily = "Courier";
        return;
      }
    }
  }
}

// ```` About the width, padding and margin arguments to this routine, these are
// statically in the CSS file, so I can eliminate them, but I'm juggling other
// things right now and will have to do this some other time.

function initMenuBar(menuID, horizontal, rollover,
                     // The parameters on the next line are used to dynamically
                     // determine the dimensions of the menu bar and so appear
                     // here instead of being statically in the CSS file.
                     menuBarBorderWidth, menuBarPadding, menuHeadMargin, separatorMargin,
                     menuOnDelay, menuOffDelay)
{
  // I evidently don't yet know enough to implement delays for turn on and turn
  // off of the menus, so for now I will force the delays to zero.  If you move
  // rapidly between menu headers it will screw up and display multiple menus in
  // both IE and Chrome.
  //
  // It works better when I remove some code from the menuOff function,
  // look at the stuff that is disabled with "if (0)".  Including this evidently
  // takes extra time causing the routine to enter itself again before it is
  // done.  Hmmm.  A problem for another day.
  //
  // A later note.  After changing from div's to ul/li's the performance seems
  // to have picked up quite a bit, so when I get a chance I might want to
  // experiement again with turning the turn-on/turn-off delays back on.
  menuOnDelay = 0;
  menuOffDelay = 0;
  menuBar = elem(menuID);
  var menus = menuBar.childNodes;
  // The menu bar is a <ul> with a list of <li>'s, each of which contains a
  // single <div> that is either a label, menu head or separator.  Visit every
  // menu head setting up the drop down menus for menu heads that have them.
  // Also set the event handlers.
  for (var i=0; i<menus.length; i++) {
    var li = menus[i];
    // Skip non-elements
    if (li.nodeType != 1) continue;

    // We're at the <li>, descend to its lone child, the <div>, which is the
    // menu head
    var liChildren = li.childNodes;
    var menuHead = null;
    for (var j=0; j<liChildren.length; j++) {
      menuHead = liChildren[j];
      if (menuHead.nodeType == 1) break;
    }
    if (!menuHead) continue;         // Do nothing if can't find <div>

    // Now we should be at the menu head.  It contains two items, the second
    // optional:
    //
    //   1. Plain text or a link, contained inside an anchor (<a>).
    //   2. The menu body, if there is one.

    // Fetch the anchor (<a>).
    var menuHeadChildren = menuHead.childNodes;
    var anchor = null;
    var ndx;
    for (ndx=0; ndx<menuHeadChildren.length; ndx++) {
      anchor = menuHeadChildren[ndx];
      if (anchor.nodeType == 1) break;
    }
    if (!anchor) continue;      // Do nothing if can't find the anchor, menu malformed

    // Set up the dynamic actions, including for the menu body if one is
    // present.
    menuHead.onDelay = menuOnDelay;
    menuHead.offDelay = menuOffDelay;
    // Look for a menu body, there doesn't have to be one
    var menuBody = null;
    while (++ndx < menuHeadChildren.length) {
      menuBody = menuHeadChildren[ndx];
      if (menuBody.nodeType == 1 && menuBody.className.indexOf("menubody") != -1) break;
    }
    if (menuBody) {
      menuHead.menuBody = menuBody;
      if (rollover) {
        // Set up event handlers to turn dynamic menus on and off upon rollover
        menuHead.onmouseover = function(e) { menuOn(e, this, true) };
        menuHead.onmouseout = function(e) { menuOff(e, this, true) };
      } else {
        // Set up event handlers to turn dynamic menus on and off upon mouse click
        menuHead.onclick = function(e) { menuClick(e, this) };
        // Set up event handlers to change class appropriately, but don't change
        // the menu
        menuHead.onmouseover = function(e) { menuOn(e, this, false) };
        menuHead.onmouseout = function(e) { menuOff(e, this, false) };
      }
    } else {
      // Set up event handlers to change classes controlling display of link
      // or label when there's no dynamic menu
      menuHead.onmouseover = function() {
        this.className="menuhead mhon";
      }
      menuHead.onmouseout = function() {
        this.className="menuhead mhoff";
      }
    }
  }
}

var clickState = false;
function menuClick(event, menu) {
  if (!clickState) {
    clickState = true;
    menuOn(event, menu, true);
  } else {
    clickState = false;
    menuOff(event, menu, true);
  }
}

function menuOn(event, menuHead, rollover) {
  if (!event) {
    event = window.event;
  }
  var menuBody = menuHead.menuBody;
  if (menuBodyOn) {
    if (menuBodyOn == menuBody) {
      // The menu body we want to turn on is already on
      if (menuBodyOffTimeoutID) {
	clearTimeout(menuBodyOffTimeoutID);
	menuBodyOffTimeoutID = 0;
      }
      return;
    } else {
      menuBodyOn.display = 'none';
      menuBodyOn = null;
    }
  }
  if (menuBodyOnTimeoutID) {
    clearTimeout(menuBodyOnTimeoutID);
  }
  if (menuHead.onDelay) {
    menuBodyOnTimeoutID =
      setTimeout(function() {menuOnNow(menuHead, menuBody, rollover)}, menu.onDelay);
  } else {
    menuOnNow(menuHead, menuBody, rollover);
  }
  return;

  function menuOnNow(menuHead, menuBody, rollover) {
    menuBodyOnTimeoutID = 0;
    menuHead.className = "menuhead mhon";
    if (rollover && menuBody) {
      menuBody.className = "menubody mbon";
      menuBodyOn = menuBody;
    }
  }

}

function menuOff(event, menuHead) {
  var menuBody = menuHead.menuBody;
  if (0) {
    // This might help at some point, not sure, keep around for now
    if (!event) {
      event = window.event;
    }
    if (event.srcElement.nodeName != 'DIV') return;
    var toObj = (event.relatedTarget) 
      ? event.relatedTarget
      : event.toElement;
    while (toObj) {
      if (toObj == menuHead || toObj == menuBody) {
        return;
      }
      toObj = toObj.offsetParent;
    }
  }
  if (menuBodyOnTimeoutID) {
    clearTimeout(menuBodyOnTimeoutID);
  }
  if (menuHead.offDelay) {
    menuBodyOffTimeoutID =
      setTimeout(function() {menuOffNow(menuHead, menuBody);}, menu.offDelay);
  } else {
    menuOffNow(menuHead, menuBody);
  }
}

function menuOffNow(menuHead, menuBody) {
  menuBodyOffTimeoutID = 0;
  menuHead.className = "menuhead mhoff";
  if (menuBody) {
    menuBody.className = "menubody mboff";
  }
  menuBodyOn = null;
}

function winopen(anywindow, bar, loc, mbar, scrbar, w, h, res) {
  if (w == 0 || h == 0) {
    // We assume a normal "^N" operation is wanted if no height or width is specified
    window.open (anywindow, "_blank");
  } else {
    window.open (anywindow, "_blank",
                 "toolbar="+bar+",location="+loc+",menubar="+mbar+",scrollbars="+scrbar+
                 ",width="+w+",height="+h+",resizeable="+res+",status=no"); 
  }
}

function selectRange(elem) {
  var rng;
  if (document.createRange) {
    rng = document.createRange();
    rng.selectNode(elem);
    var selection = window.getSelection();
    selection.removeAllRanges();
    selection.addRange(rng);
  } else if (document.body.createTextRange) {
    rng = document.body.createTextRange();
    rng.moveToElementText(elem);
    rng.select();
  }
}

// This is the version that works with IE, but not with Chrome or FF.  Trying to
// figure out why.
function copy(value) {
  var flashcopier = 'flashcopier';
  if(!elem(flashcopier)) {
    var divholder = document.createElement('div');
    divholder.id = flashcopier;
    document.body.appendChild(divholder);
  }
  elem(flashcopier).innerHTML = '';
  var divinfo =
    '<embed src="_clipboard.swf" ' +
    'FlashVars="clipboard=' + encodeURIComponent(value) + '" ' +
    'width="0" height="0" type="application/x-shockwave-flash"></embed>';
  elem(flashcopier).innerHTML = divinfo;
}

// Use this improved version when I figure out why the original is not working.
function copy_new(value) {
  var flashcopier = 'flashcopier';
  var flashCopierObj = elem(flashcopier);
  if(!flashCopierObj) {
    var divholder = document.createElement('div');
    divholder.id = flashcopier;
    document.body.appendChild(divholder);
    flashCopierObj = elem(flashcopier);
  }
  flashCopierObj.innerHTML = '';
  var divinfo =
    '<embed src="_clipboard.swf" ' +
    'FlashVars="clipboard=' + encodeURIComponent(value) + '" ' +
    'width="0" height="0" type="application/x-shockwave-flash"></embed>';
  flashCopierObj.innerHTML = divinfo;
}

function setOpacity(object, opacity) {
  if (object == null) {
    return;
  }
  object.style.opacity = opacity;
  object.style.filter = 'alpha(opacity=' + opacity*100 + ')';
  object.style.mozOpacity = opacity;
}

function threadIconJS(object, tid, fid, hot, newmsg, closed) {
  ajaxReplaceHTML
    (object, 'AjaxHandler.cgi?control=threadicon&t=' + tid + '&f=' + fid + '&h=' + 
     hot + '&n=' + newmsg + '&c=' + closed + '&z=' + uniqueValue++);
}

function responseTextJS(object, replyMsgID, linkText, nextLinkText, noted) {
  ajaxReplaceHTML
    (object, 'AjaxHandler.cgi?control=responsetext&m=' + replyMsgID + '&lt=' + linkText +
     '&nxtlt=' + nextLinkText + '&n=' + noted + '&z=' + uniqueValue++);
}

function threadResponseJS(object, replyThreadID, nextRS) {
  ajaxReplaceHTML
    (object, 'AjaxHandler.cgi?control=threadresponse&t=' + replyThreadID
     + '&rs=' + nextRS + '&z=' + uniqueValue++);
}

function rateMessageJS(object, msgID, mbrID, rating) {
  ajaxReplaceHTML
    (object, 'AjaxHandler.cgi?control=rm&m=' + msgID
     + '&mbrid=' + mbrID + '&r=' + rating + '&z=' + uniqueValue++);
}

function updateMarkedJS(object, msgID, newValue) {
  ajaxReplaceHTML
    (object, 'AjaxHandler.cgi?control=pmmark&m=' + msgID 
     + '&val=' + newValue + '&z=' + uniqueValue++, true);
}

function updateOpenedJS(object, msgID, newValue) {
  ajaxReplaceHTML
    (object, 'AjaxHandler.cgi?control=pmopened&m=' + msgID
     + '&val=' + newValue + '&z=' + uniqueValue++, true);
  object.parentNode.parentNode.className = newValue
    ? "al1 szs"
    : "al1 szsb";
}

function pmLoadContactBox(object, clID, inputBoxName) {
  // inputBoxName is 'to' or 'cc' or 'bcc'
  return ajaxGetString
    ('AjaxHandler.cgi?control=pmcontactbox&clid=' + clID + '&boxname=' + inputBoxName);
}

function pmReloadContactBox(object, inputBoxName) {
  var clID = object.options[object.selectedIndex].value;
  return ajaxGetString
    ('AjaxHandler.cgi?control=pmcontactbox&clid=' + clID + '&boxname=' + inputBoxName);
}

function pmAppendToInputBox(idName, mbrName) {
  var object = elem(idName);
  if (object.value == null || object.value == '')
    object.value = mbrName;
  else
    object.value += ', ' +  mbrName;
}

function checkAddresseeList(object) {
  // Put all addressees into one list
  var addresseeList;
  if (object.tolist.value.length > 0)
    addresseeList = object.tolist.value;
  if (object.cclist.value.length > 0)
    if (addresseeList.length > 0)
      addresseeList = addresseeList + ', ' + object.cclist.value;
    else
      addresseeList = object.cclist.value;
  if (object.bcclist.value.length > 0)
    if (addresseeList.length > 0)
      addresseeList = addresseeList + ', ' + object.bcclist.value;
    else
      addresseeList = object.bcclist.value;
  if (addresseeList == null || addresseeList.length == 0)
    return true;
  var addresseeErrors =
    ajaxGetString('AjaxHandler.cgi?control=cal&al=' + addresseeList);
  if (addresseeErrors != null && addresseeErrors.length > 0) {
    var confirmText =
      'There are errors in your address list.  These are not valid member names:\n\n' +
      addresseeErrors + '\n\nSend anyway?';
    return confirm(confirmText);
  }
  return true;
}

// This issues a blocking Ajax get request for a string.  String is returned to
// caller.
function ajaxGetString(url) {
  var request = HTTP.newRequest();
  var timeoutId;

  request.open('GET', url, false);
  timeoutId = setTimeout(cancelRequest, 5000);
  request.send(null);
  clearTimeout(timeoutId);
  if (request.status == 200)
    return request.responseText;
  return null;

  function cancelRequest() {
    request.abort();
  }
}

// This issues a non-blocking Ajax get request for a string.  When the string
// becomes available it is returned in the argument to the callback function
// cbFunc.
function ajaxGetStringNB(url, cbFunc, timeout) {
  var request = HTTP.newRequest();
  var timeoutId;

  request.onreadystatechange = function() {
    if (request.readyState == 4 && request.status == 200) {
      clearTimeout(timeoutId);
      cbFunc(request.responseText);
    }
  }

  if (typeof(timeout) == 'undefined')
    timeout = 5000;           // Default timeout of 5 seconds
  request.open('GET', url);
  timeoutId = setTimeout(cancelRequest, timeout);
  request.send(null);

  function cancelRequest() {
    request.abort();
  }
}

// This issues an Ajax get request with a couple twiddles: the pointer is
// changed to wait state (unless noWaitCursor is true) until the request is
// satisfied or times out, and until then the object blinks.
function ajaxReplaceHTML(object, url, noWaitCursor) {
  hb.off(0);
  var request = HTTP.newRequest();
  var intervalId = setInterval(changeOpacity, 30);
  var opacity = 1;
  var opacityDir;
  changeOpacity();
  request.onreadystatechange = function() {
    if (request.readyState == 4 && request.status == 200) {
      clearInterval(intervalId);
      clearTimeout(timeoutId);
      if (object != null) {
        if (request.responseText != '**unchanged**') {
             object.parentNode.innerHTML = request.responseText;
        }
        if (!noWaitCursor)
          object.style.cursor='pointer'; 
        setOpacity(object, 1);
      }
    }
  }
  // The z field is because if the URL does not change the browser will not
  // actually submit this request to the server, but will instead just provide
  // the data that was returned the last time the URL was invoked.  By making
  // sure z has a different value for every call to request.open() we insure
  // that the request is always sent to the server.  The request must always go
  // to the server since the server is updating the database in response,
  // and so there is no guarantee that subsequent identical queries will
  // returned the same value since they'd each be operating on different data.
  request.open('GET', url);
  request.send(null);
  if (object != null) {
    if (!noWaitCursor)
      object.style.cursor='wait'; 
  }
  var timeoutId = setTimeout(cancelRequest, 5000);
  function changeOpacity() {
    if (object == null) {
      return;
    }
    if (opacity <= .3) {
      opacityDir = 1;
    } else if (opacity >= 1) {
      opacityDir = -1;
    }
    opacity += opacityDir * .1;
    setOpacity(object, opacity);
  }
  function cancelRequest() {
    if (object == null) {
      return;
    }
    request.abort();
    setOpacity(object, 1);
    clearInterval(intervalId);
    object.style.cursor='pointer'; 
  }
}
