/*
 * Javascript for hierarchical pop-up menus for Durham Bird Club website
 * Written by (c) S.J.Buckle (software genius) December 2007
 */

/*
 * The constant menu_hanging_ms stores the number of milliseconds after which
 * the user leaves an option that has a pop-up submenu, before that submenu
 * should disappear.  This delay is necessary because of the way the HTML and
 * this JavaScript are currently written.  At the moment they are written so
 * that the mouse leaving the link (HTML element of type <a href="...">...</a>)
 * on a menu option is detected, but the mouse leaving the outermost
 * rectangular DIV of the option is not.  This means that when the 'mouseout'
 * event is detected, the mouse may still be within the rectangular DIV *and*
 * the user may be moving it over to the pop-up submenu.  We do not want the
 * submenu to disappear before the user has a chance to get the mouse over it.
 * Originally the value of this constant was 250 milliseconds, but one DBC
 * member complained that she could not get the mouse across to the submenus
 * fast enough.  After it was changed to 333 milliseconds, this member
 * indicated that it was now OK.
 */
var menu_hanging_ms = 333

/*
 * function link_mouseover(link)
 *
 * This function is the JavaScript which is run when the user moves their mouse
 * cursor over a link (HTML object of type <a href="...">...</a>) within a menu
 * option 'div' (object of type <div>...</div> and CSS class 'option').  It
 * protects any containing higher-level pop-up submenus from being made
 * invisible, then makes all other pop-up submenus invisible; it then displays
 * any lower-level pop-up submenus which are associated with this menu option.
 *
 * The single parameter 'link' is a reference to the link object.
 */
function link_mouseover(link)
{
  var ancestor

  /* Acquire a reference to the option 'div' (HTML object of type <div>...
   * </div> and CSS class 'option') which contains the link.  Do this by
   * assigning a reference to the link's parent object to a variable
   * 'ancestor'.  If the CSS class of 'ancestor' is not 'option', then ancestor
   * is assigned a reference to *its* parent node, and the check is repeated,
   * and so on until 'ancestor' points to a 'div' of CSS class 'option'.
   */

  for ( ancestor = link.parentNode;
    ancestor.className != 'option';
    ancestor = ancestor.parentNode )
  {}

  var option = ancestor

  /*
   * Protect any containing higher-level submenus (HTML objects of type
   * <div>...</div> and CSS class 'submenu') from being made invisible, by
   * setting the value of their recorded attribute 'descendent-hovered' to 'y'
   * (standing for 'yes').  The code which will otherwise make these submenu(s)
   * disappear is, firstly, the next section of this function, which calls
   * object_hide_unhovered_below() and passes the top-level menu object;
   * secondly, the function mouseout_timer() which frequently executes on an
   * asynchronous basis while the user is moving the mouse cursor around the
   * menu.  More specifically, mouseout_timer() is executed 'menu_hanging_ms'
   * milliseconds after the user moves out of a menu option, which could have
   * happened any number of times during the last 'menu_hanging_ms'
   * 'milliseconds' BEFORE the user moved over THIS menu option.
   *
   * Another thing that next bit of code does is highlight (e.g. change colour
   * to red) the link associated with each containing higher-level option 'div'
   * (HTML object of type <div>...</div> and CSS class 'option').  Note that
   * this code does NOT highlight the link for which this function was
   * executed, i.e. the link that the user moved their mouse cursor over: this
   * link can be automatically highlighted by way of an a:hover { } clause in
   * style sheet (CSS document), it is not necessary to do it in JavaScript.
   */

  for ( ancestor = option.parentNode;
    ancestor.className != 'menu';
    ancestor = ancestor.parentNode )
  {
    if (ancestor.className == 'submenu')
    {
      ancestor.setAttribute( 'descendent-hovered', 'y' )
    }
    else if (ancestor.className == 'option')
    {
      option_highlight_first_link(ancestor)
    }
  }

  /*
   * It is quite possible that there are still other pop-up submenus displaying
   * which no longer should be, since they are not 'below' the option which the
   * user has now moved over.  If we didn't do anything now, these would
   * disappear anyway after a few milliseconds, when the function
   * mouseout_timer() next executes asynchronously.  However, it looks a lot
   * better if they disappear immediately the user moves their mouse cursor
   * over a different option.  We achieve this by calling the function
   * object_hide_unhovered_below() with a reference to the top-level menu
   * 'div'.  Fortunately, a by-product of the previous chunk of code is that
   * 'ancestor' now points to the top-level menu object, so we can pass its
   * value as the parameter to object_hide_unhovered_below().  What
   * object_hide_unhovered_below() does is traverse the entire menu structure
   * below the object passed to it (in this case this will be the entire menu
   * structure, full stop) and for any objects in that structure which have CSS
   * class 'submenu' (i.e. submenus) it checks if their value of the
   * 'descendent-hovered' attribute is 'y'.  If it is NOT, it causes that
   * submenu to disappear (by setting its 'visibility' attribute to 'hidden' -
   * this attribute is defined by CSS).  The submenus which are NOT associated
   * with OUR option will NOT have 'descendent-hovered' == 'y', in other words,
   * before we started setting 'descendent-hovered' to 'y' for submenus below
   * our option, NO submenus had this attribute set.  How do we know this?
   * Because, before the user moved their mouse cursor over our option, he or
   * she MUST have moved it OUT of another option, and whenever this happens,
   * the function menu_mouseout() is executed, and this makes sure that ALL
   * submenus have this attribute set to 'n'.
   */

  var menu = ancestor

  object_hide_unhovered_below(menu)

  /*
   * The last step in this function is to make sure that any submenus LOWER in
   * the hierarchy than the option hovered over by the user ARE displayed.  In
   * other words, the submenus 'below' our option.  Note that we do not have to
   * worry about displaying the submenu which CONTAINS our option, nor any of
   * its 'ancestor' submenus - they must already be visible, otherwise the user
   * would not have been able to move the mouse cursor over our option!  This
   * last step is achieved by the call to option_hover( option, 'y' ).  The
   * second parameter, by way of its value 'y', indicates that we do not need
   * to highlight the link for the option being passed (e.g. change its colour
   * to red) because, as already mentioned, this can be done using the
   * a:hover { ... } clause in the CSS file.
   */

  option_hover( option, 'y' )
}

/*
 * function option_highlight_first_link(option)
 *
 * This function must be passed an option 'div' (HTML object of type <div>...
 * </div> and CSS class 'option).  It highlights (e.g. changes the colour to
 * red) the link (HTML object of type <a href="...">...</a>) which is a child
 * of this option 'div'.  Although it is called 'option_highlight_FIRST_link()'
 * according to the way the HTML and JavaScript for the menus are currently
 * designed, there should only be ONE link which is a child of the option
 * 'div'.
 */
function option_highlight_first_link(option)
{
  for ( var child = option.firstChild;
    child != null;
    child = child.nextSibling )
  {
    if (child.tagName == 'A')
    {
      child.style.color = 'red'
      return
    }
  }
}

/*
 * function object_hide_unhovered_below(obj)
 *
 * This function traverses the menu structure below the object passed to it
 * and, for any objects in that structure which have CSS class 'submenu' (i.e.
 * submenus) it checks if their value of the 'descendent-hovered' attribute is
 * 'y'.  If it is NOT, it causes that submenu to disappear by setting its
 * 'visibility' attribute to 'hidden' (this attribute is defined by CSS).
 */
function object_hide_unhovered_below(obj)
{
  for ( var child = obj.firstChild; child != null; child = child.nextSibling )
  {
    if (child.className == 'submenu')
    {
      object_hide_unhovered_below(child)

      if (child.getAttribute('descendent-hovered') != 'y')
      {
        child.style.visibility = 'hidden'
      }
    }
    else if (child.className == 'option')
    {
      object_hide_unhovered_below(child)
    }
  }
}

/*
 * function option_hover( option, mouse_really_over )
 *
 * This is the function that causes any pop-up submenus associated with a menu
 * option to appear, i.e. to 'pop up'.  If the value of the parameter
 * mouse_really_over is not 'y', this function also highlights (e.g. changes
 * the colour to red) the link associated with the passed option (if
 * mouse_really_over IS 'y', i.e. the user's mouse cursor is actually hovering
 * over this option), then the link can be highlighted using the a:hover {...}
 * clause in the CSS file.
 *
 * If there is a submenu associated with the given option, the first option on
 * the submenu is passed again, recursively, to this function: in other words,
 * if there is ANOTHER submenu associated with the first option on the first
 * submenu, THAT submenu will also 'pop up', and that first option will also be
 * highlighted; the same check will then repeated for the first option on the
 * SECOND submenu, and so on (recursively).
 */
function option_hover( option, mouse_really_over )
{
  var child
  var link = null
  var submenu = null
  var href = null

  /*
   * The following code finds the first child object of 'option' which is
   * either a link or a submenu.  It it is a link, it ensures that it is
   * highlighted (e.g. its colour changes to red), and that is all that this
   * function does.  The function returns the value of the link's href
   * qualifier.  If the first child is a submenu, then this submenu needs to be
   * 'popped up', and then the first option on that submenu needs to be
   * processed by this function, recursively.  This will be done by the
   * subsequent bit of code.
   */

  for ( child = option.firstChild;
    child != null;
    child = child.nextSibling )
  {
    if ((link == null) && (child.tagName == 'A'))
    {
      link = child
      href = link.href

      if (mouse_really_over != 'y')
      {
        link.style.color = 'red'
      }
    }
    else if ((submenu == null) && (child.className == 'submenu'))
    {
      submenu = child;
    }
  }

  if (submenu != null)
  {
    /*
     * The first child of the option passed to this function was a submenu.  We
     * need to 'pop up' this submenu.  Check whether the 'left' and 'top'
     * coordinates for the submenu, which specify where it should appear, have
     * already been calculated and recorded for this submenu.
     */

    if (submenu.getAttribute('positioned') != 'y')
    {
      /*
       * The 'left' and 'top' coordinates of this submenu have not already been
       * calculated.  We need to calculate them.  Start by determining the x
       * and y offsets of the 'origin' of this option, relative to the top-left
       * corner of the web page.  The 'origin' of an object is the point from
       * which CSS measures coordinates specified for that object.
       */
      cache_origin_offset(option)

      /*
       * Now that we have the x and y offsets of the origin of the option,
       * relative to the top-left corner of the web page, we can determine the
       * actual ( x, y ) coordinates of the option, relative to the top-left
       * corner of the web page.
       */
      var option_doc_x =
        parseInt(option.getAttribute('origin_offset_x')) + option.offsetLeft;
      var option_doc_y =
        parseInt(option.getAttribute('origin_offset_y')) + option.offsetTop;

      /*
       * Determine the x and y offsets of the 'origin' of the submenu, relative
       * to the top-left corner of the web page.
       */
      cache_origin_offset(submenu)

      /*
       * Obtain a reference to the parent menu or submenu of this submenu.
       */

      var ancestor

      for ( ancestor = option.parentNode;
           (ancestor.className != 'submenu')
        && (ancestor.className != 'menu');
        ancestor = ancestor.parentNode )
      {}

      /*
       * Determine the width of the parent menu or submenu and use this to
       * position THIS submenu to the right of all of THAT submenu's options
       * (including THIS option).
       */

      parent_menu_offset_width = ancestor.offsetWidth

      submenu.style.left =
        (option_doc_x - parseInt(submenu.getAttribute('origin_offset_x'))
        + parent_menu_offset_width - 1) + 'px'
      submenu.style.top =
        (option_doc_y - 1 - parseInt(submenu.getAttribute('origin_offset_y')))
        + 'px'

      /*
       * Record the fact that this submenu has now been positioned, so we don't
       * have to do it again.
       */
      submenu.setAttribute( 'positioned', 'y' )
    }

    /*
     * The submenu has now been positioned.  Make it appear ('pop up').
     */
    submenu.style.visibility = 'visible'

    /*
     * Protect the submenu from being made invisible by setting the value of
     * its recorded attribute 'descendent-hovered' to 'y' (standing for 'yes').
     * If we did not do this, the submenu could be made invisible again at any
     * time by the function mouseout_timer().  This function frequently
     * executes on an asynchronous basis and it makes all submenus invisible
     * that do not have this attribute set to 'y'.  More specifically,
     * mouseout_timer() is executed 'menu_hanging_ms' milliseconds after the
     * user moves out of a menu option.  This could actually have happened any
     * number of times during the last 'menu_hanging_ms' 'milliseconds' BEFORE
     * the user moved over THIS menu option, so at this point in time there
     * could be any number of timers waiting to expire and potentially trigger
     * an execution of mouseout_timer().
     */
    submenu.setAttribute( 'descendent-hovered', 'y' )

    /*
     * Obtain a reference to the first option on this newly popped-up submenu.
     */
    var first_option = null

    for ( child = submenu.firstChild;
      (first_option == null) && (child != null);
      child = child.nextSibling )
    {
      if (child.className == 'option')
      {
        first_option = child
      }
    }

    /*
     * Call this function recursively for the first option on the submenu, to
     * highlight that option and pop up any submenus which associated with IT.
     */
    if (first_option != null)
    {
      href = option_hover( first_option, 'n' )

      link.href = href
    }
  }

  return href
}

/*
 * function cache_origin_offset(obj)
 *
 * This function determines and records the offset of the given object's
 * origin from the top-left corner of the web page.  The object's origin is the
 * point that CSS measures coordinates for that object from.  For example, if
 * the CSS attribute 'left' of the object were set to a given value, the
 * x-coordinate of the object, relative to the top-left corner of the web page,
 * would become the set value PLUS the x offset of the given object's origin.
 *
 * This function first checks whether the offset of the given object's origin
 * has already been determined, in which case it will have been recorded
 * (cached) as values of the attributes origin_offset_x and origin_offset_y for
 * the object.  If the x and y offsets of the object's origin have not already
 * been determined and cached, this function determines and caches them.  The x
 * and y offsets of the object's origin are determined by first determining
 * the x and y offsets of the object's parent's origin.  Then the x and y
 * offsets of the object's origin, RELATIVE to the PARENT'S origin, are added.
 *
 * Note that the x and y offsets of the PARENT'S origin are determined by a
 * recursive call to this function: thus, if THEY have already been calculated
 * and cached previously, the cached values will be re-used.
 */
function cache_origin_offset(obj)
{
  if (obj.getAttribute('origin_offset_cached') != 'y')
  {
    var offset_parent = obj.offsetParent

    if (offset_parent == null)
    {
      /*
       * The object passed to this function has no parent object.  It is the
       * top-level HTML object, perhaps 'body' or 'document'.  In this case the
       * desired x and y offsets of the object's origin, from the top-left
       * corner of the web page, can be found as the values of the CSS
       * attributes offsetLeft and offsetTop.
       */
      obj.setAttribute( 'origin_offset_x', obj.offsetLeft )
      obj.setAttribute( 'origin_offset_y', obj.offsetTop )
    }
    else
    {
      /*
       * The object passed to this function has a parent object.  Determine and
       * cache the x and y offsets of the parent's origin, from the top-left
       * corner of the web page, by calling this function again recursively.
       * Then add the x and y offsets of the object's origin, RELATIVE to the
       * PARENT'S origin.  Then record these new x and y offsets as the values
       * of the attributes origin_offset_x and origin_offset_y for the object.
       */
      cache_origin_offset(offset_parent)

      obj.setAttribute( 'origin_offset_x',
        parseInt(offset_parent.getAttribute('origin_offset_x'))
        + offset_parent.offsetLeft )

      obj.setAttribute( 'origin_offset_y',
        parseInt(offset_parent.getAttribute('origin_offset_y'))
        + offset_parent.offsetTop )
    }

    /*
     * Record the fact that the x and y offsets of the object's origin have
     * been determined and cached.
     */
    obj.setAttribute( 'origin_offset_cached', 'y' )
  }
}

/*
 * function object_unhover(obj)
 *
 * This function traverses the menu structure below object 'obj' and resets the
 * value of the 'descendent-hovered' attribute for all submenus here to 'n'.
 * This does NOT (immediately) cause the submenus to disappear, but it DOES
 * mark them as available for 'disappearing' and they will be disappeared the
 * next time the function object_hide_unhovered() is called for these submenus.
 * This function also un-highlights any links in the menu structure (e.g.
 * changes the colour of the text of such links from red back to white).
 */
function object_unhover(obj)
{
  if (   (obj.className == 'menu')
      || (obj.className == 'option')
     )
  {
    for ( var child = obj.firstChild;
      child != null;
      child = child.nextSibling )
    {
      object_unhover(child)
    }
  }
  else if (obj.className == 'submenu')
  {
    if (obj.getAttribute('descendent-hovered') == 'y')
    {
      obj.setAttribute( 'descendent-hovered', 'n' )

      for ( var child = obj.firstChild;
        child != null;
        child = child.nextSibling )
      {
        object_unhover(child)
      }
    }
  }
  else if (   (obj.tagName == 'A')
           && (obj.style.color == 'red')
          )
  {
    obj.style.color = '' /* go back to the colour defined in the style sheet */
  }
}

var current_mouseout_timer_id = 0

/*
 * function menu_mouseout(option)
 *
 * This function is the JavaScript which is run whenever the user moves the
 * mouse cursor OUT of a menu option, be this on the main menu or on a pop-up
 * submenu.  On the assumption that this means that the user is no longer
 * hovering over ANY option, this function marks ALL pop-up submenus as
 * eligible for disappearance.  However, it does not 'disappear' them
 * immediately.  Rather, it schedules the function 'mouseout_timer()' to be run
 * after a delay of 'menu_hanging_ms'.  When this time expires, the function
 * 'mouseout_timer()' will run and WILL 'disappear' all submenus which are
 * marked for disappearance.  HOWEVER, by that time, the user might have moved
 * the mouse cursor over a link on one of the menu's (or still visible sub-
 * menu's) options, triggering the execution of link_mouseover(), which may
 * have marked one or more of the submenus as no longer eligible for
 * disappearance.  This gives the user time, after moving their mouse cursor
 * out of a menu option, to move it over to one of the options on any visible
 * pop-up submenus, before they disappear.
 *
 * Note that when this function is executed, it wants to have the effect of
 * 'cancel' all currently running timers, because the fact that it has been
 * marker
 */
function menu_mouseout(option)
{
  /*
   * Obtain a reference to the top-level menu object (HTML object of type
   * <div>...</div> and CSS class 'menu').
   */
  var ancestor

  for ( ancestor = option;
    ancestor.className != 'menu';
    ancestor = ancestor.parentNode )
  {}

  var menu = ancestor

  /*
   * Mark all submenus below this object as eligible for disappearance.
   */
  object_unhover(menu)

  /*
   * Schedule the function mouseout_timer() to be executed after the expiry of
   * a timer of duration 'menu_hanging_ms' milliseconds.
   */
  setTimeout(
    "mouseout_timer('" + menu.id + "', " + ++current_mouseout_timer_id + " )",
    menu_hanging_ms )
}

/*
 * function mouseout_timer( menu_id, mouseout_timer_id )
 *
 * This function 'disappears' all submenus below the object identified by the
 * parameter 'menu_id', except for those which are marked as non-eligible for
 * disappearance, by virtue of the fact that they have the value 'y' for the
 * attribute 'descendent-hovered'.
 */
function mouseout_timer( menu_id, mouseout_timer_id )
{
  if (mouseout_timer_id == current_mouseout_timer_id )
  {
    var menu = document.getElementById(menu_id)

    for ( var child = menu.firstChild;
      child != null;
      child = child.nextSibling )
    {
      if (child.className == 'option')
      {
        object_hide_unhovered_below(child)
      }
    }
  }
}
