// Copyright (C) 2007-2010 Bristle Software, Inc.
// 
// This program 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 1, or (at your option)
// any later version.
// 
// This program 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 this program; if not, write to the Free Software
// Foundation, Inc.

/******************************************************************************
* com.bristle.jslib.Event.js
*******************************************************************************
* Purpose:
*       This file contains routines to manage HTML events.
* Usage:
*       - The typical scenario for using this file from an HTML file is:
*         <script language='JavaScript' src='com.bristle.jslib.Event.js'></script>
*         Call the various functions that reside here.
* Assumptions:
* Effects:
*       - None.
* Anticipated Changes:
* Notes:
* Implementation Notes:
* Portability Issues:
* Revision History:
*   $Log$
******************************************************************************/

// Create the "namespace" to hold the functions in this file.
com.bristle.jslib.Event = {};


// Keycode constants, for use with window.event.keycode
// For a more complete list, see Appendix C of O'Reilly DHTML book.
com.bristle.jslib.Event.intKEYCODE_BACKSPACE   = 8;
com.bristle.jslib.Event.intKEYCODE_ENTER       = 13;
com.bristle.jslib.Event.intKEYCODE_ESC         = 27;
com.bristle.jslib.Event.intKEYCODE_END         = 35;
com.bristle.jslib.Event.intKEYCODE_HOME        = 36;
com.bristle.jslib.Event.intKEYCODE_LEFT_ARROW  = 37;
com.bristle.jslib.Event.intKEYCODE_UP_ARROW    = 38;
com.bristle.jslib.Event.intKEYCODE_RIGHT_ARROW = 39;
com.bristle.jslib.Event.intKEYCODE_DOWN_ARROW  = 40;

// Constants to do with event capture.
com.bristle.jslib.Event.blnCAPTURE = true;

/******************************************************************************
* Fire the specified event for the specified HTML control.
*
* In some browsers, it doesn't actually fire the event.  Instead it executes
* the code specified in the onxxx property of the control.  This has largely 
* the same effect as firing the event, but if the code referred to the special
* "event" object that value may be null.
*
*@param objControl          Control to fire event for.
*@param strEventName        Name of event to fire.
*@param blnEvenIfDisabled   If true, enable the control, then fire the event, 
*                           then restore the enabled/disabled status of the 
*                           control.  Otherwise, the event may not fire.
*@return None.
*@throws com.bristle.jslib.Exception.intEXC_UNABLE_TO_GENERATE_UNIQUE_VAR_NAME
*                           when unable to generate a unique internal name to 
*                           use in the event firing mechanism.  Only happens
*                           in browsers with no native support for fireEvent()
*                           and only after generating many internal names that
*                           already existed.
******************************************************************************/
com.bristle.jslib.Event.blnFIRE_EVENT_EVEN_IF_DISABLED = true;
com.bristle.jslib.Event.fireEvent =
function(objControl, strEventName, blnEvenIfDisabled)
{
    if (typeof (objControl.fireEvent) == "undefined")
    {
        // There is no built-in fireEvent() method of the specified
        // object.  Instead, run the code that would have run if the
        // event had fired.

        // Get, as a String, the name of the property that contains the 
        // Javascript code for the event.  
        // For example:
        //              objControl.onclick
        var strFullEventName = "objControl." + strEventName;

        // Get, as a JavaScript Function, the Javascript code for the event.
        // For example:
        //              function onclick(event) {
        //                  return cmdOK_onClick(event, this);
        //              }
        // Can't run this code directly, as:
        //    eval(funcJSCode);
        // because that doesn't execute the function; it declares the function.
        var funcJSCode = eval(strFullEventName);

        // Get, as a String, the Javascript code for the event.
        // For example:
        //              function onclick(event) {
        //                  return cmdOK_onClick(event, this);
        //              }
        var strJSCode = funcJSCode.toString();

        // Split into function header and function body.
        // For example:
        //              function onclick(event)
        // and:
        //              {
        //                  return cmdOK_onClick(event, this);
        //              }
        var intOpenBracePosition = strJSCode.indexOf("{");
        var strFunctionHeader = strJSCode.substring
                                    (0, 
                                     intOpenBracePosition - 1);
        var strFunctionBody = strJSCode.substring
                                    (intOpenBracePosition, 
                                     strJSCode.length);

        // Generate a random function name.
        // For example:
        //              com.bristle.jslib.Util.temp56948
        var strFunctionName = com.bristle.jslib.Util.generateUniqueVarName();

        // Get the parameter list from the header.
        // For example:
        //              (event)
        var intOpenParenPosition = strFunctionHeader.indexOf("(");
        var strFunctionParamList = 
                        strFunctionHeader.substring
                                    (intOpenParenPosition
                                    ,strFunctionHeader.length);

        // Update the function header to use the randomly generated name,
        // and the syntax for attaching the function to the namespace of
        // this Javascript library.  The use of the namespace prevents 
        // global name collisions, and the use of the random name prevents
        // local name collisions.
        // For example:
        //              com.bristle.jslib.Util.temp56948 = function(event)
        // Note:  Do not switch to the regular syntax for functions:
        //              function com.bristle.jslib.Util.temp56948(event)
        //          because it causes Firefox (version 1.5.0.10 at least) to 
        //          silently do nothing when the code is executed.
        strFunctionHeader = strFunctionName 
                          + " = function" 
                          + strFunctionParamList;

        // Change all occurrences of "this" in the function body to hard-coded
        // references to objControl.
        // For example:
        //              {
        //                  return cmdOK_onClick
        //                          (event, document.getElementById("cmdOK"));
        //              }
        strFunctionBody = strFunctionBody.replace
                                ("this", 
                                 "document.getElementById('" + objControl.id + "')");

        // Generate a string to call the function with the randomly generated 
        // name and the same parameters.
        // For example:
        //              com.bristle.jslib.Util.temp56948(event)
        var strFunctionCall = strFunctionName + strFunctionParamList;
        
        // Replace the event param (if any) in the call, with null.  Don't 
        // have a way to get the object representing the fired event.
        //?? This only works if the event procedure can handle a null event.
        //?? Any better ideas?
        // For example:
        //              com.bristle.jslib.Util.temp56948(null)
        strFunctionCall = strFunctionCall.replace("(event)", "(null)");
        strFunctionCall = strFunctionCall.replace("(event,", "(null,");

        // Create a string that deletes the function.
        // Note:  This doesn't necessarily cause garbage collection, but it 
        //        shouldn't hurt, and allows the same name to be reused by
        //        com.bristle.jslib.Util.generateUniqueVarName(). 
        // For example:
        //              delete com.bristle.jslib.Util.temp56948
        var strFunctionDelete = "delete " +  strFunctionName

        // Create a string that declares the function, calls, it and deletes it.
        // Note:  Deleting 
        // For example:
        //              com.bristle.jslib.Util.temp56948 = function(event)
        //              {
        //                  return cmdOK_onClick
        //                          (event, document.getElementById("cmdOK"));
        //              }
        //              com.bristle.jslib.Util.temp56948(null);
        //              delete com.bristle.jslib.Util.temp56948;
        strJSCode
                        = strFunctionHeader 
                        + "\n" + strFunctionBody 
                        + "\n" + strFunctionCall + ";"
                        + "\n" + strFunctionDelete + ";"
                        ;

        // Execute the Javascript code.
        eval(strJSCode);
    }
    else
    {                
        var blnSavedDisabledFlag;
        if (blnEvenIfDisabled)
        {
            blnSavedDisabledFlag = objControl.disabled;
            objControl.disabled = false;
        }
        objControl.fireEvent(strEventName);
        if (blnEvenIfDisabled)
        {
            objControl.disabled = blnSavedDisabledFlag;
        }
    }
    return true;
}

/******************************************************************************
* Queue a focus() event to occur to the specified HTML control soon.
* 
* This is useful when called from event procedures like onchange.
* When the user types a new value in a text box and then tabs to the 
* next control, the onchange event occurs *before* the focus is moved 
* to the next control in the tab order.  Setting the focus back to 
* the original control during the onchange event is not useful because
* the focus is still at that control, and will be changed to the new 
* control shortly.  Queueing the focus() event causes it to occur 
* *after* focus has moved to the next control in the tab order.
******************************************************************************/
com.bristle.jslib.Event.queueFocusEvent =
function(ctlIn)
{
    // Note:  Must pass a function pointer to setTimeout().  Passing
    //        a string, as:
    //            window.setTimeout
    //                  ("document.getElementById('" + ctlIn.id + "').focus()", 
    //                   50, 
    //                   "JavaScript");
    //        works fine for controls that are statically part of the 
    //        Web page, but not for controls added on the fly via
    //        dynamic HTML (setting the innerHTML property of a DIV).
    //        In that case, the "document.getElementById('xxx').focus()" line 
    //        raises an exception in the thread generated by setTimeout(),
    //        complaining that the focus() method is not supported.
    // Note:  Be sure to pass a function pointer:
    //            ctlIn.focus
    //        not the value returned by a function call:
    //            cltIn.focus()
    //        Otherwise, the setTimeout() call doesn't compile.
    window.setTimeout(ctlIn.focus, 
                      50, 
                      "JavaScript");
}

/******************************************************************************
* Get the keyCode from the specified event.
*
*@param evt     The event containing the keyCode
*@return        The keyCode, or null.
*@throws        None.
******************************************************************************/
com.bristle.jslib.Event.getEventKeyCode =
function(evt)
{
    if (!evt) return null;
    if (evt.keyCode && evt.keyCode != 0)
    {
        return evt.keyCode;
    }
    else if (evt.charCode)      // Netscape uses charCode, not keyCode, for
                                // some events.
    {
        return evt.charCode;
    }
    else
    {
        return null;
    }
}

/******************************************************************************
* Return true if the specified event was triggered by the right mouse button.
*
*@param evt The Event to check.  Default: window.event
*@return    True if right mouse; false otherwise.
******************************************************************************/
com.bristle.jslib.Event.isRightMouseButtonEvent = 
function(evt) 
{
    // Get event which IE does not pass as a param like other browsers do.
    if (com.bristle.jslib.Util.isMissingNullOrUndefined(evt))
    {
         evt = window.event;
    }

    return (evt.type == "contextmenu" || evt.button == 2);
}

/******************************************************************************
* Fire the onClick event for the HTML control with the specified id.
*
* In some browsers, it doesn't actually fire the event.  Instead it executes
* the code specified in the onclick property of the control.  This has largely 
* the same effect as firing the event, but if the code referred to the special
* "event" object that value may be null.
*
*@param strId   The "id" property of the control to fire onClick for
*@return        None.
*@throws com.bristle.jslib.Exception.intEXC_UNABLE_TO_GENERATE_UNIQUE_VAR_NAME
*                           when unable to generate a unique internal name to 
*                           use in the event firing mechanism.  Only happens
*                           in browsers with no native support for fireEvent()
*                           and only after generating many internal names that
*                           already existed.
******************************************************************************/
com.bristle.jslib.Event.fireClick =
function(strId)
{
    com.bristle.jslib.Event.fireEvent
                (document.getElementById(strId),
                 "onclick",
                 !com.bristle.jslib.Event.blnFIRE_EVENT_EVEN_IF_DISABLED);
}

/******************************************************************************
* Fire the onClick event for the HTML control with the specified id, if the 
* keyCode of the specified event is the Enter key.
*
* In some browsers, it doesn't actually fire the event.  Instead it executes
* the code specified in the onclick property of the control.  This has largely 
* the same effect as firing the event, but if the code referred to the special
* "event" object that value may be null.
*
*@param strId   The "id" property of the control to fire onClick for
*@param evt     The event containing the keyCode
*@return        True if the event was the Enter key.
*@throws com.bristle.jslib.Exception.intEXC_UNABLE_TO_GENERATE_UNIQUE_VAR_NAME
*                           when unable to generate a unique internal name to 
*                           use in the event firing mechanism.  Only happens
*                           in browsers with no native support for fireEvent()
*                           and only after generating many internal names that
*                           already existed.
******************************************************************************/
com.bristle.jslib.Event.fireClickOnEnter =
function(strId, evt)
{
    if (com.bristle.jslib.Event.getEventKeyCode(evt) == 
                                    com.bristle.jslib.Event.intKEYCODE_ENTER)
    {
        com.bristle.jslib.Event.fireClick(strId);
        return true;
    }
    return false;
}

/******************************************************************************
* Fire the onClick event for the HTML control with the specified id, if the 
* keyCode of the specified event is the Esc key.
*
* In some browsers, it doesn't actually fire the event.  Instead it executes
* the code specified in the onclick property of the control.  This has largely 
* the same effect as firing the event, but if the code referred to the special
* "event" object that value may be null.
*
*@param strId   The "id" property of the control to fire onClick for
*@param evt     The event containing the keyCode
*@return        True if the event was the Esc key.
*@throws com.bristle.jslib.Exception.intEXC_UNABLE_TO_GENERATE_UNIQUE_VAR_NAME
*                           when unable to generate a unique internal name to 
*                           use in the event firing mechanism.  Only happens
*                           in browsers with no native support for fireEvent()
*                           and only after generating many internal names that
*                           already existed.
******************************************************************************/
com.bristle.jslib.Event.fireClickOnEsc =
function(strId, evt)
{
    if (com.bristle.jslib.Event.getEventKeyCode(evt) == 
                                    com.bristle.jslib.Event.intKEYCODE_ESC)
    {
        com.bristle.jslib.Event.fireClick(strId);
        return true;
    }
    return false;
}

/******************************************************************************
* Move focus to the HTML control with the specified id, if the keyCode of the 
* specified event is the Enter key.
*
*@param strId   The "id" property of the control to move focus to.
*@param evt     The event containing the keyCode
*@return        True if the event was the Enter key.
*@throws        None.
******************************************************************************/
com.bristle.jslib.Event.moveFocusOnEnter =
function(strId, evt)
{
    if (com.bristle.jslib.Event.getEventKeyCode(evt) == 
                                    com.bristle.jslib.Event.intKEYCODE_ENTER)
    {
        document.getElementById(strId).focus();
        return true;
    }
    return false;
}

/******************************************************************************
* Return true if the specified event is the Esc key.
*
*@param evt     The event containing the keyCode
*@return        True if the event was the Esc key.
*@throws        None.
******************************************************************************/
com.bristle.jslib.Event.doNothingOnEsc =
function(evt)
{
    if (com.bristle.jslib.Event.getEventKeyCode(evt) == 
                                    com.bristle.jslib.Event.intKEYCODE_ESC)
    {
        return true;
    }
    return false;
}

/******************************************************************************
* Return true if the specified event is the Backspace key.
*
*@param evt     The event containing the keyCode
*@return        True if the event was the Backspace key.
*@throws        None.
******************************************************************************/
com.bristle.jslib.Event.isBackspaceKey =
function(evt)
{
    if (com.bristle.jslib.Event.getEventKeyCode(evt) == 
                                    com.bristle.jslib.Event.intKEYCODE_BACKSPACE)
    {
        return true;
    }
    return false;
}

/******************************************************************************
* Return the HTML element (not just a DOM node) that was the original target 
* of the event, before it began "bubbling" (aka "propagating") up through the
* node hierarchy to enclosing elements like the body.
*
*@param evt The Event to check.  Default: window.event
*@return    The HTML element.
*@throws com.bristle.jslib.Exception.intEXC_UNSUPPORTED_BROWSER
******************************************************************************/
com.bristle.jslib.Event.target = 
function(evt) 
{
    // Get event which IE does not pass as a param like other browsers do.
    if (com.bristle.jslib.Util.isMissingNullOrUndefined(evt))
    {
         evt = window.event;
    }

    if (typeof(evt.target) != "undefined")
    {
        // Most browsers
        return evt.target;
    }
    else if (typeof(evt.srcElement) != "undefined")
    {
        // Internet Explorer
        return evt.srcElement;
    }
    else 
    {
        throw new com.bristle.jslib.Exception.Exception
            (com.bristle.jslib.Exception.intEXC_UNSUPPORTED_BROWSER
            ,"Unable to get the target of the specified event"
            ,"com.bristle.jslib.Event.target"
            );
    }
}

/******************************************************************************
* Return false if the specified event is the Backspace key as a shortcut for 
* the browser Back button, true if it is being used to delete chars in an 
* enabled text box or if it is not a Backspace at all.
*
*@param evt Event to check for backspace key.
******************************************************************************/
com.bristle.jslib.Event.isNotBackspaceAsBrowserBackButton = 
function(evt) 
{
    // Get event which IE does not pass as a param like other browsers do.
    if (typeof(evt) == "undefined")
    {
         evt = window.event;
    }

    if (!com.bristle.jslib.Event.isBackspaceKey(evt))
    {
        return true;
    }

    // Note: None of these are needed for Firefox 2.0.0.8 or IE 6.0.2800.1106.
    //       Returning false is sufficient.
    // if (typeof (evt.returnValue) != "undefined")
    // {
    //     evt.returnValue = false;
    // }
    // if (typeof (evt.cancelBubble) != "undefined")
    // {
    //     evt.cancelBubble = true;
    // }
    // if (typeof (evt.stopPropagation) != "undefined")
    // {
    //     evt.stopPropagation();
    // }
    // if (typeof (evt.keyCode) != "undefined")
    // {
    //     evt.keyCode = 0;
    // }

    // Get the HTML element in which Backspace was typed.
    var nodeTarget;
    try
    {
        nodeTarget = com.bristle.jslib.Event.target(evt);
    }
    catch(exception) 
    {
        // Can't tell where the Backspace was pressed.  Don't cancel it.
        return true;
    }

    // Don't cancel Backspace for these HTML elements except when they
    // are readonly.  When not readonly, they make good use of Backspace 
    // (to delete chars), and they prevent it from causing the browser 
    // Back operation anyhow.
    // Cancel Backspace for all other HTML elements.
    var strNodeName = nodeTarget.nodeName;
    if (   (strNodeName == "INPUT" || strNodeName == "TEXTAREA")
        && !nodeTarget.readOnly)
    {
        return true;
    }
    else
    {
        return false;
    }
}

/******************************************************************************
* Disable the Backspace key as a shortcut for the browser Back button, 
* but allow it to delete chars in enabled text boxes.
******************************************************************************/
com.bristle.jslib.Event.disableBackspaceAsBrowserBackButton =
function()
{
    // Note: This must be assigned via the onkeydown property of the document
    //       object, not as an HTML attribute of the HTML body element.
    //       Otherwise, it doesn't work in IE 6.0.2800.1106.
    document.onkeydown=com.bristle.jslib.Event.isNotBackspaceAsBrowserBackButton;
}

