var fdjt_buildhost='vilya.pair.com';
var fdjt_buildtime='Tue Mar 9 12:30:25 EST 2010';
/* -*- Mode: Javascript; -*- */

/* Copyright (C) 2009 beingmeta, inc.
   This file was created from several component files and is
    part of the FDJT web toolkit (www.fdjt.org)

   This program comes with absolutely NO WARRANTY, including implied
   warranties of merchantability or fitness for any particular
   purpose.

   The copyright notice of the individual files are all prefixed by
    a copyright notice of the form "Copyright (C) ...".

    Use, modification, and redistribution of this program is permitted
    under either the GNU General Public License (GPL) Version 2 (or
    any later version) or under the GNU Lesser General Public License
    (version 3 or later).

    These licenses may be found at www.gnu.org, particularly:
      http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
      http://www.gnu.org/licenses/lgpl-3.0-standalone.html

*/

/* -*- Mode: Javascript; -*- */

var fdjt_jsutils_id="$Id: jsutils.js 221 2010-02-21 01:12:56Z haase $";
var fdjt_jsutils_version=parseInt("$Revision: 221 $".slice(10,-1));

/* Copyright (C) 2009 beingmeta, inc.
   This file is a part of the FDJT web toolkit (www.fdjt.org)
   This file provides extended Javascript utility functions
    of various kinds.

   This program comes with absolutely NO WARRANTY, including implied
   warranties of merchantability or fitness for any particular
   purpose.

    Use, modification, and redistribution of this program is permitted
    under either the GNU General Public License (GPL) Version 2 (or
    any later version) or under the GNU Lesser General Public License
    (version 3 or later).

    These licenses may be found at www.gnu.org, particularly:
      http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
      http://www.gnu.org/licenses/lgpl-3.0-standalone.html

*/

/* Handy characters to know */

var fdjt_nbsp="\u00A0";
var fdjt_middot="\u00B7";
var fdjt_emdash="\u2013";
var fdjt_endash="\u2014";
var fdjt_lsq="\u2018";
var fdjt_rsq="\u2019";
var fdjt_ldq="\u201C";
var fdjt_rdq="\u201D";

/* Logging */

var _fdjt_trace_load=false;

/* This needs to be customized for non-DOM ECMAScript */

function fdjtLog(string)
{
  if ((window.console) && (window.console.log) &&
      (window.console.count))
    window.console.log.apply(window.console,arguments);
}

// Insert these for temporary logging statements, which will be easier
// to find
function fdjtTrace(string)
{
  if ((window.console) && (window.console.log) &&
      (window.console.count))
    window.console.log.apply(window.console,arguments);
}

// Insert these for breakpoints you can set
function fdjtBreak(string)
{
  if ((window.console) && (window.console.log) &&
      (window.console.count))
    window.console.log.apply(window.console,arguments);
  return false;
}

// This goes to an alert if it can't get to the console
function fdjtWarn(string)
{
  if ((window.console) && (window.console.log) &&
      (window.console.count))
    window.console.log.apply(window.console,arguments);
  else alert(string);
}

// Individually for file loading messages
function fdjtLoadMessage(string)
{
  if ((_fdjt_trace_load) && (window.console) && (window.console.log) &&
      (window.console.count))
    window.console.log.apply(window.console,arguments);
}

/* For monitoring return values */
function fdjtWatch(x,message,data)
{
  if (data)
    fdjtLog("[%f] %s returning %o given %o",fdjtET(),message,x,data);
  else fdjtLog("[%f] %s returning %o",fdjtET(),message,x);
  return x;
}

/* Object add/drop operations */


function fdjtAdd(obj,field,val,nodup)
{
  if (nodup) 
    if (obj.hasOwnProperty(field)) {
      var vals=obj[field];
      if (fdjtIndexOf(vals,val)<0)  
	obj[field].push(val);
      else {}}
    else obj[field]=new Array(val);
  else if (obj.hasOwnProperty(field))
    obj[field].push(val);
  else obj[field]=new Array(val);
  if ((obj._all) && (fdjtIndexOf(obj._all,field)<0))
    obj._all.push(field);
}

function fdjtDrop(obj,field,val)
{
  if (!(val))
    /* Drop all vals */
    obj[field]=new Array();
  else if (obj.hasOwnProperty(field)) {
    var vals=obj[field];
    var pos=fdjtIndexOf(vals,val);
    if (pos<0) return;
    else vals.splice(pos,1);}
  else {}
}

function fdjtTest(obj,field,val)
{
  var vals;
  if (typeof val === "undefined")
    return (((obj.hasOwnProperty) ?
	     (obj.hasOwnProperty(field)) : (obj[field])) &&
	    ((obj[field].length)>0));
  else if (obj.hasOwnProperty(field)) 
    if (obj[field].indexOf(val)<0)
      return false;
    else return true;
  else return false;
}

function fdjtInsert(array,value)
{
  if (fdjtIndexOf(array,value)<0) array.push(value);
}

function fdjtRemove(array,value,count)
{
  var pos=fdjtIndexOf(array,value);
  if (pos<0) return array;
  array.splice(pos,1);
  if (count) {
    count--; while ((count>0) && ((pos=fdjtIndexOf(array,value,pos))>=0)) {
      array.splice(pos,1); count--;}}
  return array;
}

function fdjtIndexOf(array,elt,pos)
{
  if (array.indexOf)
    if (pos)
      return array.indexOf(elt,pos);
    else return array.indexOf(elt);
  else {
    var i=pos||0;
    while (i<array.length)
      if (array[i]===elt) return i;
      else i++;
    return -1;}
}

function fdjtContains(array,elt)
{
  if (array.indexOf)
    return (array.indexOf(elt)>=0);
  else {
    var i=0; var len=array.length;
    while (i<len)
      if (array[i]===elt) return true;
      else i++;
    return false;}
}

/*
if (!(Array.indexOf))
  Array.indexOf=function(elt) {
    var i=0; while (i<this.length)
	       if (this[i]===elt) return i;
	       else i++;
    return -1;};
*/

/* Maintaining inverted indices of values */

function fdjtIndexAdd(index,obj,rel,val)
{
  var subindex;
  if (index.hasOwnProperty(field))
    subindex=index[rel];
  else {
    subindex={}; index[rel]=subindex;}
  if (subindex.hasOwnProperty(val)) {
    var objects=subindex[val];
    if (fdjtIndexOf(objects,obj)<0)
      objects.push(obj);}
  else subindex[val]=new Array(obj);
}

function fdjtIndexDrop(index,obj,rel,val)
{
  var subindex; var vals; var pos;
  if (index.hasOwnProperty(rel)) {
    var subindex=index[rel];
    if (subindex.hasOwnProperty(val)) {
      var objects=subindex[val]; var pos;
      if ((pos=fdjtIndexOf(objects,obj))>=0)
	subindex[val]=objects.splice(pos,1);}}
}

function fdjtIndexFind(index,rel,val)
{
  var subindex;
  if (index.hasOwnProperty(rel)) {
    var subindex=index[rel];
    if (subindex.hasOwnProperty(val))
      return subindex[val];}
  return [];
}

/* Turning an arguments object into an array. */

function fdjtArguments(argobj,start)
{
  var i=start||0;
  var result=new Array(argobj.length-i);
  while (i<argobj.length) {
    result[i]=argobj[i]; i++;}
  return result;
}

/* Fast set operations */

var _fdjt_idcounter=0;

function fdjtObjID(elt)
{
  return (elt._fdjtid)||(elt._fdjtid=(++_fdjt_idcounter));
}

function _fdjt_set_sortfn(a,b)
{
  if (a===b) return 0;
  else if (typeof a === typeof b) {
    if (typeof a === "number")
      return a-b;
    else if (typeof a === "string")
      if (a<b) return -1;
      else return 1;
    else if (a._fdjtid)
      if (b._fdjtid) return a._fdjtid-b._fdjtid;
      else {
	b._fdjtid=++_fdjt_idcounter;
	return -1;}
    else if (b._fdjtid) {
      a._fdjtid=++_fdjt_idcounter;
      return 1;}
    else {
      a._fdjtid=++_fdjt_idcounter;
      b._fdjtid=++_fdjt_idcounter;
      return -1;}}
  else if (typeof a < typeof b) return -1;
  else return 1;
}

function _fdjt_length_sortfn(a,b)
{
  if (a.length===b.length) return 0;
  else if (a.length<b.length) return -1;
  else return 1;
}

function fdjtSet(arg,destructive)
{
  if (!(arg)) return new Array();
  else if (arg instanceof Array)
    if (arg.length<2) return arg;
    else if ((arg._sortlen) && ((arg._sortlen) === (arg.length)))
      return arg;
    else {
      if (!(destructive)) arg=arg.slice(0);
      arg.sort(_fdjt_set_sortfn);
      var read=1; var write=1; var len=arg.length;
      var cur=arg[0];
      while (read<len) 
	if (arg[read]===cur) read++;
	else cur=arg[write++]=arg[read++];
      arg._sortlen=write;
      arg.length=write;
      return arg;}
  else return new Array(arg);
}

function fdjt_intersect(set1,set2)
{
  var results=new Array();
  var i=0; var j=0; var len1=set1.length; var len2=set2.length;
  while ((i<len1) && (j<len2))
    if (set1[i]===set2[j]) {
      results.push(set1[i]); i++; j++;}
    else if (_fdjt_set_sortfn(set1[i],set2[j])<0) i++;
    else j++;
  results._sortlen=results.length;
  return results;
}

function fdjtIntersect()
{
  if (arguments.length===0) return new Array();
  else if (arguments.length===1)
    return fdjtSet(arguments[0],true);
  else if (arguments.length===2)
    return fdjt_intersect(fdjtSet(arguments[0],true),
			  fdjtSet(arguments[1],true));
  else {
    var i=0; while (i<arguments.length)
	       if (!(arguments[i])) return new Array();
	       else if ((typeof arguments[i] === "object") &&
			(arguments[i] instanceof Array) &&
			(arguments[i].length===0))
		 return new Array();
	       else i++;
    var copied=arguments.slice(0);
    copied.sort(fdjt_len_sortfn);
    var results=fdjtSet(copied[0],true);
    i=1; while (i<copied.length) {
      results=fdjt_intersect(results,fdjtSet(copied[i++],true));
      if (results.length===0) return results;}
    return results;}
}

function fdjt_union(set1,set2)
{
  var results=new Array();
  var i=0; var j=0; var len1=set1.length; var len2=set2.length;
  while ((i<len1) && (j<len2))
    if (set1[i]===set2[j]) {
      results.push(set1[i]); i++; j++;}
    else if (_fdjt_set_sortfn(set1[i],set2[j])<0)
      results.push(set1[i++]);
    else results.push(set2[j++]);
  while (i<len1) results.push(set1[i++]);
  while (j<len2) results.push(set2[j++]);
  results._sortlen=results.length;
  return results;
}

function fdjtUnion()
{
  if (arguments.length===0) return new Array();
  else if (arguments.length===1) return fdjtSet(arguments[0]);
  else if (arguments.length===2)
    return fdjt_union(fdjtSet(arguments[0],true),
		      fdjtSet(arguments[1],true));
  else {
    var result=fdjtSet(arguments[0],true);
    var i=1; while (i<arguments.length) {
      result=fdjt_union(result,fdjtSet(arguments[i++],true));}
    return result;}
}

function fdjtDifference(set1,set2)
{
  var results=new Array();
  var i=0; var j=0;
  set1=fdjtSet(set1); set2=fdjtSet(set2);
  var len1=set1.length; var len2=set2.length;
  while ((i<len1) && (j<len2))
    if (set1[i]===set2[j]) {
      i++; j++;}
    else if (_fdjt_set_sortfn(set1[i],set2[j])<0)
      results.push(set1[i++]);
    else j++;
  while (i<len1) results.push(set1[i++]);
  results._sortlen=results.length;
  return results;
}

// This could be faster, but we can do that later
function fdjtOverlaps(set1,set2,destructive)
{
  var i=0; var j=0;
  set1=fdjtSet(set1,destructive||false);
  set2=fdjtSet(set2,destructive|false);
  var len1=set1.length; var len2=set2.length;
  while ((i<len1) && (j<len2))
    if (set1[i]===set2[j]) return true;
    else if (_fdjt_set_sortfn(set1[i],set2[j])<0) i++;
    else j++;
  return false;
}

// So could this
function fdjtOverlap(set1,set2)
{
  var i=0; var j=0; var overlap=0;
  set1=fdjtSet(set1); set2=fdjtSet(set2);
  var len1=set1.length; var len2=set2.length;
  while ((i<len1) && (j<len2))
    if (set1[i]===set2[j]) overlap++;
    else if (_fdjt_set_sortfn(set1[i],set2[j])<0) i++;
    else j++;
  return overlap;
}

/* Getting element info */

var fdjt_element_info={};

function fdjtEltInfo(elt)
{
  return (((elt._fdjtid)&&(fdjt_element_info[elt._fdjtid]))||
	  (_fdjt_newinfo(elt)));
    
}

function _fdjt_newinfo(elt)
{
  var objid=fdjtObjID(elt);
  var info=fdjt_element_info[objid];
  if (info) return info;
  return (fdjt_element_info[objid]={});
}

/* Converting numeric HTML entities */

function fdjtUnEntify(string)
{
  return string.replace(/&#(\d+);/g,
			function(whole,paren) {
			  return String.fromCharCode(+paren);});
}

/* Other utility functions */

function fdjtIsEmptyString(string)
{
  if (typeof string === "string") 
    if (string.length===0) return true;
    else if (string.search(/\S/g)>=0)
      return false;
    else return true;
  else return false;
}

function fdjtFindSplit(string,split,escape)
{
  var start=0;
  var next;
  while ((next=string.indexOf(split,start))>=0) 
    if ((escape) && (next>0) && (string[next-1]===escape))
      start=next+1;
    else return next;
  return -1;
}

function fdjtStringSplit(string,split,escape,mapfn)
{
  if ((mapfn) || (escape)) {
    var results=[];
    var start=0; var next;
    while ((next=string.indexOf(split,start))>=0) 
      if ((escape) && (next>0) && (string[next-1]===escape))
	start=next+1;
      else if ((mapfn) && (next>start)) {
	results.push(mapfn(string.slice(start,next))); start=next+1;}
      else if (next>start) {
	results.push(string.slice(start,next)); start=next+1;}
      else start=next+1;
    if (string.length>start)
      if (mapfn) results.push(mapfn(string.slice(start)));
      else results.push(string.slice(start));
    return results;}
  else return string.split(split);
}

function fdjtSemiSplit(string,escape,mapfn)
{
  if ((mapfn) || (escape)) {
    var results=[];
    var start=0; var next;
    while ((next=string.indexOf(';',start))>=0) 
      if ((escape) && (next>0) && (string[next-1]===escape))
	start=next+1;
      else if ((mapfn) && (next>start)) {
	results.push(mapfn(string.slice(start,next))); start=next+1;}
      else if (next>start) {
	results.push(string.slice(start,next)); start=next+1;}
      else start=next+1;
    if (string.length>start)
      if (mapfn) results.push(mapfn(string.slice(start)));
      else results.push(string.slice(start));
    return results;}
  else return string.split(';');
}

function fdjtLineSplit(string,escapes,mapfn)
{
  if ((mapfn) || (escape)) {
    var results=[];
    var start=0; var next;
    while ((next=string.indexOf('\n',start))>=0) 
      if ((escape) && (next>0) && (string[next-1]===escape))
	start=next+1;
      else if ((mapfn) && (next>start)) {
	results.push(mapfn(string.slice(start,next))); start=next+1;}
      else if (next>start) {
	results.push(string.slice(start,next)); start=next+1;}
      else start=next+1;
    if (string.length>start)
      if (mapfn) results.push(mapfn(string.slice(start)));
      else results.push(string.slice(start));
    return results;}
  else return string.split('\n');
}

function fdjtStringTrim(string)
{
  var start=string.search(/\S/); var end=string.search(/\s+$/g);
  if ((start===0) && (end<0)) return string;
  else return string.slice(start,end);
}

function fdjtStdSpace(string)
{
  return string.replace(/\s+/," ").replace(/(^\s)|(\s$)/,"");
}

function fdjtFlatten(string)
{
  return string.replace(/\s+/," ");
}

function fdjtStripMarkup(string)
{
  return string.replace(/<[^>]*>/g,"");
}

function fdjtUnEscape(string)
{
  if (string.indexOf('\\')>=0)
    return string.replace(/\\(.)/g,"$1");
  else return string;
}

function fdjtPadNum(num,digits)
{
  var ndigits=
    ((num<10)?(1):(num<100)?(2):(num<1000)?(3):(num<10000)?(4):
     (num<100000)?(5):(num<1000000)?(6):(num<1000000)?(7):
     (num<100000000)?(8):(num<1000000000)?(9):(num<10000000000)?(10):(11));
  var nzeroes=digits-ndigits;
  switch (nzeroes) {
  case 0: return ""+num;
  case 1: return "0"+num;
  case 2: return "00"+num;
  case 3: return "000"+num;
  case 4: return "0000"+num;
  case 5: return "00000"+num;
  case 6: return "000000"+num;
  case 7: return "0000000"+num;
  case 8: return "00000000"+num;
  case 9: return "000000000"+num;
  case 10: return "0000000000"+num;
  default: return ""+num;}
}

/* More string functions */

function fdjtHasPrefix(string,prefix)
{
  return ((string.indexOf(prefix))===0);
}

function fdjtHasSuffix(string,suffix)
{
  return ((string.lastIndexOf(suffix))===(string.length-suffix.length));
}

function fdjtCommonPrefix(string1,string2,brk,foldcase)
{
  var i=0; var last=0;
  while ((i<string1.length) && (i<string2.length))
    if ((string1[i]===string2[i])||
	((foldcase)&&(string1[i].toLowerCase()===string2[i].toLowerCase())))
      if (brk)
	if (brk===string1[i]) {last=i-1; i++;}
	else i++;
      else last=i++;
    else break;
  if (last>0) return string1.slice(0,last+1);
  else return false;
}

function fdjtCommonSuffix(string1,string2,brk,foldcase)
{
  var i=string1.length, j=string2.length; var last=0;
  while ((i>=0) && (j>=0))
    if ((string1[i]===string2[j])||
	((foldcase)&&(string1[i].toLowerCase()===string2[i].toLowerCase())))
      if (brk)
	if (brk===string1[i]) {last=i+1; i--; j--;}
	else {i--; j--;}
      else {last=i; i--; j--;}
    else break;
  if (last>0) return string1.slice(last);
  else return false;
}

function fdjtStripSuffix(string)
{
  var start=string.search(/\.\w+$/);
  if (start>0) return string.slice(0,start);
  else return string;
}

/* Prefix trees */

function fdjtPrefixAdd(ptree,string,i)
{
  var strings=ptree.strings;
  if (i===string.length) 
    if ((strings.indexOf) ?
	(strings.indexOf(string)>=0) :
	(fdjtIndexOf(strings,string)>=0))
      return false;
    else {
      strings.push(string);
      return true;}
  else if (ptree.splits) {
    var splitchar=string[i];
    var split=ptree[splitchar];
    if (!(split)) {
      // Create a new split
      split={};
      split.strings=[];
      // We don't really use this, but it might be handy for debugging
      split.splitchar=splitchar;
      ptree[splitchar]=split;
      ptree.splits.push(split);}
    if (fdjtPrefixAdd(split,string,i+1)) {
      strings.push(string);
      return true;}
    else return false;}
  else if (ptree.strings.length<5)
    if ((strings.indexOf) ?
	(strings.indexOf(string)>=0) :
	(fdjtIndexOf(strings,string)>=0))
      return false;
    else {
      strings.push(string);
      return true;}
  else {
    // Subdivide
    ptree.splits=[];
    var strings=ptree.strings;
    var j=0; while (j<strings.length) 
	       fdjtPrefixAdd(ptree,strings[j++],i);
    return fdjtPrefixAdd(ptree,string,i);}
}

function fdjtPrefixFind(ptree,prefix,i,plen)
{
  if (!(plen)) plen=prefix.length;
  if (i===plen)
    return ptree.strings;
  else if (ptree.strings.length<=5) {
    var strings=ptree.strings;
    var results=[];
    var j=0; while (j<strings.length) {
      var string=strings[j++];
      if (fdjtHasPrefix(string,prefix)) results.push(string);}
    if (results.length) return results;
    else return false;}
  else {
    var split=ptree[prefix[i]];
    if (split) return fdjtPrefixFind(split,prefix,i+1,plen);
    else return false;}
}

/* Getting key/char codes */

var _fdjt_char_codes={
  "enter":13,"tab":9,"backspace":8,"esc":27,
  "shift":16,"ctrl":17,"alt":17,"break":19,
  "capslock":20,"pageup":33,"pagedown":33,
  "end":34,"home":36,"custom":91,
  "leftarrow":37,"rightarrow":39,
  "uparrow":38,"downarrow":40,
  "insert":45,"delete":46};

/* This converts a string into a vector of integers with
   positive numbers representing character codes and negative
   numbers representing keycodes. */
function fdjtStringToKCodes(string)
{
  var vec=[];
  var i=0; while (i<string.length) {
    if (string[i]==="\\") {
      vec.push(string.charCodeAt(i+1));
      i=i+2;}
    else if (string[i]==="[") {
      var end=string.indexOf("]",i+1);
      if (end<0) {
	fdjtWarn("Unmatched [ in KCode spec %s",string);
	i=i+1;}
      else {
	var probe=string.slice(i+1,end);
	if (typeof _fdjt_char_codes[probe] === "number") 
	  vec.push(-(_fdjt_char_codes[probe]));
	else if (typeof parseInt(probe,16) === "number")
	  vec.push(-(parseInt(probe)));
	i=end+1;}}
    else vec.push(string.charCodeAt(i++));}
  return vec;
}

/* Internationalization */

var fdjt_language='en';
var fdjt_translations={};
var fdjt_default_translations={};
fdjt_translations.en=fdjt_default_translations;

function _(string)
{
  var fmt=fdjt_translations[string]||string;
  if (arguments.length===1)
    return fmt;
  else {
    var output=""; var scan=0; var next=fmt.indexOf('%',scan);
    while ((next>=0) && (next>=scan)) {
      if (fmt[next+1]==="%") {
	next=fmt.indexOf('%',next+2); continue;}
      else {
	var numlen=fmt.slice(next+1).search(/\D/);
	if (numlen===0) {
	  next=fmt.indexOf('%',next+2); continue;}
	output=output+fmt.slice(scan,next);
	var num=((numlen<0)?(parseInt(fmt.slice(next+1))):
		 (parseInt(fmt.slice(next+1,next+1+numlen))));
	if (num>arguments.length)
	  output=output+"??";
	else {
	  var arg=arguments[num];
	  if (typeof arg === "object")
	    if (arg.toHuman)
	      output=output+arg.toHuman(fdjt_language);
	    else if (arg.toFDJTString)
	      output=output+arg.toFDJTString();
	    else output=output+arg;
	  else output=output+arg;
	  if (numlen<0) scan=fmt.length;
	  else scan=next+1+numlen;}
	next=fmt.indexOf('%',scan);}}
    output=output+fmt.slice(scan);
    return output;}
}

/* Time functions */

function fdjtTime()
{
  return (new Date()).getTime();
}

function fdjtTick()
{
  return (new Date()).getTime()/1000;
}

function fdjtTickString(tick)
{
  return (new Date(tick*1000)).toString();
}

function fdjtTickDate(tick)
{
  return (new Date(tick*1000)).toDateString();
}

function fdjtIntervalString(interval)
{
  if (interval===1)
    return _("%1 second",interval);
  else if (interval<10)
    return _("%1 seconds",interval);
  else if (interval<60)
    return _("~%1 seconds",Math.round(interval/60));
  else if (interval<120) {
    var minutes=Math.floor(interval/60);
    var seconds=Math.round(interval-(minutes*60));
    if (seconds===1)
      return _("one minute, one second");
    else return _("one minute, %1 seconds",seconds);}
  else if (interval<3600) {
    var minutes=Math.floor(interval/60);
    return _("~%1 minutes",minutes);}
  else if (interval<(2*3600)) {
    var hours=Math.floor(interval/3600);
    var minutes=Math.round((interval-(hours*3600))/60);
    if (minutes===1)
      return _("one hour and one minutes");
    else return _("one hour, %1 minutes",minutes);}
  else if (interval<(24*3600)) {
    var hours=Math.floor(interval/3600);
    return _("~%1 hours",hours);}
  else if (interval<(2*24*3600)) {
    var hours=Math.floor((interval-24*3600)/3600);
    if (hours===1)
      return _("one day and one hour");
    else return _("one day, %1 hours",hours);}
  else if (interval<(7*24*3600)) {
    var days=Math.floor(interval/(24*3600));
    return _("%1 days",days);}
  else if (interval<(14*24*3600)) {
    var days=Math.floor((interval-(7*24*3600))/(24*3600));
    if (days===1)
      return "one week and one day";
    else return _("one week and %1 days",days);}
  else {
    var weeks=Math.floor(interval/(7*24*3600));
    var days=Math.round((interval-(days*7*24*3600))/(7*24*3600));
    return _("%1 weeks, %2 days",weeks,days);}
}

function fdjtShortIntervalString(interval)
{
  // This is designed for short intervals
  if (interval<0.001)
    return Math.round(interval*1000000)+"us";
  else if (interval<0.1)
    return Math.round(interval*1000)+"ms";
  else if (interval<120)
    return (Math.round(interval*100)/100)+"s";
  else {
    var min=Math.round(interval/60);
    var secs=Math.round(interval-min*6000)/100;
    return min+"m"+secs+"s";}
}

function fdjtRunTimes(pname,start)
{
  var point=start; var report="";
  var i=2; while (i<arguments.length) {
    var phase=arguments[i++]; var time=arguments[i++];
    report=report+"; "+phase+": "+
      ((time.getTime()-point.getTime())/1000)+"s";
    point=time;}
  return pname+" "+((point.getTime()-start.getTime())/1000)+"s"+report;
}

function fdjtDiffTime(time1,time2)
{
  if (!(time2)) time2=new Date();
  var diff=time1.getTime()-time2.getTime();
  if (diff>0) return diff/1000; else return -(diff/1000);
}

function fdjtDeltaTime(time1)
{
  var time2=new Date();
  var diff=time1.getTime()-time2.getTime();
  time1.setTime(time2.getTime());
  if (diff>0) return diff/1000; else return -(diff/1000);
}

var fdjt_loaded=(new Date()).getTime();
function fdjtDeltaTime(time1)
{
  var time2=new Date();
  var diff=time1.getTime()-time2.getTime();
  time1.setTime(time2.getTime());
  if (diff>0) return diff/1000; else return -(diff/1000);
}

function fdjtElapsedTime(arg)
{
  if (!(arg)) arg=new Date();
  return (arg.getTime()-fdjt_loaded)/1000;
}

function fdjtET()
{
  var arg=new Date();
  return (arg.getTime()-fdjt_loaded)/1000;
}


/* Setups */

var fdjt_setup_started=false;
var fdjt_setup_done=false;
var fdjt_setups=[];
var fdjt_final_setups=[];

function fdjtAddSetup(fcn,final)
{
  if (fcn instanceof Array) {
    var i=0; while (i<fcn.length) fdjtAddSetup(fcn[i++],final);}
  else if (fdjt_setup_done) {
    if ((fdjtIndexOf(fdjt_setups,fcn)>=0)||
	(fdjtIndexOf(fdjt_final_setups,fcn)>=0))
      return;
    fdjtWarn("Running setup %o late",fcn);
    fcn();}
  else if (final)
    if (fdjtIndexOf(fdjt_final_setups,fcn)<0)
      fdjt_final_setups.push(fcn);
    else {}
  else if (fdjtIndexOf(fdjt_setups,fcn)<0)
    fdjt_setups.push(fcn);
  else {}
}

function _fdjtWindowId()
{
  if (window)
    if (window.name)
      if (window.location)
	return "#"+window.name+"@"+window.location;
      else return "#"+window.name;
    else return "window@"+window.location;
  else return "nowindow";
}

function fdjtSetup()
{
  if (fdjt_setup_started) return;
  fdjt_setup_started=true;
  if ((fdjt_buildhost)&&(fdjt_buildtime))
    fdjtLog("[%fs] Starting fdjtSetup (built on %s on %s) for %s",
	    fdjtElapsedTime(),fdjt_buildhost,fdjt_buildtime,_fdjtWindowId());
  else fdjtLog("[%fs] Starting fdjtSetup for %o@%o",
	       fdjtElapsedTime(),(((window)&&(window.name))||"anonymous"));
  var i=0; while (i<fdjt_setups.length) fdjt_setups[i++]();
  var i=0; while (i<fdjt_final_setups.length) fdjt_final_setups[i++]();
  fdjtLog("[%fs] Finished fdjtSetup for %s",fdjtElapsedTime(),_fdjtWindowId());
  fdjt_setup_done=true;
}

/* All done, just begun */

fdjtLoadMessage("Loaded jsutils.js");

/* Emacs local variables
;;;  Local variables: ***
;;;  compile-command: "make; if test -f ../makefile; then cd ..; make; fi" ***
;;;  End: ***
*/
/* -*- Mode: Javascript; -*- */

var fdjt_oids_id="$Id: jsutils.js 190 2009-12-04 22:51:23Z haase $";
var fdjt_oids_version=parseInt("$Revision: 190 $".slice(10,-1));

/* Copyright (C) 2009 beingmeta, inc.
   This file is a part of the FDJT web toolkit (www.fdjt.org)
   This file provides extended Javascript utility functions
    of various kinds.

   This program comes with absolutely NO WARRANTY, including implied
   warranties of merchantability or fitness for any particular
   purpose.

    Use, modification, and redistribution of this program is permitted
    under either the GNU General Public License (GPL) Version 2 (or
    any later version) or under the GNU Lesser General Public License
    (version 3 or later).

    These licenses may be found at www.gnu.org, particularly:
      http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
      http://www.gnu.org/licenses/lgpl-3.0-standalone.html

*/

var fdjtOIDs={};
var fdjtPools={};
var fdjt_pool_count=0;
var fdjt_oid_count=0;
// We reserve the first 16 million fdjtids for dynamic allocation
var fdjt_next_pool=(256*256*256);
// Whether to trace OID initialization
var fdjt_trace_oids=false;

/* Get a pool */

function fdjtPool(base)
{
  if (fdjtPools[base]) return fdjtPools[base];
  this.base=base; this.idbase=fdjt_next_pool;
  fdjt_next_pool=fdjt_next_pool+(256*256*16);
  fdjtPools[base]=this;
  return this;
}

fdjtPool.ref=function(base) {
  return fdjtPools[base]||(new fdjtPool(base));};

/* sBook OIDs */

function fdjtOID(string,slots)
{
  if (fdjtOIDs[string])
    if (slots) {
      var data=fdjtOIDs[string];
      for (var slotid in slots) data[slotid]=slots[slotid];
      return data;}
    else return fdjtOIDs[string];
  if (string.length<3) return false;
  if (!((string[0]===':')&&(string[1]==='@'))) return false;
  var slash=string.indexOf('/'); 
  if (slash<0) return false;
  if (fdjt_trace_oids) fdjtLog("Creating oid %s from %o",string,slots);
  var offstart=string.length-5;
  var poolid=((slash<offstart)?(string.slice(0,offstart)):(string.slice(0,slash)));
  var offid=parseInt(((slash<offstart)?(string.slice(offstart)):(string.slice(slash+1))),16);
  var pool=fdjtPools[poolid]||(new fdjtPool(poolid));
  if (!(slots)) slots=this;
  slots._fdjtid=pool.idbase+offid;
  fdjtOIDs[string]=slots;
  fdjt_oid_count++;
  return slots;
}

function fdjtImportOID(slots)
{
  if (slots.oid)
    if (fdjtOIDs[slots.oid]) {
      var data=fdjtOIDs[slots.oid];
      for (var slotid in slots) data[slotid]=slots[slotid];
      return data;}
    else return new fdjtOID(slots.oid,slots);
  else return false;
}

/*
    http://www.JSON.org/json2.js
    2009-06-29

    Public Domain.

    NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.

    See http://www.JSON.org/js.html

    This file creates a global JSON object containing two methods: stringify
    and parse.

        JSON.stringify(value, replacer, space)
            value       any JavaScript value, usually an object or array.

            replacer    an optional parameter that determines how object
                        values are stringified for objects. It can be a
                        function or an array of strings.

            space       an optional parameter that specifies the indentation
                        of nested structures. If it is omitted, the text will
                        be packed without extra whitespace. If it is a number,
                        it will specify the number of spaces to indent at each
                        level. If it is a string (such as '\t' or '&nbsp;'),
                        it contains the characters used to indent at each level.

            This method produces a JSON text from a JavaScript value.

            When an object value is found, if the object contains a toJSON
            method, its toJSON method will be called and the result will be
            stringified. A toJSON method does not serialize: it returns the
            value represented by the name/value pair that should be serialized,
            or undefined if nothing should be serialized. The toJSON method
            will be passed the key associated with the value, and this will be
            bound to the object holding the key.

            For example, this would serialize Dates as ISO strings.

                Date.prototype.toJSON = function (key) {
                    function f(n) {
                        // Format integers to have at least two digits.
                        return n < 10 ? '0' + n : n;
                    }

                    return this.getUTCFullYear()   + '-' +
                         f(this.getUTCMonth() + 1) + '-' +
                         f(this.getUTCDate())      + 'T' +
                         f(this.getUTCHours())     + ':' +
                         f(this.getUTCMinutes())   + ':' +
                         f(this.getUTCSeconds())   + 'Z';
                };

            You can provide an optional replacer method. It will be passed the
            key and value of each member, with this bound to the containing
            object. The value that is returned from your method will be
            serialized. If your method returns undefined, then the member will
            be excluded from the serialization.

            If the replacer parameter is an array of strings, then it will be
            used to select the members to be serialized. It filters the results
            such that only members with keys listed in the replacer array are
            stringified.

            Values that do not have JSON representations, such as undefined or
            functions, will not be serialized. Such values in objects will be
            dropped; in arrays they will be replaced with null. You can use
            a replacer function to replace those with JSON values.
            JSON.stringify(undefined) returns undefined.

            The optional space parameter produces a stringification of the
            value that is filled with line breaks and indentation to make it
            easier to read.

            If the space parameter is a non-empty string, then that string will
            be used for indentation. If the space parameter is a number, then
            the indentation will be that many spaces.

            Example:

            text = JSON.stringify(['e', {pluribus: 'unum'}]);
            // text is '["e",{"pluribus":"unum"}]'


            text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
            // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'

            text = JSON.stringify([new Date()], function (key, value) {
                return this[key] instanceof Date ?
                    'Date(' + this[key] + ')' : value;
            });
            // text is '["Date(---current time---)"]'


        JSON.parse(text, reviver)
            This method parses a JSON text to produce an object or array.
            It can throw a SyntaxError exception.

            The optional reviver parameter is a function that can filter and
            transform the results. It receives each of the keys and values,
            and its return value is used instead of the original value.
            If it returns what it received, then the structure is not modified.
            If it returns undefined then the member is deleted.

            Example:

            // Parse the text. Values that look like ISO date strings will
            // be converted to Date objects.

            myData = JSON.parse(text, function (key, value) {
                var a;
                if (typeof value === 'string') {
                    a =
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
                    if (a) {
                        return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
                            +a[5], +a[6]));
                    }
                }
                return value;
            });

            myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
                var d;
                if (typeof value === 'string' &&
                        value.slice(0, 5) === 'Date(' &&
                        value.slice(-1) === ')') {
                    d = new Date(value.slice(5, -1));
                    if (d) {
                        return d;
                    }
                }
                return value;
            });


    This is a reference implementation. You are free to copy, modify, or
    redistribute.

    This code should be minified before deployment.
    See http://javascript.crockford.com/jsmin.html

    USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
    NOT CONTROL.
*/

/*jslint evil: true */

/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
    call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
    getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
    lastIndex, length, parse, prototype, push, replace, slice, stringify,
    test, toJSON, toString, valueOf
*/

// Create a JSON object only if one does not already exist. We create the
// methods in a closure to avoid creating global variables.

var JSON = JSON || {};

(function () {

    function f(n) {
        // Format integers to have at least two digits.
        return n < 10 ? '0' + n : n;
    }

    if (typeof Date.prototype.toJSON !== 'function') {

        Date.prototype.toJSON = function (key) {

            return isFinite(this.valueOf()) ?
                   this.getUTCFullYear()   + '-' +
                 f(this.getUTCMonth() + 1) + '-' +
                 f(this.getUTCDate())      + 'T' +
                 f(this.getUTCHours())     + ':' +
                 f(this.getUTCMinutes())   + ':' +
                 f(this.getUTCSeconds())   + 'Z' : null;
        };

        String.prototype.toJSON =
        Number.prototype.toJSON =
        Boolean.prototype.toJSON = function (key) {
            return this.valueOf();
        };
    }

    var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
        escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
        gap,
        indent,
        meta = {    // table of character substitutions
            '\b': '\\b',
            '\t': '\\t',
            '\n': '\\n',
            '\f': '\\f',
            '\r': '\\r',
            '"' : '\\"',
            '\\': '\\\\'
        },
        rep;


    function quote(string) {

// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe escape
// sequences.

        escapable.lastIndex = 0;
        return escapable.test(string) ?
            '"' + string.replace(escapable, function (a) {
                var c = meta[a];
                return typeof c === 'string' ? c :
                    '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
            }) + '"' :
            '"' + string + '"';
    }


    function str(key, holder) {

// Produce a string from holder[key].

        var i,          // The loop counter.
            k,          // The member key.
            v,          // The member value.
            length,
            mind = gap,
            partial,
            value = holder[key];

// If the value has a toJSON method, call it to obtain a replacement value.

        if (value && typeof value === 'object' &&
                typeof value.toJSON === 'function') {
            value = value.toJSON(key);
        }

// If we were called with a replacer function, then call the replacer to
// obtain a replacement value.

        if (typeof rep === 'function') {
            value = rep.call(holder, key, value);
        }

// What happens next depends on the value's type.

        switch (typeof value) {
        case 'string':
            return quote(value);

        case 'number':

// JSON numbers must be finite. Encode non-finite numbers as null.

            return isFinite(value) ? String(value) : 'null';

        case 'boolean':
        case 'null':

// If the value is a boolean or null, convert it to a string. Note:
// typeof null does not produce 'null'. The case is included here in
// the remote chance that this gets fixed someday.

            return String(value);

// If the type is 'object', we might be dealing with an object or an array or
// null.

        case 'object':

// Due to a specification blunder in ECMAScript, typeof null is 'object',
// so watch out for that case.

            if (!value) {
                return 'null';
            }

// Make an array to hold the partial results of stringifying this object value.

            gap += indent;
            partial = [];

// Is the value an array?

            if (Object.prototype.toString.apply(value) === '[object Array]') {

// The value is an array. Stringify every element. Use null as a placeholder
// for non-JSON values.

                length = value.length;
                for (i = 0; i < length; i += 1) {
                    partial[i] = str(i, value) || 'null';
                }

// Join all of the elements together, separated with commas, and wrap them in
// brackets.

                v = partial.length === 0 ? '[]' :
                    gap ? '[\n' + gap +
                            partial.join(',\n' + gap) + '\n' +
                                mind + ']' :
                          '[' + partial.join(',') + ']';
                gap = mind;
                return v;
            }

// If the replacer is an array, use it to select the members to be stringified.

            if (rep && typeof rep === 'object') {
                length = rep.length;
                for (i = 0; i < length; i += 1) {
                    k = rep[i];
                    if (typeof k === 'string') {
                        v = str(k, value);
                        if (v) {
                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
                        }
                    }
                }
            } else {

// Otherwise, iterate through all of the keys in the object.

                for (k in value) {
                    if (Object.hasOwnProperty.call(value, k)) {
                        v = str(k, value);
                        if (v) {
                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
                        }
                    }
                }
            }

// Join all of the member texts together, separated with commas,
// and wrap them in braces.

            v = partial.length === 0 ? '{}' :
                gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
                        mind + '}' : '{' + partial.join(',') + '}';
            gap = mind;
            return v;
        }
    }

// If the JSON object does not yet have a stringify method, give it one.

    if (typeof JSON.stringify !== 'function') {
        JSON.stringify = function (value, replacer, space) {

// The stringify method takes a value and an optional replacer, and an optional
// space parameter, and returns a JSON text. The replacer can be a function
// that can replace values, or an array of strings that will select the keys.
// A default replacer method can be provided. Use of the space parameter can
// produce text that is more easily readable.

            var i;
            gap = '';
            indent = '';

// If the space parameter is a number, make an indent string containing that
// many spaces.

            if (typeof space === 'number') {
                for (i = 0; i < space; i += 1) {
                    indent += ' ';
                }

// If the space parameter is a string, it will be used as the indent string.

            } else if (typeof space === 'string') {
                indent = space;
            }

// If there is a replacer, it must be a function or an array.
// Otherwise, throw an error.

            rep = replacer;
            if (replacer && typeof replacer !== 'function' &&
                    (typeof replacer !== 'object' ||
                     typeof replacer.length !== 'number')) {
                throw new Error('JSON.stringify');
            }

// Make a fake root object containing our value under the key of ''.
// Return the result of stringifying the value.

            return str('', {'': value});
        };
    }


// If the JSON object does not yet have a parse method, give it one.

    if (typeof JSON.parse !== 'function') {
        JSON.parse = function (text, reviver) {

// The parse method takes a text and an optional reviver function, and returns
// a JavaScript value if the text is a valid JSON text.

            var j;

            function walk(holder, key) {

// The walk method is used to recursively walk the resulting structure so
// that modifications can be made.

                var k, v, value = holder[key];
                if (value && typeof value === 'object') {
                    for (k in value) {
                        if (Object.hasOwnProperty.call(value, k)) {
                            v = walk(value, k);
                            if (v !== undefined) {
                                value[k] = v;
                            } else {
                                delete value[k];
                            }
                        }
                    }
                }
                return reviver.call(holder, key, value);
            }


// Parsing happens in four stages. In the first stage, we replace certain
// Unicode characters with escape sequences. JavaScript handles many characters
// incorrectly, either silently deleting them, or treating them as line endings.

            cx.lastIndex = 0;
            if (cx.test(text)) {
                text = text.replace(cx, function (a) {
                    return '\\u' +
                        ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
                });
            }

// In the second stage, we run the text against regular expressions that look
// for non-JSON patterns. We are especially concerned with '()' and 'new'
// because they can cause invocation, and '=' because it can cause mutation.
// But just to be safe, we want to reject all unexpected forms.

// We split the second stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
// replace all simple value tokens with ']' characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or ']' or
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.

            if (/^[\],:{}\s]*$/.
test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {

// In the third stage we use the eval function to compile the text into a
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
// in JavaScript: it can begin a block or an object literal. We wrap the text
// in parens to eliminate the ambiguity.

                j = eval('(' + text + ')');

// In the optional fourth stage, we recursively walk the new structure, passing
// each name/value pair to a reviver function for possible transformation.

                return typeof reviver === 'function' ?
                    walk({'': j}, '') : j;
            }

// If the text is not JSON parseable, then a SyntaxError is thrown.

            throw new SyntaxError('JSON.parse');
        };
    }
}());
/* -*- Mode: Javascript; -*- */

var fdjt_domutils_id="$Id: domutils.js 227 2010-03-08 21:47:03Z haase $";
var fdjt_domutils_version=parseInt("$Revision: 227 $".slice(10,-1));

/* Copyright (C) 2001-2009 beingmeta, inc.
   This file is a part of the FDJT web toolkit (www.fdjt.org)
   This file provides extended functionality for web applications,
   especially for manipulating the DOM in various ways. 

   This program comes with absolutely NO WARRANTY, including implied
   warranties of merchantability or fitness for any particular
   purpose.

    Use, modification, and redistribution of this program is permitted
    under either the GNU General Public License (GPL) Version 2 (or
    any later version) or under the GNU Lesser General Public License
    (version 3 or later).

    These licenses may be found at www.gnu.org, particularly:
      http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
      http://www.gnu.org/licenses/lgpl-3.0-standalone.html

*/

var _fdjt_trace_dom=false;
var _fdjt_trace_domedits=false;
var _fdjt_trace_domsearches=false;
var _fdjt_trace_classedits=false;

/* Getting elements by ID */

function $(eltarg)
{
  if (typeof eltarg == 'string')
    return document.getElementById(eltarg);
  else return eltarg;
}

// Returns a string describing an element
function fdjtEltID(elt)
{
  var classname=elt.className;
  var id=elt.id;
  return "<"+elt.tagName+
    ((classname)?("."+classname.replace(/\s+/g,".")):(""))+
    ((id)?("#"+id):(""))+
    ">";
}

/* Getting event targets */

function $T(evt)
{
  if (!(evt)) evt=event;
  return evt.target||evt.srcElement;
}

// Like $, but more needy, it outputs a warning if a given element is
// not found.
function fdjtNeedElt(arg,name)
{
  if (typeof arg == 'string') {
    var elt=document.getElementById(arg);
    if (elt) return elt;
    else if (name)
      fdjtWarn("Invalid element ("+elt+") reference: "+arg);
    else fdjtWarn("Invalid element reference: "+arg);
    return null;}
  else if (arg) return arg;
  else {
    if (name)
      fdjtWarn("Invalid element ("+elt+") reference: "+arg);
    else fdjtWarn("Invalid element reference: "+arg);
    return null;}
}

/* Getting display style information */

var fdjt_tag_display_styles={
  "DIV": "block","P": "block","BLOCKQUOTE":"block",
  "H1": "block","H2": "block","H3": "block","H4": "block",
  "H5": "block","H6": "block","H7": "block","H8": "block",
  "UL": "block","LI": "list-item",
  "DL": "block","DT": "list-item","DD": "list-item",
  "SPAN": "inline","EM": "inline","STRONG": "inline",
  "TT": "inline","DEFN": "inline","A": "inline",
  "TD": "table-cell","TR": "table-row",
  "TABLE": "table", "PRE": "preformatted"};

function fdjtGuessDisplayStyle(elt)
{
  return "inline";
}

function fdjtDisplayStyle(elt)
{
  return (((window.getComputedStyle)&&(window.getComputedStyle(elt,null))&&
	   (window.getComputedStyle(elt,null).display))||
	  (fdjt_tag_display_styles[elt.tagName])||
	  (fdjtGuessDisplayStyle(elt)));
}

function fdjtIsBlockElt(elt)
{
  return (fdjtDisplayStyle(elt)==="block");
}

function fdjtIsPreformatted(elt)
{
  return (fdjtDisplayStyle(elt)==="preformatted");
}

function fdjtIsTextInput(target)
{
  while (target)
    if ((target.tagName==="INPUT") ||
	(target.tagName==="TEXTAREA") ||
	(target.className==="fdjttextinput"))
      return true;
    else target=target.parentNode;
  return false;
}

function fdjtIsClickactive(target)
{
  while (target)
    if (((target.tagName==='A')&&(target.href))||
	(target.tagName==="INPUT") ||
	(target.tagName==="TEXTAREA") ||
	(target.tagName==="SELECT") ||
	(target.tagName==="OPTION") ||
	(fdjtHasClass(target,"fdjtclickactive")))
      return true;
    else target=target.parentNode;
  return false;
}

/* DOMish utils */

function fdjtNodify(arg)
{
  if (typeof arg === "string")
    return document.createTextNode(arg);
  else if (typeof arg != "object")
    if (arg.toString)
      return document.createTextNode(arg.toString());
    else return document.createTextNode("#@!*%!");
  else if (arg.nodeType)
    return arg;
  else if (arg.toHTML)
    return (arg.toHTML());
  else if (arg.toString)
    return fdjtSpan("nodified",arg.toString());
  else return document.createTextNode("#@!*%!");
}

// These are selectors (<tag,class,id,attrib> vectors)
//  for nodes which don't include any text.
var fdjt_notext_rules=[];

function fdjtTextify(arg,flat,inside)
{
  if (arg.nodeType)
    if (arg.nodeType===3)
      if (flat)
	return fdjtFlatten(arg.nodeValue);
      else return arg.nodeValue;
    else if (arg.nodeType!==1) return false;
    else if ((arg.fdjtNoText) ||
	     (arg.getAttribute("NOTEXTIFY")) ||
	     (fdjtElementMatches(arg,fdjt_notext_rules))) {
      arg.fdjtNoText=true;
      return false;}
    else {
      var children=arg.childNodes;
      var display_type=fdjtDisplayStyle(arg);
      var string=""; var suffix="";
      // Figure out what suffix and prefix to use for this element
      // If inside is false, don't use anything.
      if (!(inside)) {}
      else if (!(display_type)) {}
      else if (display_type==="inline") {}
      else if (flat) suffix=" ";
      else if ((display_type==="block") ||
	       (display_type==="table") ||
	       (display_type==="preformatted")) {
	string="\n"; suffix="\n";}
      else if (display_style==="table-row") suffix="\n";
      else if (display_style==="table-cell") string="\t";
      else {}
      var i=0; while (i<children.length) {
	var child=children[i++];
	if ((child.nodeType) && (child.nodeType===3))
	  if (flat)
	    string=string+fdjtFlatten(child.nodeValue);
	  else string=string+child.nodeValue;
	else {
	  var stringval=fdjtTextify(child,flat,true);
	  if (stringval) string=string+stringval;}}
      return string+suffix;}
  else return false;
}

function fdjtHasParent(node,parent)
{
  while (node)
    if (node===parent) return true;
    else if (node===document) return false;
    else node=node.parentNode;
  return false;
}

function fdjtHasContent(node,recur)
{
  if (node.childNodes) {
    var children=node.childNodes;
    var i=0; while (i<children.length) {
      var child=children[i++];
      if (child.nodeType===3)
	if (child.nodeValue.search(/\w/g)>=0) return true;
	else {}
      else if ((recur) && (child.nodeType===1))
	if (fdjtHasContent(child)) return true;
	else {}}
    return false;}
  else return false;
}

function fdjtHasText(node)
{
  if (node.childNodes) {
    var children=node.childNodes;
    var i=0; while (i<children.length) {
      var child=children[i++];
      if (child.nodeType===3)
	if (child.nodeValue.search(/\w/g)>=0) return true;
	else {}}
    return false;}
  else return false;
}

/* Attribute functions of various sorts */

// We define this because .hasAttribute isn't everywhere
function fdjtHasAttrib(elt,attribname,attribval)
{
  if (!(attribval))
    if (elt.hasAttribute)
      return elt.hasAttribute(attribname);
    else if (elt.getAttribute)
      if (elt.getAttribute(attribname))
	return true;
      else return false;
    else return false;
  else if ((elt.getAttribute) &&
	   (elt.getAttribute(attribname)==attribval))
    return true;
  else return false;
}

function fdjtCacheAttrib(elt,attribname,xform,dflt)
{
  var aval;
  if (elt.hasOwnProperty(attribname)) return elt[attribname];
  else if ((elt.getAttribute) &&
	   (aval=elt.getAttribute(attribname))) {
    var val=((xform) ? (xform(aval)) : (aval));
    elt[attribname]=val;
    return val;}
  else if (dflt) {
    elt[attribname]=dflt;
    return dflt;}
  else return false;
}

// We define this because .hasAttribute isn't everywhere
function fdjtSetAttrib(elt,attribname,attribval)
{
  if (typeof elt === 'string') elt=document.getElementById(elt);
  if ((elt) || (typeof elt === 'string')) {
    fdjtWarn("Bad element %o for fdjtSetAttrib",elt);
    return;}
  else if (elt.setAttribute)
    elt.setAttribute(attribname,attribval);
  else if (elt.length) {
    var i=0; while (i<elt.length) {
      var e=elt[i++];
      if (e.setAttribute) e.setAttribute(attribname,attribval);}}
  else {
    fdjtWarn("Bad element %o for fdjtSetAttrib",elt);
    return;}
}

// This takes a series of space or separated elt ids and resolves
// them
function fdjtGetIds(string)
{
  var elts=string.split(/;|\W/g);
  var results=[];
  var i=0; while (i<elts.length) {
    var elt=document.getElementById(elts[i++]);
    if (elt) results.push(elt);}
  return results;
}

var fdjt_meta_foldcase=true;
// This gets a META content field
function fdjtGetMeta(name,foldcase)
{
  if (typeof foldcase==='undefined') foldcase=fdjt_meta_foldcase;
  var uppername=((foldcase)&&(name.toUpperCase()));
  var elts=document.getElementsByTagName("META");
  var i=0; while (i<elts.length)
	     if ((elts[i]) && (elts[i].name===name))
	       return elts[i].content;
	     else if ((foldcase)&&(elts[i])&&
		      (elts[i].name.toUpperCase()===uppername))
	       return elts[i].content;
	     else i++;
  return false;
}

var fdjt_link_foldcase=true;
// This gets a LINK href field
function fdjtGetLink(name,foldcase)
{
  if (typeof foldcase==='undefined') foldcase=fdjt_link_foldcase;
  var uppername=((foldcase)&&(name.toUpperCase()));
  var elts=document.getElementsByTagName("LINK");
  var i=0; while (i<elts.length)
	     if ((elts[i]) && (elts[i].rel===name))
	       return elts[i].href;
	     else if ((foldcase)&&(elts[i])&&
		      (elts[i].rel.toUpperCase()===uppername))
	       return elts[i].href;
	     else i++;
  return false;
}

/* This is a kludge to force a redisplay when the browser
   doesn't neccessarily do it automatically.  (you know who
   I'm talking about, IE!) */
function fdjtRedisplay(arg)
{
  if (!(arg)) return;
  else if (arguments.length>1) {
    var i=0; while (i<arguments.length) fdjtRedisplay(arguments[i++]);}
  else if (typeof arg === "string")
    fdjtRedisplay($(arg));
  else if (arg instanceof Array) {
    var i=0; while (i<arg.length) fdjtRedisplay(arg[i++]);}
  else if ((arg.nodeType) && (arg.nodeType===1)) {
    var oldclass=arg.className;
    arg.className=null;
    arg.className=oldclass;}
  else return;
}

/* Manipluating class names */

var _fdjt_whitespace_pat=/(\s)+/;
var _fdjt_trimspace_pat=/^(\s)+|(\s)+$/;
var _fdjt_classpats={};

function _fdjtclasspat(name)
{
  if (typeof name === "string")
    return (_fdjt_classpats[name])||(_fdjtmakeclasspat(name));
  else if (name instanceof RegExp)
    return name;
  else throw { name: "invalid class name", irritant: name};
}

function _fdjtmakeclasspat(name)
{
  var rx=new RegExp("\\b"+name+"\\b","g");
  _fdjt_classpats[name]=rx;
  return rx;
}

function fdjtHasClass(elt,classname,attrib)
{
  var classinfo=((attrib) ? (elt.getAttribute(attrib)||"") : (elt.className));
  if ((classinfo) &&
      ((classinfo===classname) ||
       (classinfo.search(_fdjtclasspat(classname))>=0)))
    return true;
  else return false;
}

function fdjtAddClass(elt,classname,attrib)
{
  if (elt===null) return null;
  else if (typeof elt === "string")
    if (elt==="") return false;
    else if (elt[0]==='#') {
      var elts=new Array();
      var ids=elt.split('#');
      var i=0; while (i<ids.length) {
	var e=document.getElementById(ids[i++]);
	if (e) elts.push(e);}
      elt=elts;}
    else elt=document.getElementById(elt);
  if (!(elt)) return false;
  else if (elt instanceof Array) {
    var i=0; while (i<elt.length) {
      var e=elt[i++]; fdjtAddClass(e,classname,(attrib||false));}}
  else {
    var classinfo=
      (((attrib) ? (elt.getAttribute(attrib)||"") :(elt.className))||null);
    var class_regex=_fdjtclasspat(classname);
    var newinfo=classinfo;
    if (_fdjt_trace_classedits)
      fdjtLog("Adding %s '%s' to (%s) on %o",
	      (attrib||"class"),classname,classinfo,elt);
    if ((classinfo===null) || (classinfo==""))
      newinfo=classname;
    else if (classinfo===classname)
      return false;
    else if (classinfo.search(class_regex)>=0)
      return false;
    else newinfo=classname+" "+classinfo;
    if (attrib) {
      elt.setAttribute(attrib,newinfo);
      // This sometimes trigger a CSS update that doesn't happen otherwise
      elt.className=elt.className;}
    else elt.className=newinfo;
    return true;}
}
function fdjtClassAdder(elt,classname)
{
  return function() {
    if (elt) fdjtAddClass(elt,classname);}
}

function fdjtDropClass(elt,classname,attrib,keep)
{
  if (elt===null) return null;
  else if (typeof elt === "string")
    if (elt==="") return false;
    else if (elt[0]==='#') {
      var elts=new Array();
      var ids=elt.split('#');
      var i=0; while (i<ids.length) {
	var e=document.getElementById(ids[i++]);
	if (e) elts.push(e);}
      elt=elts;}
    else elt=document.getElementById(elt);
  if (!(elt)) return false;
  else if (elt instanceof Array) {
    var i=0; while (i<elt.length) {
      var e=elt[i++]; fdjtDropClass(e,classname,(attrib||false));}}
  else {
    var classinfo=
      (((attrib) ? (elt.getAttribute(attrib)||"") :(elt.className))||null);
    var class_regex=_fdjtclasspat(classname);
    var newinfo=classinfo;
    if (_fdjt_trace_classedits)
      fdjtLog("Dropping %s '%s' from (%s) on %o",
	      (attrib||"class"),classname,classinfo,elt);
    if ((classinfo===null) || (classinfo==="")) return false;
    else if (classinfo===classname) 
      newinfo=null;
    else if (classinfo.search(class_regex)>=0) 
      newinfo=classinfo.replace(class_regex,"");
    else return false;
    if (newinfo)
      newinfo=newinfo.
	replace(_fdjt_whitespace_pat," ").
	replace(_fdjt_trimspace_pat,"");
    if (attrib)
      if (newinfo) {
	elt.setAttribute(attrib,newinfo);
	elt.className=elt.className;}
      else if (!(keep)) {
	elt.removeAttribute(attrib);
	elt.className=elt.className;}
      else {}
    else elt.className=newinfo;}
}
function fdjtClassDropper(elt,classname)
{
  return function() {
    if (elt) fdjtDropClass(elt,classname);}
}

function fdjtToggleClass(elt,classname,attrib,keep)
{
  if (elt===null) return null;
  else if (typeof elt === "string")
    if (elt==="") return false;
    else if (elt[0]==='#') {
      var elts=new Array();
      var ids=elt.split('#');
      var i=0; while (i<ids.length) {
	var e=document.getElementById(ids[i++]);
	if (e) elts.push(e);}
      elt=elts;}
    else elt=document.getElementById(elt);
  if (!(elt)) return false;
  else if (elt instanceof Array) {
    var i=0; while (i<elt.length) {
      var e=elt[i++]; fdjtToggleClass(e,classname,(attrib||false));}}
  else {
    var classinfo=
      (((attrib) ? (elt.getAttribute(attrib)||"") :(elt.className))||null);
    var class_regex=_fdjtclasspat(classname);
    var newinfo=classinfo;
    if (_fdjt_trace_classedits)
      fdjtLog("Toggling %s '%s' from (%s) on %o",
	      (attrib||"class"),classname,classinfo,elt);
    if ((classinfo===null) || (classinfo===""))
      newinfo=classname;
    else if (classinfo===classname) 
      newinfo=null;
    else if (classinfo.search(class_regex)>=0) 
      newinfo=
	classinfo.replace(class_regex,"").
	replace(_fdjt_whitespace_pat," ");
    else newinfo=classinfo+" "+classname;
    if (newinfo)
      newinfo=newinfo.replace(_fdjt_whitespace_pat," ");
    if (attrib)
      if (newinfo) elt.setAttribute(attrib,newinfo);
      else if (!(keep)) elt.removeAttribute(attrib);
      else elt.setAttribute(attrib,"");
    else elt.className=newinfo;
    if (attrib) elt.className=elt.className;}
}

function fdjtSwapClass(elt,classname,newclass,attrib)
{
  if (elt===null) return null;
  else if (typeof elt === "string")
    if (elt==="") return false;
    else if (elt[0]==='#') {
      var elts=new Array();
      var ids=elt.split('#');
      var i=0; while (i<ids.length) {
	var e=document.getElementById(ids[i++]);
	if (e) elts.push(e);}
      elt=elts;}
    else elt=document.getElementById(elt);
  if (!(elt)) return false;
  else if (elt instanceof Array) {
    var i=0; while (i<elt.length) {
      var e=elt[i++]; fdjtSwapClass(e,classname,(attrib||false));}}
  else {
    var classinfo=
      (((attrib) ? (elt.getAttribute(attrib)||"") :(elt.className))||null);
    var class_regex=_fdjtclasspat(classname);
    var newinfo=classinfo;
    if ((classinfo) && ((classinfo.search(class_regex))>=0)) 
      newinfo=
	classinfo.replace(class_regex,newclass).
	replace(_fdjt_whitespace_pat," ");
    else if (_fdjt_trace_dom) {
	fdjtLog
	  ("Couldn't swap %s '%s' to replace non-existing '%s', just adding to %o",
	   (attrib||"class"),newclass,classname,elt);
	return fdjtAddClass(elt,newclass,(attrib||false));}
    else return fdjtAddClass(elt,newclass,(attrib||false));
    if (newinfo)
      newinfo=newinfo.
	replace(_fdjt_whitespace_pat," ");
    if (attrib)
      if (newinfo) elt.setAttribute(attrib,newinfo);
      else elt.removeAttribute(attrib);
    else elt.className=newinfo;
    if (attrib) elt.className=elt.className;}
}

/* Next and previous elements */

function fdjtNextElement(node)
{
  if (node.nextElementSibling)
    return node.nextElementSibling;
  else {
    var scan=node;
    while (scan=scan.nextSibling) {
      if (!(scan)) return null;
      else if (scan.nodeType==1) break;
      else {}}
    return scan;}
}

function fdjtPreviousElement(node)
{
  if (node.previousElementSibling)
    return node.previousElementSibling;
  else {
    var scan=node;
    while (scan=scan.previousSibling) 
      if (!(scan)) return null;
      else if (scan.nodeType==1) break;
      else {}
    return scan;}
}

/* Going forward and backward */

function fdjtForward(node,exclude)
{
  if ((node.childNodes)&&((node.childNodes.length)>0))
    return node.childNodes[0];
  else while (node)
	 if (node.nextSibling)
	   return node.nextSibling;
	 else node=node.parentNode;
  return false;
}

function fdjtNext(node,exclude)
{
  while (node)
    if (node.nextSibling)
      return node.nextSibling;
    else node=node.parentNode;
  return false;
}

function fdjtForwardNode(node,test)
{
  var scan=fdjtForward(node);
  while (scan)
    if (scan.nodeType===1)
      if (!(test)) return scan;
      else if (test(scan)) return scan;
      else scan=fdjtNext(scan);
    else scan=fdjtForward(scan);
  return false;
}

function fdjtNextNode(node,test)
{
  var scan=fdjtNext(node);
  while (scan)
    if (scan.nodeType===1)
      if (!(test)) return scan;
      else if (test(scan)) return scan;
      else scan=fdjtNext(scan);
    else scan=fdjtNext(scan);
  return false;
}

/* Searching by tag name */

function fdjtGetParentByTagName(node,tagname)
{
  var scan;
  if (typeof node == "string")
    scan=document.getElementById(node);
  else scan=node;
  tagname=tagname.toUpperCase();
  while ((scan) && (scan.parentNode)) 
    if (scan.tagName===tagname) return scan;
    else if (node===document) return null;
    else scan=scan.parentNode;
  if ((scan) && (scan.tagName===tagname)) return scan;
  else return null;
}

function fdjtGetChildrenByTagName(under,tagname)
{
  if (typeof under === 'string') {
    under=document.getElementById(under);
    if (under===null) return new Array();}
  tagname=tagname.toUpperCase();
  if (under===null)
    if (document.getElementsByTagName)
      return document.getElementsByTagName(tagname);
    else return _fdjtGetChildrenByTagName(document,tagname,new Array());
  else if (under.getElementsByTagName)
    return under.getElementsByTagName(tagname);
  else return _fdjtGetChildrenByTagName(under,tagname,new Array());
}
function _fdjtGetChildrenByTagName(under,tagname,results)
{
  if ((under.nodeType===1) && (under.tagName===tagname))
    results.push(under);
  var children=under.childNodes;
  if (children) {
    var i=0; while (i<children.length)
	       if (children[i].nodeType===1)
		 _fdjtGetChildrenByTagName(children[i++],tagname,results);
	       else i++;}
  return results;
}

/* Searching by class name */

function fdjtGetParentByClassName(node,classname)
{
  var scan;
  if (typeof node === "string") scan=document.getElementById(node);
  else scan=node;
  while ((scan) && (scan.parentNode))
    if (scan.className===classname)
      return scan;
    else if ((scan.className) && (fdjtHasClass(scan,classname)))
      return scan;
    else if (scan===document) return null;
    else scan=scan.parentNode;
  if ((scan) && (scan.className===classname)) return scan;
  else return null;
}

function fdjtGetChildrenByClassName(under,classname)
{
  if (typeof under === 'string')
    under=document.getElementById(under);
  if ((under) && (under.getElementsByClassName))
    return under.getElementsByClassName(classname);
  else if ((under===null) && (document.getElementsByClassName))
    return document.getElementsByClassName(classname);
  else if (under===null)
    return _fdjtGetChildrenByClassName(document,classname,new Array());
  else return _fdjtGetChildrenByClassName(under,classname,new Array());
}
function _fdjtGetChildrenByClassName(under,classname,results)
{
  if ((under.nodeType===1) && (under.className===classname))
    results.push(under);
  var children=under.childNodes;
  if (children) {
    var i=0; while (i<children.length)
	       if (children[i].nodeType===1)
		 _fdjtGetChildrenByClassName(children[i++],classname,results);
	       else i++;}
  return results;
}     

/* Kind of legacy */

function fdjtGetElementsByClassName(classname,under_arg)
{
  if (!(under_arg))
    return fdjtGetChildrenByClassName(null,classname);
  else if (typeof under_arg === 'string') {
    var under=document.getElementById(under_arg);
    if (under==null) return new Array();
    else return fdjtGetChildrenByClassName(under,classname);}
  else return fdjtGetChildrenByClassName(under_arg,classname);
}

function fdjtGetElementsByTagName(tagname,under_arg)
{
  if (!(under_arg))
    return fdjtGetChildrenByTagName(null,tagname.toUpperCase());
  else if (typeof under_arg === 'string') {
    var under=document.getElementById(under_arg);
    if (under==null) return new Array();
    else return fdjtGetChildrenByTagName(under,tagname.toUpperCase());}
  else return fdjtGetChildrenByTagName(under_arg,tagname);
}

/* Searching by attribute */

function fdjtGetParentByAttrib(node,attribName,attribValue)
{
  var scan;
  if (typeof node === "string") scan=document.getElementById(node);
  else node=scan;
  if (attribValue)
    while ((scan) && (scan.parentNode))
      if (scan.getAttribute(attribName)==attribValue)
	return scan;
      else if (scan===document) return null;
      else parent=scan.parentNode;
  else while ((scan) && (scan.parentNode))
    if ((scan.hasAttribute) ? (scan.hasAttribute(attribName)) :
	(!(!(scan.getAttribute(attribName)))))
      return scan;
    else if (scan===document) return null;
    else scan=scan.parentNode;
  return null;
}

function fdjtGetChildrenByAttrib(under,attribName,attribValue)
{
  if (typeof under === 'string')
    under=document.getElementById(under);
  if (attribValue)
    return _fdjtGetChildrenByAttribValue
      (under,attribName,attribValue,new Array());
  else return _fdjtGetChildrenByAttrib(under,attribName,new Array());
}
function _fdjtGetChildrenByAttrib(under,attribname,results)
{
  if ((under.nodeType==1) &&
      ((under.hasAttribute) ? (under.hasAttribute(attribname)) :
       (under.getAttribute(attribname))))
    results.push(under);
  var children=under.childNodes;
  if (children) {
    var i=0; while (i<children.length)
	       if (children[i].nodeType==1)
		 _fdjtGetChildrenByAttrib(children[i++],attribname,results);
	       else i++;}
  return results;
}
function _fdjtGetChildrenByAttribValue(under,attribname,attribval,results)
{
  if ((under.nodeType==1) &&
      (under.getAttribute(attribname)==attribval))
    results.push(under);
  var children=under.childNodes;
  if (children) {
    var i=0; while (i<children.length)
	       if (children[i].nodeType==1)
		 _fdjtGetChildrenByAttribValue
		   (children[i++],attribname,attribval,results);
	       else i++;}
  return results;
}

/* Searching by selector */

function fdjtParseSelector(spec)
{
  var tagname=null, classname=null, idname=null;
  var dotpos=spec.indexOf('.'), hashpos=spec.indexOf('#');
  if ((dotpos<0) && (hashpos<0))
    tagname=spec.toUpperCase();
  else if (dotpos>=0) {
    classname=spec.slice(dotpos+1);
    if (dotpos>0) tagname=spec.slice(0,dotpos).toUpperCase();}
  else if (hashpos>=0) {
    idname=spec.slice(hashpos+1);
    if (hashpos>0) tagname=spec.slice(0,hashpos).toUpperCase();}
  else if (dotpos<hashpos) {
    if (dotpos>0) tagname=spec.slice(0,dotpos);
    classname=spec.slice(dotpos+1,hashpos);
    idname=spec[hashpos];}
  else {
    if (hashpos>0) tagname=spec.slice(0,hashpos).toUpperCase();
    classname=spec.slice(dotpos);
    idname=spec.slice(hashpos+1,dotpos);}
  if ((tagname==="") || (tagname==="*")) tagname=null;
  if ((classname==="") || (classname==="*")) tagname=null;
  if ((idname==="") || (idname==="*")) tagname=null;
  return new Array("selector",tagname,classname,idname);
}

function fdjtElementMatches(elt,selector)
{
  if (!(selector)) return false;
  else if (typeof selector === "string") {
    var spec=fdjtParseSelector(selector);
    return (((spec[1]===null) || (elt.tagName===spec[1])) &&
	    ((spec[2]===null) || (spec[2]===elt.className) ||
	     ((elt.className)&&(elt.className.search(_fdjtclasspat(spec[2]))>=0))) &&
	    ((spec[3]===null) || (elt.id===spec[3])));}
  else if (!(selector instanceof Array)) return false;
  else if (selector.length===0) return false;
  else if (selector[0]==="selector") {
    var spec=selector;
    return (((spec[1]===null) || (elt.tagName===spec[1])) &&
	    ((spec[2]===null) || (spec[2]===elt.className) ||
	     ((elt.className)&&(elt.className.search(_fdjtclasspat(spec[2]))>=0))) &&
	    ((spec[3]===null) || (elt.id===spec[3])));}
  else {
    var i=0; while (i<selector.length)
	       if (fdjtElementMatches(elt,selector[i]))
		 return true;
	       else i++;
    return false;}
}

function fdjtElementMatchesSpec(elt,spec)
{
  return (((spec[1]===null) || (elt.tagName===spec[1])) &&
	  ((spec[2]===null) || (spec[2]===elt.className) ||
	   (elt.className.search(_fdjtclasspat(spec[2]))>=0)) &&
	  ((spec[3]===null) || (elt.id===spec[3])));
}

function fdjtGetParents(elt,selector,results)
{
  if (!(results)) results=new Array();
  if (selector instanceof Array) {
    var i=0; while (i<selector.length) 
      fdjtGetParents(elt,selector[i++],results);
    return results;}
  else {
    var scan=elt;
    while (scan) {
      if (fdjtIndexOf(results,scan)>=0) {}
      else if (fdjtElementMatchesSpec(scan,spec)) 
	results.push(scan);
      else {}
      if (scan===document) return results;
      scan=scan.parentNode;}
    return results;}
}

function fdjtGetParent(elt,selector)
{
  var scan=elt;
  while (scan)
    if (fdjtElementMatches(scan,selector))
      return scan;
    else if (scan===document) return null;
    else scan=scan.parentNode;
  return null;
}

function fdjtGetChildren(elt,selector,results)
{
  if (!(results)) results=new Array();
  if (typeof selector === "string")
    selector=fdjtParseSelector(selector);
  if (!((typeof selector === "object") &&
	(selector instanceof Array) &&
	(selector.length>0)))
    return results;
  else if (selector[0]==="selector") {
    if (selector[3]) {
      var candidate=document.getElementById(selector[3]);
      if (candidate)
	if (fdjtElementMatchesSpec(candidate,selector)) {
	  var scan=candidate;
	  while (scan)
	    if (scan===elt) break;
	    else if (scan===document) break;
	    else scan=scan.parentNode;
	  if (scan===elt) results.push(candidate);
	  return results;}
	else return results;}
    else if (selector[2]) {
      var candidates=fdjtGetChildrenByClassName(elt,selector[2]);
      var i=0; while (i<candidates.length) {
	var candidate=candidates[i++];
	if (fdjtElementMatchesSpec(candidate,selector))
	  results.push(candidate);}
      return results;}
    else if (selector[1]) {
      var candidates=fdjtGetChildrenByTagName(elt,selector[1]);
      var i=0; while (i<candidates.length)
		 results.push(candidates[i++]);
      return results;}
    else return results;}
  else {
    var i=0; while (i<selector.length)
	       fdjtGetChildren(elt,selector[i++],results);
    return results;}
}

function fdjtGetFirstChild(elt,selector)
{
  if (typeof selector === "string")
    return fdjtGetFirstChild(elt,fdjtParseSelector(selector));
  else if (elt.nodeType!==1) return false;
  var children=elt.childNodes;
  var i=0, n=children.length;
  while (i<n) {
    var child=children[i++];
    if (child.nodeType!==1) continue;
    if (fdjtElementMatchesSpec(child,selector))
      return child;
    else {
      var grandchild=fdjtGetFirstChild(child,selector);
      if (grandchild) return grandchild;}}
  return false;
}

function $$(selector,cxt) 
{
  var elt=((cxt) ? (cxt) : (document));
  return fdjtGetChildren(elt,selector,new Array());
}

function $P(selector,cxt) 
{
  return fdjtGetParent(cxt,selector);
}

/* Adding/Inserting nodes */

function fdjtAddElements(elt,elts,start,finish)
{
  var i=start||0;
  var lim=(((finish)&&(finish<elts.length))?(finish):(elts.length));
  var curstring=false;
  if (elt===null) return null;
  while (i<lim) {
    var arg=elts[i++]; 
    if (!(arg)) continue;
    else if ((typeof arg === 'string') || (typeof arg === "number")) {
      if (curstring) curstring=curstring+arg;
      else if (typeof arg === 'string') curstring=arg;
      else curstring=arg.toString();
      continue;}
    else {
      if (curstring) {
	elt.appendChild(document.createTextNode(curstring));
	curstring=false;}
      if (arg.nodeType) elt.appendChild(arg);
      else if (arg instanceof Array)
	fdjtAddElements(elt,arg,0);
      else elt.appendChild(fdjtNodify(arg));}}
  if (curstring)
    elt.appendChild(document.createTextNode(curstring));
  return elt;
}

function fdjtAddElementsTraced(elt,elts,start,finish)
{
  var curstring=false;
  var i=start||0;
  var lim=(((finish)&&(finish<elts.length))?(finish):(elts.length));
  fdjtTrace("fdjtAddElementsTraced to %o from %o from %o to %o",
	    elt,elts,i,lim);
  if (elt===null) return null;
  while (i<lim) {
    var arg=elts[i++]; 
    fdjtTrace("Adding %o to %o",arg,elt);
    if (!(arg)) continue;
    else if ((typeof arg === 'string') || (typeof arg === "number")) {
      if (curstring) curstring=curstring+arg;
      else if (typeof arg === 'string') curstring=arg;
      else curstring=arg.toString();
      continue;}
    else {
      if (curstring) {
	elt.appendChild(document.createTextNode(curstring));
	curstring=false;}
      if (arg.nodeType) elt.appendChild(arg);
      else if (arg instanceof Array)
	fdjtAddElements(elt,arg,0);
      else elt.appendChild(fdjtNodify(arg));}}
  if (curstring)
    elt.appendChild(document.createTextNode(curstring));
  return elt;
}

function fdjtAddAttributes(elt,attribs)
{
  if (elt===null) return null;
  if (attribs) {
    for (key in attribs) {
      if (key=='title')
	elt.title=attribs[key];
      else if (key=='name')
	elt.name=attribs[key];
      else if (key=='id')
	elt.id=attribs[key];
      else if (key=='value')
	elt.value=attribs[key];
      else elt.setAttribute(key,attribs[key]);}
    return elt;}
  else return elt;
}

function fdjtInsertElementsBefore(elt,before,elts,i)
{
  if (elt===null) return null;
  if ((_fdjt_trace_dom) || (_fdjt_trace_domedits))
    fdjtLog("Inserting "+elts+" elements "
	   +"into "+elt
	   +" before "+before
	   +" starting with "+elts[0]);
  while (i<elts.length) {
    var arg=elts[i++];
    if (!(arg)) {}
    else if (arg instanceof Array) {
      var j=0; while (j<arg.length) {
	var e=arg[j++];
	if (arg) elt.insertBefore(fdjtNodify(e),before);}}
    else elt.insertBefore(fdjtNodify(arg),before);}
  return elt;
}

/* Higher level functions, use lexpr/rest/var args */

function fdjtAppend(elt_arg)
{
  var elt=null;
  if (elt_arg===null) return null;
  else if (typeof elt_arg == 'string')
    elt=document.getElementById(elt_arg);
  else if (elt_arg) elt=elt_arg;
  if (elt) return fdjtAddElements(elt,arguments,1);
  else fdjtWarn("Invalid DOM argument: "+elt_arg);
}

function fdjtPrepend(elt_arg)
{
  var elt=null;
  if (elt_arg===null) return null;
  else if (typeof elt_arg == 'string')
    elt=document.getElementById(elt_arg);
  else if (elt_arg) elt=elt_arg;
  if (elt)
    if (elt.firstChild)
      return fdjtInsertElementsBefore(elt,elt.firstChild,arguments,1);
    else return fdjtAddElements(elt,arguments,1);
  else fdjtWarn("Invalid DOM argument: "+elt_arg);
}

function fdjtInsertBefore(before_arg)
{
  var parent=null; var before=null;
  if (before_arg===null) return null;
  else if (typeof before_arg == 'string') {
    before=document.getElementById(before_arg);
    if (before==null) {
      fdjtWarn("Invalid DOM before argument: "+before_arg);
      return;}}
  else before=before_arg;
  if ((before) && (before.parentNode))
    elt=before.parentNode;
  else {
    if (before===before_arg)
      fdjtWarn("Invalid DOM before argument: "+before_arg);
    else fdjtWarn("Invalid DOM before argument: "+before_arg+"="+before);
    return;}
  return fdjtInsertElementsBefore(elt,before,arguments,1);
}

function fdjtInsertAfter(after_arg)
{
  var parent=null, after=null;
  if (after_arg===null) return null;
  else if (typeof after_arg == 'string') {
    after=document.getElementById(after_arg);
    if (after==null) {
      fdjtWarn("Invalid DOM after argument: "+after_arg);
      return;}}
  else after=after_arg;
  if ((after) && (after.parentNode))
    elt=after.parentNode;
  else {
    if (after===after_arg)
      fdjtWarn("Invalid DOM after argument: "+after_arg);
    else fdjtWarn("Invalid DOM after argument: "+after_arg+"="+after);
    return;}
  if (after.nextSibling)
    return fdjtInsertElementsBefore(elt,after.nextSibling,arguments,1);
  else return fdjtAddElements(elt,arguments,1);
}

function fdjtReplace(cur_arg,newnode)
{
  var cur=null;
  if (cur_arg===null) return null;
  else if (typeof cur_arg === 'string')
    cur=document.getElementById(cur_arg);
  else cur=cur_arg;
  if (cur) {
    var parent=cur.parentNode;
    var replacement=fdjtNodify(newnode);
    parent.replaceChild(replacement,cur);
    if (typeof cur_arg === "string") {
      cur.id=null; replacement.id=cur_arg; }
    else if ((cur.id) && (!(replacement.id))) {
      replacement.id=cur.id; cur.id=null;}
    return replacement;}
  else {
    fdjtWarn("Invalid DOM replace argument: "+cur_arg);
    return;}
}

function fdjtDelete(cur_arg)
{
  var cur=null;
  if (cur_arg===null) return null;
  else if (typeof cur_arg == "string")
    cur=document.getElementById(cur_arg);
  else cur=cur_arg;
  if (cur) {
    var parent=cur.parentNode;
    parent.removeChild(cur);
    return null;}
  else {
    fdjtWarn("Invalid DOM replace argument: "+cur_arg);
    return;}
}

/* Element Creation */

function fdjtNewElement(tag,classname)
{
  var elt=document.createElement(tag);
  if ((typeof classname === "string") &&
      (classname.length>0)) {
    if (classname[0]==="#") {
      var dotpos=classname.indexOf(".");
      if (dotpos>0) {
	elt.id=classname.slice(1,dotpos);
	elt.className=classname.slice(dotpos+1).replace(/[.]/g," ");}
      else elt.id=classname.slice(1);}
    else if (classname[0]===".") {
      var hashpos=classname.indexOf("#");
      if (hashpos>0) {
	elt.id=classname.slice(hashpos+1);
	elt.className=classname.slice(1,hashpos).replace(/[.]/g," ");}
      else elt.className=classname.slice(1).replace(/[.]/g," ");}
    else elt.className=classname;}
  if (arguments.length>2)
    fdjtAddElements(elt,arguments,2);
  return elt;
}

function fdjtNewElementW(tag,classname,attribs)
{
  var elt=fdjtNewElement(tag,classname);
  fdjtAddAttributes(elt,attribs);
  if (arguments.length>3)
    fdjtAddElements(elt,arguments,3);
  return elt;
}

function fdjtNewElt(eltspec)
{
  var hashpos=eltspec.indexOf('#'); var dotpos=eltspec.indexOf('.');
  var tagend=(((hashpos>0) && (dotpos>0)&&((hashpos<dotpos)?hashpos:dotpos))
	      ||((hashpos>0) ? (hashpos) : ((dotpos>0)&&(dotpos))));
  var elt=((tagend)?
	   (fdjtNewElement(eltspec.slice(0,tagend),eltspec.slice(tagend))) :
	   (fdjtNewElement(eltspec)));
  if (arguments.length>1)
    fdjtAddElements(elt,arguments,1);
  return elt;
}

function fdjtElt(eltspec)
{
  var hashpos=eltspec.indexOf('#'); var dotpos=eltspec.indexOf('.');
  var tagend=(((hashpos>0) && (dotpos>0)&&((hashpos<dotpos)?hashpos:dotpos))
	      ||((hashpos>0) ? (hashpos) : ((dotpos>0)&&(dotpos))));
  var elt=((tagend)?
	   (fdjtNewElement(eltspec.slice(0,tagend),eltspec.slice(tagend))) :
	   (fdjtNewElement(eltspec)));
  if (arguments.length>1)
    fdjtAddElements(elt,arguments,1);
  return elt;
}

function fdjtEltW(eltspec,attribs)
{
  var hashpos=eltspec.indexOf('#'); var dotpos=eltspec.indexOf('.');
  var tagend=(((hashpos>0) && (dotpos>0)&&((hashpos<dotpos)?hashpos:dotpos))
	      ||((hashpos>0) ? (hashpos) : ((dotpos>0)&&(dotpos))));
  var elt=((tagend)?
	   (fdjtNewElement(eltspec.slice(0,tagend),eltspec.slice(tagend))) :
	   (fdjtNewElement(eltspec)));
  if (attribs)
    for (var key in attribs)
      elt.setAttribute(key,attribs[key]);
  if (arguments.length>1)
    fdjtAddElements(elt,arguments,2);
  return elt;
}

function fdjtWithId(elt,id)
{
  elt.id=id;
  return elt;
}

function fdjtId(elt,id)
{
  elt.id=id;
  return elt;
}

function fdjtWithTitle(elt,title)
{
  elt.title=title;
  return elt;
}

function fdjtSpan(classname)
{
  var elt=((classname) ?
	   (fdjtNewElement('span',classname)) :
	   (document.createElement('span')));
  fdjtAddElements(elt,arguments,1);
  return elt;
}

function fdjtSpanW(classname,attribs)
{
  var elt=fdjtNewElementW('span',classname,attribs);
  fdjtAddElements(elt,arguments,2);
  return elt;
}

function fdjtDiv(classname)
{
  var elt=((classname) ?
	   (fdjtNewElement('div',classname)) :
	   (document.createElement('div')));
  fdjtAddElements(elt,arguments,1);
  return elt;
}

function fdjtDivW(classname,attribs)
{
  var elt=fdjtNewElementW('div',classname,attribs);
  fdjtAddElements(elt,arguments,2);
  return elt;
}

function fdjtImage(url,classname,alt,title)
{
  if (!(classname)) classname=null;
  var elt=((classname) ? (fdjtNewElement("img",classname)) :
	   (document.createElement('img')));
  elt.src=url;
  if (typeof alt == "string") elt.alt=alt;
  if (title) elt.title=title;
  else if (alt) elt.title=alt;
  return elt;
}

function fdjtImageW(url,attribs)
{
  if (!(attribs)) attribs=false;
  var elt=document.createElement('img');
  elt.src=url;
  if (attribs) fdjtAddAttributes(elt,attribs);
  return elt;
}

function fdjtAnchor(url)
{
  var elt=document.createElement('a');
  elt.href=url;
  fdjtAddElements(elt,arguments,1);
  return elt;
}

function fdjtAnchorC(url,spec)
{
  var elt=fdjtNewElement("a",spec);
  elt.href=url;
  fdjtAddElements(elt,arguments,2);
  return elt;
}

function fdjtAnchorW(url,attribs)
{
  var elt=document.createElement('a');
  elt.href=url;
  fdjtAddAttributes(elt,attribs);
  fdjtAddElements(elt,arguments,2);
  return elt;
}

function fdjtInput(type,name,value,classname,title)
{
  var elt=fdjtNewElement('INPUT',classname);
  elt.type=type; elt.name=name;
  if (!(value)) elt.value=null;
  else if (typeof value === 'string') elt.value=value;
  else if (value.toFormString)
    elt.value=value.toFormString()||value.toString();
  else elt.value=value.toString();
  if (title) elt.title=title;
  return elt;
}

function fdjtCheckbox(name,value,checked)
{
  var elt=document.createElement('input');
  elt.type='checkbox'; elt.name=name; elt.value=value;
  if (checked) elt.checked=true;
  else elt.checked=false;
  return elt;
}

function fdjtHR(attribs,classinfo) { return fdjtEltW(classinfo||"HR",attribs); }
function fdjtBR(attribs,classinfo) { return fdjtEltW(classinfo||"BR",attribs); }

/* Grid functions */

function fdjtGrid()
{
  var table=document.createElement('table');
  table.className='grid';
  table.layout='auto'; table.rules='none';
  table.cellspacing=0; table.cellpadding=0;
  fdjtAddElements(table,fdjtArguments(arguments));
  return table;
}

function fdjtParseGridSize(gridsize)
{
  if (!(gridsize)) return {ncols: false, nrows: false};
  else if (typeof gridsize !== 'string')
    return {ncols: false, nrows: false};
  var break_at=gridsize.indexOf('x');
  var parsed={nrows: false,ncols: false};
  if (break_at<0) break_at=gridsize.indexOf(',');
  if (break_at<0) return parsed;
  var ncols=parseInt(gridsize.slice(0,break_at));
  var nrows=parseInt(gridsize.slice(break_at+1));
  if (ncols>1) parsed.ncols=ncols;
  if (nrows>1) parsed.nrows=nrows;
  return parsed;
}

function fdjtGridify(elt)
{
  if (!(elt)) return false;
  else if (!(elt.tagName))
    return fdjtElt("TD.gridelt",elt);
  else if ((elt.tagName==='TH')||(elt.tagName==='TD'))
    return elt;
  else if ((elt.getAttribute)&&(elt.getAttribute("grid"))) {
    var td=fdjtElt("TD.gridelt");
    var gridsize=fdjtParseGridSize(elt.getAttribute("grid"));
    if (gridsize.ncols) td.setAttribute('colspan',gridsize.ncols);
    if (gridsize.nrows) td.setAttribute('rowspan',gridsize.nrows);
    return td;}
  else return fdjtElt("TD.gridelt",elt);
}

function fdjtGridRow()
{
  var row=fdjtElt("TR.gridrow");
  var i=0; var lim=arguments.length;
  while (i<lim) {
    fdjtAppend(row,fdjtGridify(arguments[i++]));}
  return row;
}

function ROW() { return fdjtGridRow.apply(this,arguments); }
function GRID(size)
{
  var td=fdjtElt("TD.gridelt");
  var gridsize=fdjtParseGridSize(size);
  if (gridsize.ncols) td.setAttribute('colspan',gridsize.ncols);
  if (gridsize.nrows) td.setAttribute('rowspan',gridsize.nrows);
  fdjtAddElements(td,arguments,1);
  return td;
}

function fdjtTR()
{
  var tr_elt=document.createElement('tr');
  fdjtAddElements(tr_elt,arguments);
  return tr_elt;
}

function fdjtTD()
{
  var td_elt=document.createElement('td');
  fdjtAddElements(td_elt,arguments);
  return td_elt;
}

/* Dealing with selections */

function fdjtGetSelection(elt)
{
  if ((elt.tagName==='INPUT') || (elt.tagName==='TEXTAREA')) {
    // fdjtLog('start='+elt.selectionStart+'; end='+elt.selectionEnd+
    //   '; value='+elt.value);
    if ((elt.value) && (elt.selectionStart) && (elt.selectionEnd) &&
	(elt.selectionStart!=elt.selectionEnd)) 
      return elt.value.slice(elt.selectionStart,elt.selectionEnd);
    else return null;}
  else if (window.getSelection)
    return window.getSelection();
  else return null;
}

/* Forcing IDs */

var _fdjt_idcounter=0, fdjt_idbase=false;

function fdjtForceId(about)
{
  if (about.id)
    return about.id;
  else {
    if (!(fdjt_idbase))
      fdjt_idbase="TMPID"+(1000000+(Math.floor((1000000-1)*Math.random())))+"S";
    var tmpid=fdjt_idbase+_fdjt_idcounter++;
    while (document.getElementById(tmpid))
      tmpid=fdjt_idbase+_fdjt_idcounter++;
    about.id=tmpid;
    return tmpid;}
}

/* Checking element visibility */

function fdjtIsVisible(elt,partial)
{
  if (!(partial)) partial=false;
  var top = elt.offsetTop;
  var left = elt.offsetLeft;
  var width = elt.offsetWidth;
  var height = elt.offsetHeight;
  var winx=window.pageXOffset;
  var winy=window.pageYOffset;
  var winxedge=winx+window.innerWidth;
  var winyedge=winy+window.innerHeight;
  
  while(elt.offsetParent) {
    if (elt===window) break;
    elt = elt.offsetParent;
    top += elt.offsetTop;
    left += elt.offsetLeft;}

  /*
  fdjtTrace("fdjtIsVisible%s top=%o left=%o height=%o width=%o",
	    ((partial)?("(partial)"):""),
	    top,left,height,width);
  fdjtTrace("fdjtIsVisible winx=%o winy=%o winxedge=%o winyedge=%o",
	    winx,winy,winxedge,winyedge);
  */

  if (partial)
    // There are three cases we check for:
    return (
	    // top of element in window
	    ((top > winy) && (top < winyedge) &&
	     (left > winx) && (left < winxedge)) ||
	    // bottom of element in window
	    ((top+height > winy) && (top+height < winyedge) &&
	     (left+width > winx) && (left+width < winxedge)) ||
	    // top above/left of window, bottom below/right of window
	    (((top < winy) || (left < winx)) &&
	     ((top+height > winyedge) && (left+width > winxedge))));
  else return ((top > winy) && (left > winx) &&
	       (top + height) <= (winyedge) &&
	       (left + width) <= (winxedge));
}

function fdjtIsAtTop(elt,delta)
{
  if (!(delta)) delta=50;
  var top = elt.offsetTop;
  var left = elt.offsetLeft;
  var width = elt.offsetWidth;
  var height = elt.offsetHeight;
  var winx=window.pageXOffset;
  var winy=window.pageYOffset;
  var winxedge=winx+window.innerWidth;
  var winyedge=winy+window.innerHeight;
  
  while(elt.offsetParent) {
    elt = elt.offsetParent;
    top += elt.offsetTop;
    left += elt.offsetLeft;}

  return ((top>winx) && (top<winyedge) && (top<winx+delta));
}

/* Getting cumulative offsets */

function fdjtGetOffset(elt,withstack,top)
{
  var result={};
  var top = elt.offsetTop;
  var left = elt.offsetLeft;
  var stack = ((withstack) ? (new Array(elt)) : false);
  var width=elt.offsetWidth;
  var height=elt.offsetHeight;

  while (elt.offsetParent) {
    if ((top)&&(elt===top)) break;
    elt = elt.offsetParent;
    if (withstack) withstack.push(elt);
    top += elt.offsetTop;
    left += elt.offsetLeft;}

  result.left=left; result.top=top;
  result.right=left+width; result.bottom=top+height;
  result.width=width; result.height=height;

  if (stack) result.stack=stack;

  return result;
}

function fdjtComputeOffsets(node,recache)
{
  if (!(node)) return node;
  else if ((!(recache)) && (node.Xoff)) return node;
  else {
    var parent=fdjtComputeOffsets(node.offsetParent,recache);
    if (parent) {
      var xoff=((node.offsetLeft)||(0))+parent.Xoff;
      var yoff=((node.offsetTop)||(0))+parent.Yoff;
      node.Xoff=xoff; node.Yoff=yoff;
      return node;}
    else {
      node.Xoff=node.offsetLeft; node.Yoff=node.offsetTop;
      return node;}}
}

/* Computing "flat width" of a node */

var _fdjt_vertical_flat_width=8;
var _fdjt_vertical_tags=["P","DIV","BR","UL","LI","BLOCKQUOTE",
			 "H1","H2","H3","H4","H5","H6"];
var _fdjt_flat_width_fns={};
function _fdjt_compute_flat_width(node,sofar)
{
  if (node.nodeType===Node.ELEMENT_NODE) {
    if (node.getAttribute("flatwidth")) {
      var fw=node.getAttribute("flatwidth");
      if (typeof fw === "string") {
	if ((fw.length>0) && (fw[0]=='+'))
	  sofar=sofar+parseInt(fw.slice(1));
	else {
	  var fwnum=parseInt(fw);
	  if (typeof fwnum==="number") return sofar+fwnum;}}
      else if (typeof fw === "number") return sofar+fw;}
    else if (typeof _fdjt_flat_width_fns[node.tagName] === "function")
      sofar=sofar+_fdjt_flat_width_fns[node.tagName](node);
    else if (fdjtIndexOf(_fdjt_vertical_tags,node.tagName)>=0)
      sofar=sofar+_fdjt_vertical_flat_width;
    if (node.hasChildNodes()) {
      var children=node.childNodes;
      var i=0; while (i<children.length) {
	var child=children[i++];
	if (child.nodeType===3)
	  if (fdjtIsEmptyString(child.nodeValue)) {}
	  else sofar=sofar+child.nodeValue.length;
	else if (child.nodeType===1)
	  sofar=_fdjt_compute_flat_width(child,sofar);
	else {}}
      return sofar;}
    else return sofar;}
  else if (node.nodeType===3)
    if (fdjtIsEmptyString(node.nodeValue))
      return sofar;
    else return sofar+node.nodeValue.length;
  else return sofar;
}

var _fdjt_tag_widths=
  {"P": 8,"BR": 8,"UL": 8,"LI": 8,"BLOCKQUOTE": 8,
   "H1": 8,"H2": 8,"H3": 8,"H4": 8,"H5": 8,"H6": 8};

function _fdjt_flat_width(node,sofar,childless)
{
  if (node.nodeType===1)
    if (node.getAttribute("flatwidth")) {
      var fwa=node.getAttribute("flatwidth");
      var fw=parseInt(fwa);
      if (fw) return sofar+fw;
      else return sofar;}
    else {
      var children=node.childNodes; var start=sofar;
      sofar=sofar+_fdjt_tag_widths[node.tagName]||0;
      if (childless) return sofar;
      else if (children) {
	var i=0; while (i<children.nodes) {
	  var child=children[i++];
	  if (child.nodeType===3)
	    sofar=sofar+child.nodeValue.length;
	  else if (child.nodeType===1)
	    sofar=_fdjt_flat_width(node,sofar);
	  else {}}}
      return sofar;}
  else if (child.nodeType===3)
    return sofar+child.nodeValue.length;
  else return sofar;
}

function fdjtTagWidth(node)
{
  return _fdjt_tag_widths[node.tagName]||0;
}

function fdjtFlatWidth(node)
{
  return _fdjt_flat_width(node,0);
}

/* Transplanting nodes */

var fdjt_transplant_rules=[];

function fdjtTransplant(orig,transplant_rules,use_defaults)
{
  if ((transplant_rules) && (fdjtElementMatches(orig,transplant_rules)))
    return false;
  else if (((!(transplant_rules)) || (use_defaults)) &&
	   (fdjtElementMatches(orig,fdjt_transplant_rules)))
    return false;
  var node=orig.cloneNode(true);
  if (transplant_rules) {
    var toremove=fdjtGetChildren(node,transplant_rules);
    var i=0; while (i<toremove.length) fdjtDelete(toremove[i++]);}
  if ((!(transplant_rules)) || (use_defaults)) {
    var toremove=fdjtGetChildren(node,fdjt_transplant_rules);
    var i=0; while (i<toremove.length) fdjtDelete(toremove[i++]);}
  return node;
}

/* Looking up elements, CSS-style, in tables */

function fdjtLookupElement(table,elt)
{
  var tagname=elt.tagName;
  var classnames=((elt.className)?(elt.className.split(' ')):new Array(false));
  var idname=elt.id;
  var probe=false;
  var i=0; while (i<classnames.length) {
    var classname=classnames[i++];
    if ((idname) && (classname))
      probe=table[tagname+"."+classname+"#"+idname];
    if (probe) return probe;
    else if ((idname) && (classname))
      probe=table["."+classname+"#"+idname];
    if (probe) return probe;
    else if (idname)
      probe=table[tagname+"#"+idname];
    if (probe) return probe;
    else if (classname)
      probe=table[tagname+"."+classname];
    if (probe) return probe;
    else if (idname)
      probe=table["#"+idname];
    if (probe) return probe;
    else if (classname)
      probe=table["."+classname];
    if (probe) return probe;
    else probe=table[tagname];
    if (probe) return probe;}
  return false;
}

/* Guessing IDs to use from the DOM */

function fdjtGuessAnchor(about)
{
  /* This looks around a DOM element to try to find an ID to use as a
     target for a URI.  It especially catches the case where named
     anchors are used. */
  // console.log('Guessing anchors for '+about+' '+about.tagName);
  var probe=_fdjt_get_node_id(about);
  if (probe) return probe;
  else if (probe=_fdjt_get_parent_name(about)) return probe;
  else if (probe=_fdjt_get_node_id(fdjtNextElement(about))) return probe;
  else if (probe=_fdjt_get_node_id(fdjtPreviousElement(about))) return probe;
  else {
    var embedded_anchors=fdjtGetChildrenByTagName(about,'A');
    if (embedded_anchors==null) return null;
    var i=0;
    while (i<embedded_anchors.length) 
      if (probe=_fdjt_get_node_id(embedded_anchors[i])) return probe;
      else i++;
    return null;}
}

function _fdjt_get_node_id(node)
{
  // console.log('Checking '+node+' w/id '+node.id+' w/name '+node.name);
  if (node===null) return false;
  else if (node.id) return node.id;
  else if ((node.tagName=='A') && (node.name))
    return node.name;
  else return false;
}

function _fdjt_get_parent_name(node)
{
  var parent=node.parentNode;
  if ((parent) && (parent.tagName==='A') && (node.name))
    return node.name;
  else return false;
}

function fdjtResolveHash(eltarg)
{
  if (typeof eltarg == 'string') {
    var elt=document.getElementById(eltarg);
    if (elt) return elt;
    else {
      var elts=document.getElementsByName(eltarg);
      if ((elts) && (elts.length>0)) return elts[0];
      else return false;}}
  else return eltarg;
}

/* Getting selected text */

function fdjtSelectedText()
{
  var sel;
  if (window.getSelection) 
    sel=window.getSelection();
  else if (document.getSelection)
    sel=document.getSelection();
  else if (document.selection) 
    sel=document.selection.createRange().text;
  else sel=false;
  if ((sel)&&(sel.toString)) {
    var string=sel.toString();
    if (fdjtIsEmptyString(string)) return false;
    else return string;}
  else return false;
}

/* Accessing stylesheets */

function fdjtFindCSS(selector,first)
{
  var sheets=document.styleSheets;
  var rules=[];
  var i=0; while (i<sheets.length) {
    var sheet=sheets[i++];
    var rules=sheet.cssRules;
    var j=0; while (j<rules.length) {
      var rule=rules[j++];
      if (rule.selectorText.search(selector)>0)
	if (first) return new Array(rule);
	else rules.push(rule);}}
  return rules;
}

/* Searching content */

function fdjtSearchContent(node,spec,id,results)
{
  if (!(results)) results=[];
  if (!id) id=false;
  if (node.nodeType===3)
    if (id) 
      if (node.nodeValue.search(spec)>=0)
	results.push(id);
      else {}
    else {}
  else if (node.nodeType===1) {
    if (node.id) id=node.id;
    var children=node.childNodes;
    var i=0; while (i<children.length) {
      var child=children[i++];
      fdjtSearchContent(child,spec,id,results);}}
  else {}
  return results;
}

/* Accessing cookies */

function fdjtGetCookie(name,parse)
{
  var cookies=document.cookie;
  var namepat=new RegExp("(^|(; ))"+name+"=");
  var pos=cookies.search(namepat);
  var valuestring;
  if (pos>=0) {
    var start=cookies.indexOf('=',pos)+1;
    var end=cookies.indexOf(';',start);
    if (end>0) valuestring=cookies.slice(start,end);
    else valuestring=cookies.slice(start);}
  else return false;
  if (parse)
    return JSON.parse(decodeURIComponent(valuestring));
  else return decodeURIComponent(valuestring);
}

function fdjtSetCookie(name,value,expires,path,domain)
{
  if (value) {
    var valuestring=
      ((typeof value === 'string') ? (value) :
       (value.toJSON) ? (value.toJSON()) :
       (value.toString) ? (value.toString()) : (value));
    var cookietext=name+"="+encodeURIComponent(valuestring);
    if (expires)
      if (typeof(expires)==='string')
	cookietext=cookietext+'; '+expires;
      else if (expires.toGMTString)
	cookietext=cookietext+"; expires="+expires.toGMTString();
      else if (typeof(expires)==='number')
	if (expires>0) {
	  var now=new Date();
	  now.setTime(now.getTime()+expires);
	  cookietext=cookietext+"; expires="+now.toGMTString;}
	else cookietext=cookietext+"; expires=Sun 1 Jan 2000 00:00:00 UTC";
      else {}
    if (path) cookietext=cookietext+"; path="+path;
    // This certainly doesn't work generally and might not work ever
    if (domain) cookietext=cookietext+"; domain="+domain;
    // fdjtTrace("Setting cookie %o cookietext=%o",name,cookietext);
    document.cookie=cookietext;}
  else fdjtClearCookie(name,path,domain);
}

function fdjtClearCookie(name,path,domain)
{
  var valuestring="ignoreme";
  var cookietext=name+"="+encodeURIComponent(valuestring)+
    "; expires=Sun 1 Jan 2000 00:00:00 UTC";
  if (path) cookietext=cookietext+"; path="+path;
  // This certainly doesn't work generally and might not work ever
  if (domain) cookietext=cookietext+"; domain="+domain;
  // fdjtTrace("Clearing cookie %o: text=%o",name,cookietext);
  document.cookie=cookietext;
}

/* Getting anchors (making up IDs if neccessary) */

var _fdjt_idcounter=0, fdjt_idbase=false;

function fdjtGetAnchor(about)
{
  /* This does a get anchor and creates an id if neccessary */
  var probe=fdjtGuessAnchor(about);
  if (probe) return probe;
  else {
    if (!(fdjt_idbase))
      fdjt_idbase="TMPID"+(1000000+(Math.floor((1000000-1)*Math.random())))+"S";
    var tmpid=fdjt_idbase+_fdjt_idcounter++;
    while (document.getElementById(tmpid))
      tmpid=fdjt_idbase+_fdjt_idcounter++;
    about.id=tmpid;
    return tmpid;}
}

function fdjtGetAnchor(about)
{
  /* This does a get anchor and creates an id if neccessary */
  var probe=fdjtGuessAnchor(about);
  if (probe) return probe;
  else return fdjtForceId(about);
}

var fdjtUnloaders=[];

function fdjtOnUnload(evt)
{
  evt=evt||event||null;
  var i=0; var n=fdjtUnloaders.length;
  while (i<n) {
    if ((fdjtUnloaders[i])&&(fdjtUnloaders[i].call))
      fdjtUnloaders[i].call(this,evt);
    fdjtUnloaders[i]=false;
    i++;}
}

/* Various set up things */

var fdjt_domutils_setup=false;

function fdjtDomutils_setup()
{
  if (fdjt_domutils_setup) return;
  fdjt_transplant_rules.push(fdjtParseSelector("A"));
  fdjt_transplant_rules.push(fdjtParseSelector("BR"));
  fdjt_transplant_rules.push(fdjtParseSelector("HR"));
  var textless=fdjtGetMeta("DOM:TEXTFREE");
  if (textless) {
    textless=textless.split(';');
    var i=0; while (i<textless.length) {
      var sel=fdjtParseSelector(textless[i++]);
      fdjt_transplant_rules.push(sel);
      fdjt_notext_rules.push(sel);}}
  if (document.onunload) 
    fdjtUnloaders.push(document.onunload);
  document.onunload=fdjtOnUnload;
  fdjt_domutils_setup=true;
}

fdjtLoadMessage("Loaded domutils.js");

/* Emacs local variables
;;;  Local variables: ***
;;;  compile-command: "make; if test -f ../makefile; then cd ..; make; fi" ***
;;;  End: ***
*/
/* -*- Mode: Javascript; -*- */

var fdjt_handlers_id="$Id: handlers.js 226 2010-03-06 10:27:14Z haase $";
var fdjt_handlers_version=parseInt("$Revision: 226 $".slice(10,-1));

/* Copyright (C) 2001-2009 beingmeta, inc.
   This file is a part of the FDJT web toolkit (www.fdjt.org)
   This file provides DHTML handlers for a variety of UI conventions
   and interactions.

   This program comes with absolutely NO WARRANTY, including implied
   warranties of merchantability or fitness for any particular
   purpose.

    Use, modification, and redistribution of this program is permitted
    under either the GNU General Public License (GPL) Version 2 (or
    any later version) or under the GNU Lesser General Public License
    (version 3 or later).

    These licenses may be found at www.gnu.org, particularly:
      http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
      http://www.gnu.org/licenses/lgpl-3.0-standalone.html

*/

var graphics_root="/graphics/";

fdjtLoadMessage("Loading handlers.js");

/* INPUT SHOWHELP */

function fdjtShowHelp_onfocus(evt)
{
  evt=evt||event||null;
  var target=$T(evt);
  fdjtCheshire_stop(evt);
  while (target)
    if ((target.nodeType==1) &&
	((target.tagName === 'INPUT') || (target.tagName === 'TEXTAREA')) &&
	((target.hasAttribute) ? (target.hasAttribute('HELPTEXT')) :
	 (target.getAttribute('HELPTEXT')))) {
      var helptext=$(target.getAttribute('HELPTEXT'));
      if (helptext) {
	if (fdjtIsBlockElt(helptext))
	  helptext.style.display='block';
	else helptext.style.display='inline';}
      return;}
    else target=target.parentNode;
}

function fdjtShowHelp_ontarget(target)
{
  while (target)
    if ((target.nodeType==1) &&
	((target.tagName === 'INPUT') || (target.tagName === 'TEXTAREA')) &&
	((target.hasAttribute) ? (target.hasAttribute('HELPTEXT')) :
	 (target.getAttribute('HELPTEXT')))) {
      var helptext=$(target.getAttribute('HELPTEXT'));
      if (helptext) {
	if (fdjtIsBlockElt(helptext))
	  helptext.style.display='block';
	else helptext.style.display='inline';}
      return;}
    else target=target.parentNode;
}

function fdjtHideHelp_onblur(evt)
{
  evt=evt||event||false;
  var target=$T(evt);
  while (target)
    if ((target.nodeType==1) &&
	((target.tagName === 'INPUT') || (target.tagName === 'TEXTAREA')) &&
	((target.hasAttribute) ? (target.hasAttribute('HELPTEXT')) :
	 (target.getAttribute('HELPTEXT')))) {
      var helptext=document.getElementById(target.getAttribute('HELPTEXT'));
      if (helptext) {
	setTimeout(function() {if (helptext) helptext.style.display='none';},300);}
      return;}
    else target=target.parentNode;
}

/* SHOWHIDE */

function fdjtShowHide_onclick(evt)
{
  evt=evt||event||false;
  var target=$T(evt);
  fdjtCheshire_stop(evt);
  // fdjtLog('target='+target);
  while (target.parentNode) {
    var tohide=fdjtCacheAttrib(target,"clicktohide",fdjtSemiSplit);
    var toshow=fdjtCacheAttrib(target,"clicktoshow",fdjtSemiSplit);
    var totoggle=fdjtCacheAttrib(target,"clicktotoggle",fdjtSemiSplit);
    if (tohide) {
      var i=0; while (i<tohide.length) {
	var elt=document.getElementById(tohide[i++]); 
	if (elt) elt.style.display='none';}}
    if (toshow) {
      var i=0; while (i<toshow.length) {
	var elt=document.getElementById(toshow[++i]);
	if (elt)
	  if (fdjtIsBlockElt(elt)) elt.style.display='block';
	  else elt.style.display='block';}}
    if (totoggle) {
      var i=0; while (i<totoggle.length) {
	var elt=document.getElementById(totoggle[i++]);
	if (elt) {
	  var display=elt.style.display;
	  if ((display===null) || (display===''))
	    display=window.getComputedStyle(elt,null).display;
	  if (display==='none') {
	    var style=((elt.fdjtSavedStyle) ||
		       (elt.getAttribute("DISPLAYSTYLE")) ||
		       (fdjt_tag_display_styles[elt.tagName]) ||
		       "inline");
	    elt.style.display=style;}
	  else {
	    elt.fdjtSavedStyle=elt.style.display;
	    elt.style.display='none';}
	  target.setAttribute('displayed',elt.style.display);}}}
    if ((tohide) || (toshow) || (totoggle)) {
      if (evt.preventDefault) evt.preventDefault(); else evt.returnValue=false;
      evt.cancelBubble=true;
      return false;}
    target=target.parentNode;}
}

/* SETCLEAR */

function fdjtSetClear_onclick(evt)
{
  evt=evt||event||null;
  var target=$T(evt);
  fdjtCheshire_stop(evt);
  while (target.parentNode)
    if ((fdjtHasAttrib(target,'SETONCLICK'))) {
      var toset=target.getAttribute('SETONCLICK');
      var attrib=target.getAttribute('SETATTRIB');
      var val=target.getAttribute('ATTRIBVAL');
      if (val===null) val='set';
      if ((toset) && (toset!='')) toset=toset.split(',');
      else return;
      if (toset) {
	var i=0; while (i<toset.length) {
	  var elt=document.getElementById(toset[i++]);
	  if (elt===null) return;
	  if (fdjtHasAttrib(elt,attrib))
	    elt.removeAttribute(attrib);
	  else elt.setAttribute(attrib,val);}}
      return;}
    else target=target.parentNode;
}

/* Autoprompt */

function fdjtAutoPrompt_onfocus(evt)
{
  evt=evt||event||null;
  var elt=$T(evt);
  fdjtCheshire_stop(evt);
  if ((elt) && (fdjtHasAttrib(elt,'isempty'))) {
    elt.value='';
    elt.removeAttribute('isempty');}
  fdjtShowHelp_onfocus(evt);
}

function fdjtAutoPrompt_ontarget(elt)
{
  if ((elt) && (fdjtHasAttrib(elt,'isempty'))) {
    elt.value='';
    elt.removeAttribute('isempty');}
  fdjtShowHelp_ontarget(elt);
}

function fdjtAutoPrompt_onblur(evt)
{
  evt=evt||event||null;
  var elt=$T(evt);
  if (elt.value==='') {
    elt.setAttribute('isempty','yes');
    var prompt=(elt.prompt)||(elt.getAttribute('prompt'));
    if ((prompt===null) && (elt.className==='autoprompt'))
      prompt=elt.title;
    elt.value=prompt;}
  else elt.removeAttribute('isempty');
  fdjtHideHelp_onblur(evt);
}

function fdjtAutoPrompt_setup(elt)
{
  if (!(elt)) elt=document.body;
  var elements=$$('INPUT',elt).concat($$('TEXTAREA',elt));
  var i=0; if (elements) while (i<elements.length) {
      var elt=elements[i++];
      if (((elt.tagName==="TEXTAREA") ||
	   ((elt.tagName==="INPUT") && (elt.type==='text'))) &&
	  ((fdjtHasClass(elt,'autoprompt')) ||
	   (elt.prompt) || (fdjtHasAttrib(elt,'prompt')))) {
	var prompt=(elt.prompt)||elt.getAttribute('prompt');
	if (!(prompt))
	  if (fdjtHasClass(elt,'autoprompt'))
	    prompt=elt.title;
	  else continue;
	if (prompt)
	  if ((!(elt.value)) || (elt.value=='') || (elt.value===prompt)) {
	    elt.value=prompt;
	    elt.setAttribute('isempty','yes');
	    fdjtRedisplay(elt);}
	if ((!(elt.onfocus)) && (!(elt.getAttribute("onfocus"))))
	  elt.onfocus=fdjtAutoPrompt_onfocus;
	if ((!(elt.onblur)) && (!(elt.getAttribute("onblur"))))
	  elt.onblur=fdjtAutoPrompt_onblur;}}
}

// Removes autoprompt text from empty fields
function fdjtAutoPrompt_cleanup(form)
{
  var elements=((form) ?
		($$('INPUT',form).concat($$('TEXTAREA',form))) :
		($$('INPUT').concat($$('TEXTAREA'))));
  var i=0; if (elements) while (i<elements.length) {
      var elt=elements[i++];
      if (fdjtHasAttrib(elt,'isempty')) elt.value="";}
}

// Removes autoprompt text from empty fields
function fdjtAutoPrompt_onsubmit(evt)
{
  evt=evt||event;
  if (evt) {
    var target=$P("FORM",evt.target);
    fdjtAutoPrompt_cleanup(target);}
}

/* Tabs */

function fdjtTab_onclick(evt,shown)
{
  if (!(shown)) shown="shown";
  fdjtCheshire_stop(evt);
  var elt=$T(evt);
  if (elt) {
    while (elt.parentNode)
      if (fdjtHasAttrib(elt,"contentid")) break;
      else elt=elt.parentNode;
    if ((!(elt)) || (!(elt.getAttribute))) return;
    var select_var=elt.getAttribute('selectvar');
    var tabcookie=elt.getAttribute('tabcookie');
    var content_id=elt.getAttribute('contentid');
    var content=document.getElementById(content_id);
    var select_elt=document.getElementById(select_var+'_INPUT');
    var parent=elt.parentNode;
    var sibs=parent.childNodes;
    var tabdomain=false; var tabpath=false;
    if (parent.getAttribute('tabcookies')) {
      var tcinfo=parent.getAttribute('tabcookies');
      var slashpos=tcinfo.indexOf('/');
      if (slashpos) {
	tabdomain=tcinfo.slice(0,slashpos);
	tabpath=tcinfo.slice(0,slashpos);}
      else tabdomain=tcinfo;}
    if (content===null) {
      fdjtLog("No content for "+content_id);
      return;}
    // if (evt.preventDefault) evt.preventDefault(); else evt.returnValue=false; 
    // evt.cancelBubble=true;
    // This lets forms pass tab information along
    if (select_elt) select_elt.value=content_id;
    var i=0; while (i<sibs.length) {
      var node=sibs[i++];
      if ((node.nodeType===1) && (fdjtHasAttrib(node,"contentid"))) {
	var cid=node.getAttribute('contentid');
	var cdoc=document.getElementById(cid);
	if (node===elt) {}
	else if (fdjtHasAttrib(node,shown)) {
	  node.removeAttribute(shown);
	  if (cdoc) {
	    cdoc.removeAttribute(shown);
	    cdoc.className=cdoc.className;}
	  fdjtRedisplay(node,cdoc);}}}
    if (fdjtHasAttrib(elt,shown))
      elt.removeAttribute(shown);
    else elt.setAttribute(shown,'yes');
    if (fdjtHasAttrib(content,shown))
      content.removeAttribute(shown);
    else content.setAttribute(shown,'yes');
    if (tabcookie) {
      var eqpos=tabcookie.indexOf('=');
      if (eqpos<0) fdjtSetCookie(tabcookie,cid);
      else fdjtSetCookie(tabcookie.slice(0,eqpos),
			 tabcookie.slice(eqpos+1),
			 false,tabdomain,tabpath);}
    // Force a redisplay on CSS-challenged browsers
    fdjtRedisplay(elt,content);
    return false;}
}

function fdjtSelectTab(tabbar,contentid)
{
  var tabs=$$(".tab",tabbar);
  var i=0; while (i<tabs.length) {
    var tab=tabs[i++];
    if (tab.getAttribute("contentid"))
      if ((tab.getAttribute("contentid"))===contentid)
	tab.setAttribute("shown","shown");
      else if (tab.getAttribute("shown")) {
	var cid=tab.getAttribute("contentid");
	var content=$(cid);
	tab.removeAttribute("shown");
	if (!(content))
	  fdjtWarn("No reference for tab content %o",cid);
	else content.removeAttribute("shown");}
      else tab.removeAttribute("shown");}
  if ($(contentid))
    $(contentid).setAttribute("shown","shown");
  else fdjtWarn("No reference for tab content %o",contentid);
}

function fdjtSelectedTab(tabbar)
{
  var tabs=$$(".tab",tabbar);
  var i=0; while (i<tabs.length) {
    var tab=tabs[i++];
    if (tag.getAttribute("shown"))
      return tag.getAttribute("contentid");}
  return false;
}

/* Anchor submit buttons */

function fdjtAnchorSubmit_onclick(evt)
{
  var target=evt.target;
  var anchor=fdjtGetParentByTagName(target,'a');
  var form=fdjtGetParentByTagName(anchor,'form');
  if (anchor.getAttribute("NAME"))
    form.appendChild
      (fdjtInput("HIDDEN",anchor.getAttribute("NAME"),
		 anchor.getAttribute("VALUE")));
  // This should really come up with some way to run the onsbumit
  // handler
  fdjtAutoPrompt_cleanup(form);
  return form.submit();
}

function fdjtAnchorSubmit_setup(root)
{
  root=root||document.body;
  var asubmit=fdjtGetChildrenByClassName(root,"fdjtsubmit");
  var i=0; while (i<asubmit.length)
	     asubmit[i++].onclick=fdjtAnchorSubmit_onclick;
}

/* Adding search engines */

var _fdjt_browser_coverage="Mozilla/Firefox/Netscape 6";

function fdjtAddBrowserSearchPlugin(spec,name,cat)
{
  if ((typeof window.sidebar === "object") &&
      (typeof window.sidebar.addSearchEngine === "function")) 
    window.sidebar.addSearchEngine (spec+'.src',spec+".png",name,cat);
  else alert(browser_coverage+" is needed to install a search plugin");
}

/* FLEXPAND click */

/* Enables region which expand and contract on a click. */

function fdjtFlexpand_onclick(event)
{
  var target=event.target; var functional=false;
  fdjtCheshire_stop(event);
  while (target.parentNode)
    if (fdjtHasAttrib(target,'expanded')) break;
    else if (target.tagName==='A')
      return;
    else if ((target.tagName==='SELECT') ||
	     (target.tagName==='INPUT') ||
	     (target.className=='checkspan') ||
	     (target.onclick!=null)) {
      functional=true;
      target=target.parentNode;}
    else target=target.parentNode;
  if (target) {
    if (target.getAttribute('expanded')==="yes")
      if (functional) {}
      else {
	target.setAttribute("expanded","no");
	target.style.maxHeight=null;}
    else {
      target.setAttribute("expanded","yes");
      target.style.maxHeight='inherit';}}
}

/* Closing the window */

function fdjtCloseWindow(interval)
{
  if (interval)
    window.setTimeout(_fdjt_close_window,interval*1000);
  else window.close();
}

function _fdjt_close_window(event)
{
  window.close();
}

/* checkspan handling */

function fdjtCheckSpan(varname,value,checked,title)
{
  var checkbox=fdjtInput("CHECKBOX",varname,value);
  var checkspan=fdjtSpan("checkspan");
  if (checked) {
    checkspan.setAttribute('ischecked','true');
    checkbox.checked=true;}
  else {
    checkbox.checked=false;}
  checkspan.onclick=fdjtCheckSpan_onclick;
  var elements=[]; var checkbox_added=false;
  var i=4; while (i<arguments.length) {
    var arg=arguments[i++];
    if (arg===true) {
      elements.push(checkbox);
      checkbox_added=true;}
    else elements.push(arg);}
  if (!(checkbox_added)) fdjtAppend(checkspan,checkbox);
  if (elements.length)
    fdjtAddElements(checkspan,elements);
  else fdjtAppend(checkspan,value);
  if (title) checkspan.title=title;
  return checkspan;
}

function fdjtCheckSpan_update(checkspan,checked,evt)
{
  var inputs=fdjtGetChildrenByTagName(checkspan,'INPUT');
  var checkspans=fdjtGetChildrenByClassName(checkspan,"checkspan");
  if (checked)
    checkspan.setAttribute('ischecked','true');
  else checkspan.removeAttribute('ischecked');
  {var i=0; while (i<inputs.length) {
      var input=inputs[i++];
      if ((input.type==='radio') || (input.type==='checkbox'))
	input.checked=checked;}}
  {var i=0; while (i<checkspans.length) {
      if (checked)
	checkspans[i++].setAttribute('ischecked','true');
      else checkspans[i++].removeAttribute('ischecked');}}
  if (!(evt)) {}
  else if (checkspan.onchange) checkspan.onchange(evt);
  else if (checkspan.getAttribute("onchange")) {
    checkspan.onchange=new Function("event",checkspan.getAttribute("onchange"));
    checkspan.onchange(evt);}
}

function fdjtCheckSpan_onclick(evt)
{
  evt=evt||event||null;
  var target=$T(evt); var checkspan=$P(".checkspan",target);
  fdjtCheshire_stop(evt);
  while (target.parentNode) {
    if (target.nodeType!=1) target=target.parentNode;
    else if (fdjtHasClass(target,'checkspan')) break;
    else if (target.tagName==='A') return;
    else if (target.tagName==='INPUT') return;
    else target=target.parentNode;}
  if (!((target) && (fdjtHasClass(target,'checkspan')))) return;
  var inputs=fdjtGetChildrenByTagName(target,'INPUT');
  var i=0; while (i<inputs.length) {
    var input=inputs[i++];
    if (input.disabled) {}
    else if ((input.type==='radio') || (input.type==='checkbox'))
      if (input.checked)
	return fdjtCheckSpan_update(checkspan,false,evt);
      else return fdjtCheckSpan_update(checkspan,true,evt);
    else i++;}
}

function fdjtCheckSpan_onchange(evt)
{
  evt=evt||event||null;
  var target=$T(evt); var checkspan=$P(".checkspan",target);
  fdjtCheckSpan_update(checkspan,target.checked,evt);
  $T(evt).blur();
}

function fdjtCheckSpan_setup(checkspan)
{
  if (!(checkspan)) {
    var elements=fdjtGetElementsByClassName('checkspan');
    var i=0; if (elements) while (i<elements.length) {
	var checkspan=elements[i++];
	fdjtCheckSpan_setup(checkspan);}}
  else {
    var checkspans=((fdjtHasClass(checkspan,"checkspan"))?
		    (new Array(checkspan)):
		    (fdjtGetElementsByClassName('checkspan',checkspan)));
    var i=0; while (i<checkspans.length) {
      var checkspan=checkspans[i++];
      if (!(checkspan.onclick)) checkspan.onclick=fdjtCheckSpan_onclick;
      var inputs=fdjtGetElementsByTagName("INPUT",checkspan);
      var j=0; while (j<inputs.length) {
	var input=inputs[j++];
	if ((input.type==='radio')||(input.type==='checkbox')) {
	  var checked=input.checked;
	  if (checked === null) {
	    child.checked=false;
	    checkspan.removeAttribute('ischecked');}
	  else if (checked) {
	    checkspan.setAttribute('ischecked','yes');}
	  else checkspan.removeAttribute('ischecked');
	  break;}}}}
}

/* Class toggling */

function fdjtToggleClass_onclick(evt)
{
  evt=(evt)||(event);
  if ((evt.target)&&(evt.target.getAttribute)&&
      (evt.target.getAttribute("toggle"))) {
    var toggle=evt.target.getAttribute("toggle");
    var eqpos=toggle.indexOf('=');
    var elt=$(toggle.slice(0,eqpos));
    var classname=toggle.slice(eqpos+1);
    if ((elt)&&(classname)) fdjtToggleClass(elt,classname);}
}

/* Flashing */

/* This is intended for translucent entities which get rendered
   temporarily opaque for a time, typically when they change. */

function fdjtFlash(elt,milliseconds,opacity)
{
  elt=$(elt);
  if (typeof opacity === 'string') {
    fdjtAddClass(elt,opacity);
    setTimeout(function() {fdjtDropClass(elt,opacity);},milliseconds);}
  else {
    var oldopacity=elt.style.opacity;
    elt.style.opacity=opacity;
    setTimeout(function() {elt.style.opacity=oldopacity;},milliseconds);}
}

/* Cheshire handling */

var fdjt_cheshireelt=null;
var fdjt_cheshiresteps=false;
var fdjt_cheshirecountdown=false;
var fdjt_cheshiretimer=false;
var fdjtCheshire_finish=null;

function fdjtCheshire_handler(event)
{
  if ((fdjt_cheshiresteps) &&
      (fdjt_cheshirecountdown<=0)) {
    fdjtLog('closing window');
    clearInterval(fdjt_cheshiretimer);
    if (fdjtCheshire_finish)
      fdjtCheshire_finish();
    else window.close();}
  else if ((fdjt_cheshiresteps) &&
	   (fdjt_cheshirecountdown)) {
    fdjt_cheshirecountdown=fdjt_cheshirecountdown-1;
    var ratio=(fdjt_cheshirecountdown/fdjt_cheshiresteps)*0.8;
    // console.log('opacity='+ratio);
    fdjt_cheshireelt.style.opacity=ratio;}
  else {}
}

function fdjtCheshire_start(eltid,interval,steps)
{
  if (typeof(interval) === 'undefined') interval=30;
  if (typeof(steps) === 'undefined') steps=interval*2;
  if (typeof(eltid) === 'undefined')
     fdjt_cheshireelt=document.body;
  else if (typeof(eltid) === 'string')
    fdjt_cheshireelt=document.getElementById(eltid);
  else fdjt_cheshireelt=eltid;
  if (fdjt_cheshireelt===null) fdjt_cheshireelt=document.body;
  // fdjtLog('Starting cheshire over '+interval+' for '+steps);
  fdjt_cheshireelt.style.opacity=.80;
  fdjt_cheshiresteps=steps;
  fdjt_cheshirecountdown=steps;
  fdjt_cheshiretimer=setInterval(fdjtCheshire_handler,(1000*interval)/steps);
}

function fdjtCheshire_stop(event)
{
  if (fdjt_cheshireelt) {
    var msg_elt=document.getElementById('CHESHIREMSG');
    var alt_msg_elt=document.getElementById('CHESHIREALTMSG');
    if (msg_elt) msg_elt.style.display='none';
    if (alt_msg_elt)
      if (fdjt_block_eltp(alt_msg_elt))
	alt_msg_elt.style.display='block';
      else alt_msg_elt.style.display='inline';
    fdjt_cheshireelt.style.opacity='inherit';
    clearInterval(fdjt_cheshiretimer);
    fdjt_cheshiresteps=false;
    fdjt_cheshirecountdown=false;}
}

function fdjtCheshire_onclick(event)
{
  fdjt_stop_cheshire(event);
}

/* Text input */

function fdjtTextInput_onkeypress(evt,handler)
{
  var ch=evt.charCode, kc=evt.keyCode;
  if (kc===13) {
    var elt=$T(evt);
    var val=elt.value;
    elt.value="";
    handler(val);
    return false;}
  else return;
}
function fdjtMultiText_onkeypress(evt,tagname)
{
  var enter_chars=
    fdjtCacheAttrib($T(evt),"enterchars",fdjtStringToKCodes,[-13]);
  if (!(tagname)) tagname="span";
  var ch=evt.charCode, kc=evt.keyCode;
  if ((kc===13)||
      ((enter_chars) && (enter_chars.indexOf) &&
       ((enter_chars.indexOf(ch)>=0) ||
	(enter_chars.indexOf(-kc)>=0)))) {
    var elt=$T(evt); var value, new_elt;
    if (elt.value==="") return;
    if (elt.handleMultiText) 
      value=elt.handleMultiText(elt.value);
    if ((typeof value === "object") && (value.nodeType))
      new_elt=value;
    else {
      new_elt=fdjtNewElement(tagname,(elt.className||"multitext"),
			     fdjtCheckbox(elt.name,elt.value,true),
			     elt.value);
      // Just in case we're combining these two functions, it doesn't
      // really make sense for the checkspan to have an autoprompt class
      fdjtDropClass(new_elt,"autoprompt");
      fdjtAddClass(new_elt,"checkspan");
      fdjtCheckSpan_setup(new_elt);
      new_elt.setAttribute("ischecked","yes");}
    fdjtInsertAfter(elt," ",new_elt);
    elt.value="";
    if (evt.preventDefault) evt.preventDefault(); else evt.returnValue=false;
    evt.cancelBubble=true;
    return false;}
  else return true;
}

/* Text checking */

/* This handles automatic handling of embedded content, for
 * example tags or other markup. It creates an interval timer
 * to check for changes in the value of an input field.
 * eltid is the textfield to monitor
 * textfn is the function to parse its value
 * changefn is the function to call on the parse result when it changes
 * interval is how often to check for changes
 */
function fdjtTextract(eltid,textfn,changefn,interval)
{
  /* Default the value for interval and normalize it to
     milliseconds if it looks like its actually seconds. */
  if (interval==null) interval=4000;
  else if (interval<200) interval=interval*1000;
  var elt=document.getElementById(eltid);
  if (elt==null) return;
  var text=elt.value, parsed=textfn(text);
  if (parsed) changefn(parsed);
  // fdjtLog('Init text='+text);
  // fdjtLog('Init parsed='+parsed);
  // fdjtLog('Init interval='+interval);
  var loop_fcn=function(event) {
    if (elt.value!=text) {
      var new_parsed=textfn(elt.value);
      // console.log('New text='+elt.value);
      //console.log('New parsed='+new_parsed);
      text=elt.value;
      if (new_parsed!=parsed) {
	parsed=new_parsed;
	changefn(parsed);}}};
  window.setInterval(loop_fcn,interval);
}

/* Font size adjustment */

function fdjtAdjustFontSize(elt)
{
  var target_width=elt.getAttribute('targetwidth');
  var target_height=elt.getAttribute('targetheight');
  var actual_width=elt.clientWidth;
  var actual_height=elt.clientHeight;
  if (((target_width==null) || (actual_width<target_width)) &&
      ((target_height==null) || (actual_height<target_height)))
    return;
  var x_ratio=((target_width==null) ? (1.0) : (target_width/actual_width));
  var y_ratio=((target_height==null) ? (1.0) : (target_height/actual_height));
  var do_ratio=((x_ratio<y_ratio) ? (x_ratio) : (y_ratio));
  elt.style.fontSize=(do_ratio*100.0)+"%";
  // The code below, if it worked, would shrink and then expand the element
  // However, it doesn't work because the actual width doesn't get updated
  // automatically
  /*
  var step=(1.0-do_ratio)/5;
  var new_ratio=do_ratio;
  elt.style.fontSize=(do_ratio*100.0)+"%";
  while (((target_width==null) || (actual_width<target_width)) &&
	 ((target_height==null) || (actual_height<target_height))) {
    do_ratio=new_ratio;
    new_ratio=do_ratio+step;
    elt.style.fontSize=(do_ratio*100.0)+"%";
    actual_width=elt.clientWidth;
    actual_height=elt.clientHeight;}
  */
}

function fdjtAdjustFontSizes()
{
  var elts=fdjtGetElementsByClassName('autosize');
  if (elts) {
    var i=0; while (i<elts.length)
      fdjtAdjustFontSize(elts[i++]);}
}

/* Handling CSS based reduction: shrinking font sizes to fit */

function fdjtMarkReduced(elt)
{
  if (elt) {
    var target_width=elt.getAttribute('targetwidth');
    var target_height=elt.getAttribute('targetheight');
    var actual_width=elt.clientWidth;
    var actual_height=elt.clientHeight;
    if (((target_width===null) || (actual_width<target_width)) &&
	((target_height===null) || (actual_height<target_height)))
      return;
    else {
      var classinfo=elt.className;
      if (classinfo) 
	if (classinfo.search(/\breduced\b/)>=0) {}
	else elt.className=classinfo+' reduced';
      else elt.className='reduced';
      // fdjtLog('Reducing '+elt+' to class '+elt.className);
    }}
  else {
    var elts=fdjtGetElementsByClassName('autoreduce');
    var i=0; while (i<elts.length) fdjtMarkReduced(elts[i++]);}
}

/* Co-highlighting */

var fdjt_cohi_classname="cohi";
var fdjt_trace_cohi=false;
var _fdjt_cohi_name=false;

function fdjtCoHi_highlight(namearg,classname_arg)
{
  var classname=((classname_arg) || (fdjt_cohi_classname));
  var newname=((typeof namearg === 'string') ? (namearg) : (namearg.name));
  if (_fdjt_cohi_name===newname) return;
  if (fdjt_trace_cohi)
    fdjtLog("[%fs] Changing '%s' highlight from %o to %o",
	    fdjtElapsedTime(),classname,_fdjt_cohi_name,(newname||"false"));
  if (_fdjt_cohi_name) {
    var drop=document.getElementsByName(_fdjt_cohi_name);
    var i=0, n=drop.length;
    if (fdjt_trace_cohi)
      fdjtLog("[%fs] Uncohighlighting %d elements named %s with '%s'",
	      fdjtElapsedTime(),n,_fdjt_cohi_name,classname);
    while (i<n) fdjtDropClass(drop[i++],classname);}
  _fdjt_cohi_name=newname||false;
  if (newname) {
    var elts=document.getElementsByName(newname);
    var n=elts.length, i=0;
    if (fdjt_trace_cohi)
      fdjtLog("[%fs] Cohlighting %d elements named %s with '%s'",
	      fdjtElapsedTime(),n,newname,classname);
    while (i<n) fdjtAddClass(elts[i++],classname);}
}

var fdjtCoHi_delay=100;
var _fdjt_cohi_timer={};

function fdjtCoHi_onmouseover(evt,classname_arg)
{
  var target=$T(evt);
  while (target)
    if ((target.tagName==='INPUT') || (target.tagName==='TEXTAREA') ||
	((target.tagName==='A') && (target.href)))
      return;
    else if (target.name) break;  
    else target=target.parentNode;
  if (!(target)) return;
  fdjtDelay
    (fdjtCoHi_delay,fdjtCoHi_highlight,target.name,_fdjt_cohi_timer);
}

function fdjtCoHi_onmouseout(evt,classname_arg)
{
  var target=$T(evt);
  while (target)
    if ((target.name) && (target.name===_fdjt_cohi_name)) {
      fdjtDelay
	(fdjtCoHi_delay,fdjtCoHi_highlight,false,_fdjt_cohi_timer);
      break;}
    else target=target.parentNode;
}

/* Radio Selection */

/* This is a generalization of 'radio buttons' which handle
   arbitrary elements.  This basically gives ELT the SELNAME
   class and removes the SELNAME class from all other elements
   of class RADIONAME under UNDER.  If RADIONAME is not specified,
   the RADIO attribute of UNDER is used, and if UNDER is not specified,
   UNDER is identified by searching up from ELT to find a RADIO
   attribute. */

function fdjtRadioSelect(elt,under,radioname,selname,toggle)
{
  if (!(radioname))
    if (under) radioname=((under.getAttribute("RADIO"))||("radio"));
    else {
      under=fdjtGetParentByAttrib(elt,"RADIO");
      if (under) radioname=under.getAttribute("RADIO");
      else {under=document.body; radioname="radio";}}
  else if (!(under)) {
    under=fdjtGetParentByAttrib(elt,"RADIO",radioname);
    if (!(under)) under=document.body;}
  else {}
  // fdjtLog("RadioSelect of %o with %o/%o under %o",elt,radioname,selname,under);
  var target=fdjtGetParentByClassName(elt,radioname);
  if (!(selname)) selname="radioselected";
  if ((toggle) && (fdjtHasClass(elt,selname)))
    fdjtDropClass(elt,selname);
  else {
    var all=fdjtGetChildrenByClassName(under,radioname);
    var i=0; while (i<all.length) {
      var node=all[i++]; fdjtDropClass(node,selname);}
    fdjtAddClass(elt,selname);}
}

function fdjtRadioToggle(elt,under,radioname,selname)
{
  return fdjtRadioSelect(elt,under,radioname,selname,true);
}

/* Delaying handlers */

var fdjt_trace_delays=false;
var fdjt_global_delays=[];

function fdjtDelay(delay,handler,arg,context,delayname)
{
  if (!(context)) context=arg;
  else if (context===true) context=fdjt_global_delays;
  if (!(delayname)) delayname=handler.name||"delay";
  if (context[delayname]) {
    var info=context[delayname];
    /* Don't delay for what you're already doing. */
    if ((info.handler===handler) && (info.arg===arg)) {
      if (fdjt_trace_delays) {
	var now=new Date();
	fdjtLog("(%s:%dms) Alreadying delaying #%o from %o:%o %o of %o with %o",
		now.toLocaleTimeString(),now.getTime(),
		info.timeout,info.start.toLocaleTimeString(),info.start.getTime(),
		(info.handler.name||info.handler),info.arg,info,info.start,
		delayname,context);}
      return;}
    if (fdjt_trace_delays)
      fdjtLog("Clearing delay %o[%o]=%o %o(%o)",
	      context,delayname,info,info.handler,info.arg);
    clearTimeout(info.timeout);
    context[delayname]=false;}
  var info={}; info.handler=handler; info.arg=arg; info.start=new Date();
  context[delayname]=info;
  info.timeout=setTimeout(function() {
      if (fdjt_trace_delays) {
	var now=new Date();
	fdjtLog("(%s:%d(e)) Gratifying delay #%o after %oms %o of %o info=%o %o(%o)",
		now.toLocaleTimeString(),fdjtElapsedTime(now),
		info.timeout,now.getTime()-info.start.getTime(),
		context,delayname,
		info,(info.handler.name||info.handler),info.arg);}
      context[delayname]=false;
      handler(arg);},
    delay);
  if (fdjt_trace_delays)
    fdjtLog("(%s:%dms) Set delay [#%o:%oms] %o on %o; info=%o %o(%o)",
	    info.start.toLocaleTimeString(),info.start.getTime(),
	    info.timeout,delay,delayname,context,
	    info,info.handler,info.arg);
}

/* Gradual transformation */

function fdjtGradually(nsteps,interval,startval,endval,stepfn)
{
  var args=[]; var timer;
  var delta=(endval-startval)/nsteps; 
  var val=startval; args.push(startval);
  var i=5; while (i<arguments.length) args.push(arguments[i++]);
  stepfn.apply(this,args);
  timer=window.setInterval(function() {
      val=val+delta;
      if (((delta<0) && (val<endval)) ||
	  ((delta>0) && (val>endval))) {
	args[0]=false; stepfn.apply(this,args);
	clearInterval(timer);}
      else {
	args[0]=val; stepfn.apply(this,args);}},
    interval);
  return timer;
}

/* Checking control */

/* Some events, like onselect, don't seem to get control key information.
   This checks control key information and updates the target to reflect it.
   To cover most of the bases, this should probably be on onkeyup, onkeydown,
   and a few others.
*/

function fdjtCheckControl_onevent(evt)
{
  evt=evt||event||null;
  var target=$T(evt);
  if (typeof evt.ctrlKey === 'undefined') return;
  if (evt.ctrlKey) target.setAttribute('controldown','yes');
  else target.removeAttribute('controldown');
}

function fdjtCheckShift_onevent(evt)
{
  evt=evt||event||null;
  var target=$T(evt);
  if (typeof evt.shiftKey === 'undefined') return;
  if (evt.shiftKey) target.setAttribute('shiftdown','yes');
  else target.removeAttribute('shiftdown');
}

function fdjtCheckAlt_onevent(evt)
{
  evt=evt||event||null;
  var target=$T(evt);
  if (typeof evt.altKey === 'undefined') return;
  if (evt.altKey) target.setAttribute('altdown','yes');
  else target.removeAttribute('altdown');
}

function fdjtCancelBubble(evt)
{
  evt=evt||event||null;
  evt.cancelBubble=true;
}

function fdjtCancelEvent(evt)
{
  evt=evt||event||null;
  if (evt.preventDefault) evt.preventDefault();
  else evt.returnValue=false;
  evt.cancelBubble=true;
}

/* Setup */

function fdjtNodeSetup()
{
  var setups=fdjtGetChildrenByClassName(document.body,"onsetup");
  var i=0; while (i<setups.length) {
    var node=setups[i++];
    if (node.onsetup) node.onsetup.call(node);
    else if (node.getAttribute("onsetup")) {
      var fn=new Function(node.getAttribute("onsetup"));
      fn.call(node);}}
}

fdjtAddSetup(fdjtDomutils_setup);
fdjtAddSetup(fdjtAutoPrompt_setup);
fdjtAddSetup(fdjtCheckSpan_setup);
//fdjtAddSetup(fdjtAdjustFontSizes);
fdjtAddSetup(fdjtMarkReduced);
fdjtAddSetup(fdjtNodeSetup,true);
fdjtAddSetup(fdjtAnchorSubmit_setup);

fdjtLoadMessage("Loaded handlers.js");

/* Emacs local variables
;;;  Local variables: ***
;;;  compile-command: "make; if test -f ../makefile; then cd ..; make; fi" ***
;;;  End: ***
*/

/* -*- Mode: Javascript; -*- */

var fdjt_scrolling_id="$Id: handlers.js 188 2009-12-01 04:52:57Z haase $";
var fdjt_scrolling_version=parseInt("$Revision: 188 $".slice(10,-1));

/* Copyright (C) 2001-2009 beingmeta, inc.
   This file is a part of the FDJT web toolkit (www.fdjt.org)
   This file provides DHTML handlers for a variety of UI conventions
   and interactions.

   This program comes with absolutely NO WARRANTY, including implied
   warranties of merchantability or fitness for any particular
   purpose.

    Use, modification, and redistribution of this program is permitted
    under either the GNU General Public License (GPL) Version 2 (or
    any later version) or under the GNU Lesser General Public License
    (version 3 or later).

    These licenses may be found at www.gnu.org, particularly:
      http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
      http://www.gnu.org/licenses/lgpl-3.0-standalone.html

*/

/* More consistent scrollintoview */

var fdjt_use_native_scroll=false;

function _fdjt_get_scroll_offset(wleft,eleft,eright,wright)
{
  var result;
  if ((eleft>wleft) && (eright<wright)) return wleft;
  else if ((eright-eleft)<(wright-wleft)) 
    return eleft-Math.floor(((wright-wleft)-(eright-eleft))/2);
  else return eleft;
}

function fdjtScrollIntoView(elt,topedge)
{
  if ((topedge!==0) && (!topedge) && (fdjtIsVisible(elt)))
    return;
  else if ((fdjt_use_native_scroll) && (elt.scrollIntoView)) {
    elt.scrollIntoView(top);
    if ((topedge!==0) && (!topedge) && (fdjtIsVisible(elt,true)))
      return;}
  else {
    var top = elt.offsetTop;
    var left = elt.offsetLeft;
    var width = elt.offsetWidth;
    var height = elt.offsetHeight;
    var winx=window.pageXOffset;
    var winy=window.pageYOffset;
    var winxedge=winx+window.innerWidth;
    var winyedge=winy+window.innerHeight;
    
    while(elt.offsetParent) {
      elt = elt.offsetParent;
      top += elt.offsetTop;
      left += elt.offsetLeft;}

    var targetx=_fdjt_get_scroll_offset(winx,left,left+width,winxedge);
    var targety=
      (((topedge)||(topedge===0)) ?
       ((typeof topedge === "number") ? (top+topedge) : (top)) :
       (_fdjt_get_scroll_offset(winy,top,top+height,winyedge)));

    window.scrollTo(targetx,targety);}
}

/* Scrolling control */

var _fdjt_saved_scroll=false;
var _fdjt_preview_elt=false;

function fdjtScrollSave(ss)
{
  if (ss) {
    ss.scrollX=window.scrollX; ss.scrollY=window.scrollY;}
  else {
    if (!(_fdjt_saved_scroll)) _fdjt_saved_scroll={};
    _fdjt_saved_scroll.scrollX=window.scrollX;
    _fdjt_saved_scroll.scrollY=window.scrollY;}
  // fdjtXTrace("Saved scroll %o",_fdjt_saved_scroll);
}

function fdjtScrollRestore(ss)
{
  if (_fdjt_preview_elt) {
    fdjtDropClass(_fdjt_preview_elt,"previewing");
    _fdjt_preview_elt=false;}
  if ((ss) && (typeof ss.scrollX === "number")) {
    // fdjtLog("Restoring scroll to %d,%d",ss.scrollX,ss.scrollY);    
    window.scrollTo(ss.scrollX,ss.scrollY);
    return true;}
  else if ((_fdjt_saved_scroll) &&
	   ((typeof _fdjt_saved_scroll.scrollY === "number") ||
	    (typeof _fdjt_saved_scroll.scrollX === "number"))) {
    // fdjtLog("Restoring scroll to %o",_fdjt_saved_scroll);
    window.scrollTo(_fdjt_saved_scroll.scrollX,_fdjt_saved_scroll.scrollY);
    _fdjt_saved_scroll=false;
    return true;}
  else return false;
}

function fdjtScrollDiscard(ss)
{
  if (ss) {
    ss.scrollX=false; ss.scrollY=false;}
  else _fdjt_saved_scroll=false;
}

function fdjtScrollTo(target,id,context,discard,topedge)
{
  fdjtScrollDiscard(discard);
  if (id) document.location.hash=id;
  if (context) {
    setTimeout(function() {
	fdjtScrollIntoView(context,topedge);
	if (!(fdjtIsVisible(target))) {
	  fdjtScrollIntoView(target,topedge);}},
      100);}
  else setTimeout(function() {fdjtScrollIntoView(target,topedge);},100);
}

function fdjtScrollPreview(target,context,delta)
{
  /* Stop the current preview */
  if (!(target)) {
    fdjtStopPreview(); return;}
  /* Already previewing */
  if (target===_fdjt_preview_elt) return;
  if (!(_fdjt_saved_scroll)) fdjtScrollSave();
  if ((_fdjt_preview_elt) && (_fdjt_preview_elt.className))
    fdjtDropClass(_fdjt_preview_elt,"previewing");
  if (target===document.body) 
    _fdjt_preview_elt=false;
  else {
    _fdjt_preview_elt=target;
    fdjtAddClass(target,"previewing");}
  if (!(context))
    fdjtScrollIntoView(target,delta);
  else {
    fdjtScrollIntoView(context,delta);
    if (!(fdjtIsVisible(target))) {
      fdjtScrollIntoView(context);
      if (!(fdjtIsVisible(target))) 
	fdjtScrollIntoView(target);}}
}

function fdjtClearPreview()
{
  if ((_fdjt_preview_elt) && (_fdjt_preview_elt.className))
    fdjtDropClass(_fdjt_preview_elt,"previewing");
  _fdjt_preview_elt=false;
}

function fdjtSetScroll(x,y,elt)
{
  var targetx; var targety;
  var winx=window.pageXOffset;
  var winy=window.pageYOffset;
  var winwidth=window.innerWidth;
  var winheight=window.innerHeight;
  var winxedge=winx+window.innerWidth;
  var winyedge=winy+window.innerHeight;
  if (!(elt)) {
    if (typeof x === 'number')
      if (x>0) targetx=x;
      else targetx=document.body.offsetHeight+x;
    else targetx=winx;
    if (typeof y === 'number')
      if (y>0) targety=y;
      else targety=document.body.offsetWidth+y;
    else targety=winy;}
  else {
    var top = elt.offsetTop;
    var left = elt.offsetLeft;
    var width = elt.offsetWidth;
    var height = elt.offsetHeight;
    while(elt.offsetParent) {
      elt = elt.offsetParent;
      top += elt.offsetTop;
      left += elt.offsetLeft;}
    if (typeof x === 'number') {
      if (x>0) targetx=left-x;
      else targetx=left-(winwidth+x-width);}
    else targetx=winx;
    if (typeof y === 'number') {
      if (y>0) targety=top-y;
      else targety=top-(winyheight+y-height);}
    else targety=winy;}
	     
  window.scrollTo(targetx,targety);
}

/* -*- Mode: Javascript; -*- */

var fdjt_richtips_id="$Id: richtips.js 188 2009-12-01 04:52:57Z haase $";
var fdjt_richtips_version=parseInt("$Revision: 188 $".slice(10,-1));

/* Copyright (C) 2007-2009 beingmeta, inc.
   This file is a part of the FDJT web toolkit (www.fdjt.org)
   This file provides for mouseover tooltips with rich content

   This program comes with absolutely NO WARRANTY, including implied
   warranties of merchantability or fitness for any particular
   purpose.

    Use, modification, and redistribution of this program is permitted
    under either the GNU General Public License (GPL) Version 2 (or
    any later version) or under the GNU Lesser General Public License
    (version 3 or later).

    These licenses may be found at www.gnu.org, particularly:
      http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
      http://www.gnu.org/licenses/lgpl-3.0-standalone.html
*/

/* RICHTIPS
     are overlayed tooltips displayed in translucent rectangles. */

/* Set to false to suppress richtip display. */
var fdjt_display_richtips=true;
/* This is the current 'live' richtip displayed. */
var fdjt_live_richtip=false;

/* How long to wait before actually getting the richtip element */
var fdjt_richtip_delay=300;
var fdjt_richtip_pending=false;
var fdjt_richtip_timeout=null;

var fdjt_richtip_classfns={};

function fdjtFindRichTip(elt)
{
  while (elt)
    if (elt.richTip) return elt.richTip;
    else elt=elt.parentNode;
  return false;
}

function fdjtGetRichParent(elt)
{
  var richtip;
  while (elt)
    if ((elt.richTip) || (elt.getRichTip) ||
	((elt.className) && (fdjt_richtip_classfns[elt.className])))
      return elt;
    else elt=elt.parentNode;
  return null;
}

function fdjtGetRichTip(elt)
{
  var parent=fdjtGetRichParent(elt);
  var richtip=false;
  if (parent)
    if (parent.richTip) return parent.richTip;
    else if (parent.getRichTip)
      richtip=(parent.getRichTip)(elt);
    else richtip=(fdjt_richtip_classfns[parent.className])(parent);
  else return false;
  if (richtip) {
    fdjtAppend(document.body,richtip);
    parent.richTip=richtip;}
  return richtip;
}

function fdjtRichTip_display(elt)
{
  if (!(fdjt_display_richtips)) return;
  var richtip=fdjtGetRichTip(elt);
  if (!(richtip)) return;
  if (fdjt_live_richtip===richtip) {
    fdjtAddClass(fdjt_live_richtip,"live");
    return;}
  else if (fdjt_live_richtip)
    fdjtDropClass(fdjt_live_richtip,"live");
  fdjt_live_richtip=richtip;
  var xoff=0, yoff=0; var node=elt;
  while (node) {
    xoff=xoff+node.offsetLeft;
    yoff=yoff+node.offsetTop;
    node=node.offsetParent;}
  yoff=yoff+elt.offsetHeight;
  richtip.style.left=xoff+'px';
  richtip.style.top=yoff+'px';
  fdjtAddClass(richtip,"live");
  if ((xoff+richtip.offsetWidth)>window.innerWidth)
    xoff=xoff-((xoff+richtip.offsetWidth)-window.innerWidth);
  if (xoff<0) xoff=1;
  richtip.style.left=xoff+'px';
  richtip.style.top=yoff+'px';
}

function fdjtRichTip_onmouseover(evt)
{
  evt=evt||event||null;
  var target=fdjtGetRichParent($T(evt));
  if ((!(target)) && (fdjt_live_richtip)) {
    fdjtDropClass(fdjt_live_richtip,"live");
    fdjt_live_richtip=false;}
  if (fdjt_richtip_pending===target) return;
  else if (fdjt_richtip_pending) {
    clearTimeout(fdjt_richtip_timeout);
    fdjt_richtip_timeout=false;
    fdjt_richtip_pending=false;}
  if (target) {
    fdjt_richtip_pending=target;
    fdjt_richtip_timeout=
      setTimeout(function (evt) {
	  fdjtRichTip_display(target);},
	fdjt_richtip_delay);}
  else if (fdjt_live_richtip) {
    fdjtDropClass(fdjt_live_richtip,"live");
    fdjt_live_richtip=false;}
}

function fdjtRichTip_onclick(evt)
{
  evt=evt||event||null;
  var target=fdjtFindRichTip($T(evt));
  if (target) 
    if (fdjtHasClass(target,"static")) {
      fdjtDropClass(target,"static");
      fdjtDropClass(target,"live");
      fdjt_live_richtip=false;}
    else fdjtAddClass(target,"static");
  return false;
}

function fdjtRichTip_onmouseout(evt)
{
  evt=evt||event||null;
  if (!(fdjt_display_richtips)) return;
  if (fdjt_live_richtip) {
    fdjtDropClass(fdjt_live_richtip,"live");
    fdjt_live_richtip=false;}
  if (fdjt_richtip_pending) {
    clearTimeout(fdjt_richtip_timeout);
    fdjt_richtip_timeout=false;
    fdjt_richtip_pending=false;}
}

function fdjtRichTip_minimize(evt) {
  evt=evt||event||null;
  fdjtAddClass($P(".richtip",$T(evt)),"minimized"); }

function fdjtRichTip_maximize(evt) {
  evt=evt||event||null;
  fdjtDropClass($P(".richtip",$T(evt)),"minimized");}

function fdjtRichTip_hide(evt) {
  evt=evt||event||null;
  fdjtDropClass($P(".richtip",$T(evt)),"static");}

function fdjtRichTip_init(elt) {
  var minbutton=fdjtImage(graphics_root+"minimize.png","minimize button","-");
  var maxbutton=fdjtImage(graphics_root+"maximize.png","maximize button","+");
  var closebutton=fdjtImage(graphics_root+"hide.png","hide button","x");
  var controls=fdjtSpan("controls",minbutton,maxbutton,closebutton);
  minbutton.onclick=fdjtRichTip_minimize;
  maxbutton.onclick=fdjtRichTip_maximize;
  closebutton.onclick=fdjtRichTip_hide;
  fdjtPrepend(elt,controls);
  return elt;}

/* Emacs local variables
;;;  Local variables: ***
;;;  compile-command: "make; if test -f ../makefile; then cd ..; make; fi" ***
;;;  End: ***
*/
/* -*- Mode: Javascript; -*- */

var fdjt_completion_id="$Id: completion.js 215 2010-02-14 21:50:25Z haase $";
var fdjt_completion_version=parseInt("$Revision: 215 $".slice(10,-1));

/* Copyright (C) 2009 beingmeta, inc.
   This file is a part of the FDJT web toolkit (www.fdjt.org)
   This file provides for input completion.

   This program comes with absolutely NO WARRANTY, including implied
   warranties of merchantability or fitness for any particular
   purpose.

    Use, modification, and redistribution of this program is permitted
    under either the GNU General Public License (GPL) Version 2 (or
    any later version) or under the GNU Lesser General Public License
    (version 3 or later).

    These licenses may be found at www.gnu.org, particularly:
      http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
      http://www.gnu.org/licenses/lgpl-3.0-standalone.html
*/

var fdjt_trace_completion_setup=false;
var fdjt_trace_completion=false;
var fdjt_detail_completion=false;

/* Completion */

// Always set to distinguish no options from false
var FDJT_COMPLETE_OPTIONS=1;
// Whether the completion element is a cloud (made of spans)
var FDJT_COMPLETE_CLOUD=2;
// Whether to require that completion match an initial segment
var FDJT_COMPLETE_ANYWORD=4;
// Whether to match case in keys to completions
var FDJT_COMPLETE_MATCHCASE=8;
// Whether to an enter character always picks a completion
var FDJT_COMPLETE_EAGER=16;
// Whether the key fields may contain disjoins (e.g. (dog|friend))
// to be accomodated in matching
var FDJT_COMPLETE_DISJOINS=32;

var fdjt_complete_options=FDJT_COMPLETE_OPTIONS;
var fdjt_maxcomplete_default=50;

// This parses the options for completion
function _fdjt_get_complete_opts(arg)
{
  if (!(arg)) return fdjt_complete_options;
  else if (typeof arg === "number") return arg;
  else if (typeof arg === "string") {
    var opt=
      (((arg.search(/\banywhere\b/)<0)?(0):((FDJT_COMPLETE_ANYWORD)))|
       ((arg.search(/\bmatchcase\b/)<0)?(0):((FDJT_COMPLETE_MATCHCASE)))|
       ((arg.search(/\bcloud\b/)<0)?(0):((FDJT_COMPLETE_CLOUD)))|
       ((arg.search(/\beager\b/)<0)?(0):((FDJT_COMPLETE_EAGER)))|
       ((arg.search(/\bdisjoins\b/)<0)?(0):((FDJT_COMPLETE_DISJOINS)))|
       FDJT_COMPLETE_OPTIONS);
    // fdjtTrace("Getting complete options from %o=%o",arg,opt);
    return opt;}
  else if (arg.completeopts)
    return arg.completeopts;
  else if (arg.input_elt)
    return _fdjt_get_complete_opts(arg.input_elt);
  else if (arg.getAttribute) {
    var opts=_fdjt_get_complete_opts(arg.getAttribute("completeopts"));
    arg.completeopts=opts;
    // fdjtTrace("Got completeopts for %o %o/%o",arg,opts,arg.completeopts);
    return opts;}
  else return fdjt_complete_options;
}


// This gets the completions element for an input element
// A completion element is a "cloud" if new completions are typically
// spans rather than divs.
function fdjt_get_completions(input_elt,create)
{
  var completions_name=input_elt.getAttribute("COMPLETIONS");
  if (!(completions_name)) {
    var id=input_elt.name+"_COMPLETIONS";
    var elt=$(id);
    input_elt.setAttribute("COMPLETIONS",id);}
  // This is the case where the COMPLETIONS element is
  // the ID of another element
  var elt=$(input_elt.getAttribute("COMPLETIONS"));
  if (!(elt))
    if (create)
      elt=fdjtCompletions
	(input_elt.getAttribute("COMPLETIONS"),[],
	 _fdjt_get_complete_opts(input_elt));
    else return false;
  if (!(elt.getAttribute('INPUTID'))) {
    var input_id=fdjtForceId(input_elt);
    elt.setAttribute('INPUTID',input_id);}
  return elt;
}

function fdjt_get_completions_input(completions)
{
  if (completions.getAttribute('INPUTID'))
    return document.getElementById(completions.getAttribute('INPUTID'));
  else return false;
}

// Creates a completions DIV with a list of completions
function fdjtCompletions(id,completions,opts)
{
  var div=fdjtDiv("completions");
  div.id=id;
  div.onclick=fdjtComplete_onclick;
  fdjtAddCompletions(div,completions,opts);
  return div;
}

// Converts a value to a completion element (a DOM node)
function fdjtCompletionElt(completion,opts)
{
  var key=false; var value; var content=[]; var title=false;
  // key is what we look up on, value is what we 'get', content is
  // what we display, title is the tooltip, completion_elt is the DOM 
  // node itself
  if ((completion)&&((completion.getCompletionEntry))) {
    var ce=completion.getCompletionEntry();
    if ((typeof ce === 'string')||(typeof ce !== 'object')) {
      key=content=ce; value=completion;}
    else if (ce.nodeType) return ce;
    else completion=ce;}
  else if (completion.nodeType) return completion;
  else if (typeof completion === 'string') 
    key=value=content=completion;
  else if (typeof completion !== 'object') {
    key=content=completion.toString(); value=completion;}
  else {
    key=completion.key;
    value=completion.value||key;
    title=completion.title||key;
    content=completion.content||key;}
  var elt=((opts&FDJT_COMPLETE_CLOUD) ?
	   (fdjtSpan("completion",content)):
	   (fdjtDiv("completion",content)));
  elt.key=key;
  elt.value=value;
  if (title) elt.title=title;
  return elt;
}

function fdjtAddCompletion(div,completion,opts,init)
{
  var divinfo=fdjtEltInfo(div);
  if (!(div.nodeType)) throw {name: 'NotANode', irritant: div};
  if (!(completion.nodeType)) throw {name: 'NotANode', irritant: completion};
  if (!(opts)) opts=_fdjt_get_complete_opts(div);
  if (!(divinfo.allcompletions)) fdjtInitCompletions(div,false,opts);
  // fdjtTrace("add completion %o to %o",completion,div);
  var ac=divinfo.allcompletions;
  if ((init)||
      (((ac.indexOf)?(ac.indexOf(completion)):
	(fdjtIndexOf(ac,completion)))<0)) {
    var prefixtree=divinfo.prefixtree;
    var vmap=divinfo.valuemap;
    var cmap=divinfo.completionmap;
    var smap=divinfo.stringmap;
    var stdkey=fdjtStdSpace(completion.key);
    if (!(opts&FDJT_COMPLETE_MATCHCASE)) {
      var lower=stdkey.toLowerCase();
      smap[lower]=stdkey;
      stdkey=lower;}
    if (!(prefixtree)) {
      prefixtree=divinfo.prefixtree={};
      prefixtree.strings=[];}
    if (!(cmap)) cmap=divinfo.completionmap={};
    if (!(fdjtHasParent(completion,div)))
      fdjtAppend(div,completion," ");
    fdjtAddKeys(completion,prefixtree,cmap,stdkey,
		(opts&FDJT_COMPLETE_ANYWORD));
    if (divinfo.allcompletions)
      divinfo.allcompletions.push(completion);
    else divinfo.allcompletions=new Array(completion);
    if (fdjtHasClass(completion,"cue")) 
      if (divinfo.allcues)
	divinfo.allcues.push(completion);
      else divinfo.allcues=new Array(completion);
    var value=completion.value;
    var variations=fdjtGetChildrenByClassName(completion,"variation");
    fdjtAdd(vmap,value,completion);
    var i=0; while (i<variations.length) {
      var variation=variations[i++];
      var stdkey=fdjtStdSpace(variation.key);
      variation.value=value;
      if (!(opts&FDJT_COMPLETE_MATCHCASE)) stdkey=stdkey.toLowerCase();
      fdjtAddKeys(variation,prefixtree,cmap,stdkey,
		  (opts&FDJT_COMPLETE_ANYWORD));}}
}

function fdjtAddKeys(value,ptree,cmap,keystring,anywhere)
{
  var keys=((anywhere)?(keystring.split(/\W/g)):[]).concat(keystring);
  var i=0; while (i<keys.length) {
    var key=keys[i++];
    fdjtPrefixAdd(ptree,key,0);
    if (cmap) {
      if ((cmap[key])&&(cmap.hasOwnProperty(key)))
	cmap[key].push(value);
      else cmap[key]=new Array(value);
      cmap._count++;}}
}

function fdjtInitCompletions(div,completions,opts)
{
  var divinfo=fdjtEltInfo(div);
  if (divinfo.allcompletions) return;
  divinfo.allcompletions=[];
  divinfo.allcues=[];
  divinfo.prefixtree={};
  divinfo.prefixtree.strings=[];
  divinfo.completionmap={}; divinfo.completionmap._count=0;
  divinfo.valuemap={};
  if (!(opts&FDJT_COMPLETE_MATCHCASE)) divinfo.stringmap={};
  var start=new Date();
  var completions=(completions)||fdjtGetChildrenByClassName(div,"completion");
  if (fdjt_trace_completion_setup)
    fdjtTrace("[%fs] Initializing %d completions for %o",
	      fdjtElapsedTime(),completions.length,div);
  var i=0; while (i<completions.length)
	     fdjtAddCompletion(div,completions[i++],opts,true);
  if (fdjt_trace_completion_setup)
    fdjtTrace("[%fs][%fs] Initialized %d completions, %d strings, %d items for %o",
	      fdjtElapsedTime(),fdjtDiffTime(start),
	      completions.length,divinfo.prefixtree.strings.length,
	      divinfo.completionmap._count,
	      div);
}

// Adds a vector of completions to a completions DIV
function fdjtAddCompletions(div,completions,opts)
{
  if (typeof div === "string") div=document.getElementById(div);
  if (!(div.nodeType)) throw {name: 'NotANode', irritant: div};
  if ((completions) && (completions instanceof Array)) {
    var i=0; while (i<completions.length) 
	       fdjtAddCompletion(div,fdjtCompletionElt(completions[i++]),opts);}
  return div;
}

// This looks for completions matching *string* among the completions
//  associated with input_elt.  Note that *string* might not be the value
//  of input_elt, but might just be a component returned by fdjtCompletionText.
function fdjtComplete(input_elt,string,options,maxcomplete)
{
  if (!(string)) string=fdjtCompletionText(input_elt);
  if (!(options))
    options=input_elt.completeopts||_fdjt_get_complete_opts(input_elt);
  if (typeof maxcomplete==='undefined')
    maxcomplete=
      input_elt.maxcomplete||(fdjtCacheAttrib(input_elt,"maxcomplete"))||
      fdjt_maxcomplete_default;
  var container=fdjt_get_completions(input_elt);
  var cinfo=fdjtEltInfo(container);
  var cmap=cinfo.completionmap;
  if (!(cinfo.allcompletions))
    fdjtInitCompletions(container,false,options);
  if (fdjt_trace_completion)
    fdjtLog("[%f] Complete on '%o' in %o against %o, cs=%o",
	    fdjtElapsedTime(),string,input_elt,container,
	    cinfo._curstring);
  var start=new Date();
  var timer=new Date();
  var heads=[]; var variations=[]; var strings=[]; var values=[];
  if ((!string) || (fdjtIsEmptyString(string))) {
    var empty=[];
    fdjtAddClass(container,"noinput");
    empty.exact=[];
    empty.values=[];
    empty.strings=[];
    var displayed=cinfo._displayed;
    if (displayed) {
      var i=0; while (i<displayed.length) {
	var node=displayed[i++];
	node.removeAttribute('displayed');}}
    if ((fdjt_trace_completion)&&(displayed))
      fdjtLog("[%f] Hiding %d displayed items in %o",
	      fdjtElapsedTime(),displayed.length,container);
    cinfo._displayed=[];
    return empty;}
  else if ((cinfo._curstring) && (string===cinfo._curstring)) {
    fdjtDropClass(container,"noinput");
    if (fdjt_trace_completion)
      fdjtLog("[%f] Using %d cached results for %s against %o",
	      fdjtElapsedTime(),container._results.length,string,container);
    heads=cinfo._results;
    variations=heads.variations;
    strings=heads.strings;
    values=heads.values;}
  else {
    var qstring=((options&FDJT_COMPLETE_MATCHCASE)?(string):
		 (string.toLowerCase()));
    var prefixtree=cinfo.prefixtree;
    strings=fdjtPrefixFind(prefixtree,qstring,0);
    if (fdjt_detail_completion)
      fdjtLog("[%f][%fs] Found %d strings from %s/%s",
	      fdjtElapsedTime(),fdjtDeltaTime(timer),
	      strings.length,qstring,string);
    if ((!(strings))||(strings.length===0)) {
      heads.variations=variations;
      heads.exact=[];
      heads.strings=strings;
      heads.values=values;
      cinfo._results=heads;
      cinfo._curstring=string;}
    else {
      var i=0; while (i<strings.length) {
	var completions=cmap[strings[i++]];
	var j=0; while (j<completions.length) {
	  var completion=completions[j++];
	  var head=(completion.completehead)||(fdjtGetCompleteHead(completion));
	  if (head===true) {
	    if (heads.indexOf(completion)<0) heads.push(completion);}
	  else if (heads.indexOf(head)<0) {
	    variations.push(completion);
	    heads.push(head);}}}
      // Accumulate values
      i=0; while (i<heads.length) values.push(heads[i++].value);
      /* Convert non-canonicalized strings */
      if (cinfo.stringmap) {
	var smap=cinfo.stringmap;
	var norms=[];
	var j=0; while (j<strings.length) {
	  norms[j]=smap[strings[j]]||strings[j];
	  j++;}
	heads.strings=norms;}
      else heads.strings=strings;
      if (fdjt_trace_completion)
	fdjtLog("[%f][%fs] Got %d(%d) matches from %d strings matching %s/%s",
		fdjtElapsedTime(),fdjtDeltaTime(timer),
		heads.length,variations.length,
		strings.length,string,qstring);}}
  var displayed=cinfo._displayed||[]; var hidden=0; var revealed=0;
  var counts=fdjtGetCompletionCounts(container);
  var overload=((maxcomplete)&&(heads.length>maxcomplete));
  i=0; while (i<displayed.length) {
    var node=displayed[i++];
    if ((overload)||((heads.indexOf(node)<0)&&(variations.indexOf(node)<0))) {
      node.removeAttribute("displayed"); hidden++;}}
  if (fdjt_detail_completion)
    fdjtLog("[%f][%fs] Hid %d of %d displayed items",
	    fdjtElapsedTime(),fdjtDeltaTime(timer),hidden,displayed.length);
  i=0; while (i<counts.length) {
    counts[i++].innerHTML=""+heads.length;}
  fdjtDropClass(container,"noinput");
  if (overload) {
    cinfo._displayed=[];
    fdjtAddClass(container,"maxcomplete");}
  else {
    fdjtDropClass(container,"maxcomplete");
    i=0; while (i<heads.length) {
      var head=heads[i++]; 
      head.setAttribute('displayed','yes');}
    i=0; while (i<variations.length) {
      var node=variations[i++]; 
      node.setAttribute('displayed','yes');}
    if (fdjt_detail_completion)
      fdjtLog("[%f][%fs] Revealed %d heads and %d variations",
	      fdjtElapsedTime(),fdjtDeltaTime(timer),
	      heads.length,variations.length);
    cinfo._displayed=heads.concat(variations);}
  if (fdjt_trace_completion)
    if (heads.length>10)
      fdjtTrace("[%f][%fs] Completing on %o over %o yields %d results",
		fdjtElapsedTime(),fdjtDiffTime(start),
		string,input_elt,heads.length);
    else fdjtTrace("[%f][%fs] Completing over %o on %o yields %d results: %o",
		   fdjtElapsedTime(),fdjtDiffTime(start),input_elt,
		   string,heads.length,heads.values);
  input_elt.completeString=string;
  heads.heads=heads; heads.variations=variations;
  cinfo._curstring=string;
  cinfo._results=heads;
  fdjtRedisplay(container);
  heads.exact=cmap[qstring]||[];
  heads.variations=variations;
  if (heads.length===0) fdjtAddClass(container,"noresults");
  return heads;
}
  
function fdjtGetCompleteHead(completion)
{
  var head=completion.completehead;
  if (head) return head;
  if (fdjtHasClass(completion,"variation")) {
    head=fdjtGetParentByClassName(completion,"completion");
    if (!(head)) {
      fdjtWarn("Couldn't find completion parent of %o",completion);
      completion.completehead=head=true;}
    else completion.completehead=head;}
  else completion.completehead=head=true;
  return head;
}

function fdjtGetCompletionCounts(container)
{
  var cinfo=fdjtEltInfo(container);
  if (cinfo._completioncounts)
    return cinfo._completioncounts;
  else {
    var children=fdjtGetChildrenByClassName(container,"completioncount");
    cinfo._completioncounts=children;
    return children;}
}

// This 'forces' a completion presumably when the user indicates a decisive
// action (like a return) as opposed to milder requests.
function fdjtForceComplete(input_elt)
{
  if (input_elt.value==="") return;
  var completions=fdjtComplete(input_elt,false,false);
  if (fdjt_trace_completion)
    fdjtLog("Trying to force completion on %o:",input_elt,completions);
  if ((completions) && (completions.length>0)) 
    return fdjtHandleCompletion(input_elt,completions[0],false);
  else return false;
}
 
function fdjtCompletionText(input_elt)
{
  if (input_elt.getCompletionText)
    return input_elt.getCompletionText();
  else if (fdjtHasAttrib(input_elt,"isempty"))
    return "";
  else return input_elt.value;
}

function fdjtHandleCompletion(input_elt,elt,value)
{
  if (!(value))
    value=elt.value||(fdjtCacheAttrib(elt,"value"));
  if (_fdjt_completion_timer) 
    clearTimeout(_fdjt_completion_timer);
  if (!(elt)) return;
  if (fdjt_trace_completion)
    fdjtLog("Handling completion on %o with %o (%o):",
	    input_elt,elt,value);
  if (input_elt.oncomplete) {
    return input_elt.oncomplete(elt,value||elt.value);}
  else if (input_elt.getAttribute("ONCOMPLETE")) {
    input_elt.oncomplete=
      new Function("elt","value",
		   input_elt.getAttribute("ONCOMPLETE"));
    return input_elt.oncomplete(elt,value||elt.value);}
  else {
    input_elt.value=value||elt.value;
    input_elt.focus();}
}

function fdjtComplete_onclick(evt)
{
  evt=evt||event||null;
  var target=$T(evt);
  // fdjtTrace("complete onclick %o",target);
  while (target)
    if ((target.key) ||
	((target.getAttribute) &&
	 (target.getAttribute('key')))) break;
    else target=target.parentNode;
  // fdjtTrace("complete onclick/2 %o",target);
  if (!(target)) return;
  var completions=target;
  while (completions)
    if (completions.getAttribute("INPUTID")) break;
    else completions=completions.parentNode;
  if (!(completions)) return;
  var input_elt=fdjt_get_completions_input(completions);
  var value=((target.value) ||
	     (target.getAttribute("value")) ||
	     (target.key));
  fdjtHandleCompletion(input_elt,target,value);
  fdjtDropClass(completions,"open");
}

var _fdjt_completion_timer=false;

function fdjtComplete_show(evt)
{
  evt=evt||event||null;
  var target=$T(evt);
  var keycode=evt.keyCode;
  var value=fdjtCompletionText(target);
  var options=target.completeopts||_fdjt_get_complete_opts(target);
  if (_fdjt_completion_timer) 
    clearTimeout(_fdjt_completion_timer);
  if (value!="")
    fdjt_completion_timer=
      setTimeout(function () {
	  fdjtComplete(target,value,options);},100);
}

function fdjtComplete_toggleshowall(evt)
{
  evt=evt||event||null;
  var target=$T(evt);
  var container=$P(".completions",target);
  if (fdjtHasClass(container,"maxcomplete"))
    fdjtSwapClass(container,"maxcomplete","showmaxcomplete");
  else if (fdjtHasClass(container,"showmaxcomplete"))
    fdjtSwapClass(container,"showmaxcomplete","maxcomplete");
}

function fdjtComplete_onfocus(evt)
{
  evt=evt||event||null;
  fdjtAutoPrompt_onfocus(evt);
  fdjtComplete($T(evt));
}

function fdjtComplete_setup(target)
{
  fdjtComplete(target);
}

function fdjtCheckComplete(target)
{
  if ((!(target.completeString))||
      (target.completeString!==fdjtCompletionText(target)))
    fdjtComplete(target);
}

function fdjtComplete_onkey(evt)
{
  evt=evt||event||null;
  var target=$T(evt);
  var keycode=evt.keyCode;
  var charcode=evt.charCode;
  var value=fdjtCompletionText(target);
  var options=target.completeopts||_fdjt_get_complete_opts(target);
  var cchars=
    fdjtCacheAttrib($T(evt),"enterchars",fdjtStringToKCodes,[-13]);    
  if (_fdjt_completion_timer) 
    clearTimeout(_fdjt_completion_timer);
  // fdjtTrace("Complete_onkey %o, cchars=%o",evt,cchars);
  if (((keycode) && (fdjtIndexOf(cchars,-keycode)>=0)) ||
      ((charcode) && (fdjtIndexOf(cchars,charcode)>=0))) {
    // These complete right away
    var results=fdjtComplete(target,false,options);
    // fdjtTrace("Escape complete on %o, results=%o",target,results);
    if (evt.preventDefault) evt.preventDefault(); else evt.returnValue=false;
    evt.cancelBubble=true;
    if (results.length>0) {
      var completions=fdjt_get_completions(target);
      fdjtHandleCompletion(target,results[0],results[0].value);
      fdjtDropClass(completions,"open");}
    else {}}
  else {
    if (fdjt_trace_completion)
      fdjtTrace("[%fs] Delaying handler fdjtComplete on %o",
		fdjtElapsedTime(),target);
    fdjtDelay(200,fdjtComplete,target,target,"completedelay");}
  return true;
}

function fdjtSetCompletions(id,completions)
{
  var current=document.getElementById(id);
  if (fdjt_trace_completion)
    if (current===completions)
      fdjtLog("[%f] Completions for #%s are unchanged",fdjtElapsedTime(),id);
    else fdjtLog("[%f] Setting current completions #%s=%o to %o/%d",
		 fdjtElapsedTime(),id,current,completions,
		 $$(".completion",completions).length);
  if (!(current)) {
    fdjtWarn("Can't find current completions #%s",id);
    return;}
  fdjtReplace(current,completions);
  completions.id=id;
}

/* Setting cues */

function fdjtSetCompletionCues(div,cues,onmissing)
{
  var cinfo=fdjtEltInfo(div);
  var cur=cinfo._cues||[];
  var cmap=cinfo.completionmap;
  var vmap=cinfo.valuemap;
  var newcues=[];
  var i=0; var ilen=cues.length;
  while (i<ilen) {
    var cue=cues[i++];
    if (typeof cue === 'string')
      if (vmap[cue]) {
	var values=vmap[cue];
	var j=0; var jlen=values.length;
	while (j<jlen) newcues.push(values[j++]);}
      else if (onmissing) {
	var new_elt=onmissing(div,cue);
	if (new_elt) {
	  fdjtAdd(vmap,cue,new_elt);
	  newcues.push(new_elt);}}
      else {}
    else if (fdjtHasClass(cue,"completion"))
      newcues.push(cue);
    else {}}
  var i=0; while (i<newcues.length) {
    var cue=newcues[i++];
    if (!(fdjtContains(cur,cue))) {fdjtAddClass(cue,"cue");}}
  var i=0; while (i<cur.length) {
    var elt=cur[i++];
    if (!(fdjtContains(newcues,elt))) fdjtDropClass(elt,"cue");}
  cinfo._cues=newcues;
}

/* Emacs local variables
;;;  Local variables: ***
;;;  compile-command: "make; if test -f ../makefile; then cd ..; make; fi" ***
;;;  End: ***
*/
/*
    http://www.JSON.org/json2.js
    2009-06-29

    Public Domain.

    NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.

    See http://www.JSON.org/js.html

    This file creates a global JSON object containing two methods: stringify
    and parse.

        JSON.stringify(value, replacer, space)
            value       any JavaScript value, usually an object or array.

            replacer    an optional parameter that determines how object
                        values are stringified for objects. It can be a
                        function or an array of strings.

            space       an optional parameter that specifies the indentation
                        of nested structures. If it is omitted, the text will
                        be packed without extra whitespace. If it is a number,
                        it will specify the number of spaces to indent at each
                        level. If it is a string (such as '\t' or '&nbsp;'),
                        it contains the characters used to indent at each level.

            This method produces a JSON text from a JavaScript value.

            When an object value is found, if the object contains a toJSON
            method, its toJSON method will be called and the result will be
            stringified. A toJSON method does not serialize: it returns the
            value represented by the name/value pair that should be serialized,
            or undefined if nothing should be serialized. The toJSON method
            will be passed the key associated with the value, and this will be
            bound to the object holding the key.

            For example, this would serialize Dates as ISO strings.

                Date.prototype.toJSON = function (key) {
                    function f(n) {
                        // Format integers to have at least two digits.
                        return n < 10 ? '0' + n : n;
                    }

                    return this.getUTCFullYear()   + '-' +
                         f(this.getUTCMonth() + 1) + '-' +
                         f(this.getUTCDate())      + 'T' +
                         f(this.getUTCHours())     + ':' +
                         f(this.getUTCMinutes())   + ':' +
                         f(this.getUTCSeconds())   + 'Z';
                };

            You can provide an optional replacer method. It will be passed the
            key and value of each member, with this bound to the containing
            object. The value that is returned from your method will be
            serialized. If your method returns undefined, then the member will
            be excluded from the serialization.

            If the replacer parameter is an array of strings, then it will be
            used to select the members to be serialized. It filters the results
            such that only members with keys listed in the replacer array are
            stringified.

            Values that do not have JSON representations, such as undefined or
            functions, will not be serialized. Such values in objects will be
            dropped; in arrays they will be replaced with null. You can use
            a replacer function to replace those with JSON values.
            JSON.stringify(undefined) returns undefined.

            The optional space parameter produces a stringification of the
            value that is filled with line breaks and indentation to make it
            easier to read.

            If the space parameter is a non-empty string, then that string will
            be used for indentation. If the space parameter is a number, then
            the indentation will be that many spaces.

            Example:

            text = JSON.stringify(['e', {pluribus: 'unum'}]);
            // text is '["e",{"pluribus":"unum"}]'


            text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
            // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'

            text = JSON.stringify([new Date()], function (key, value) {
                return this[key] instanceof Date ?
                    'Date(' + this[key] + ')' : value;
            });
            // text is '["Date(---current time---)"]'


        JSON.parse(text, reviver)
            This method parses a JSON text to produce an object or array.
            It can throw a SyntaxError exception.

            The optional reviver parameter is a function that can filter and
            transform the results. It receives each of the keys and values,
            and its return value is used instead of the original value.
            If it returns what it received, then the structure is not modified.
            If it returns undefined then the member is deleted.

            Example:

            // Parse the text. Values that look like ISO date strings will
            // be converted to Date objects.

            myData = JSON.parse(text, function (key, value) {
                var a;
                if (typeof value === 'string') {
                    a =
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
                    if (a) {
                        return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
                            +a[5], +a[6]));
                    }
                }
                return value;
            });

            myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
                var d;
                if (typeof value === 'string' &&
                        value.slice(0, 5) === 'Date(' &&
                        value.slice(-1) === ')') {
                    d = new Date(value.slice(5, -1));
                    if (d) {
                        return d;
                    }
                }
                return value;
            });


    This is a reference implementation. You are free to copy, modify, or
    redistribute.

    This code should be minified before deployment.
    See http://javascript.crockford.com/jsmin.html

    USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
    NOT CONTROL.
*/

/*jslint evil: true */

/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
    call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
    getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
    lastIndex, length, parse, prototype, push, replace, slice, stringify,
    test, toJSON, toString, valueOf
*/

// Create a JSON object only if one does not already exist. We create the
// methods in a closure to avoid creating global variables.

var JSON = JSON || {};

(function () {

    function f(n) {
        // Format integers to have at least two digits.
        return n < 10 ? '0' + n : n;
    }

    if (typeof Date.prototype.toJSON !== 'function') {

        Date.prototype.toJSON = function (key) {

            return isFinite(this.valueOf()) ?
                   this.getUTCFullYear()   + '-' +
                 f(this.getUTCMonth() + 1) + '-' +
                 f(this.getUTCDate())      + 'T' +
                 f(this.getUTCHours())     + ':' +
                 f(this.getUTCMinutes())   + ':' +
                 f(this.getUTCSeconds())   + 'Z' : null;
        };

        String.prototype.toJSON =
        Number.prototype.toJSON =
        Boolean.prototype.toJSON = function (key) {
            return this.valueOf();
        };
    }

    var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
        escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
        gap,
        indent,
        meta = {    // table of character substitutions
            '\b': '\\b',
            '\t': '\\t',
            '\n': '\\n',
            '\f': '\\f',
            '\r': '\\r',
            '"' : '\\"',
            '\\': '\\\\'
        },
        rep;


    function quote(string) {

// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe escape
// sequences.

        escapable.lastIndex = 0;
        return escapable.test(string) ?
            '"' + string.replace(escapable, function (a) {
                var c = meta[a];
                return typeof c === 'string' ? c :
                    '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
            }) + '"' :
            '"' + string + '"';
    }


    function str(key, holder) {

// Produce a string from holder[key].

        var i,          // The loop counter.
            k,          // The member key.
            v,          // The member value.
            length,
            mind = gap,
            partial,
            value = holder[key];

// If the value has a toJSON method, call it to obtain a replacement value.

        if (value && typeof value === 'object' &&
                typeof value.toJSON === 'function') {
            value = value.toJSON(key);
        }

// If we were called with a replacer function, then call the replacer to
// obtain a replacement value.

        if (typeof rep === 'function') {
            value = rep.call(holder, key, value);
        }

// What happens next depends on the value's type.

        switch (typeof value) {
        case 'string':
            return quote(value);

        case 'number':

// JSON numbers must be finite. Encode non-finite numbers as null.

            return isFinite(value) ? String(value) : 'null';

        case 'boolean':
        case 'null':

// If the value is a boolean or null, convert it to a string. Note:
// typeof null does not produce 'null'. The case is included here in
// the remote chance that this gets fixed someday.

            return String(value);

// If the type is 'object', we might be dealing with an object or an array or
// null.

        case 'object':

// Due to a specification blunder in ECMAScript, typeof null is 'object',
// so watch out for that case.

            if (!value) {
                return 'null';
            }

// Make an array to hold the partial results of stringifying this object value.

            gap += indent;
            partial = [];

// Is the value an array?

            if (Object.prototype.toString.apply(value) === '[object Array]') {

// The value is an array. Stringify every element. Use null as a placeholder
// for non-JSON values.

                length = value.length;
                for (i = 0; i < length; i += 1) {
                    partial[i] = str(i, value) || 'null';
                }

// Join all of the elements together, separated with commas, and wrap them in
// brackets.

                v = partial.length === 0 ? '[]' :
                    gap ? '[\n' + gap +
                            partial.join(',\n' + gap) + '\n' +
                                mind + ']' :
                          '[' + partial.join(',') + ']';
                gap = mind;
                return v;
            }

// If the replacer is an array, use it to select the members to be stringified.

            if (rep && typeof rep === 'object') {
                length = rep.length;
                for (i = 0; i < length; i += 1) {
                    k = rep[i];
                    if (typeof k === 'string') {
                        v = str(k, value);
                        if (v) {
                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
                        }
                    }
                }
            } else {

// Otherwise, iterate through all of the keys in the object.

                for (k in value) {
                    if (Object.hasOwnProperty.call(value, k)) {
                        v = str(k, value);
                        if (v) {
                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
                        }
                    }
                }
            }

// Join all of the member texts together, separated with commas,
// and wrap them in braces.

            v = partial.length === 0 ? '{}' :
                gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
                        mind + '}' : '{' + partial.join(',') + '}';
            gap = mind;
            return v;
        }
    }

// If the JSON object does not yet have a stringify method, give it one.

    if (typeof JSON.stringify !== 'function') {
        JSON.stringify = function (value, replacer, space) {

// The stringify method takes a value and an optional replacer, and an optional
// space parameter, and returns a JSON text. The replacer can be a function
// that can replace values, or an array of strings that will select the keys.
// A default replacer method can be provided. Use of the space parameter can
// produce text that is more easily readable.

            var i;
            gap = '';
            indent = '';

// If the space parameter is a number, make an indent string containing that
// many spaces.

            if (typeof space === 'number') {
                for (i = 0; i < space; i += 1) {
                    indent += ' ';
                }

// If the space parameter is a string, it will be used as the indent string.

            } else if (typeof space === 'string') {
                indent = space;
            }

// If there is a replacer, it must be a function or an array.
// Otherwise, throw an error.

            rep = replacer;
            if (replacer && typeof replacer !== 'function' &&
                    (typeof replacer !== 'object' ||
                     typeof replacer.length !== 'number')) {
                throw new Error('JSON.stringify');
            }

// Make a fake root object containing our value under the key of ''.
// Return the result of stringifying the value.

            return str('', {'': value});
        };
    }


// If the JSON object does not yet have a parse method, give it one.

    if (typeof JSON.parse !== 'function') {
        JSON.parse = function (text, reviver) {

// The parse method takes a text and an optional reviver function, and returns
// a JavaScript value if the text is a valid JSON text.

            var j;

            function walk(holder, key) {

// The walk method is used to recursively walk the resulting structure so
// that modifications can be made.

                var k, v, value = holder[key];
                if (value && typeof value === 'object') {
                    for (k in value) {
                        if (Object.hasOwnProperty.call(value, k)) {
                            v = walk(value, k);
                            if (v !== undefined) {
                                value[k] = v;
                            } else {
                                delete value[k];
                            }
                        }
                    }
                }
                return reviver.call(holder, key, value);
            }


// Parsing happens in four stages. In the first stage, we replace certain
// Unicode characters with escape sequences. JavaScript handles many characters
// incorrectly, either silently deleting them, or treating them as line endings.

            cx.lastIndex = 0;
            if (cx.test(text)) {
                text = text.replace(cx, function (a) {
                    return '\\u' +
                        ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
                });
            }

// In the second stage, we run the text against regular expressions that look
// for non-JSON patterns. We are especially concerned with '()' and 'new'
// because they can cause invocation, and '=' because it can cause mutation.
// But just to be safe, we want to reject all unexpected forms.

// We split the second stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
// replace all simple value tokens with ']' characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or ']' or
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.

            if (/^[\],:{}\s]*$/.
test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {

// In the third stage we use the eval function to compile the text into a
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
// in JavaScript: it can begin a block or an object literal. We wrap the text
// in parens to eliminate the ambiguity.

                j = eval('(' + text + ')');

// In the optional fourth stage, we recursively walk the new structure, passing
// each name/value pair to a reviver function for possible transformation.

                return typeof reviver === 'function' ?
                    walk({'': j}, '') : j;
            }

// If the text is not JSON parseable, then a SyntaxError is thrown.

            throw new SyntaxError('JSON.parse');
        };
    }
}());
/* -*- Mode: Javascript; -*- */

/* Copyright (C) 2007-2009 beingmeta, inc.
   This file is a part of the FDJT web toolkit (www.fdjt.org)
   This file provides an abstraction layer for Ajax calls

   This program comes with absolutely NO WARRANTY, including implied
   warranties of merchantability or fitness for any particular
   purpose.

    Use, modification, and redistribution of this program is permitted
    under either the GNU General Public License (GPL) Version 2 (or
    any later version) or under the GNU Lesser General Public License
    (version 3 or later).

    These licenses may be found at www.gnu.org, particularly:
      http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
      http://www.gnu.org/licenses/lgpl-3.0-standalone.html
*/

var fdjt_ajax_id="$Id: ajax.js 192 2009-12-19 00:12:02Z haase $";
var fdjt_ajax_version=parseInt("$Revision: 192 $".slice(10,-1));

var fdjt_trace_ajax=false;

function fdjtComposeAjaxURI(base_uri,args)
{
  var uri=base_uri; var need_amp=false;
  if (base_uri[-1]==='&') need_amp=false;
  else if (base_uri.indexOf('?')>=0) need_amp=true;
  else uri=base_uri+"?";
  var i=0; while (i<args.length) {
    uri=uri+((need_amp) ? ("&") : (""))+args[i]+"="+args[i+1];
    i=i+2;}
  return uri;
}

function fdjtAjaxCall(callback,base_uri,args)
{
  var req=new XMLHttpRequest();
  req.onreadystatechange=function () {
    if ((req.readyState == 4) && (req.status == 200)) {
      callback(req);}};
  var uri=fdjtComposeAjaxURI(base_uri,arguments);
  req.open("GET",uri,true);
  req.send(null);
}

function fdjtAjaxTextCall(callback,base_uri)
{
  return fdjtAjaxCall(function(req) {
      callback(req.responseText);},
    base_uri,fdjtArguments(arguments,2));
}

function fdjtAjaxJSONCall(callback,base_uri)
{
  return fdjtAjaxCall(function(req) {
      callback(JSON.parse(req.responseText));},
    base_uri,fdjtArguments(arguments,2));
}

function fdjtAjaxXMLCall(callback,base_uri)
{
  return fdjtAjaxCall(function(req) {
      callback(req.responseXML);},
    base_uri,fdjtArguments(arguments,2));
}

function fdjtJSONPCall(uri,id,cleanup)
{
  if ((id)&&($(id))) return false;
  var script_elt=fdjtNewElement("SCRIPT");
  if (id) script_elt.id=id;
  if (cleanup) script_elt.oncleanup=cleanup;
  script_elt.language='javascript';
  script_elt.src=uri;
  document.body.appendChild(script_elt);
}

function fdjtJSONPFinish(id)
{
  var script_elt=$(id);
  if (!(script_elt)) return;
  if (script_elt.oncleanup) script_elt.oncleanup();
  fdjtDelete(script_elt);
}

/* AJAX submit */

function _fdjtAddParam(parameters,name,value)
{
  return ((parameters)?(parameters+"&"):(""))+
    name+"="+encodeURIComponent(value);
}

function fdjtFormParams(form)
{
  fdjtAutoPrompt_cleanup(form);
  var parameters=false;
  var inputs=fdjtGetChildrenByTagName(form,"INPUT");
  var i=0; while (i<inputs.length) {
    var input=inputs[i++];
    if ((!(input.disabled)) &&
	(((input.type==="radio") || (input.type==="checkbox")) ?
	 (input.checked) : (true)))
      parameters=_fdjtAddParam(parameters,input.name,input.value);}
  var textareas=fdjtGetChildrenByTagName(form,"TEXTAREA");
  i=0; while (i<textareas.length) {
    var textarea=textareas[i++];
    if (!(textarea.disabled)) {
      parameters=_fdjtAddParam(parameters,textarea.name,textarea.value);}}
  var selectboxes=fdjtGetChildrenByTagName(form,"SELECT");
  i=0; while (i<selectboxes.length) {
    var selectbox=selectboxes[i++]; var name=selectbox.name;
    var options=fdjtGetChildrenByTagName(selectbox,"OPTION");
    var j=0; while (j<options.length) {
      var option=options[j++];
      if (option.selected)
	parameters=_fdjtAddParam(parameters,name,option.value);}}
  return parameters;
}

function fdjtAjaxSubmit(form)
{
  var bridge=form.ajaxbridge||false;
  var ajax_uri=(form.ajaxuri)||fdjtCacheAttrib(form,"ajaxuri");
  if (!(ajax_uri)) return false;
  if ((bridge)&&(bridge!==window)) 
    try {
      if (fdjt_trace_ajax) fdjtLog("Bridge AJAX submit to %s for %o through %o",ajax_uri,form,bridge);
      return bridge.fdjtAjaxSubmit(form);}
    catch (ex) {
      fdjtLog("Bridge call yielded %o",ex);
      return false;}
  var callback=false;
  // Whether to do AJAX synchronously or not.
  var syncp=(form.synchronous)||fdjtCacheAttrib(form,"synchronous");
  if (form.oncallback) callback=form.oncallback;
  else if (form.getAttribute("ONCALLBACK")) {
    callback=new Function
      ("req","form",input_elt.getAttribute("ONCALLBACK"));
    form.oncallback=callback;}
  if (fdjt_trace_ajax)
    fdjtLog("Direct %s AJAX submit to %s for %o with callback %o",
	    ((syncp)?("synchronous"):("asynchronous")),
	    ajax_uri,form,callback);
  // Firefox doesn't run the callback on synchronous calls
  var success=false; var callback_run=false;
  var req=new XMLHttpRequest();
  var params=fdjtFormParams(form);
  fdjtAddClass(form,"submitting");
  if (form.method==="GET")
    req.open('GET',ajax_uri+"?"+params,(!(syncp)));
  else req.open('POST',ajax_uri,(!(syncp)));
  req.onreadystatechange=function () {
    callback_run=true;
    if (fdjt_trace_ajax)
      fdjtLog("Got callback (%d,%d) %o for %o through %o, calling %o",
	      req.readyState,req.status,req,ajax_uri,bridge,callback);
    if ((req.readyState === 4) && (req.status>=200) && (req.status<300)) {
      fdjtDropClass(form,"submitting");
      success=true;
      callback(req,form);}};
  try {
    if (form.method==="GET") req.send();
    else {
      req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
      // req.setRequestHeader("Content-length", params.length);
      // req.setRequestHeader("Connection", "close");
      req.send(params);}
    success=true;}
  catch (ex) {}
  if ((syncp) && (!(callback_run))) {
    if (fdjt_trace_ajax)
      fdjtLog("Running callback (%d,%d) %o for %o through %o, calling %o",
	      req.readyState,req.status,req,ajax_uri,bridge,callback);
    if ((req.readyState === 4) && (req.status>=200) && (req.status<300)) {
      fdjtDropClass(form,"submitting");
      success=true;
      callback(req,form);}};
  return success;
}

function fdjtJSONPSubmit(form)
{
  var jsonp_uri=(form.jsonpuri)||fdjtCacheAttrib(form,"jsonpuri");
  if (!(jsonp_uri)) return false;
  var success=false;
  var jsonid=((form.id)?("JSONP"+form.id):("FORMJSONP"));
  var params=fdjtFormParams(form);
  fdjtAddClass(form,"submitting");
  try {
    fdjtJSONPCall
      (jsonp_uri+"?"+params,jsonid,
       function(){fdjtDropClass(form,"submitting")});}
  catch (ex) {
    fdjtJSONPFinish(jsonid);
    fdjtWarn("Attempted JSONP call signalled %o",ex);
    return false;}
  return true;
}

function fdjtForm_onsubmit(evt)
{
  evt=evt||event||null;
  var form=$T(evt);
  fdjtAutoPrompt_cleanup(form);
  if (fdjtHasClass(form,"submitting")) {
    fdjtDropClass(form,"submitting");
    return;}
  if (form.fdjtlaunchfailed) return;
  form.fdjtsubmit=true;
  fdjtAddClass(form,"submitting");
  if (fdjtAjaxSubmit(form)) {
    if (evt.preventDefault) evt.preventDefault(); else evt.returnValue=false;
    return;}
  else if (fdjtJSONPSubmit(form)) {
    if (evt.preventDefault) evt.preventDefault(); else evt.returnValue=false;
    return;}
  else return;
}

/* Synchronous calls */

function fdjtAjaxGet(callback,base_uri,args)
{
  var req=new XMLHttpRequest();
  var uri=fdjtComposeAjaxURI(base_uri,args);
  req.open("GET",uri,false);
  req.send(null);
  return callback(req);
}

function fdjtAjaxGetText(base_uri)
{
  return fdjtAjaxGet(function (req) { return req.responseText; },
		     base_uri,fdjtArguments(arguments,1));
}

function fdjtAjaxGetJSON(base_uri)
{
  return fdjtAjaxGet(function (req) { return JSON.parse(req.responseText); },
		     base_uri,fdjtArguments(arguments,1));
}

function fdjtAjaxGetXML(base_uri)
{
  return fdjtAjaxGet(function (req) { return JSON.parse(req.responseXML); },
		     base_uri,fdjtArguments(arguments,1));
}

function fdjtAjaxProbe(uri)
{
  var req=new XMLHttpRequest();
  req.open("GET",uri,false);
  req.send(null);
  if ((req.readyState === 4) && (req.status >= 200) && (req.status < 300))
    return true;
  else return false;
}

/* Emacs local variables
;;;  Local variables: ***
;;;  compile-command: "make; if test -f ../makefile; then cd ..; make; fi" ***
;;;  End: ***
*/
