/*****************************************************************************
 * $Header$
 * $Author$
 * $Revision$
 * $Date$
 *
 * UI controller.
 *
 * Copyright: Neolane 2001-2012
 *****************************************************************************/

// 
// browser detection
// 

// browser engine name
var isGecko = function()
{
  var ua = navigator.userAgent.toLowerCase(); 
  var bIsGecko = (ua.indexOf('gecko') != -1 && ua.indexOf('safari') == -1);
  return function(){return bIsGecko;}
}();

var isAppleWebKit = function()
{
  var ua = navigator.userAgent.toLowerCase(); 
  var bIsAppleWebKit = (ua.indexOf('applewebkit') != -1);
  return function(){return bIsAppleWebKit;}
}();

// browser name
var isKonqueror = function()
{
  var ua = navigator.userAgent.toLowerCase(); 
  var bIsKonqueror = (ua.indexOf('konqueror') != -1);
  return function(){return bIsKonqueror;}
}();

var isSafari = function()
{
  var ua = navigator.userAgent.toLowerCase(); 
  var bIsSafari = (ua.indexOf('safari') != - 1);
  return function(){return bIsSafari;}
}();

var isIOs = function()
{
  var ua = navigator.userAgent.toLowerCase(); 
  var bIsIOs = (ua.indexOf('iphone') != - 1) || (ua.indexOf('ipod') != - 1) || (ua.indexOf('ipad') != - 1);
  return function(){return bIsIOs;}
}();

var isOmniweb = function()
{
  var ua = navigator.userAgent.toLowerCase(); 
  var bIsOmniweb = (ua.indexOf('omniweb') != - 1);
  return function(){return bIsOmniweb;}
}();

var isOpera = function()
{
  var ua = navigator.userAgent.toLowerCase(); 
  var bIsOpera = (ua.indexOf('opera') != -1);
  return function(){return bIsOpera;}
}();

var isAol = function()
{
  var ua = navigator.userAgent.toLowerCase(); 
  var bIsAol = (ua.indexOf('aol') != -1);
  return function(){return bIsAol;}
}();

var isIE = function()
{
  var ua = navigator.userAgent.toLowerCase(); 
  var bIsIE = (ua.indexOf('msie') != -1 && !isOpera() && (ua.indexOf('webtv') == -1) );
  return function(){return bIsIE;}
}();

var getIEVersion = function()
{
  var version = -1; // Return value assumes failure.
  if( isIE() )
  {
    var re  = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
    if (re.exec(navigator.userAgent) != null)
      version = parseFloat( RegExp.$1 );
  }
  return function(){return version;}
}();

var isMozilla = function()
{
  var ua = navigator.userAgent.toLowerCase(); 
  var bIsMozilla = (isGecko() && ua.indexOf('gecko/') + 14 == ua.length);
  return function(){return bIsMozilla;}
}();

var isNS = function()
{
  var ua = navigator.userAgent.toLowerCase(); 
  var bIsNS = ( isGecko() ? (ua.indexOf('netscape') != -1) : ( (ua.indexOf('mozilla') != -1) && !isOpera() && !isSafari() && (ua.indexOf('spoofer') == -1) && (ua.indexOf('compatible') == -1) && (ua.indexOf('webtv') == -1) && (ua.indexOf('hotjava') == -1) ) );
  return function(){return bIsNS;}
}();

/** Helper function used to fix transparency bug under IE */
function IEFixImage()
{
  if ( this.src.indexOf(".png") != -1 )
  { // fix the PNG transparency bug under IE
    var src = this.src;
	  this.src = "/xtk/img/blank.gif";
	  this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft." +
				  "AlphaImageLoader(src='" + src + "',sizingMethod='scale')";
	}
  else if ( this.src.indexOf(".bmp") != -1 )
    // make magenta color transparent
    this.style.filter = "progid:DXImageTransform.Microsoft.Chroma(color=#FF00FF)";
}

function Navigator() {}

Navigator.prototype.fixImage = function(img)
{
  if (isIE())
    img.onload = IEFixImage;
}

/** Constructor
  *
  */
function UIController()
{
  this.ctx = null                       /* the XML context */
  this.aFileXPath = new Array()
  this.soapRouterUrl = window.location.protocol + "//" + window.location.host  + "/nl/jsp/soaprouter.jsp"
   
  this.workingTimeZone = null

/** Map of observers (using xpath as key).
  * 
  * Observer properties are:
  *
  * object: object to notify.
  * method: method of the object 'object' to call.
  * name:   name of the observer (must be unique). Used to avoid to notify the 
  *         observer which has modified the context. 
  * 
  * Remarks:
  * - if the object 'object' has a method onSubmit(), that method is 
  *   automatically called on form submission. */
  this.observers = new Hash()
  
  this.childrenObservers = new Array()  /* observers registered on OBSERVE_CHILDREN mode */
}

UIController.prototype = new Navigator()

UIController.prototype.OBSERVE_XPATH      = 1
UIController.prototype.OBSERVE_CHILDREN   = 2

/** Register an XPATH observer.
  *
  * @xpath  XPATH to observe.
  * @object object to notify.
  * @mode   registration mode (OBSERVE_XPATH or OBSERVE_CHILDREN). */ 
UIController.prototype.registerObserver = function(xpath, obj, method, name, mode)
{
  var observer = { "object": obj, "method": method, "name": name, "mode": mode }
  if ( mode == this.OBSERVE_XPATH )
  {
    var registration = this.observers.get(xpath)
    if ( registration == undefined )
    { // first registration on this xpath
      // => we create a array to store registrations
      registration = new Array(observer)
      this.observers.set(xpath, registration)
    }
    else
      registration.push(observer)
  }
  else
  {
    observer.xpath = xpath
    this.childrenObservers.push(observer)
  }
  
  return observer
}

/** Notify all observers.
  *
  * @startPath      if defined limit the notification of observers registered
  *                 on a path starting by startPath. 
  * @value          new value for the given XPath. Can be undefined.
  * @ignoreObserver name of an observer to ignore. */
UIController.prototype.notifyAllObservers = function(startPath, value, ignoreObserver)
{
  for (var xpath in this.observers.items)
  {
    if ( startPath != undefined && xpath.indexOf(startPath) != 0 )
      // skip this xpath
      continue
      
    var observers = this.observers.get(xpath)
    var newValue = value
    if ( newValue == undefined || xpath!=startPath)
    {
      // get value from the context
      newValue = getXPathValue(this.ctx, xpath)
      if (startPath == undefined)
      {
        // Store initial value (use later to reset context of active filters)
        this.setValue(xpath+"_init", newValue)
      }
    }
    
    for (var i=0; i < observers.length; i++)
    {
      // get the value from the context
      var observer = observers[i]
      if ( ignoreObserver != observer.name )
        // notify the observer
        observer.method.call(observer.object, xpath, newValue)
    }
  }
  
  // notify observers registered on mode OBSERVE_CHILDREN
  var nObservers = this.childrenObservers.length;
  for (var i=0; i < nObservers; i++)
  {
    var observer = this.childrenObservers[i]
    var newValue = value
    if ( newValue == undefined || observer.xpath!=startPath)
      // get value from the context
      newValue = findElement(this.ctx, observer.xpath)
    if ( ignoreObserver != observer.name && (startPath == undefined || startPath.indexOf(observer.xpath) == 0) )
      observer.method.call(observer.object, startPath ? startPath : observer.xpath, newValue)
  }
}

/** Reset observers with their initial value */
UIController.prototype.resetObservers = function()
{
  for (var xpath in this.observers.items)
  {
    var value = getXPathValue(this.ctx, xpath+"_init")
    this.setValue(xpath, value)
  }
}

/** Return true if one of observers has a modified initial value */
UIController.prototype.isModifiedObservers = function()
{
  for (var xpath in this.observers.items)
  {
    var value1 = getXPathValue(this.ctx, xpath)
    var value2 = getXPathValue(this.ctx, xpath+"_init")
    if (value1 != value2)
      return true  
  }

  return false
}


UIController.prototype.setTimeZone = function(strTimeZone)
{
  this.workingTimeZone = new Timezone(strTimeZone)
}

/** Set a value in the context.
  *
  * @xpath  XPATH to modify.
  * @value  value to set. */ 
UIController.prototype.setValue = function(xpath, value, ignoreObserver, asCDATA)
{
  setXPathValue(this.ctx.documentElement, xpath, value, asCDATA)
  this.notifyAllObservers(xpath, value, ignoreObserver)
}

UIController.prototype.setTimestamp = function(xpath, dateTime, ignoreObserver)
{
  var offsetDate = new Date(dateTime.getTime()-(new Date()).getTimezoneOffset()*60000) 
  offsetDate = this.workingTimeZone.offset(offsetDate, true)
  this.setValue(xpath, Format.toISO8601(offsetDate),ignoreObserver)
}

UIController.prototype.getTimestamp = function(strValue)
{
  var browserDate = Format.parseDateTimeInter(Format.nvl(strValue, ''));
  if( browserDate != null )
  {
    var offsetDate = this.workingTimeZone.offset(browserDate)
    offsetDate = new Date(offsetDate.getTime()+(new Date()).getTimezoneOffset()*60000) 

    return offsetDate
  }
}

/** Set a date part in the context. (from a combo)
  *
  * @xpath    XPATH to modify.
  * @value    value to set. 
  * @datePart date part to update (DD, MM, MN, Y2, Y4, H4, H2, MI) */ 
UIController.prototype.setDateValue = function(xpath, value, datePart, ignoreObserver)
{
  var strSettedDate = getXPathValue(this.ctx, xpath)
  var date = Format.parseDateTimeInter(Format.nvl(strSettedDate, ''));
  if( date && value != "" )
  { // we have a valid date, so we can use it to set date part to handle time zone

    // set all date parts 
    var aDateParts = ["H4", "H2", "MI", "DD", "MM", "MN", "Y2", "Y4"]
    for( var iDatePartIdx = 0 ; iDatePartIdx < aDateParts.length ; iDatePartIdx++ )
    {
      var input = document.getElementById(ignoreObserver + "_" + aDateParts[iDatePartIdx])
      if( input )
        date.setDatePart(aDateParts[iDatePartIdx], input.value)
    }

    this.setTimestamp(xpath, date, ignoreObserver)
  }
  else
  {
  var aDate, aTime
  if( strSettedDate != null && strSettedDate.length > 0 )
  {
    var aDateTime = strSettedDate.split(" ")
      aDate = aDateTime[0].split(/[\/\-]/)
    var aTime = null
    if (aDateTime[1])
      aTime = aDateTime[1].split(":")
    else
      aTime = ["XX", "XX", "XX"]
  }
  else
  {
    aDate = ["XXXX", "XX", "XX"]
    aTime = ["XX", "XX", "00"]
  }
  
  if( value == "" )
    value = "XX"
    
  if( datePart == "Y4" || datePart == "Y2" )
  {
    if( value == "XX" )      
      aDate[0] = "XXXX"
    else
      aDate[0] = value
  }
  else if( datePart == "MN" || datePart == "MM" )
    aDate[1] = value
  else if( datePart == "DD" )
    aDate[2] = value
  else if( datePart == "H4" || datePart == "H2" )
    aTime[0] = value
  else if( datePart == "MI" )
    aTime[1] = value
  
    var strNewDate = aDate[0] + "-" + aDate[1].padLeft(2) + "-" + aDate[2].padLeft(2) + " " 
                + aTime[0].padLeft(2) + ":" + aTime[1].padLeft(2) + ":" + aTime[2].padLeft(2) 

    // if the date is fully setted, so use JS date object to apply timezone offset
    date = Format.parseDateTimeInter(strNewDate); 
    if( date )
      strNewDate = Format.toISO8601(date)
    else 
    {
      // remove the UTC designator from a previously full date
      var iZoneDesignatorPos = strNewDate.indexOf("Z")
      if( iZoneDesignatorPos > -1 )
        strNewDate = strNewDate.substring(0, iZoneDesignatorPos)
    }

    this.setValue(xpath, strNewDate, ignoreObserver)
  }
}


/** Get a value from the context.
  *
  * @xpath  XPATH to get.
  * @return the value. */
UIController.prototype.getValue = function(xpath)
{
  // Special case for compatibility purposes (ctx => cookie)
  if( xpath == "/ctx/__sessiontoken" )
    return this.getSessionToken()
  
  return getXPathValue(this.ctx, xpath)
}

/** Show or hide a cell containing a container, an input or a static.
  *
  * @name name of the cell to hide.
  * @visible true or false. */
UIController.prototype.setCellVisibility = function(name, visible)
{
  function show(node, visible)
  {
    if ( node != null && node.style != undefined )
      node.style.display = visible ? "" : "none";
  }
  
  show(document.getElementById(name + "-label-left"), visible);
  show(document.getElementById(name + "-cell"), visible);
  show(document.getElementById(name + "-label-right"), visible);
}

/** Submit the page to the server.
  *
  * @strAction      action at the origin of the submition (next, previous, validate, refresh). 
  * @strTarget      target for the form submission (_blank, _parent, ...).
  * @strTransition  name of the ougoing transition in diagram design. *
  * @bForceNoFile   file are already uploaded, don't reupload it 
  * @bNoWait        don't display waiting box*/
  
UIController.prototype.submit = function(strAction, strTarget, strTransition, bForceNoFile, bNoWait)
{
  // call the method onSubmit for all registered observers
  for (var xpath in this.observers.items)
  {
    var observers = this.observers.get(xpath)
    for (var i=0; i < observers.length; i++)
      if ( observers[i].object.onSubmit != undefined )
        if ( observers[i].object.onSubmit.call(observers[i].object) == false )
          return false
  }
  
  var nObservers = this.childrenObservers.length;
  for (var i=0; i < nObservers; i++)
  {
    var observer = this.childrenObservers[i]
    if ( observer.object.onSubmit != undefined )
      if ( observer.object.onSubmit.call(observer.object) == false )
        return false
  }
  
  var formObject = document.getElementById("page-form")  
  var bHasFile = false
  if( !bForceNoFile )
  {
    for( var strInputId in this.aFileXPath )
      if( formObject[strInputId].value.toString().length > 0 )
      {
        bHasFile = true
        break;
      }
  }

  if( formObject["userAction"] ) 
    formObject["userAction"].value = strAction;
  else // ## Compatibility with previous webApp/report
    formObject["action"].value = strAction;

  if ( strTransition != undefined )
    formObject["transition"].value = strTransition    

  if( bHasFile && (strAction == 'next' || strAction == 'submit') )
  {
    var dialog = new HtmlDialog(xtk_core.fileUploading(), "OK", this, true)
    dialog.buttonOk.style.display = "none"
    dialog.buttonCancel.style.display = "none"
    dialog.setSize("30em")
    dialog.show()

    formObject.enctype = "multipart/form-data"
    if( isIE() )
    {
      formObject.setAttribute("enctype", "multipart/form-data")
      formObject.setAttribute("encoding", "multipart/form-data")
    }
    formObject.action = "/nl/jsp/uploadFile.jsp" 
    formObject.target = "uploadFileTarget"
    this.formTarget = strTarget 
  }
  else
  {
    if( strTarget != undefined && strTarget.length > 0 )
      formObject.target = strTarget
    formObject["ctx"].value    = toXMLString(this.ctx)

    if( !bNoWait && (strTarget == "_self" || strTarget == "_parent") )
    {
      var divOverToDisable = document.getElementById("overToDisable")
      if( divOverToDisable != null )
        divOverToDisable.style.display = ''
      var divLoading = document.getElementById("divLoading")
      if( divLoading != null )
        divLoading.style.display = ''
    }
  } 
  
  // Submit the form
  formObject.submit()

  return true
}

// Retrieve the session token from the context
UIController.prototype.getSessionToken = function()
{
  var strSessionToken = getXPathValue(this.ctx, "/ctx/__sessiontoken")
  // Try the cookie if this failed
  if( strSessionToken == "" && document.cookie && document.cookie.__sessiontoken )
    strSessionToken = document.cookie.__sessiontoken.value
  if( strSessionToken == "" )
    strSessionToken = (new Cookie("__sessiontoken")).getValue()

  return strSessionToken
}

// Register file in controller
UIController.prototype.addFileInput = function(strInputId, strXPath)
{
  this.aFileXPath[strInputId] = strXPath
}

// Called by uploadFile.jsp when all files are downloaded
// Update context with md5 and go to next activity
UIController.prototype.uploadFileCallBack = function(aFilesInfo)
{
  var formObject = document.getElementById("page-form");
  for( var strInputId in this.aFileXPath )
    formObject[strInputId].value = ""
    
  // add file information in context
  if( aFilesInfo ) 
    for( var iIndex = 0 ; iIndex < aFilesInfo.length ; iIndex++ )
    {
      var xpath =  this.aFileXPath[aFilesInfo[iIndex].paramName]
      this.setValue(xpath + "/@md5", aFilesInfo[iIndex].md5);      
      this.setValue(xpath + "/@originalName", aFilesInfo[iIndex].fileName);
      this.setValue(xpath + "/@id", 0);
    }

  formObject.action = ""
  formObject.target = ""
  if( isIE() )
  {
    formObject.setAttribute("enctype", "application/x-www-form-urlencoded")
    formObject.setAttribute("encoding", "application/x-www-form-urlencoded")
  }  
  else
    formObject.enctype = ""

  this.submit(formObject["userAction"].value, this.formTarget, formObject["transition"].value, true, true)
}

// Update file input diplay
UIController.prototype.changeFileInputVisibiliy = function(strInputId, bIsEditing)
{
  if( bIsEditing )
  {
    document.getElementById(strInputId).style.display = "inline"
    document.getElementById(strInputId).disabled = false
    document.getElementById(strInputId+"-edit").style.display = "none"
    document.getElementById(strInputId+"-fileName").style.display = "inline"
  }
  else if( document.getElementById(strInputId).value.length == 0 )
  {
    document.getElementById(strInputId).style.display = "none"
    document.getElementById(strInputId).disabled = true
    document.getElementById(strInputId+"-edit").style.display = "inline"
    document.getElementById(strInputId+"-fileName").style.display = "none"
    if( document.getElementById(strInputId+"-downloadLink") )
      document.getElementById(strInputId+"-downloadLink").style.display = "inline"
  }  
}


/******************************************************************************
 * MouseEventHelper : cross browser functions related to mouse events.
 *****************************************************************************/
var MouseEventHelper = {}

// Retrieves an event
MouseEventHelper.getEvent = function(e)
{
  return e || window.event || {pageX:0, pageY:0}  // top left by default
}

// return the target of a mouse event
// NB : even if the event was captured and bubbled up, the target remains
// the element the event took place on.
MouseEventHelper.getEventTarget = function(e)
{
  var event = this.getEvent(e)
  return event.target || event.srcElement // ff || ie
}

// return the target the mouse will be going over after an onmouseout event
MouseEventHelper.getEventOutTarget = function(e)
{
  var event = this.getEvent(e)
  return event.relatedTarget || event.toElement // ff || ie
}

// Retrieve the mouse coordinates of a mouse event in absolute.
MouseEventHelper.getCoordinates = function(e)
{
  var event = this.getEvent(e)
  
  var fX = event.pageX || event.x + document.documentElement.scrollLeft || 0
  var fY = event.pageY || event.y + document.documentElement.scrollTop  || 0
  
  // return type is recursively compatible for convenience
  return { pageX:fX, pageY:fY }
}

// Return the distance between two mouse positions
MouseEventHelper.getDistance = function(coords1, coords2)
{
  return Math.sqrt(Math.pow(coords2.pageX - coords1.pageX, 2) 
                 + Math.pow(coords2.pageY - coords1.pageY, 2))
}

// Stop a mouse event from bubbling
MouseEventHelper.stopBubbling = function(e)
{
  var event = this.getEvent(e)
  if( event )
  {
    event.cancelBubble = true
    if( event.stopPropagation )
      event.stopPropagation()
  }
}


/******************************************************************************
 * HTMLHelper : generic functions dealing with html nodes.
 *****************************************************************************/
HTMLHelper = {}

/* Get the absolute pos of an html node
 * @param node : html node
 * @return an object with x and y properties.
 */
HTMLHelper.getAbsolutePosition = function(node)
{
  var iLeft = 0
  var iTop = 0
  
  if( node.offsetParent )
  {
    do
    {
      iLeft += node.offsetLeft;
      iTop += node.offsetTop;
    }
    while( node = node.offsetParent );
  }
  
  return { x:iLeft, y:iTop }
}

/* Return the style value of the given property for the given element.
 * Some browsers (IE...) like javascript-like style names (e.g. "marginTop")
 * while others like css-like ones (e.g. "margin-top").
 */
HTMLHelper.getStyle = function(element, strStyleProperty)
{
  var strStyle
  
  if( element.currentStyle)
    strStyle = element.currentStyle[strStyleProperty];
  else if (window.getComputedStyle)
    strStyle = document.defaultView.getComputedStyle(element, null).getPropertyValue(strStyleProperty);
  
  // Try to convert javascript-style property to css-style if this failed.
  if( !strStyle && strStyleProperty.search(/-/) == -1 )
  {
    var toCSSStyle = function(strMatch)
    {
      return "-" + strMatch.toLowerCase()
    }
    strStyleProperty = strStyleProperty.replace(/[A-Z]/, toCSSStyle)
    strStyle = HTMLHelper.getStyle(element, strStyleProperty)
  }
  
  return strStyle
}

/* append a dom tree or its equivalent string representation under a
 * given HTML node, replacing its actual children (if any).
 * @NB : be sure to catch exceptions ;)
 */
HTMLHelper.setChild = function(htmlNode, childNode, bThrowOnEmptyString)
{
  if( typeof childNode == "string" && !(childNode == "" && bThrowOnEmptyString) )
    htmlNode.innerHTML = childNode
  else if( typeof childNode == "object" )
  {
    // Erase its brothers
    htmlNode.innerHTML = ""
    htmlNode.appendChild(childNode)
  }
  else
    throw "HTMLHelper.setChild : wrong parameters"
}

/* Sets a style onto a node. */
HTMLHelper.setStyle = function(htmlNode, strStyle)
{
  if( typeof strStyle != "string" )
    return
  
  if( isIE() && htmlNode.style.setAttribute )
    htmlNode.style.setAttribute('cssText', strStyle) // ie <= 7
  else
    htmlNode.setAttribute("style", strStyle)  // standard
}

// Log informations through the firebug console if it exists, or uses alert.
// @param strLogFunction : firebug ones => log, info, warn, error.
HTMLHelper.logger = function(object, strLogFunction)
{
  if( !strLogFunction )
    strLogFunction = "log"
  
  if( window.console && window.console[strLogFunction] )
    window.console[strLogFunction](object)
  else
  {
    if( object == null )
      object = "null"
    else if( object == undefined )
      object = "undefined"
  }
}

/******************************************************************************
 * Generic free functions
 *****************************************************************************/

/* Returns a clone of the parameter
 */
var clone = function(srcInstance)
{
  // return null and non objects directly
  if( !srcInstance || typeof(srcInstance) != 'object' )
    return srcInstance;
  
  /* We call the object constructor to get a new instance of the same class */
  var newInstance = srcInstance.constructor();
  
  /* We copy the object properties reccursively */
  for(var i in srcInstance)
    newInstance[i] = clone(srcInstance[i]);
  
  return newInstance;
}


/******************************************************************************
 * HelpBubbleHelper : Helps the HelpBubble class to handle help bubbles.
 *****************************************************************************/
var HelpBubbleHelper = {}

/* Manually set the helpBubble skeleton parent (where the helpBubble structure will be
 * attached). This can be useful if the default parent (cf. getHelpBubble) doesn't
 * work as expected.
 */
HelpBubbleHelper.setHelpBubbleSkeletonParent = function(htmlElement)
{
  if( !htmlElement || typeof htmlElement != "object" )
    throw "HelpBubbleHelper.setHelpBubbleSkeletonParent : Wrong parameter"
  
  this.ndHelpBubbleSkeletonParent = htmlElement
}


/* Returns the skeleton parent : by default the body element.
 */
HelpBubbleHelper.getHelpBubbleSkeletonParent = function()
{
  return this.ndHelpBubbleSkeletonParent || document.getElementsByTagName("body")[0]
}

/* Creates the helpBubble skeleton (by calling createHelpBubble) after finding 
 * an appropriate godfather.
 */
HelpBubbleHelper.getHelpBubble = function()
{
  // Return the helpBubble skeleton right now if it already got constructed
  if( !this.helpBubbleSkeleton )
  {
    // hide the helpBubble on mouse down events by default
    var ndHelpBubbleSkeletonParent = this.getHelpBubbleSkeletonParent()
    if( !ndHelpBubbleSkeletonParent )
      throw "No suitable parent was found to attach the skeleton to. "
          + "Please specify one by calling setHelpBubbleSkeletonParent with "
          + "an appropriate html element."
    
    var originalOnclick = ndHelpBubbleSkeletonParent.onclick
    ndHelpBubbleSkeletonParent.onclick = function(e)
    {
      if( originalOnclick )
        originalOnclick(e)
      HelpBubbleHelper.hideHelpBubble()
    }
    
    this.helpBubbleSkeleton = this.createHelpBubble(ndHelpBubbleSkeletonParent)
    
    if( !this.helpBubbleSkeleton )
      throw "Couldn't attach the helpBubble skeleton to its parent node!"
  }
  
  return this.helpBubbleSkeleton
}

/* Creates the helpBubble skeleton and append it under the given element.
 * A helpBubble basically consists of a pointer div (hidden by default), 
 * a title div and a content div.
 * @param dummyDivParent : parent element which should be at the end of the
 *        HTML document if you don't want IE to mess up its z-indices.
 */
HelpBubbleHelper.createHelpBubble = function(dummyDivParent)
{
  var helpBubbleContainer = document.createElement("div")
  helpBubbleContainer.className = "helpBubbleContainer"
  
  // Prevent the helpBubble from being hidden on mouse click
  helpBubbleContainer.onclick = function(e)
  {
    MouseEventHelper.stopBubbling()
  }
  
  var helpBubblePointer = document.createElement("div")
  helpBubblePointer.className = "pointer"
  helpBubbleContainer.pointer = helpBubbleContainer.appendChild(helpBubblePointer)
  
  var helpBubbleTitle = document.createElement("div")
  helpBubbleTitle.className = "title"
  helpBubbleContainer.optionalTitle = helpBubbleContainer.appendChild(helpBubbleTitle)
  
  var helpBubble = document.createElement("div")
  helpBubble.className = "helpBubble"
  helpBubbleContainer.content = helpBubbleContainer.appendChild(helpBubble)
  
  // Old IEs can't understand the min-width statement
  if( isIE() && HTMLHelper.getStyle(helpBubbleContainer, "minWidth") == undefined )
    helpBubbleContainer.style.width = "200px"
  
  // Use a dummy div to wrap the helpBubble for easier handling
  var dummyDiv = dummyDivParent.appendChild(document.createElement("div"))
  dummyDiv.style.position = "absolute"
  dummyDiv.style.display = "none"
  dummyDiv.helpBubbleContainer = dummyDiv.appendChild(helpBubbleContainer)
  
  return dummyDiv
}

// Set additionnal class names for the helpBubble container so that users will be
// able to modify its style easily.
HelpBubbleHelper.setClassNames = function(strClassNames)
{
  var helpBubble = this.getHelpBubble()
  helpBubble.helpBubbleContainer.className = "helpBubbleContainer " + strClassNames
}

// Displays a textual helpBubble
HelpBubbleHelper.displayHelpBubble = function(event, helpBubbleContent, options)
{
  var helpBubble = HelpBubbleHelper.getHelpBubble()
  
  // Hide any previous helpBubble
  this.hideHelpBubble()
  
  var bIsOnClick = options.eAttachmentMode == HelpBubbleOptions.EAttachmentMode.ON_CLICK
  
  // Handle the insertion of the content into the helpBubble skeleton divs
  var handleDiv = function(div, elementToInsert)
  {
    try
    {
      HTMLHelper.setChild(div, elementToInsert, true)
      div.style.display = "block"
    }
    catch(e)
    {
      div.style.display = "none"  // Silent catch, don't display
    }
  }
  
  var helpBubbleContainer = helpBubble.helpBubbleContainer
  
  // Handle the title
  handleDiv(helpBubbleContainer.optionalTitle, helpBubbleContent.title)
  
  // Handle the content
  handleDiv(helpBubbleContainer.content, helpBubbleContent.content)
  
  // Handle the helpBubble position
  var helpBubbleLeft
  var helpBubbleTop
  
  // Force display so we can get the helpBubble dimensions
  helpBubble.style.display = "block"
  
  // Handle the helpBubble position
  if( bIsOnClick )
  {
    // Use the link position so the pointer div will point to it
    var pos = HTMLHelper.getAbsolutePosition(helpBubbleContent.link)
    helpBubbleLeft = pos.x
    helpBubbleTop = pos.y
  }
  else
  {
    // Use the mouse position
    var coords = MouseEventHelper.getCoordinates(event)
    var iTopMargin = HTMLHelper.getStyle(helpBubbleContainer, "marginTop") || 0
    iTopMargin = parseInt(iTopMargin)
    
    helpBubbleLeft = coords.pageX
    helpBubbleTop = coords.pageY - iTopMargin
    
    // Check possible overflow on over
    var helpBubbleWidth = helpBubbleContainer.offsetWidth
    var helpBubbleHeight = helpBubbleContainer.offsetHeight
    var documentWidth = document.documentElement.clientWidth
                      + document.documentElement.scrollLeft
    var documentHeight = document.documentElement.clientHeight
                      + document.documentElement.scrollTop
    
    var bForcedOffset = false
    var iVerticalDiff = helpBubbleTop + iTopMargin + helpBubbleHeight - documentHeight
    if( iVerticalDiff > 0 )
    {
      if( helpBubbleHeight > document.documentElement.clientHeight - iVerticalDiff )
      {
        // /!\ the helpBubble is too tall to be put above the mouse
        helpBubbleTop = documentHeight - helpBubbleHeight - iTopMargin
      }
      else
      {
        var iCursorSize = 20
        helpBubbleTop = Math.max(0, helpBubbleTop - helpBubbleHeight - iCursorSize)
      }
      
      bForcedOffset = true
    }
    if( helpBubbleLeft + helpBubbleWidth > documentWidth )
    {
      helpBubbleLeft = Math.max(0, documentWidth - helpBubbleWidth)
      bForcedOffset = true
    }
  }
  
  // Set the helpBubble position
  helpBubble.style.left = helpBubbleLeft + "px"
  helpBubble.style.top = helpBubbleTop + "px"
  
  // Hide/unhide the pointer div. If we had to move the helpBubble away because
  // it overflowed outside the document, we don't show the pointer.
  if( ( bIsOnClick || options.bForcePointer ) && !bForcedOffset )
    helpBubbleContainer.pointer.style.visibility = "visible"
  else
    helpBubbleContainer.pointer.style.visibility = "hidden"
  
  // Set other infos useful for the helpBubbles events.
  helpBubble.bIsDisplayed = true
  helpBubble.bWasDisplayedOnOver = !bIsOnClick
  helpBubble.link = helpBubbleContent.link // memo
  
  // Cancel event bubbling
  MouseEventHelper.stopBubbling(event)
}

HelpBubbleHelper.hideHelpBubble = function()
{
  var helpBubble = HelpBubbleHelper.getHelpBubble()
  
  // Clear any current timeout
  clearTimeout(HelpBubbleHelper.timerRef)
  
  if( helpBubble && helpBubble.parentNode )
  {
    helpBubble.style.display = "none"
    helpBubble.bIsDisplayed = false
    helpBubble.bWasDisplayedOnOver = false
  }
}

/******************************************************************************
 * HelpBubble : uses HelpBubbleHelper to allow easy help bubbles instanciation
 * @usage : if you want to put a helpBubble while hovering a given html element,
 *   simply call : new HelpBubble(htmlElement, "Popup text")
 * @more : for more advanced usage, see the constructor parameters.
 *****************************************************************************/

/* HelpBubble options. Cf. its default member for available options */
var HelpBubbleOptions = {}

/* Enum containing the possible attachment modes for the helpBubble */
HelpBubbleOptions.EAttachmentMode =
{
  ON_CLICK : 1,
  ON_OVER : 2
}

/* Default options given for the HelpBubble constructor.
 * If you want to specify other options, simply build a similar object and
 * redefine only what you need to.
 */
HelpBubbleOptions.baseOptions =
{
  eAttachmentMode : HelpBubbleOptions.EAttachmentMode.ON_OVER,
  strClassNames : "", // No additionnal class names by default (allows users 
                      // to redefine the css of their helpBubble easily.
  
  // ON_OVER related options :
  iTimeout : 500,     // Time before showing the helpBubble, in ms.
  bForcePointer : false,  // force the display of the pointer div
  bFollowMouse : true,    // make the bubble follow the mouse on mouse move
  iRedrawingDistance : 10 // Distance below which the popup won't be redrawn
}

/* Constructor : validates the parameters, 
 * @param parentElement : html element on which you want to display the help
 * @param helpBubbleContent : content to display in the helpBubble. Should be 
 *   an object with a "title" and a "content" elements (which should be strings
 *   or xml trees), or simply the content element itself.
 * @param options : cf HelpBubbleOptions description above.
 * @throws an exception if anything bad happens (wrong param or creation fail)
 */
function HelpBubble(parentElement, helpBubbleContent, options)
{
  // check the params correctness and store them into the this.
  this.parentElement = parentElement
  this.helpBubbleContent = helpBubbleContent
  this.options = options
  
  var bOk = this.NormaliseParameters()
  if( !bOk )
    throw "Wrong parameters. Check them before calling the HelpBubble constructor."
  
  // Some functions we will define below won't have the right "this" any more 
  // when they will be called, so we use the local thisRef variable instead.
  var thisRef = this
  
  if( thisRef.options.eAttachmentMode == HelpBubbleOptions.EAttachmentMode.ON_CLICK )
  {
    // Keep track of the parent element so that we can get its absolute 
    // position later on.
    thisRef.helpBubbleContent.link = parentElement
    
    parentElement.onclick = function(e)
    {
      // Always use the same helpBubble skeleton
      var helpBubble = HelpBubbleHelper.getHelpBubble()
      
      if( !helpBubble.bIsDisplayed || helpBubble.link != parentElement )
      {
        // Reset the bubble onmouseout for onclick events
        helpBubble.helpBubbleContainer.onmouseout = null
        
        HelpBubbleHelper.setClassNames(thisRef.options.strClassNames)
        HelpBubbleHelper.displayHelpBubble(e, thisRef.helpBubbleContent, thisRef.options)
      }
      else
        HelpBubbleHelper.hideHelpBubble()
      
      return false  // Avoid resubmiting the page
    }
  }
  else
  {
    // used to keep track of the mouse last location
    var oldMouseCoords
    
    // Update the mouse coords 
    var updateOldMouseCoords = function(e)
    {
      oldMouseCoords = MouseEventHelper.getCoordinates(e)
    }
    
    // Make the bubble follow the mouse on over
    var followMouse = function(e)
    {
      // Always use the same helpBubble skeleton
      var helpBubble = HelpBubbleHelper.getHelpBubble()
      
      // Display a helpBubble only if it was not displayed via onclick
      // and the mouse position changed too much
      if( helpBubble.bIsDisplayed && helpBubble.bWasDisplayedOnOver )
      {
        // Avoid redrawing it if the mouse didn't move far enough
        var currentMouseCoords = MouseEventHelper.getCoordinates(e)
        var iDistance = MouseEventHelper.getDistance(oldMouseCoords, 
                                                     currentMouseCoords)
        if( iDistance > thisRef.options.iRedrawingDistance )
        {
          HelpBubbleHelper.displayHelpBubble(e, thisRef.helpBubbleContent, thisRef.options)
          oldMouseCoords = currentMouseCoords
        }
      }
      
      // Reuse the same timer ref
      HelpBubbleHelper.timerRef = setTimeout(
        function()
        {
          parentElement.onmousemove = followMouse
        }, 200)
      
      // Avoid sluggying the client
      parentElement.onmousemove = null
    }
    
    // On mouse over : set a timer to display the help
    parentElement.onmouseover = function(e)
    {
      // Always use the same helpBubble skeleton
      var helpBubble = HelpBubbleHelper.getHelpBubble()
      
      // Clear any current timeout
      clearTimeout(HelpBubbleHelper.timerRef)
      
      if( !helpBubble.bIsDisplayed )
      {
        // Set the user provided css class names
        HelpBubbleHelper.setClassNames(thisRef.options.strClassNames)
        // Set the oldMouseCoords with the current coords.
        oldMouseCoords = MouseEventHelper.getCoordinates(e)
        
        // Show help after a given time
        HelpBubbleHelper.timerRef = setTimeout(
          function()
          {
            // Prevent the bubble from surviving when the mouse goes outside.
            helpBubble.helpBubbleContainer.onmouseout = parentElement.onmouseout
            
            // Display the bubble
            HelpBubbleHelper.displayHelpBubble(oldMouseCoords, 
                                               thisRef.helpBubbleContent, 
                                               thisRef.options)
            
            if( thisRef.options.bFollowMouse )
              parentElement.onmousemove = followMouse
            else
              parentElement.onmousemove = null
          }, thisRef.options.iTimeout)
        
        // Set onmousemove to update the coords we will give to displayHelpBubble
        parentElement.onmousemove = updateOldMouseCoords
      }
    }
    
    parentElement.onmouseout = function(e)
    {
      // Check for event bubbling before hiding the helpBubble
      var node = MouseEventHelper.getEventOutTarget(e)
      var helpBubble = HelpBubbleHelper.getHelpBubble()
      
      while( node && node != parentElement && node != helpBubble )
        node = node.parentNode
      
      // => if the event out target was inside our parentElement (node is true)
      // then we don't reset what ought to be.
      // NB : if node was inside the help bubble, we ignore this event too, in
      // order to avoid hiding it if it appeared beneath the mouse.
      
      if( !node )
      {
        // Clear any current timeout
        clearTimeout(HelpBubbleHelper.timerRef)
        
        // Reset onmousemove
        parentElement.onmousemove = null
        
        // Hide the help bubble if it was displayed on over
        if( helpBubble.bWasDisplayedOnOver )
          HelpBubbleHelper.hideHelpBubble()
      }
    }
  }
}

/* Destroys the helpBubble if you don't want it anymore
 */
HelpBubble.prototype.destroy = function()
{
  if( this.parentElement )
  {
    // Removes the event handles
    if( this.options.eAttachmentMode == HelpBubbleOptions.EAttachmentMode.ON_CLICK )
      this.parentElement.onclick = null
    else
    {
      this.parentElement.onmouseover = null
      this.parentElement.onmousemove = null
      this.parentElement.onmouseout = null
    }
  }
}

/******************************************************************************
 * Checks the consctructor parameters are what we expected
 * @return true if ok, false otherwise
 *****************************************************************************/
HelpBubble.prototype.NormaliseParameters = function()
{
  // Assert the parentElement is indeed an element
  if( !this.parentElement || typeof this.parentElement != "object" )
    return false
  
  // Internally we need the helpBubbleContent parameter to be an object with the 
  // following elements, which may have a string or object/HTML type :
  // * title : (optional) helpBubble title.
  // * content : helpBubble main text.
  // If the user gave us a single string (resp. object without a content 
  // attribute), we consider this string (resp. object) to be the desired content.
  if( typeof this.helpBubbleContent == "string" )
    this.helpBubbleContent = { content : this.helpBubbleContent }
  else if( typeof this.helpBubbleContent == "object" && !this.helpBubbleContent.content )
    this.helpBubbleContent = { content : this.helpBubbleContent }
  else if( !this.helpBubbleContent || typeof this.helpBubbleContent != "object" )
    return false
  
  // Loop through all possible options
  this.options = ( this.options && typeof this.options == "object" ) ? clone(this.options) : {}
  
  if( this.options.eAttachmentMode != HelpBubbleOptions.EAttachmentMode.ON_OVER
   && this.options.eAttachmentMode != HelpBubbleOptions.EAttachmentMode.ON_CLICK )
    this.options.eAttachmentMode = HelpBubbleOptions.baseOptions.eAttachmentMode
  
  if( typeof this.options.strClassNames != "string" )
    this.options.strClassNames = HelpBubbleOptions.baseOptions.strClassNames
  
  if( typeof this.options.iTimeout != "number" )
    this.options.iTimeout = HelpBubbleOptions.baseOptions.iTimeout
  
  if( typeof this.options.bForcePointer != "boolean" )
    this.options.bForcePointer = HelpBubbleOptions.baseOptions.bForcePointer
  
  if( typeof this.options.bFollowMouse != "boolean" )
    this.options.bFollowMouse = HelpBubbleOptions.baseOptions.bFollowMouse
  
  if( typeof this.options.iRedrawingDistance != "number" )
    this.options.iRedrawingDistance = HelpBubbleOptions.baseOptions.iRedrawingDistance
  
  // OK
  return true
}

/* Create an help bubble on over for parentElement, and scan this element
 * in order to find its image to attach the same bubble on click.
 */
function HelpBubblePair(parentElement, bubbleContent, 
                        optionsOnOver, optionsOnClick)
{
  // HelpBubble on over for this element
  this.onOverBubble = new HelpBubble(parentElement, bubbleContent, optionsOnOver)
  
  // HelpBubble on click on this element's image, if any
  var innerImage = parentElement.lastChild
  while( innerImage && innerImage.nodeName != "img" 
                    && innerImage.nodeName != "IMG" )
    innerImage = innerImage.previousSibling
  
  if( innerImage )
    this.onClickBubble = new HelpBubble(innerImage, bubbleContent, optionsOnClick)
}

/* Destroys the helpBubble if you don't want it anymore
 */
HelpBubblePair.prototype.destroy = function()
{
  if( this.onOverBubble )
    this.onOverBubble.destroy()
  if( this.onClickBubble )
    this.onClickBubble.destroy()
}

// WebAppInHtmlDialog : display a webApp within a dialog
// [in] internalName : internal name of the webApp to display in the dialog
// [in] size : object to override the dialog size (default is {width: '650px', height: '360px'})
function WebAppInHtmlDialog(internalName, size, onValidFunction)
{
  this.internalName = internalName
  this.dialog = new HtmlDialog("", "", this, true)
  this.dialog.setLoading(false)
  this.onValidFunction = onValidFunction
  
  if( isIE() && getIEVersion() < 9 )
  {
    var strScrolling = 'yes'
    if (size.scrolling != undefined &&
        size.scrolling == 'no')
      strScrolling = 'no'
    var strIframe = "<iframe scrolling="+strScrolling+" name='" + internalName + "' src='/xtk/jsp/preload.htm' frameborder='0' style='overflow:auto;width:100%' height='"
    
    if( typeof size != undefined )
      strIframe += size.height 
    else
      strIframe += "350px"    
      
    strIframe += "' onload='window." + internalName + ".frameElement.style.height = (window." + internalName + ".document.body.scrollHeight + 10 +\"px\");"
               + " if( this.parentNode && this.parentNode.oDlg ) this.parentNode.oDlg.dialog.setSize(null, window." + internalName + ".frameElement.style.height-20)"
               + "'></iframe>"
    this.iframe = document.createElement(strIframe)
  }
  else
  {
    // build dialog content or get it
    this.iframe = document.createElement("iframe")
    this.iframe.style.width  = "100%"
    this.iframe.name = this.iframe.id = internalName
    this.iframe.src = "/xtk/jsp/preload.htm"
    this.iframe.frameBorder = 0
    this.iframe.onload = function ()
    {
      // resize the dialog
      var innerFrame = window[internalName].frameElement 
      innerFrame.style.height = (window[internalName].document.body.scrollHeight) + "px"
      if( this.iframe )
        this.iframe.parentDialog.dialog.setSize(null, innerFrame.style.height)
    }
    if( typeof size != undefined )
      this.iframe.style.height = size.height
    else
      this.iframe.style.height = "360px"    
  }
  
  if( isIE() )
    this.iframe.parentDialog = this
  else
    this.dialog.divContent.oDlg = this

  if( typeof size != undefined )
    this.dialog.setSize(size.width)
  else
    this.dialog.setSize("620px")

  this.dialog.buttonOk.style.display = "none"
  this.dialog.buttonCancel.style.display = "none"
  this.dialog.addContent(this.iframe)
  this.dialog.divContent.style.padding = "0"
}

// show the webApp with specified title and parameters
WebAppInHtmlDialog.prototype.show = function(strParameters, strTitle)
{
  // Don't update the iframe's src before it's complete as IE will try to GET 
  // the page on each iframe.src change
  var src = "/webApp/" + this.internalName + "?_decorate=0&_className=webAppInDialog" 
  if( strParameters != null && strParameters != "" )
    src += "&" + strParameters
  
  this.iframe.src = src
  
  if (typeof strTitle != "undefined" && strTitle != "")
    this.dialog.setTitle(strTitle)

  this.dialog.show(true)
}

WebAppInHtmlDialog.prototype.setTitle = function(strTitle)
{
  this.dialog.setTitle(strTitle)
}

WebAppInHtmlDialog.prototype.setLeftImg = function(strImg)
{
  this.dialog.setLeftImg(strImg)
}

// hide the webApp 
WebAppInHtmlDialog.prototype.webAppEnded = function(bHide, extraParameters)
{
  if( bHide )
  {
    this.dialog.show(false)
    window[this.internalName].innerHTML = "";
    this.iframe.src = ""
    this.dialog.deleteContent()
  }
  if( typeof this.onValidFunction == "function" )
    this.onValidFunction(extraParameters)
}

//---------------------------------------------------------------------------
// helper function for client side code which checks the cookie to determine
// the current mode of operation.
// @param elView : view as defined in the navtrees
// @param strConsoleParam : parameters which should be appended to console 
// URL. If it is empty, this function will use the params defined in the view.
// @param strWebParam : parameters which should be appended to web URL.
//---------------------------------------------------------------------------
function urlFromViewClientHelper(strXMLView, strConsoleParam, strWebParam)
{
  var strURL = "#"
  var elView = parseXMLString(strXMLView).documentElement
  if( elView && elView.nodeName != "parsererror" )
  {
    var bConsole = isNeolaneConsole()
    var strParam = bConsole ? strConsoleParam : strWebParam
    if( typeof strParam != "string" )
      strParam = ""
    
    // Default view params are for xtk links only
    var bDontUseDefaultParam = !bConsole || strParam != ""
    
    strURL = urlFromView(elView, bConsole, true, bDontUseDefaultParam)
    
    // We add complementary arguments
    if( bDontUseDefaultParam && strParam )
    {
      if( strURL.indexOf("?") == -1 )
        strURL += "?" + strParam
      else
        strURL += "&amp;" + strParam
    }
  }
  
  window.location.href = strURL
  
  return strURL
}

