/******************************************************************************
 * Copyright (c) 2002,2003 Peter 'Merlin' Balsiger and Fredy 'Mythos' Dobler
 * All rights reserved
 *
 * This file is part of Dungeon Master Assistant.
 *
 * Dungeon Master Assistant is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Dungeon Master Assistant is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Dungeon Master Assistant; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *****************************************************************************/

//------------------------------------------------------------------- header

/**
 * This is a set of javascript routines for gui related functions.
 * 
 * @file          gui.js
 * 
 * @author        Peter Balsiger
 *
 */

//..........................................................................

//---------------------------------------------------------------- variables

/** The object to store everything in. */
var gui = new Object();

//------------------------------- includeJS --------------------------------

/* Make sure a specific javascript file is included in the page.
 *
 * @inName the name of the file to include
 *
 */
gui.includeJS = function(inName)
{
  var scripts = document.getElementsByTagName("script");

  // setup base url of page
  var base = document.URL.replace(/\/[^\/]+?$/, "/");

  var name = inName;

  for(; name.match(/^\.\.\//); name = name.substring(3))
    base = base.replace(/\/[^\/]+?\/$/, "/"); 

  var name = base + name;

  for(var i = 0; i < scripts.length; i++)  
    if(name == scripts[i].src)
      return;

  // not found, thus we have to add it
  var script = document.createElement("script");

  script.src  = inName;
  script.type = "text/javascript";
  
  document.getElementsByTagName("head")[0].appendChild(script);
}

//..........................................................................
//------------------------------- includeCSS -------------------------------

/* Make sure a specific css file is included in the page.
 *
 * @inName the name of the file to include
 *
 */
gui.includeCSS = function(inName)
{  
  // setup base url of page
  var base = document.URL.replace(/\/[^\/]+?$/, "/");

  var name = inName;

  for(; name.match(/^\.\.\//); name = name.substring(3))
    base = base.replace(/\/[^\/]+?\/$/, "/"); 

  var name = base + name;

  for(var i = 0; i < document.styleSheets.length; i++)
    if(name == document.styleSheets[i].href)
      return;

  // add the css to the page (at first to let it be overridable)
  var link = document.createElement("link");

  link.rel  = "STYLESHEET";
  link.href = inName;
  link.type = "text/css";
  
  var head = document.getElementsByTagName("head")[0];
  head.insertBefore(link, head.firstChild);
}

//..........................................................................

// make sure we have the utility script
//gui.includeJS("/js/utility.js");

/** The id for scrolling. */
gui.scrolling = new Object();

/** The z index of the message windows */
gui.messages = new Array();

/** The z index to use */
gui.zIndex = 10000;

/** The status messages. */
gui.statusMessages = new Array();

/** The progress bar. */
gui.progress = null;

/** The values edited on the page. */
gui.values = {};

//..........................................................................

//----------------------------- iconHighlight ------------------------------

/** Highlight the icon by roll over.
 *
 * @param inElement the element to highlight
 *
 */
gui.iconHighlight = function(inElement)
{
  if(!inElement)
    return;

  // determine if we have an extension
  if(hasExtension(inElement.src))
    inElement.src = inElement.src.replace(/(.*)(\..*?)/, "$1-highlight$2");
  else
    inElement.src = inElement.src + "-highlight";
}

//..........................................................................
//------------------------------- iconNormal -------------------------------

/** Show the icon normally.
 *
 * @param inElement the icon to show normally
 *
 */
gui.iconNormal = function(inElement)
{
  if(!inElement)
    return;

  if(hasExtension(inElement.src))
    inElement.src = inElement.src.replace(/(.*)-highlight(\..*?)/, "$1$2");
  else
    inElement.src = inElement.src.replace(/(.*)-highlight/, "$1");
}

//..........................................................................
//------------------------------- highlight --------------------------------

/** Add a temporary highlight attribute to the element.
 *
 * @param inElement the element to highlight
 *
 */
gui.highlight = function(inElement)
{
  inElement.className += " tmp-highlight";
}

//..........................................................................
//---------------------------------- css -----------------------------------

/**
 * Setup some css values before any other, thus making sure they can be
 * easily overwritten by any stylesheet in a document.
 *
 * @param inRule the complete rule (or rules) to add
 *
 */
gui.css = function(inRule)
{
  if(!document.styleSheets[0])
  {
    // no stylesheet currently in the page, thus we add one
    var style = document.createElement("style");

    style.type = "text/css";

    document.getElementsByTagName('head')[0].appendChild(style);
  }

  document.styleSheets[0].insertRule(inRule, 0);
}

//..........................................................................
//-------------------------------- selected --------------------------------

/** Add a temporary selected attribute to the element.
 *
 * @param inElement the element to mark as selected
 *
 */
gui.selected = function(inElement)
{
  inElement.className += " tmp-selected";
}

//..........................................................................
//--------------------------------- normal ---------------------------------

/** Mark the element as normal again (remove temporary classes).
 *
 * @param inElement the element to make normal
 *
 */
gui.normal = function(inElement)
{
  inElement.className = inElement.className.replace(/tmp-\S*?/, "");
}

//..........................................................................
//-------------------------------- addStyle --------------------------------

/**
  *
  * Add a style to the element given
  *
  * @param       inElement the element to add to
  * @param       inStyle   the style to add
  *
  */
gui.addStyle = function(inElement, inStyle)
{
  var element = $(inElement);  
  
  if(!element)
    return;

  if(!element.className || element.className.length == 0)
    element.className = inStyle;
  else
    if(!element.className.match("(^| )" + inStyle + "( |$)"))
      element.className += " " + inStyle;
}

//..........................................................................
//------------------------------ addAllStyle -------------------------------

/**
  *
  * Add a style to the element given
  *
  * @param       inElement the element to add to
  * @param       inStyle   the style to add
  *
  */
gui.addAllStyle = function(inElement, inStyle)
{
  if($(inElement))
    gui.addStyle($(inElement), inStyle);
  else
    for(i = 1; $(inElement + i); i++)
      gui.addStyle(inElement + i, inStyle);
}

//..........................................................................
//------------------------------- removeStyle ------------------------------

/**
  *
  * Remove a style from the element given
  *
  * @param       inElement the element to remove from
  * @param       inStyle   the style to remove
  *
  */
gui.removeStyle = function(inElement, inStyle)
{
  var element = $(inElement);

  if(element)
    element.className = 
      element.className.replace(new RegExp("\\s*\\b" + inStyle + "\\b"), "")
      .replace(/^\s+/, "");
}

//..........................................................................
//----------------------------- removeAllStyle -----------------------------

/**
  *
  * Remove a style from the element given
  *
  * @param       inElement the element to remove from
  * @param       inStyle   the style to remove
  *
  */
gui.removeAllStyle = function(inElement, inStyle)
{
  if($(inElement))
    gui.removeStyle($(inElement), inStyle);
  else
    for(i = 1; $(inElement + i); i++)
      gui.removeStyle(inElement + i, inStyle);
}

//..........................................................................
//------------------------------- toggleStyle ------------------------------

/**
  *
  * Toggle the style in the given element.
  *
  * @param       inElement the element to toggle in
  * @param       inStyle   the style to toggle
  *
  */
gui.toggleStyle = function(inElement, inStyle)
{
  if(inElement.className.match(new RegExp("\\b" + inStyle + "\\b")))
    gui.removeStyle(inElement, inStyle);
  else
    gui.addStyle(inElement, inStyle);
}

//..........................................................................
//------------------------------- filterKey --------------------------------

/**
  *
  * Filter the keys of the given event.
  *
  * @param       inEvent    the event to filter in
  * @param       inKeys     the keys that are allow or not (see inPositive)
  * @param       inPositive true if the keys given are allowed, false if they
  *                         are not allowed
  *
  * @return      true if key allowed, false if not
  *
  */
gui.filterKey = function(inEvent, inKeys, inPositive)
{
  // allow special characters
  if(inEvent.which == 0 || inEvent.which == 8 || inEvent.which == 9 
     || inEvent.which == 13 
     || (inEvent.which >= 16 && inEvent.which <= 19) 
     || (inEvent.which >= 33 && inEvent.which <= 40)
     || inEvent.which == 45 || inEvent.which == 46)
    return true;

  if(inKeys.indexOf(String.fromCharCode(inEvent.which)) >= 0)
    return inPositive;
  else
    return !inPositive;
}

//..........................................................................
//----------------------------- filterPattern ------------------------------

/**
  *
  * Filter the keys of the given event.
  *
  * @param       inElement the form element to filter
  * @param       inEvent   the event to filter in
  * @param       inPattern the pattern to filter with
  *
  * @return      true if key allowed, false if not
  *
  */
gui.filterPattern = function(inElement, inEvent, inPattern)
{
  // allow special characters
  if(inEvent.which == 0 || inEvent.keyCode == 8 || inEvent.keyCode == 9 
     || inEvent.keyCode == 13 
     || (inEvent.keyCode >= 16 && inEvent.keyCode <= 19) 
     || (inEvent.keyCode >= 33 && inEvent.keyCode <= 40)
     || inEvent.keyCode == 45 || inEvent.keyCode == 46)
    return true;

  // add the new character
  var value = inElement.value.slice(0, inElement.selectionStart) 
  + String.fromCharCode(inEvent.which) 
  + inElement.value.slice(inElement.selectionStart, -1);

  return value.match(new RegExp("^" + inPattern + "$")) != null;
}

//..........................................................................

//--------------------------------- appear ---------------------------------

/** Make a HTML element appear slowly.
 *
 * @param inElement the element to appear
 * @param inChange  the amount of change per iteration
 * @param inDelay   the delay between each iteration
 *
 */
gui.appear = function(inElement, inChange, inDelay)
{
  inElement = $(inElement);

  inElement.style.display = "";

  var opacity = 0;
  var func = function ()
  {
    if(opacity >= 100)
    {
      inElement.style.MozOpacity = "1";

      return false;
    }

    if(opacity < 10)
      inElement.style.MozOpacity = "0.0" + opacity;
    else
      inElement.style.MozOpacity = "0." + opacity;
    
    opacity += inChange;

    return true;
  };
  
  return gui.repeat(func, inDelay);
}

//..........................................................................
//---------------------------------- fade ----------------------------------

/** Make a HTML element appear slowly.
 *
 * @param inElement the element to appear
 * @param inChange  the amount of change per iteration
 * @param inDelay   the delay between each iteration
 *
 */
gui.fade = function(inElement, inChange, inDelay)
{
  inElement = $(inElement);

  var opacity = 99;
  var func = function ()
  {
    if(opacity <= 0)
    {
      inElement.style.display = "none";
      inElement.style.MozOpacity = "0";

      return false;
    }

    if(opacity < 10)
      inElement.style.MozOpacity = "0.0" + opacity;
    else
      inElement.style.MozOpacity = "0." + opacity;
    
    opacity -= inChange;

    return true;
  };
  
  return gui.repeat(func, inDelay);
}  

//..........................................................................
//--------------------------------- group ----------------------------------

/** Group something together for later expansion
 *
 * @param inElement the element to group
 * @param inName    the name of the group
 * @param inColor   the color to use as background for group name
 *                  (white if empty)
 * 
 */
gui.group = function(inElement, inName)
{
  inElement = $(inElement);

  var group = document.createElement("div");
  var text  = document.createElement("span");
  
  group.className = "-gui-group";

  text.innerHTML = inName ;

  group.onmouseover = function() 
  { this.className = "-gui-group -gui-selected"; };
  group.onmouseout  = function()  { this.className = "-gui-group"; };

  group.appendChild(text);

  // must be below group styling to have correct width
  inElement.style.display  = "none";
  inElement.style.position = "relative";

  var parent = inElement.parentNode;
  var clone  = inElement.cloneNode(false);

  clone.innerHTML = inName;  
  clone.element   = inElement;
  clone.guiGroup = group;  
  clone.id       += "-grouped";
  clone.className = "-gui-group-overview";

  clone.style.display         = "";

  clone.onmouseover = function() { this.style.backgroundColor = "#FFCCCC" };
  clone.onmouseout  = function() { this.style.backgroundColor = "white" };
  clone.onmousedown = function() { this.style.borderStyle = "inset" };
  clone.onmouseup   = function() { this.style.borderStyle = "outset" };

  clone.onclick = function()
  {
    var old = this.parentNode.scrollLeft;
    this.guiGroup.style.display = "inline";
    this.element.style.display = "";
    this.style.display = "none";
    this.parentNode.scrollLeft = old;

    // update the node if anything is necessary
    if(this.parentNode.parentNode.update)
      this.parentNode.parentNode.update();

    // fix the width if necessary
    this.guiGroup.style.width = this.element.offsetWidth + "px";
  }

  group.grouped = clone;
  group.element = inElement;
  group.onclick = function()
  {
    var old = this.parentNode.parentNode.scrollLeft;
    this.grouped.style.display = "";
    this.style.display = "none";
    this.element.style.display = "none";
    this.parentNode.parentNode.scrollLeft = old;

    // update the node if anything is necessary
    if(this.parentNode.parentNode.update)
      this.parentNode.parentNode.update();
  }

  parent.insertBefore(clone, inElement);
  inElement.appendChild(group);
}

//..........................................................................
//---------------------------------- alert ---------------------------------

/** Show an alert message.
 *
 * @param inMessage the message to appear
 *
 */
gui.alert = function(inMessage)
{
  gui.messageWindow(inMessage, "/icons/alert.png", "#FFCCCC", "red", 
                    "alert-alert");
}  

//..........................................................................
//---------------------------------- info ----------------------------------

/** Show an alert message.
 *
 * @param inMessage the message to appear
 *
 */
gui.info = function(inMessage)
{
  gui.messageWindow(inMessage, "/icons/info.png", "#CCCCFF", "blue", 
                    "alert-info");
}  

//..........................................................................
//--------------------------------- debug ----------------------------------

/** Show an debug message.
 *
 * @param inMessage the message to appear
 *
 */
gui.debug = function(inMessage)
{
  // no debugging right now
  return;
  gui.messageWindow(inMessage, "/icons/bug.png", "#CCFFCC", "green", 
                    "alert-debug");
}  

//..........................................................................
//------------------------------ messageWindow -----------------------------

/**
 * Show an alert message.
 *
 * @param inMessage     the message to appear
 * @param inImage       the name of the image to display
 * @param inColor       the background color
 * @param inBorderColor the border color
 * @param inClass       the css class name for the alert
 *
 */
gui.messageWindow = function(inMessage, inImage, inColor, inBorderColor, 
                             inClass)
{
  // create the element
  var win = document.createElement("div");

  win.className = "-gui-alert -gui-" + inClass;

  // push to stack
  gui.messages.push(win);

  // the image
  var image = document.createElement("img");
  
  image.src = inImage;
  image.style.cssFloat  = "left";
  image.style.marginTop = "35px";
  
  // set the fixed styles
  win.style.height     = "0px";
  win.style.overflow   = "hidden";
  win.style.zIndex     = gui.zIndex--;
  
  // events
  win.onclick = function()
  {
    if(this.disappear)
    {
      gui.stop(this.disappear);
      
      this.disappear = null;
    }    
    else
    {
      this.disappear = gui.repeat(gui.moveOut, 20, null, this, 5);
    }
  }

  // set the text
  win.text = document.createElement("div");
  
  win.text.innerHTML = inMessage;

  win.text.style.height    = "100px";
  win.text.style.overflow  = "auto";
  win.text.style.textAlign = "center";
  
  // append to the page
  win.appendChild(image);
  win.appendChild(win.text);
  document.body.appendChild(win);

  // check size, first reset everything to make sure we get the right size
  win.text.style.height    = "";
  win.text.style.marginTop = "0px";
  win.style.display        = "";

  if(win.text.offsetHeight < 100)
    win.text.style.marginTop = (100 - win.text.offsetHeight) / 2 + "px";
  else
    win.text.style.height    = "100px";

  // move in the window
  gui.repeat(gui.moveIn, 20, null, win, 2, 100)
  
  // remove it again
  if(gui.messages.length == 1)
    gui.messageDisappear();
}  

/** This is an internal function. */
gui.messageDisappear = function()
{
  gui.messages[0].disappear = gui.delayed(function()
    {
      var win = gui.messages.shift();

      // fade out
      gui.repeat(gui.moveOut, 20, null, win, 5);       

      if(gui.messages.length == 0)
        // reset zIndex if last
        gui.zIndex = 10000;
      else
        // if we have other messages, make the first one disappear
        gui.messageDisappear();

    }, 5000);
}

//..........................................................................
//------------------------------ checkMessages -----------------------------

/** Check and display pending messages (from cookies).
 *
 */
gui.checkMessages = function()
{
  var info = readCookie("INFO"); 

  if(info)
  {
    gui.info(info);
    
    deleteCookie("INFO", "/");
  }  
}

//..........................................................................
//--------------------------------- moveIn ---------------------------------

gui.moveIn = function(inElement, inStep, inMax)
{
  inElement = $(inElement);

  // show the it
  inElement.style.display = "";

  if(inElement.height)
    inElement.height += inStep;
  else
    inElement.height = inStep;

  // everything is done
  if(inElement.height >= inMax)
    return false;

  inElement.style.height = Math.min(inMax, inElement.height) + "px";
  
  return true;
}

//..........................................................................
//-------------------------------- moveOut ---------------------------------

gui.moveOut = function(inElement, inStep)
{
  inElement = $(inElement);

  if(inElement.height)
    inElement.height -= inStep;
  else
    inElement.height = inElement.offsetHeight;

  // everything is done
  if(inElement.height <= 0)
  {
    inElement.style.display = "none";
    inElement.parentNode.removeChild(inElement);

    return false;
  }

  inElement.style.height = Math.max(0, inElement.height) + "px";

  return true;
}

//..........................................................................
//------------------------------- pushStatus -------------------------------

/** Show the current status of what is being done.
 *
 * @param inMessage the message to display
 *
 */
gui.pushStatus = function(inMessage)
{
  gui.statusMessages.push(inMessage);

  window.status = gui.statusMessages.join(" / ");
}

//..........................................................................
//-------------------------------- popStatus -------------------------------

/** Remove the last status message from the stack
 *
 */
gui.popStatus = function()
{
  gui.statusMessages.pop();

  window.status = gui.statusMessages.join(" / ");
}

//..........................................................................
//------------------------------ createWindow ------------------------------

/**
  * Create a window with contents from a web page.
  * 
  * @param       inPath  the path to the page to show
  * @param       inClass the class used for styling the window
  *
  * @return      the create iframe element
  *
  */
gui.createWindow = function(inPath, inClass)
{
  var frame = document.createElement('iframe');

  frame.src       = inPath;
  frame.className = inClass;
  
  return frame;
}

//..........................................................................
//------------------------------- showWindow -------------------------------

/**
 * Opens an inline window with the given contents.
 *
 * @parm        inID       the id of the window to create
 * @param       inClass    the style for the window
 * @param       inTitle    the window's title
 * @param       inContents the contents of the window
 *
 * @return      the window element
 *
 */
gui.showWindow = function(inID, inClass, inTitle, inContents)
{
  var window = document.createElement("div");
  window.id = inID;

  window.className = "-gui-window -gui-" + inClass;
  window.innerHTML = 
    "<div class='-gui-window-title'>"
    + inTitle
    + "</div>"
    + "<div class='-gui-window-body'>"
    + inContents
    + "</div>";

  document.body.appendChild(window);

  return window;
}

//..........................................................................
//------------------------------ removeWindow ------------------------------

/**
 * Remove and destroy the window with the given id.
 * 
 * @param       inID the id of the window to remove
 *
 */
gui.removeWindow = function(inID)
{
  try
  {
    document.body.removeChild(document.getElementById(inID));
  } 
  catch(e)
  { 
    window.console.log(e);
  }
}

//..........................................................................

//--------------------------------- repeat ---------------------------------

/** Repeat an action until an end is reached.
 *
 * @param inAction the action to do at each step (the action returns false if
 *                 end is reached)
 * @param inRepeat the delay in miliseconds between each iteration
 * @param inHandle the handle to store the id in
 *
 */
gui.repeat = function(inAction, inRepeat, inHandle, a, b, c, d)
{ 
  // execute the action
  if(inAction(a, b, c, d) == false)
    return null;

  if(!inHandle)
    inHandle = new Object()

  inHandle.timeout = window.setTimeout(function() 
    { gui.repeat(inAction, inRepeat, inHandle, a, b, c, d); }, inRepeat);

  return inHandle;
}

//..........................................................................
//-------------------------------- delayed ---------------------------------

/** Delay an action for some time.
 *
 * @param inAction the action to do at each step (the action returns false if
 *                 end is reached)
 * @param inDelay  the delay in miliseconds to wait before doing the action
 *
 * @return the reference to use to stop the action
 *
 */
gui.delayed = function(inAction, inDelay)
{
  var handle = new Object();

  handle.timeout = window.setTimeout(inAction, inDelay);

  return handle;
}

//..........................................................................
//--------------------------------- stop -----------------------------------

/** Stop a delayed or repeated action.
 *
 * @param inReference the reference obtained by delay
 *
 */
gui.stop = function(inReference)
{
  if(!inReference)
    return;

  if(inReference.timeout)
  {
    window.clearTimeout(inReference.timeout);

    inReference.timeout = null;
  }  
  else
    window.clearTimeout(inReference);
}

//..........................................................................
//-------------------------------- loadFile --------------------------------

/** Load the given file and execute it.
 *
 * @param inID the id of the file to load
 * @param inNumber the number of the file to load
 * @param inTotal  the total number of files to load
 *
 */
gui.loadFile = function(inID, inNumber, inTotal)
{
  // show the progress counter
  if(inNumber < inTotal)
  {
    if(gui.progress == null)
    {
      gui.progress = document.createElement("div");

      gui.progress.className = "-gui-progress";

      gui.progress.innerHTML = 
        "<div class='-gui-progress-start'></div>"
        + "<div class='-gui-progress-middle'></div>"
        + "<div class='-gui-progress-end'></div>";

      document.body.appendChild(gui.progress);
    }
    else
      gui.progress.style.display = "";

    // set the size
    gui.progress.childNodes[1].style.width = 
      Math.round(inNumber / inTotal * 200) + "px";

    document.body.style.cursor = "wait";
  }

  // really load the file now
  try
  {
    var request = new XMLHttpRequest();
    
    // remove the options, if any
    var url = document.location.toString().replace(/\?.*$/, "");
    
    request.open("GET", url + "-" + inNumber, false);
    request.send(null);
    
    eval(request.responseText, 0);
  }
  catch(e)
  {
    gui.alert("Could not parse '" + document.location + "-" + inNumber + ": " 
              + e);
  }

  if(inNumber >= inTotal)
  {
    gui.progress.style.display = "none";
    document.body.style.cursor = "default";
  }
}

//..........................................................................

//--------------------------------- scroll ---------------------------------

/* Make the named element scrollable.
 *
 * @param inID the id of the element to make scrollable (can also directly be
 *             the element to make scrollable)
 *
 */
gui.scroll = function(inID, inIncrement, inSpeed)
{
  var element = $(inID);

  // element found?
  if(!element)
    return null;

  // change the style for scrolling
  element.style.overflowX  = "hidden";
  element.style.whiteSpace = "nowrap";

  // add the arrows

  // create a container with the size of the current element
  var container = document.createElement("div");

  container.className = "-gui-scroll-container";

  if(element.offsetWidth > 0)
    container.style.width  = element.offsetWidth  + "px";

  if(element.offsetHeight > 0)
    container.style.height = element.offsetHeight + "px";
  
  // compute the size of the border to correctly set the height of the
  // arrows
  var pos = element.scrollLeft;

  // left arrow
  var left = document.createElement("div");
  
  if(element.scrollLeft == 0)      
    left.className = "-gui-scroll-left -gui-disabled";
  else
    left.className = "-gui-scroll-left";

  if(element.offsetHeight > 0)
    left.style.height = element.offsetHeight + "px";
                       
  left.onmouseout = function() 
  { 
    if(element.scrollLeft > 0)
      this.className = "-gui-scroll-left";
          
    gui.stopScrolling(); 
  };
  left.onmouseover = function() 
  { 
    this.className = "-gui-scroll-left -gui-highlight";
    
    element.amount        = -inIncrement;
    element.speed         = inSpeed;
    
    gui.scrollHorizontally(element); 
  };
  left.onmousedown = function()
  { element.amount *= 4; element.speed *= 4; };
  left.onmouseup = function()
  { element.amount /= 4; element.speed /= 4; };
  
  // right arrow
  var right = document.createElement("div");
  
  if(element.scrollLeft + element.offsetWidth >= element.scrollWidth)
    right.className = "-gui-scroll-right -gui-disabled";
  else
    right.className = "-gui-scroll-right";
      
  if(element.offsetHeight > 0)
    right.style.height = element.offsetHeight + "px";
                        
  right.onmouseout      = function() 
  { 
    if(element.scrollLeft + element.offsetWidth < element.scrollWidth)
      this.className = "-gui-scroll-right";
        
    gui.stopScrolling(); 
  };
  right.onmouseover     = function() 
  { 
    this.className = "-gui-scroll-right -gui-highlight";
    
    element.amount         = +inIncrement;
    element.speed          = inSpeed;
    
    gui.scrollHorizontally(element); 
  };
  right.onmousedown = function()
  { element.amount *= 4; element.speed *= 4; }
    right.onmouseup = function()
  { element.amount /= 4; element.speed /= 4; }
  
  container.appendChild(left);
  container.appendChild(right);
  
  var parent = element.parentNode;
  
  if(parent)
  {
    parent.insertBefore(container, element);
    parent.removeChild(element);
  }

  container.appendChild(element);

  container.element    = element;
  container.leftArrow  = left;
  container.rightArrow = right;
  container.update     = function() { gui.checkScrolling(this); };
  
  element.style.position = "absolute";
  element.container      = container;
  element.update         = container.update;
  
  // make the element relative to allow the nested arrows to display properly
  element.style.position = "relative";
  
  // restore scrolling position
  element.scrollLeft = pos;

  return container;
}

//..........................................................................
//----------------------------- checkScrolling -----------------------------

/* Check if the given element needs scrolling arrows or not.
 *
 * @param inElement the element to check for
 *
 */
gui.checkScrolling = function(inElement)
{
  inElement = $(inElement);

  // adjust in case it was called with the scrolled content
  if(inElement.container)
    inElement = inElement.container;

  if(inElement.element.scrollLeft == 0)      
    inElement.leftArrow.className = "-gui-scroll-left -gui-disabled";
  else
    inElement.leftArrow.className = "-gui-scroll-left";
  
  if(inElement.element.scrollLeft + inElement.element.offsetWidth 
     >= inElement.element.scrollWidth)
    inElement.rightArrow.className = "-gui-scroll-right -gui-disabled";
  else
    inElement.rightArrow.className = "-gui-scroll-right";  

  inElement.leftArrow.style.height  = inElement.element.offsetHeight + "px";
  inElement.rightArrow.style.height = inElement.element.offsetHeight + "px";  
}

//..........................................................................
//--------------------------- scrollHorizontally ---------------------------

/* Start scrolling an element horizontally.
 *
 * @param inElement the element to scroll
 *
 */
gui.scrollHorizontally = function(inElement)
{
  // stop scrolling, if already doing it
  if(gui.scrolling.timeout)
    gui.stop(gui.scrolling);

  var func = function()
  {
    if(inElement.scrollLeft == 0 && inElement.amount > 0)
      inElement.container.leftArrow.className = "-gui-scroll-left";

    if(inElement.scrollLeft + inElement.offsetWidth >= inElement.scrollWidth 
       && inElement.amount < 0)
      inElement.container.rightArrow.className = "-gui-scroll-right";

    inElement.scrollLeft += inElement.amount;

    if(inElement.scrollLeft == 0)
      inElement.container.leftArrow.className = 
        "-gui-scroll-left -gui-disabled";

    if(inElement.scrollLeft + inElement.offsetWidth >= inElement.scrollWidth)
      inElement.container.rightArrow.className = 
        "-gui-scroll-right -gui-disabled";    
  }

  gui.repeat(func, inElement.speed, gui.scrolling);
}

//..........................................................................
//----------------------------- stopScrolling ------------------------------

/* Stop a scrolling action. 
 *
 */
gui.stopScrolling = function()
{
  gui.stop(gui.scrolling);
}

//..........................................................................
//----------------------------- initScrolling ------------------------------

/** Init the scrolling stuff (after the page is loaded)
 *
 */
gui.initScrolling = function()
{
  var elements = document.getElementsByTagName("scroll");

  for(var i = 0; i < elements.length; i++)
  {    
    // find the first non text node
    var node;
    for(node = elements[i].nextSibling; node != null; node = node.nextSibling)
      if(node.nodeType == 1)
        break;

    // no non text node found
    if(!node)
      continue;

    gui.scroll(node, elements[i].getAttribute("step"), 
               elements[i].getAttribute("delay"));
  }
}

//..........................................................................

//---------------------------------- menu ----------------------------------

/* Create an display a context menu.
 *
 * @param ... triple of menu entries followed by its associated method (if the
 *            method is null, the menu entry will be ghosted; if the menu entry
 *            is null, a delimiter line will be shown, and the method ignored),
 *            in the end followed by the css class name to use for the entry
 *
 * @return    returns the menu (html) created 
 * 
 */
gui.menu = function(/* ... */)
{
  var menu = document.createElement("ul");

  menu.className = "-gui-menu";

  for(var i = 0; i < arguments.length; i += 3)
    gui.addMenuItem(menu, arguments[i], arguments[i + 1], arguments[i + 2])

  return menu
}

//..........................................................................
//------------------------------ addMenuItem -------------------------------

/* Add a menu item to the menu given.
 *
 * @param inMenu   the menu to add to
 * @param inName   the name to print for the entry (null for delimiter)
 * @param inMethod the method to call when this entry is selected (null for
 *                 disabled entry)
 * @param inClass  the class name for this entry (or null for none)
 *
 */
gui.addMenuItem = function(inMenu, inName, inMethod, inClass)
{
  var item = document.createElement("li");

  if(!inName)
  {
    item.className = "delimiter";
    item.innerHTML = "<hr />";
  }
  else
  {
    item.innerHTML = inName;
    item.menu      = inMenu;
      
    var name = "";
    if(inClass)
      name = " " + inClass;
      
    if(inMethod)
    {
      item.className = "-gui-enabled" + name;
      
      var func = inMethod;
      item.onclick = function() 
        {
          // call the original method
          func(this);
          
          // remove the menu from the page
          this.menu.parentNode.removeChild(this.menu);
        }      
    }
    else
    {
      item.className = "-gui-disabled" + name;
      
      // ignore a click
      item.onclick = function(inEvent) { inEvent.stopPropagation(); };
    }
  }
  
  inMenu.appendChild(item);  

  return item;
}

//..........................................................................
//------------------------------ toggleSubNav ------------------------------

/**
  *
  * Toggle the subnavigation of a navigation element.
  *
  * @param       inEvent
  *
  */
gui.toggleSubNav = function(inEvent)
{
  gui.toggleStyle(this, 'open');

  inEvent.preventDefault();
}

//..........................................................................
//------------------------------- addAction --------------------------------

/**
  * Add an action to the page
  *
  * @param       inLabel   the label for the action
  * @param       inAction  the function to execute on click
  * @param       inHover   the text to show when hovering
  * @param       inID      the id to use (optional)
  * @param       inClass   the css class to use (optional)
  *
  */
gui.addAction = function(inLabel, inAction, inHover, inID, inClass)
{
  var actions = $('actions');

  var action = document.createElement('div');
  var hover = document.createElement('span');
  
  action.className = 'mini-dialog button' + (inClass ? " " + inClass : "");
  action.onclick   = inAction; 
  action.__hover   = hover;

  if(inID)
    action.id = inID;

  action.innerHTML = "<div class='mini-dialog-pad'>"
  + "<div class='mini-dialog-content'>" + inLabel + "<div>"
  + "<div class='dialog'>"
  + "<div class='dialog-hd-l'>"
  + "<div class='dialog-hd-r'></div>"
  + "</div>"
  + "<div class='dialog-bd-l'>"
  + "<div class='dialog-bd-r'></div>"
  + "</div>"
  + "<div class='dialog-ft-l'>"
  + "<div class='dialog-ft'r'></div>"
  + "</div>"
  + "</div>"
  + "</div>";

  hover.className     = 'window';
  hover.innerHTML     = inHover;

  action.onmouseover = function() 
  { this.__hover.style.visibility = "visible"; };
  action.onmouseout  = function() 
  { this.__hover.style.visibility = "hidden"; };

  actions.appendChild(action);
  actions.appendChild(hover);
};

//..........................................................................
//-------------------------------- tooltip ---------------------------------

/**
 * Add a tooltip to the given element. 
 * 
 * @param       inElement the element to add to
 * @param       inText    the text/html to appear in the tooltip
 *
 */
gui.tooltip = function(inElement, inText)
{
  inElement.__tooltip = document.createElement("span");

  inElement.__tooltip.className = "window";
  inElement.__tooltip.innerHTML = inText;

  inElement.appendChild(inElement.__tooltip);

  inElement.onmouseover = function() 
  { this.__tooltip.style.visibility = "visible"; }
  inElement.onmouseout = function() 
  { this.__tooltip.style.visibility = "hidden"; }
}

//..........................................................................

//--------------------------------- reload ---------------------------------

/** Reload the current page. 
  *
  * @param inPage an optional page to use instead of the default one; this
  *               is only the page, without the path
  * 
  */
gui.reload = function(inPage)
{
  var destination = document.location.pathname;

  if(document.location.hash)
    destination = document.location.hash.substring(1);

  if(inPage)
    destination = destination.replace(/^(.*\/).*?$/, "$1" + inPage);

  document.location.href = destination;
}

//..........................................................................
//---------------------------------- edit ----------------------------------

/**
 * Execute edit on the contained item.
 *
 * @param inElement    the HTML to edit
 *
 */
gui.edit = function(inElements)
{
  for(var i = 0, element; element = inElements[i]; i++)
    gui._edit(element, 98 / inElements.length);
}

gui._edit = function(inElement, inWidth)
{
  var id       = inElement.getAttribute("id");
  var type     = inElement.getAttribute("type");
  var value    = inElement.getAttribute("value");
  var key      = inElement.getAttribute("key");
  var script   = inElement.getAttribute("script");
  var values   = inElement.getAttribute("values");

  gui.values[key] = "$undefined$";

  if(script)
    eval(script);

  var unparsed = inElement.__unparsed || "";

  // remove the double click event
  delete inElement.ondblclick;
  delete inElement.oncontextmenu;

  // we lose the focus if it is in a field that will be hidden
  document.body.focus();

  // create the field
  var edit = gui.Editable.create(id, key, type, value, values, unparsed);

  // add the unedit button
  var unedit = document.createElement("img");
   
  unedit.src         = "/icons/cancel.png";
  unedit.alt         = "cancel editing";
  unedit.title       = "Cancel Editing";
  unedit.className   = "unedit";

  var undefine = document.createElement("img");
  
  undefine.src       = "/icons/cancel.png";
  undefine.src       = "/icons/undefine.png";
  undefine.alt       = "undefine element";
  undefine.title     = "Undefine Element";
  undefine.className = "undefine";
  undefine._edit     = edit;

  var parent;

  // determine the element to attach to
  if(inside(inElement, "TD", null, 5))
  {
    parent = inElement.parentNode;
    inElement.style.display = "none";
  }
  else
    if(inElement.firstChild && inElement.firstChild.firstChild 
       && inElement.firstChild.firstChild.className == 'table icon')
    {
      // add it to an icon
      if(inElement.firstChild.firstChild && inElement.firstChild.firstChild)
        parent = inElement.firstChild.firstChild.firstChild.firstChild;
      else    
        parent = inElement.nextSibling;
    }
    else
    {
      // everything else we just format as a sibling
      parent = inElement;
      
      inElement.firstChild.style.display = "none";
    }

  if(edit.type != "list" && edit.type != "multiple") 
    edit._field.style.width = inWidth + "%";

  parent.appendChild(undefine);
  parent.appendChild(unedit);
  parent.appendChild(edit._element);

  unedit.onclick     = function() { gui.unedit(edit, unedit, undefine, 
                                               inElement); };
  unedit.onmouseout  = function() { gui.iconNormal(this); };
  unedit.onmouseover = function() { gui.iconHighlight(this); };

  undefine.onclick     = function() { gui.undefine(edit, unedit, undefine, 
                                                    inElement); };
  undefine.onmouseout  = function() { gui.iconNormal(this); };
  undefine.onmouseover = function() { gui.iconHighlight(this); };

  // store the edit field
  inElement.__edit = edit;

  // set the focus to the element
  edit.focus(); 

  // make sure the save button is visible
  document.getElementById("save").style.display = "inline";

  // prevent accidental moving away from the page
  window.onbeforeunload = function()
  {
    return "You will lose all changes you made so far.\n"
    + "Use the save button first to save all your changes";
  }
}

//..........................................................................
//--------------------------------- unedit ---------------------------------

/**
  * Unedit the given element.
  *
  * @param       inEdit     the edit element
  * @param       inCancel   the cancel button
  * @param       inUndefine the undefine button
  * @param       inOld      the original element displayed
  *
  */
gui.unedit = function(inEdit, inCancel, inUndefine, inOld)
{
  // remove the cancel button
  inCancel.parentNode.removeChild(inCancel);

  // remove the undefine button
  inUndefine.parentNode.removeChild(inUndefine);

  // remove the edit element
  inEdit._element.parentNode.removeChild(inEdit._element);

  // make sure the original element is shown again
  if(inOld)
  {
    inOld.style.display = "";

    inOld.parentNode.oncontextmenu = 
      function() { gui.edit(this.__edit); return false; };
    inOld.parentNode.ondblclick = 
      function() { gui.edit(this.__edit); }; 
  }
}

//..........................................................................
//-------------------------------- undefine --------------------------------

/**
  * Undefine the given element.
  *
  * @param       inEdit    the edit element to undefine
  * @param       inCancel  the cancel button
  * @param       inCancel  the undefine button
  * @param       inOld     the original element displayed
  *
  */
gui.undefine = function(inEdit, inCancel, inUndefine, inOld)
{
  // remove the cancel button
  inCancel.parentNode.removeChild(inCancel);

  // remove the undefine button
  inUndefine.parentNode.removeChild(inUndefine);

  // undefine the value
  inEdit.undefine()
}

//..........................................................................

//-------------------------------- unparsed --------------------------------

/** Show that some part of an input field could not be parsed.
 *
 * @param  inKey  the name of the key that could not be parsed
 * @param  inText the text that could not be parsed
 * 
 */
gui.unparsed = function(inKey, inText)
{
  // find the field that could not be parsed
  var elements = document.getElementsByTagName("dma.editable");

  for(var i = 0, element; element = elements[i]; i++)
    if(element.getAttribute("key") == inKey)
    {
      element.__unparsed = inText;
      element.innerHTML += "<span class='error'>" + inText + "</span>";
      element.style.display = "";
      element.parentNode.removeChild(element.__edit._element);

      break;
    }
}

//..........................................................................
//------------------------------ makeEditable ------------------------------

/**
 * Make all dma.editable entries editable.
 *
 */
gui.makeEditable = function()
{
  var elements = document.getElementsByTagName("dma.editable");

  var targets = [];

  for(var i = 0, element; element = elements[i]; i++)
  {
    var target = gui._locateTarget(element);

    if(!target.__edit)
    {
      if(!element.getAttribute("script"))
        targets.push(element);
      
      target.__edit = [];
    }

    target.__edit.push(element);

    target.oncontextmenu = function() 
      { 
        gui.edit(this.__edit); 
        this.oncontextmenu = undefined; 
        this.ondblclick = undefined; 

        return false; 
      }
    target.ondblclick = function() 
      { 
        gui.edit(this.__edit); 
        this.oncontextmenu = undefined; 
        this.ondblclick = undefined; 
      };     
  }

  if(location.search == "?create")
  {
    gui.editAll();

    document.getElementsByTagName("input")[1].focus();
    window.scroll(0, 0);
  }
};

//..........................................................................
//--------------------------- gui._locateTarget ----------------------------

/**
 * Locate the target for editing.
 *
 * @param       inElement the element to look for its target
 *
 * @return      the target for teh element
 *
 */
gui._locateTarget = function(inElement)
{
  if(inElement.parentNode.tagName == "TD" 
     || inElement.parentNode.tagName == "H1")
    return inElement.parentNode;

  return inElement;
};

//..........................................................................
//-------------------------------- editAll ---------------------------------

/** 
 * Edit all values on the page.
 */
gui.editAll = function()
{
  var elements = document.getElementsByTagName("dma.editable");

  var targets = new Array();
  for(var i = 0, element; element = elements[i]; i++)
  {
    var target = gui._locateTarget(element);

    if(!targets.contains(target))
    {
      gui.edit(target.__edit);    
      targets.push(target);
    }
  }
  
  document.getElementsByTagName("INPUT")[0].focus();
}

//..........................................................................

//------------------------------ mouseCoords -------------------------------

/**
 * Get the coordinates of the mouse.
 *
 * @param       the event to get the coordinates for 
 * 
 * @return      x and y coordinates of the mouse as { x: <x>, y: <y> }
 *
 */
gui.mouseCoords = function(inEvent)
{
  if(inEvent.pageX || inEvent.pageY)
    return { x: inEvent.pageX, y: inEvent.pageY };

  return { x: inEvent.clientX + document.body.scrollLeft 
              - document.body.offsetLeft, 
           y: inEvent.clientY + document.body.scrollTop  
              - document.body.offsetTop
      };
};

//..........................................................................
//-------------------------------- position --------------------------------

/**
 * Get the position of the element.
 * 
 * @param       inElement the element to get the position for
 * @param       inAbsolute if given will compute the absolute position on the
 *                         page, otherwise is the relative position in the
 *                         next relatively positioned parent
 *
 * @return      the position as {x, y} 
 *
 */
gui.position = function(inElement, inAbsolute)
{
  if(inElement == null)
    return { x: -1, y: -1 };

  var left = 0;
  var top  = 0;

  if(inAbsolute)
  {
    if(inElement == document.body)
      return { x: 0, y: 0 };

    var box = inElement.getBoundingClientRect();

    left = box.left + document.documentElement.scrollLeft;
    top = box.top + document.documentElement.scrollTop;
  }
  else
  {
    while(inElement.offsetParent)
    {
      left      += inElement.offsetLeft;
      top       += inElement.offsetTop;
      inElement  = inElement.offsetParent;
    } 

    left += inElement.offsetLeft;
    top  += inElement.offsetTop;
  }

  return { x: left, y: top };
};

//..........................................................................
//--------------------------------- isOver ---------------------------------

/**
 * Check if the given position is over the given element.
 *
 * @param inElement  the element to check over
 * @param inPosition the position to check with (as {x: x, y: y})
 * @param inFixed     true if the element if positioned fixed
 *
 * @return           true if over, false if not
 *
 */
gui.isOver = function(inElement, inPosition, inFixed)
{
  var pos = gui.position(inElement, inFixed);

  var width  = parseInt(inElement.offsetWidth);
  var height = parseInt(inElement.offsetHeight);

  return ((inPosition.x > pos.x) && (inPosition.x < (pos.x + width))  
          && (inPosition.y > pos.y) && (inPosition.y < (pos.y + height)));
};

//..........................................................................

//----- Editable -----------------------------------------------------------

/**
  * The base object for editable values.
  *
  * @param inKey     the id of the entry edited
  * @param inKey     the key uniquely defining the value
  * @param inType    the type of the editable
  * @param inValue   the initial value
  * @param inLeader  the text to put in front of the value
  * @param inLabel   the label for the field
  *
  */
gui.Editable = function(inID, inKey, inType, inValue, inLabel)
{
  this.id       = inID,
  this.key      = inKey;
  this.type     = inType;
  this.label    = inLabel;
  this._value   = (inValue == "$undefined$" ? "" : inValue);
  this._defined = inValue != "$undefined$";

  this._element   = this._createElement();
  this._unlabeled = this._element;
  
  // add the label
  if(inLabel)
  {
    var container = document.createElement("span");
  
    container.style.position = "relative";

    this._label = document.createElement("span");
    this._label.className = "label";
    this._label.innerHTML = inLabel;
    
    container.appendChild(this._element);
    container.appendChild(this._label);

    this._element = container;
  }

  this._element.__editable = this;

  if(this.key)
    gui.Editable.all.push(this);
};

gui.Editable.prototype.getValue = function()
{
  return this._getValue();
};

gui.Editable.prototype.isDefined = function()
{
  return this._defined;
}

gui.Editable.prototype.undefine = function()
{
  this._defined = false;
  this._value   = "";

  this._undefine();
}

gui.Editable.prototype.doUndefine = function()
{
  // add something here in derivations
}

gui.Editable.all = [];

gui.Editable.create = function(inID, inKey, inType, inValue, inValues, 
                               inUnparsed, inLabel)
{
  var type       = inType;
  var subtype    = null;
  var repeat     = null;

  var types      = inType.match(/^((?:\n|.)+?)\#((?:\n|.)+)$/);

  if(types)
  {
    type    = types[1];    
    subtype = types[2];
  }
  
  // extract type options, if any
  types = type.match(/(.*?)\(((?:\n|.)*)\)(.*)/);
 
  var options = null;
  if(types)
  {
    type    = types[1] + types[3];
    options = types[2];
  }

  types = type.match(/(.*)\[(.*)\]/);

  var label = null;
  if(types)
  {
    type  = types[1];
    label = types[2];
  }

  switch(type)
  {
    case "string":
      return new gui.EditableString(inID, inKey, type, inValue, label);

    case "text":
      return new gui.EditableParagraph(inID, inKey, type, inValue, label);

    case "name":
    return new gui.EditableText(inID, inKey, type, inValue, label);

    case "suggest":

      if(!options)
      {
        gui.alert("could not find suggestions in type '" + type + "'");

        return new gui.EditableText(inID, inKey, type, inValue, label);
      }

      return new gui.EditableSuggest(inID, inKey, type, inValue, options, 
                                     label);

    case "suggeststring":

      if(!options)
      {
        alert("could not find suggestions in type '" + type + "'");

        return new gui.EditableString(inID, inKey, type, inValue, label);
      }

      return new gui.EditableSuggestString(inID, inKey, type, inValue, 
                                           options, label);

    case "selection":
      return new gui.EditableSelection(inID, inKey, type, inValue, 
                                       inValues ? inValues.split(/\|\|/) : [], 
                                       label);

    case "dynselection":
      return new gui.EditableDynSelection(inID, inKey, type, inValue, options, 
                                          label);

    case "dynstringselection":
      return new gui.EditableDynStringSelection(inID, inKey, type, inValue, 
                                                options, label);

    case "date":
    {
      if(!inValue || inValue == "")
      {
        var now = new Date();
        inValue = MONTHS[now.getMonth()] + " " + now.getFullYear();
      }

      return new gui.EditableSelection(inID, inKey, type, inValue, allDates(), 
                                       label);
    }

    case "number":
    
      if(options)        
      {
        var limits = options.split(/::/);

        var start = parseInt(limits[0]);        
        var end   = limits[1] ? parseInt(limits[1]) : null;

        return new gui.EditableFiltered(inID, inKey, type, inValue, 
                                        "[+-]?\\d*", function()
        {
          var valid = true;
          var value = parseInt(this.value);

          if(value >= start && (end == null || value <= end))
            gui.removeStyle(this, "invalid");
          else
            gui.addStyle(this, "invalid");

        }, label);
                                        
      }
      else
        return new gui.EditableFiltered(inID, inKey, type, inValue, 
                                        "[+-]?\\d*", "[+-]?\\d+", label); 
      
    case "ranges":
      return new gui.EditableFiltered(inID, inKey, type, inValue, 
                                        "\\d*-?\\d*(\\\\\\d*-?\\d*)*", 
                                        "\\d+(-\\d+)?(\\\\\\d+(-\\d+)?)*", 
                                        label); 
    
    case "percent":
      return new gui.EditableFiltered(inID, inKey, type, inValue, 
                                      "[+-]?\\d*%?", "[+-]?\\d+%", label);

    case "price":
      return new gui.EditableFiltered(inID, inKey, type, inValue, 
                                      "[\\$â¬â¡â¢â£â£â¦â§â¨â©âªâ«â­â®â¯]\\s*\\d*\\.?\\d*",
                                      "[\\$â¬â¡â¢â£â£â¦â§â¨â©âªâ«â­â®â¯]\\s*\\d+(\\.\\d+)?",
                                      label);

    case "isbn":
      return new gui.EditableFiltered(inID, inKey, type, inValue,
                                      "\\d*-?\\d*-?\\d*-?\\d*-?\\d*",
                                      function()
      {
        // check if isbn is valid
        var parts = this.value.match(/^(\d+-\d+-\d+)-([09-xX])$/)
          if(parts && this.value.length == 13)
          {
            var numbers = parts[1].replace(/-/g, "");
            var check   = parts[2];
            
            var sum = 0;
            for(var i = 10, j = 0; i > 1; i--, j++)
              sum += numbers.charAt(j) * i;
            
            if(check == "" + (11 - (sum % 11)) % 11)
              gui.removeStyle(this, "invalid");
            else
              gui.addStyle(this, "invalid");
          }
          else
          {
            parts = this.value.match(/^(\d+-\d+-\d+-\d+)-(\d)$/)
              
              if(parts && this.value.length == 17)
              {
                var numbers = parts[1].replace(/-/g, "");
                var check   = parts[2];
                
                var sum = 0;
                for(var i = 0; i < 12; i++)
                  sum += numbers.charAt(i) * (i % 2 == 0 ? 1 : 3);
                
                if(check == "" + (10 - (sum % 10)) % 10)
                  gui.removeStyle(this, "invalid");
                else
                  gui.addStyle(this, "invalid");         
              }
              else
                gui.addStyle(this, "invalid");  
          }
      }, label);

    case "list":
    {
      var delim  = options.trim();
      var values = inValue.split(delim);

      // fix strings that were split in the middle
      for(var i = 0; values.length > 1 && i < values.length - 1; i++)
        if(values[i].replace(/[^\"]+/g, "").length % 2)
        {
          values[i] += values[i + 1];
          values.splice(i + 1, 1);
          i--; // do this again
        }

      // trim values
      for(var i = 0; i < values.length; i++)
        values[i] = values[i].trim();

      // ignore lists with only a single, empty element
      if(values.length == 1 && values[0].length == 0)
        values = [];

      return new gui.EditableList(inID, inKey, type, values,
                                  subtype, options, inValues);
    }

    case "multiple":
      return new gui.EditableMultiple(inID, inKey, type, inValue.split(/::/),
                                      subtype.split(/@/), 
                                      options ? options.split(/::/) : [], 
                                      inValues);

    default: 
      alert("Internal Error: Unknown edit type '" + type + "' encountered "
            + " [type: " + type + ", label: " + label + ", options: " 
            + options + ", subtype: " + subtype + "]");


      return null;
  }
}

//..........................................................................
//----- EditableField ------------------------------------------------------

/**
  * The base object for editable values with a single field.
  *
  * @param inKey     the key uniquely defining the value
  * @param inType    the type of the editable
  * @param inValue   the initial value
  * @param inLabel   the label to set
  *
  */
gui.EditableField = function(inID, inKey, inType, inValue, inLabel)
{
  gui.Editable.call(this, inID, inKey, inType, inValue, inLabel);
  
  if(!this._field)
    this._field = this._unlabeled;
  
  this._field.className  = "edit " + this.type;  
  this._field.value      = this._value;
  this._field.name       = this.key;
  this._field.id         = "field-" + this.key;
  this._field.__editable = this;
  this._field.onkeypress = function() { this.__editable._defined = true; 
                                        gui.removeStyle(this, "undefined"); };
};

extend(gui.EditableField, gui.Editable);

gui.EditableField.prototype._undefine = function()
{
  this._field.value = "";
  gui.addStyle(this._field, "undefined");
};

/**
  * Get the value entered for this editable.
  *
  * @return A string with the value for this editable.
  *
  */
gui.EditableField.prototype._getValue = function()
{
  return this._field.value;
};

/**
  * Set the width of the field.
  *
  * @param inWidth the new width to set
  *
  */
gui.EditableField.prototype.setWidth = function(inWidth)
{
  this._field.style.width = inWidth + "%";
};

/**
 * Set the focus to this field.
 *
 */
gui.EditableField.prototype.focus = function()
{
  this._field.focus();
};

//..........................................................................
//----- EditableText -------------------------------------------------------

/**
  * An object representing an editable field.
  *
  * @param inKey     the key uniquely defining the value
  * @param inType    the type of the editable
  * @param inValue   the initial
  * @param inLabel   the field's label
  *
  */
gui.EditableText = function(inID, inKey, inType, inValue, inLabel)
{
  gui.EditableField.call(this, inID, inKey, inType, inValue.removeNewlines(), 
                         inLabel);
};

extend(gui.EditableText, gui.EditableField);

/**
  * Create the element associated with this editable.
  *
  * @return the html element created
  */
gui.EditableText.prototype._createElement = function()
{
  return document.createElement("input");
};

//..........................................................................
//----- EditableString -----------------------------------------------------

/**
  * An object representing an editable field containing a string (with ").
  *
  * @param inKey     the key uniquely defining the value
  * @param inType    the type of the editable
  * @param inValue   the initial value
  * @param inLabel   the field's label
  *
  */
gui.EditableString = function(inID, inKey, inType, inValue, inLabel)
{
  gui.EditableText.call(this, inID, inKey, inType, 
                        inValue.replace(/^\s*\"([\s\S\n]*)\"\s*$/, "$1"), 
                        inLabel);
}

extend(gui.EditableString, gui.EditableText);

gui.EditableString.prototype._getValue = function()
{
  return "\"" + this._field.value + "\"";
};

//..........................................................................
//----- EditableParagraph --------------------------------------------------

/**
  * An object representing an editable field containing a text paragraph.
  *
  * @param inKey     the key uniquely defining the value
  * @param inType    the type of the editable
  * @param inValue   the initial value
  * @param inLabel   the field's label
  *
  */
gui.EditableParagraph = function(inID, inKey, inType, inValue, inLabel)
{
  gui.EditableField.call(this, inID, inKey, inType, 
                         inValue.replace(/^\s*\"([\s\S\n]*)\"\s*$/, "$1")
                         .replace(/\s*[\r\n]\s*[\r\n]\s*/g, "\x01")
                         .replace(/\s*[\r\n]\s*/g, " ")
                         .replace(/\x01/g, "\n\n"), "\"", "\"", inLabel);
}

extend(gui.EditableParagraph, gui.EditableString);

/**
  * Create the element associated with this editable.
  *
  * @return the html element created
  */
gui.EditableParagraph.prototype._createElement = function()
{
  var element = document.createElement("textarea");

  element.rows = 5;

  return element;
};

//..........................................................................
//----- EditableSelection --------------------------------------------------

/**
  * An object representing an editable selection field.
  *
  * @param inKey      the key uniquely defining the value
  * @param inType     the type of the editable
  * @param inValue    the initial value  
  * @param inSelecton an array with all the selectable values (each value can
  *                   be given as x::y, where x will be displayed, but y will
  *                   be stored)
  * @param inLabel   the field's label
  *
  */
gui.EditableSelection = function(inID, inKey, inType, inValue, inSelections, 
                                 inLabel)
{  
  this._selections = inSelections;
  gui.EditableText.call(this, inID, inKey, inType, inValue, inLabel);

  this._defined = true;
}

extend(gui.EditableSelection, gui.EditableText);

/**
  * Create the element associated with this editable.
  *
  * @return the html element created
  */
gui.EditableSelection.prototype._createElement = function()
{
  var element = document.createElement("select");

  if(this._selections)
    for(var i = 0; i < this._selections.length; i++)
    {
      var option = document.createElement("option");

      var parts = this._selections[i].split("::");

      var text  = parts[0];
      var value = parts[0];
    
      if(parts.length > 1)
        value = parts[1];

      option.innerHTML = text;
      option.value     = value;
      
      element .appendChild(option);
      
      if(value == this._value)
        element .selectedIndex = i;
    }

  this._field = element;
  return element;
};

//..........................................................................
//----- EditableDynSelection -----------------------------------------------

/**
  * An object representing an editable selection field with dynamic values.
  *
  * @param inKey      the key uniquely defining the value
  * @param inType     the type of the editable
  * @param inValue    the initial value  
  * @param inSelecton the name where to get the selections from
  * @param inLabel   the field's label
  *
  */
gui.EditableDynSelection = function(inID, inKey, inType, inValue, inSelections, 
                                    inLabel)
{  
  // replace suggestion parameters, if any
  var suggestion = inSelections.replace(/(\?|&)(\w+)=(.+?)(?=&|$)/g, 
                                        function(_, inLeader, inKey, inMatch)
    {
      // find the field containing the value
      var field = $("field-" + inKey);
      
      if(field)
        return inLeader + inKey + "=" + escape(field.value);

      return inLeader + inKey + "=" + inMatch;
    });

  var values;
  // get the selections from the server
  if(gui.suggestions[suggestion])
    values = gui.suggestions[suggestion];
  else
  {
    values = eval(ajax("/ajax/" + suggestion, null));
    gui.suggestions[suggestion] = values;
  }

  gui.EditableSelection.call(this, inID, inKey, inType, inValue, values, 
                             inLabel);
}

extend(gui.EditableDynSelection, gui.EditableSelection);

//..........................................................................
//----- EditableDynStringSelection -----------------------------------------

/**
  * An object representing an editable selection field with dynamic values.
  *
  * @param inKey      the key uniquely defining the value
  * @param inType     the type of the editable
  * @param inValue    the initial value  
  * @param inSelecton the name where to get the selections from
  * @param inLabel   the field's label
  *
  */
gui.EditableDynStringSelection = function(inID, inKey, inType, inValue, 
                                          inSelections, inLabel)
{  
  gui.EditableDynSelection.call(this, inID, inKey, inType, 
                                inValue.replace(/^\s*\"([\s\S\n]*)\"\s*$/, 
                                                "$1"), inSelections, inLabel);

  
}

extend(gui.EditableDynStringSelection, gui.EditableDynSelection);

gui.EditableDynStringSelection.prototype._getValue = 
  gui.EditableString.prototype._getValue;

//..........................................................................
//----- EditableFiltered ---------------------------------------------------

/**
  * An object representing an editable field containing a filtered string.
  *
  * @param inKey       the key uniquely defining the value
  * @param inType      the type of the editable
  * @param inValue     the initial value
  * @param inFilter    the pattern to filter keystrokes
  * @param inValid     the pattern to denote if the value is valid; instead of
  *                    a pattern this can be an arbitrary function to check for
  *                    validity and react accordingly.
  * @param inLabel     the field's label
  * @param inCondition the condition that must be true for the value, if any
  *
  */
gui.EditableFiltered = function(inID, inKey, inType, inValue, inFilter, 
                                inValid, inLabel)
{
  this.filter    = inFilter;
  this.valid     = inValid;
  gui.EditableText.call(this, inID, inKey, inType, inValue, inLabel);
}

extend(gui.EditableFiltered, gui.EditableText);

/**
  * Create the element associated with this editable.
  *
  * @return the html element created
  */
gui.EditableFiltered.prototype._createElement = function()
{
  var element = this._super._createElement();
  
  var filter = this.filter;
  element.onkeypress = function(inEvent)
  { return gui.filterPattern(this, inEvent, filter); }
  
  if(typeof(this.valid) == "function")
    element.onkeyup = this.valid;
  else
    element.onkeyup = function(inValid)
    { 
      if(this.value.match(new RegExp("^" + inValid + "$")))
        gui.removeStyle(this, "invalid");
      else
        gui.addStyle(this, "invalid");
    }.bind(element, this.valid);

  return element;
};

//..........................................................................
//----- EditableSuggest ----------------------------------------------------

/**
  * An object representing an editable field containing a text with a suggest.
  *
  * @param inKey        the key uniquely defining the value
  * @param inType       the type of the editable
  * @param inValue      the initial value
  * @param inSuggestion the name of the suggestions to use
  * @param inLabel      the field's label
  *
  */
gui.EditableSuggest = function(inID, inKey, inType, inValue, inSuggestion, 
                               inLabel)
{
  this._suggestion = inSuggestion;
  gui.EditableText.call(this, inID, inKey, inType, inValue, inLabel);
}

extend(gui.EditableSuggest, gui.EditableText);

/**
  * Create the element associated with this editable.
  *
  * @return the html element created
  */
gui.EditableSuggest.prototype._createElement = function()
{
  this._field = document.createElement("input");

  var container = document.createElement("span");
  
  container.style.position = "relative";
  container.__field        = this._field;

  // not yet there, thus we have to load it
  var loading = document.createElement("img");
  
  loading.src       = "/icons/loading.gif";
  loading.alt       = "Loading " + this._suggestion + " suggestions...";
  loading.title     = "Loading " + this._suggestion + " suggestions...";
  loading.className = "loading";

  container.appendChild(loading);

  if(!gui.suggestions[this._suggestion])
    gui.suggestions[this._suggestion] = new gui.Suggestion(this._suggestion);
  
  var suggest = gui.suggestions[this._suggestion];

  suggest.request(loading);

  var box = document.createElement("ul");

  box.className = "suggest-box";

  this._field.onkeyup      = suggest.suggest.bind(suggest, this._field, box);
  this._field.__suggestBox = box
  this._field.onblur       = function(inEvent) 
  { this.__suggestBox.style.display = "none"; };

  container.appendChild(box);
  container.appendChild(this._field);

  return container;
};

//..........................................................................
//----- EditableSuggestString ----------------------------------------------

/**
  * An object representing an editable field containing a string with a
  * suggest.
  *
  * @param inKey        the key uniquely defining the value
  * @param inType       the type of the editable
  * @param inValue      the initial value
  * @param inSuggestion the name of the suggestions to use
  * @param inLabel      the field's label
  *
  */
gui.EditableSuggestString = function(inID, inKey, inType, inValue, 
                                     inSuggestion, inLabel)
{ 
  this._suggestion = inSuggestion;
  gui.EditableString.call(this, inID, inKey, inType, inValue, inLabel);
}

// we want to have a string as base value, thus we have to extend from both
extend(gui.EditableSuggestString, gui.EditableSuggest);
extend(gui.EditableSuggestString, gui.EditableString);

// the getValue method is not taken from string, but from suggest...
gui.EditableSuggestString.prototype._getValue = 
  gui.EditableString.prototype._getValue;

//..........................................................................
//----- EditableList -------------------------------------------------------

/**
  * The editable for lists of values.
  *
  * @param inKey       the key uniquely defining the value
  * @param inType      the type of the editable
  * @param inValue     the initial value
  * @param inSubtype   the type of the subvalues
  * @param inDelimiter the delimiter between list items
  *
  */
gui.EditableList = function(inID, inKey, inType, inValues, inSubtype, 
                            inDelimiter, inSubValues)
{
  this.items     = [];
  this.delimiter = inDelimiter;
  this.subtype   = inSubtype;
  this.subvalues = inSubValues;

  gui.Editable.call(this, inID, inKey, inType, inValues);
};

extend(gui.EditableList, gui.Editable);

/**
  * Create the element associated with this editable.
  *
  * @return the html element created
  */
gui.EditableList.prototype._createElement = function()
{
  var element = document.createElement("div");

  if(this._value.length > 0)
    for(var i = 0; i < this._value.length; i++)
      element.appendChild(this._createLine(this._value[i], 
                                           this.subtype)._element);
  else
    element.appendChild(this._createLine("", this.subtype)._element);

  return element;
};

/**
  * Create a single line in the list (with images).
  *
  * @param  inValue    the value to set to
  * @param  inType     the type of the sub editable
  * @param  inPrevious the editable to add after, if any
  *
  * @return the sub editable created
  * 
  */
gui.EditableList.prototype._createLine = function(inValue, inType, inPrevious)
{
  var line = document.createElement("div");

  var item = gui.Editable.create(this.id, null, inType, inValue, 
                                 this.subvalues, null);
    
  if(inPrevious)
  {
    var i;
    for(i = 0; i < this.items.length; i++)
      if(this.items[i] == inPrevious)
      {
        this.items.splice(i + 1, 0, item);

        break;
      }

    // not found, add it to the end
    if(i >= this.items.length)
      this.items.push(item);
  }
  else
    this.items.push(item);

  line.__field = item._field;
  line.appendChild(item._element);

  item.setWidth(98);
  
  // add the buttons
  var remove = document.createElement("img");

  remove.src               = "/icons/remove.png";
  remove.alt               = "Remove";
  remove.className         = "remove";
  remove.onmouseout        = "gui.iconNormal(this);";
  remove.onmouseover       = "gui.iconHighlight(this);";
  remove.__element         = line;
  remove.__list            = this;
  remove.__editable        = item;
  remove.onclick           = function()
  {
    this.__list.items.remove(this.__editable);
    this.__element.parentNode.removeChild(this.__element);
  }
        
  var add = document.createElement("img");
  
  add.src               = "/icons/add.png";
  add.alt               = "Add";
  add.className         = "add";
  add.onmouseout        = "gui.iconNormal(this);";
  add.onmouseover       = "gui.iconHighlight(this);";
  add.__type            = this.subtype;
  add.__container       = line;
  add.__list            = this;
  add.__editable        = item;
  add.onclick           = function()
  {
    var item = this.__list._createLine("", this.__type, this.__editable);
    
    if(this.__container.nextSibling)
      this.__container.parentNode.insertBefore(item._element, 
                                               this.__container.nextSibling); 
    else
      this.__container.parentNode.appendChild(item._element);

    item.focus();
  }

  line.style.position = "relative";
  line.appendChild(remove);
  line.appendChild(add);
  item._element = line;

  return item;
}

/**
  * Get the value entered for this editable.
  *
  * @return A string with the value for this editable.
  *
  */
gui.EditableList.prototype._getValue = function()
{
  var result = null;

  for(var i = 0, item; item = this.items[i]; i++)
    if(item.getValue())
      if(result)
        result += this.delimiter + item.getValue();
      else
        result = item.getValue();

  return result || '$undefined$';
};

/**
  * Set the width of the field.
  *
  * @param inWidth the new width to set (in percents)
  *
  */
gui.EditableList.prototype.setWidth = function(inWidth)
{
  for(var i = 0, item; item = this.items[i]; i++)
    item.setWidth(inWidth * 0.95);
};

/**
 * Set the focus to this field.
 *
 */
gui.EditableList.prototype.focus = function()
{
  this.items[0].focus();
}

gui.EditableList.prototype._undefine = function()
{
  this._element.parentNode.removeChild(this._element);
  this._element = this._createElement();
  alert(this._element.innerHTML);
}

//..........................................................................
//----- EditableMultiple ---------------------------------------------------

/**
  * The editable for multiple values.
  *
  * @param inKey        the key uniquely defining the value
  * @param inType       the type of the editable
  * @param inValue      the initial values
  * @param inSubtypes   the types of the subvalues
  * @param inDelimiters the delimiter between the multiple items
  *
  */
gui.EditableMultiple = function(inID, inKey, inType, inValues, inSubtypes, 
                                inDelimiters, inSubValues)
{
  this.items      = [];
  this.delimiters = inDelimiters;
  this.subtypes   = inSubtypes;
  this.subvalues  = inSubValues

  gui.Editable.call(this, inID, inKey, inType, inValues);
};

extend(gui.EditableMultiple, gui.Editable);

/**
  * Create the element associated with this editable.
  *
  * @return the html element created
  */
gui.EditableMultiple.prototype._createElement = function()
{
  var element = document.createElement("span");

  var subvalues = new Array();

  if(this.subvalues)
    subvalues = this.subvalues.split("::");

  for(var i = 0; i < this.subtypes.length; i++)
  {
    var item = gui.Editable.create(this.id, null, this.subtypes[i], 
                                   this._value.length > i 
                                   ? this._value[i] : "", 
                                   subvalues.length > 0 
                                   ? subvalues[Math.min(i, 
                                                        subvalues.length - 1)]
                                   : null, 
                                   null);

    this.items.push(item);

    if(item.type != "list")
      item.setWidth(100 / this._value.length);

    element.appendChild(item._element);
  }

  return element;
};

/**
  * Get the value entered for this editable.
  *
  * @return A string with the value for this editable.
  *
  */
gui.EditableMultiple.prototype._getValue = function()
{
  var result = "";

  var i = 0;
  for(var item; item = this.items[i]; i++)
  {
    var value = item.getValue();

    if(value.length > 0 && value != "$undefined$")
      result += this.delimiters[i] + item.getValue();
  }

  return result + this.delimiters[i];
};

/**
  * Set the width of the field.
  *
  * @param inWidth the new width to set (in percents)
  *
  */
gui.EditableMultiple.prototype.setWidth = function(inWidth)
{
  for(var i = 0, item; item = this.items[i]; i++)
    if(this.items[i].type != "list")
      item.setWidth(inWidth * 0.98 / this.items.length);
    else
      item.setWidth(inWidth);
};

/**
 * Set the focus to this field.
 *
 */
gui.EditableMultiple.prototype.focus = function()
{
  this.items[0].focus();
}

gui.EditableMultiple.prototype._undefine = function()
{
  for(var i = 0, item; item = this.items[i]; i++)
    this.items[i].undefine();
}

//..........................................................................
//----- Suggestion ---------------------------------------------------------

/** Storage for all suggestions related information. */
gui.suggestions = {}

gui.Suggestion = function(inName)
{
  this.name      = inName;
  this._request  = null;
  this._loadings = [];
  this._index    = {};
  this._values   = null;
}

gui.Suggestion.number = 5;
gui.Suggestion.lead   = 10;

/**
 * Request some suggestion values.
 *
 * @param inLoading the html element showing the loading status
 *
 */
gui.Suggestion.prototype.request = function(inLoading)
{  
  // already obtained?
  if(this._values)
    return;

  if(!this._request)
    this._request = ajax("/ajax/" + this.name, null, this.store.bind(this));

  this._loadings.push(inLoading);
}

/**
 * Store the suggestion values.
 *
 * @param   inName   the name of the suggestion
 * @param   inValues the suggestion values
 *
 */
gui.Suggestion.prototype.store = function(inValues)
{
  // cancel all loading
  for(var i = 0, loading; loading = this._loadings[i]; i++)
    loading.parentNode.removeChild(loading);

  this._request  = null;
  this._loadings = [];

  this._values = eval("(" + inValues + ")");

  // store more intelligently (using indexes per staring characters)
  var suggestions = {};
  for(var i = 0, suggestion; suggestion = this._values[i]; i++)
  {
    for(var j = 1; j <= gui.Suggestion.lead; j++)
    {
      var name = suggestion.substr(0, j).toLowerCase();
      
      if(this._index[name])
        continue;
      
      this._index[name] = i;
    }
  }
}

/**
 * Create a suggestion menu for selecting a suggestion.
 *
 * @param inField the field using the suggest
 * @param inBox   the box containing the suggestions
 * @param inEvent the event that started the suggestion request
 *
 */
gui.Suggestion.prototype.suggest = function(inField, inBox, inEvent)
{  
  // handle special keys
  if(inEvent.which == 40) // cursor down
    for(var node = inBox.firstChild; node; node = node.nextSibling)
    {
      if(node.className == "current")
      {
        if(node.nextSibling)
        {
          node.className             = "";
          node.nextSibling.className = "current";
        }
        else
        {
          node.className = "";
          inBox.appendChild(this._createSuggestItem
                            (node.__index + 1, this._values[node.__index + 1], 
                             true));

          inBox.removeChild(inBox.firstChild);
        }

        return;
      }
    }
  
  if(inEvent.which == 38) // cursor up
    for(var node = inBox.firstChild; node; node = node.nextSibling)
    {
      if(node.className == "current")
      {
        if(node.previousSibling)
        {
          node.className                 = "";
          node.previousSibling.className = "current";
        }
        else
        {
          node.className = "";
          inBox.insertBefore(this._createSuggestItem
                             (node.__index + 1, this._values[node.__index + 1],
                              true), inBox.firstChild);

          inBox.removeChild(inBox.lastChild);
        }

        return;
      }
    }
  
  if(inEvent.which == 13 || inEvent.which == 9) // enter or tab
  {
    for(var node = inBox.firstChild; node; node = node.nextSibling)
      if(node.className == "current")
      {
        inField.value = node._value;
        
        break;
      }

    inBox.style.display = "none";

    return;
  }
  
  var i = 
    this._index[inField.value.substr(0, gui.Suggestion.lead).toLowerCase()];

  if(!i)
    i = 0;

  // make sure it is shown
  inBox.style.display = "block";

  var value = inField.value.toLowerCase();

  for(var suggestion; suggestion = this._values[i]; i++)
  {
    if(value <= suggestion.toLowerCase())
    {
      // clear any old content
      inBox.innerHTML = "";

      for(var j = 0; j < gui.Suggestion.number && this._values[i + j]; j++)
      {        
        if(value.toLowerCase() 
           != this._values[i + j].substr(0, value.length).toLowerCase())
          return;

        inBox.appendChild(this._createSuggestItem(i + j, this._values[i + j], 
                                                  j == 0));
      }

      return;
    }
  }  
}

/**
  * Create a single item in a suggestion list.
  *
  * @param  inIndex the index number of the value
  * @param  inText  the text to display
  * @param  inIsCurrent flag if this is the currently selected value
  *
  * @return the create html element
  *
  */
gui.Suggestion.prototype._createSuggestItem = function(inIndex, inText, 
                                                       inIsCurrent)
{
  var item = document.createElement("li");
  
  if(inIsCurrent)
    item.className = "current";

  item.innerHTML = inText;
  item.__index   = inIndex;
  item._value    = inText;

  return item;
}

//..........................................................................
//----- Busy ---------------------------------------------------------------

/**
  * The class represents a busy status to be displayed.
  *
  * @param       inText  The text to show for busy (will be followed by steps).
  * @param       inSteps An array with all the steps to show.
  *
  */
gui.Busy = function(inText, inSteps)
{
  this.text  = inText;
  this.steps = inSteps;
 
  this.display = document.createElement('div');
  this.display.className = 'busy';

  var loading = document.createElement('img');
  loading.src = '/icons/loading.gif';
  loading.alt = 'Loading...';

  this.display.appendChild(loading);

  this.content = document.createElement('span');

  this.display.appendChild(this.content);

  document.body.appendChild(this.display);

  this._update();
}

gui.Busy.prototype.add = function(inStep)
{
  this.steps.push(inStep);

  this._update();
}

gui.Busy.prototype.done = function(inStep)
{
  this.steps.remove(inStep);

  if(this.steps.length == 0)
    document.body.removeChild(this.display);
  else
    this._update();
}

gui.Busy.prototype._update = function()
{
  this.content.innerHTML = this.text + this.steps.join(", ") + "...";
}

//..........................................................................
//----- Draggable ----------------------------------------------------------

/**
 * A screen object that is draggable.
 *
 *
 * @param inElement the element to make draggable
 * @param inBoundary the element marking the boundary for moving this one
 *
 */
gui.Draggable = function(inElement, inBoundary)
{
  this._element    = inElement;
  this._boundary   = inBoundary || document.body;
  this._offset     = null;
  this._allowClick = true;

  // add the style to make it draggable
  gui.addStyle(this._element, "draggable");

  // note the handler for mouse down
  this._element.onmousedown = this.prepare.bind(this);
  this._element._onclick = this._element.onclick;
  this._element.onclick = this.click.bind(this);
};

//----- prepare ------------------------------------------------------------

/**
 * Prepare for dragging. We don't immediately drag, since a mouse down could
 * also simply mean a click...
 * 
 */
gui.Draggable.prototype.prepare = function(inEvent)
{
  // wait 200ms before really dragging anything, but allow moving
  this._delayed = gui.delayed(this.initiate.bind(this, inEvent), 200);

  document.onmousemove = this.initiate.bind(this, inEvent);
}

//..........................................................................
//----- click --------------------------------------------------------------

/**
 * The element was clicked. Now remove the drag init and execute the click.
 *
 * @param inFunction the original function to call
 *
 */
gui.Draggable.prototype.click = function(inEvent)
{
  this._stop();

  if(!this._allowClick)
  {
    // mouse up already passed, otherwise we would not be here; we don't want
    // this click, but maybe the next
    this._allowClick = true;

    inEvent.stopPropagation();

    return;
  }

  // do the original click, if any
  if(this._element._onclick)
    this._element._onclick(inEvent);
}

//..........................................................................
//----- initiate -----------------------------------------------------------

/**
 * Initiate the dragging process.
 *
 */
gui.Draggable.prototype.initiate = function(inEvent)
{
  gui.removeStyle(this._element, "draggable");
  gui.addStyle(this._element, "dragged");

  document.onmouseup   = this.drop.bind(this);
  document.onmousemove = this.drag.bind(this);

  // prevent text selection
  document.body.focus();
  
  // prevent normal clicks
  this._allowClick = false;

  // compute the boundary values
  var pos = gui.position(this._boundary);

  this._top    = pos.y 
    + (this._boundary.style 
       ? number(this._boundary.style.borderTopWidth) 
       + number(this._boundary.style.paddingTop)
       : 0);
  this._bottom = pos.y + this._boundary.offsetHeight 
    - (this._boundary.style 
       ? number(this._boundary.style.borderBottomWidth) 
       + number(this._boundary.style.paddingBottom)
       : 0);
  this._left   = pos.x 
    + (this._boundary.style 
       ? number(this._boundary.style.borderLeftWidth) 
       + number(this._boundary.style.paddingLeft) 
       : 0);
  this._right  = pos.x + this._boundary.offsetWidth
    - (this._boundary.style 
       ? number(this._boundary.style.borderRightWidth) 
       + number(this._boundary.style.paddingRight)
       : 0);
}

//..........................................................................
//----- drag ---------------------------------------------------------------

/**
 * Drag the element. This is usually called by the mousemove handler.
 *
 */
gui.Draggable.prototype.drag = function(inEvent)
{
  // move the element to the current mouse position
  var mouse = gui.mouseCoords(inEvent);

  if(!this._offset)
  {
    var doc = gui.position(this._element);
    
    this._offset = { x: mouse.x - doc.x, y: mouse.y - doc.y };

    // make elements relative to position wherever we like (we have to do this
    // after getting the position or the position will be wrong for float
    // elements)
    this._element.style.position = "absolute";    
  }

  var top  = mouse.y - this._offset.y;
  var left = mouse.x - this._offset.x

  // check if the movement is inside the allowed boundaries
  if(top < this._top)
    top = this._top;
  else
    if(top > this._bottom)
      top = this._bottom - 1;

  if(left < this._left)
    left = this._left;
  else
    if(left > this._right)
      left = this._right - 1;

  this._element.style.top  = top + "px";
  this._element.style.left = left + "px";
}

//..........................................................................
//----- drop ---------------------------------------------------------------

/**
 * Drop the element.
 *
 * @param inEvent the event responsible for the action
 *
 */
gui.Draggable.prototype.drop = function(inEvent)
{
  this._stop();

  gui.removeStyle(this._element, "dragged");
  gui.addStyle(this._element, "draggable");

  // reinstate all the handlers
  delete document.onmousemove;
  delete document.onmouseup;

  document.body.style.MozUserSelect = "";

  this._element.onclick = this.click.bind(this);

  // make sure we reset the offset
  delete this._offset;
  this._offset = null;
}

//..........................................................................
//----- _stop --------------------------------------------------------------

/**
 * Stop dragging.
 *
 */
gui.Draggable.prototype._stop = function()
{
  if(this._delayed)
  {
    window.clearTimeout(this._delayed.timeout);
  
    delete this._delayed;
    delete document.onmousemove;
    document.onmousemove = null;
  }
}

//..........................................................................

//..........................................................................
//----- Droppable ----------------------------------------------------------

/** An object that that can be dragged & dropped.
 *
 * @param inElement  the element to make droppable (and draggable)
 * @param inTargets  all possible drop targets (the current object may be among
 *                   the elements given here)
 * @param inBoundary the element marking the boundary for moving this one
 *
 */
gui.Droppable = function(inElement, inTargets, inBoundary)
{
  gui.Draggable.call(this, inElement, inBoundary);

  this._targets = inTargets;
}

extend(gui.Droppable, gui.Draggable, "_base");

//----- drag ---------------------------------------------------------------

/**
 * Drag the element. Also make sure any drop target moved over is marked.
 *
 * @param inEvent the event that initiated the drop
 * 
 */
gui.Droppable.prototype.drag = function(inEvent)
{
  this._base.drag.call(this, inEvent);

  var mouse = gui.mouseCoords(inEvent);

  // determine the current drop target
  var target = null;
  
  for(var i = 0; i < this._targets.length; i++)
  {
    var current = this._targets[i];

    if(!current || current == this || !current.isTargeted(mouse))
      continue;

    if(target)
    {
      if(isInside(current._element, target._element))
        target = current;
    }
    else
      target = current;
  }

  if(target != null)
    target.mark(this, mouse);
  else
    if(gui.DropTarget.current)
      gui.DropTarget.current.unmark(this, mouse);
};

//..........................................................................
//----- drop ---------------------------------------------------------------

/**
 * Drop the element.
 *
 * @param inEvent the event responsible for the action
 *
 */
gui.Droppable.prototype.drop = function(inEvent)
{
  this._base.drop.call(this, inEvent);

  if(gui.DropTarget.current) 
  {
    this._element.parentNode.removeChild(this._element);
    gui.DropTarget.current.dropped(this, gui.mouseCoords(inEvent));
  } 
  else
  {
    // put the element back to where it started
    var parent = this._element.parentNode;
    var next = this._element.nextSibling;

    parent.removeChild(this._element);
    parent.insertBefore(this._element, next);
  }

  this._element.style.top  = null;
  this._element.style.left = null;
  this._element.style.position = null;
}

//..........................................................................

//..........................................................................
//----- DropTarget ---------------------------------------------------------

/**
 * A target for a drop operation.
 * This standard drop target will add a css style 'dropping' when an element
 * is moved over it.
 *
 * @param inElement the display element for this target
 * 
 */
gui.DropTarget = function(inElement)
{
  this._element = inElement;
};

/** The drop target current selected. */
gui.DropTarget.current = null;

//---------------------------------- mark ----------------------------------

/**
 * Mark the target for beeing dropped upon.
 *
 * @param inSource the object that initiated the marking 
 * @param inMouse  the position of the mouse as {x: x, y: y}
 *
 * @return true if marked, false if not
 * 
 */
gui.DropTarget.prototype.mark = function(inSource, inMouse)
{
  if(gui.DropTarget.current == this)
    return false;

  if(gui.DropTarget.current)
    gui.DropTarget.current.unmark(inSource, inMouse);

  gui.addStyle(this._element, "dropping");
  
  gui.DropTarget.current = this;

  return true;
}

//..........................................................................
//--------------------------------- unmark ---------------------------------

/**
 * Unmark the target for beeing dropped upon.
 *
 * @param inSource the object that initiated the unmarking 
 * @param inMouse  the position of the mouse as {x: x, y: y}
 *
 */
gui.DropTarget.prototype.unmark = function(inSource, inMouse)
{
  gui.removeStyle(this._element, "dropping");

  gui.DropTarget.current = null;
};

//..........................................................................
//------------------------------- isTargeted -------------------------------

/**
 * Determine if the mouse currently targets this element.
 *
 * @param inMouse the mouse position
 *
 */
gui.DropTarget.prototype.isTargeted = function(inMouse)
{
  return this._element && this._element.style.display != "none" 
    && gui.isOver(this._element, inMouse);
}

//..........................................................................
//-------------------------------- dropped ---------------------------------

/**
 * The element was actually dropped on this.
 *
 * @param inDroppable the droppable dropped onto this one 
 * @param inMouse     the position of the mouse as {x: x, y: y}
 *
 */
gui.DropTarget.prototype.dropped = function(inDroppable, inMouse)
{
  if(gui.DropTarget.current)
    gui.DropTarget.current.unmark(inMouse);

  gui.appear(this._element, 10, 10);
}

//..........................................................................

//..........................................................................

// male all values editable
addOnLoad(gui.makeEditable);

// install scrolling on the page 
addOnLoad(gui.initScrolling);

// add the necessary style sheets (if not yet loaded)
gui.delayed(function() { gui.includeCSS("/css/gui.css"); }, 0);

// treat INFO cookies
addOnLoad(gui.checkMessages);

