/* BUILD_JS_PACKAGES: library code
   isDefaultValue addClassname removeClassname isString xlate addEvent getValue hash_keys
 */

// ******** Initialization ******** //

var requiredForms = { };

// validator function number formats (can't be stored as object
// properties since validation functions are called outside the 
// scope of the Required element)
var number_formats = [ ];
var all_number_formats = { def: { digit_group_symbol: " ", decimal_symbol: "\\." },
                           en:  { digit_group_symbol: ",", decimal_symbol: "\\." },
                           fr:  { digit_group_symbol: " ", decimal_symbol: "," },
                           de:  { digit_group_symbol: "\\.", decimal_symbol: "," },
                           es:  { digit_group_symbol: "\\.", decimal_symbol: "," },
                           it:  { digit_group_symbol: "\\.", decimal_symbol: "," }
                         };

// Automatically creates requiredForms based on document
// form properties
function createRequiredForms()
{
    for (var i = 0; i < document.forms.length; i++) {
        var name          = document.forms[i].getAttribute("name");
        var required      = (document.forms[i].getAttribute("required") == "true");
        var notify        = (document.forms[i].getAttribute("notify") == "true");
        var do_validate   = (document.forms[i].getAttribute("do_validate") == "true");
        var onrequired    = document.forms[i].getAttribute("onrequired");
        var validate_lang = document.forms[i].getAttribute("validate_lang");
        if (name && required) {
            requiredForms[name] = new RequiredForm(name, do_validate, notify, null, validate_lang);
            if (onrequired) {
                eval(onrequired);
            }
        }
    }
}

// check for automatic instantiation of a 
// required form and its elements
if (document.attachEvent) {
    this.attachEvent("onload", createRequiredForms);
}
else if (document.addEventListener) {
    this.addEventListener("load", createRequiredForms, false);
}
  
// ******** RequiredForm ******** //

// Picks up all required elements in a form
// or all forms on the page
//  -- form_name     : Name of the form (string)
//  -- do_validate   : Whether or not to do immediate validation (bool)
//  -- notify        : If do_validate == true, whether or not to alert
//                     the user of any validation errors
//  -- ignore_fields : If do_validate == true, element ids to ignore for 
//                     this validation (helps validate parts of the form
//                     without validating the whole thing) -- ignore_fields
//                     are not even instantiated as Required elements
//  -- validate_lang : If validate_lang == 'true' or validate_lang == 'strict', 
//                     number validation will take into account language/country-specific 
//                     decimal symbols and thousands separators, where possible (expecting 
//                     a js variable 'lang' to exist with the correct value).
//                     The two values for validate_lang have the following meanings:
//                        o true   : mod30 format or the specific language format are allowed
//                        o strict : only the specific language format is allowed 
function RequiredForm(form_name,do_validate,notify,ignore_fields,validate_lang)
{
    if (form_name) {
        this.form = eval("document." + form_name);

        // process elements to ignore
        var ignore_elements = { };
        if (do_validate) {
            if (ignore_fields && isNaN(ignore_fields.length)) {
                alert("RequiredForm: Fields to ignore must be an array");
                return;
            }
            else if (ignore_fields) {
                for (var i = 0; i < ignore_fields.length; i++) {
                    ignore_elements[ignore_fields[i]] = 1;
                }
            }
        }
    
        // create each Required element
        this.form.groups = { };
        for (var i = 0; i < this.form.elements.length; i++) {
            var e = this.form.elements[i];
            var required = e.getAttribute("required");
            if (required != "true" && required != "false") {
                // not required
                continue;
            }
            var elem;
            var args = { 
                name                            : e.name,
                group_id                        : e.getAttribute("group_id"),
                required                        : required,
                validator                       : e.getAttribute("validator"),
                server_validator                : e.getAttribute("server_validator"),
                server_validator_callback       : e.getAttribute("server_validator_callback")       || "",
                group_validator                 : e.getAttribute("group_validator")                 || "",
                group_server_validator          : e.getAttribute("group_server_validator")          || "",
                group_server_validator_callback : e.getAttribute("group_server_validator_callback") || "",
                onpass                          : e.getAttribute("onpass"),
                onfail                          : e.getAttribute("onfail"),
                triggers                        : e.getAttribute("triggers"),
                showvalid                       : e.getAttribute("showvalid"),
                showinvalid                     : e.getAttribute("showinvalid"),
                showerror                       : e.getAttribute("showerror"),
                marklabel                       : e.getAttribute("marklabel"),
                filter                          : e.getAttribute("filter"),
                selector                        : e.getAttribute("selector")
             };
            if (ignore_elements[e.id] != 1) {
                elem = new Required(e.id, args, false);
            }
                
            // set group validator(s)
            if (args["group_id"]) {
                // new group validators for this group_id will silently overwrite
                // previous ones, so define these only once
                var group_id               = args["group_id"];
                var group_validator        = args["group_validator"];
                var group_server_validator = args["group_server_validator"];
                if (! this.form.groups[group_id]) {
                  // validators  : validate entire group's elements by returning
                  //              data about each element in the group
                  //     - client : js function to use for group
                  //     - server : server-side script
                  // callbacks   : callback js functions to perform after the group
                  //               validation has finished
                  // validations : marked to true after the first element in the group is 
                  //               validated so the group validators don't run more than once
                  // valid       : marked to true if all elements in the group are valid,
                  //               false otherwise 
                  this.form.groups[group_id] = { elements    : { },
                                                 validators  : { client : null, server : null },
                                                 callbacks   : { client : null, server : null },
                                                 validations : { client : false, server : false },
                                                 valid       : null
                                               };
                }
                this.form.groups[group_id]["elements"][e.id] = elem;
                if (group_validator) {
                    this.form.groups[group_id]["validators"]["client"] = group_validator;
                }
                if (group_server_validator) {
                    this.form.groups[group_id]["validators"]["server"] = group_server_validator;
                }
            }
        }

        // other form properties and methods
        this.form.tally    = 0;
        this.form.validate = RequiredForm_validate;
        this.form.notify   = RequiredForm_notify;
        this.form.clear    = RequiredForm_clear;
  
        this.form.doSingleValidations             = RequiredForm_doSingleValidations;
        this.form.doGroupValidations              = RequiredForm_doGroupValidations;
        this.form.doClientGroupValidationActions  = RequiredForm_doClientGroupValidationActions;
        this.form.doClientSingleValidationActions = RequiredForm_doClientSingleValidationActions;
        this.form.doServerGroupValidationActions  = RequiredForm_doServerGroupValidationActions;
        this.form.doServerSingleValidationActions = RequiredForm_doServerSingleValidationActions;
        this.form.doElementValidation             = RequiredForm_doElementValidation;
        this.form.doElementInvalidActions         = RequiredForm_doElementInvalidActions;
        this.form.finishValidation                = RequiredForm_finishValidation;
        
        // set validation language
        if (validate_lang && lang) {
            if (validate_lang == "true") {
                number_formats = [ all_number_formats[lang], all_number_formats["def"] ];
            }
            else if (validate_lang == "strict") {
                number_formats = [ all_number_formats[lang] ];
            }
        }
      
        // validate the required form immediately on creation
        if (do_validate) {
            this.form.validate(notify,ignore_elements);
        }
  
        return this.form;
    }
    else {
        alert("RequiredForm: form_name must be supplied");
    }
}

// Note: since RequiredForm relys on assigning expando
//   properties to an alias to the form rather than form
//   element itself, to undo RequiredForm, the alias only need 
// be reset to document.<formName>
// This function only removes the "req" classname
function RequiredForm_clear()
{
    for (var i = 0; i < this.elements.length; i++) {
        var e = this.elements[i];
        var required = e.getAttribute("required");
        if (required == "true" || required == "false") {
            e.showPass();
        }
    }
}

function RequiredForm_validate(notify,ignore_elements,do_only_elements)
{
    // do form validation
    this.isvalid = true;
    if (! ignore_elements) {
        ignore_elements = { };
    }
  
    // validate runtime data structures
    this.element_values           = { };
    this.radio_elements_validated = { };
    this.failed_elements          = { empty: { }, invalid: { }, invalid_detailed: { }, ignore: { } };
    this.notified_user            = false;  //regardless of isvalid, or failed elements, or client/server validation   
                                    //this tells us whether we have already complained to the user per validation attempt 

    // validate single validators
    var doing_single_server_validations = this.doSingleValidations(ignore_elements, do_only_elements, notify);
        
    // do group validation
    var doing_group_server_validations = this.doGroupValidations(ignore_elements, do_only_elements, notify);
    
    // finish form validation actions immediately if no validations are on the server,
    // else let the return from AJAX call the rest of the validation actions 
    // (to make sure said call is complete)
    if (doing_single_server_validations || doing_group_server_validations) {
        //Indicate that we are doing server side validations and that   
        //we shouldn't proceed with existing logic until the server response returns   
        return(false); 
    }
    else {
        return this.finishValidation(notify);
    }
}

function RequiredForm_finishValidation(notify)
{
    // backwards compatibility
    this.valid = this.isvalid;
  
    // alert errors to the user
    if (notify) {
        // notify returns true or false depending on errors existing in failed elements
        this.notified_user = this.notify(this.failed_elements);
    }
    
    // reset groups values
    for (var group_id in this.groups) {
        this.groups[group_id]["validations"] = { client : false,
                                                 server : false
                                               };
        this.groups[group_id]["valid"]       = null;
    }

    return this.failed_elements;
}

function RequiredForm_doSingleValidations(ignore_elements,do_only_elements,notify)
{
    var doing_server_validations = false;        
    var radio_elements_validated = { };
    for (var i = 0; i < this.elements.length; i++) {
        var e = this.elements[i];
        var this_doing_server_validations = this.doElementValidation(e, ignore_elements, do_only_elements, notify, false);
        // any element validating on the server means we have to wait for 
        // the server to return
        if (this_doing_server_validations) {
            doing_server_validations = true;
        }
    }
    
    return doing_server_validations;
}

function RequiredForm_doGroupValidations(ignore_elements,do_only_elements,notify)
{
    var doing_server_validations = false;        
    var radio_elements_validated = { };
    for (var i = 0; i < this.elements.length; i++) {
        var e = this.elements[i];
        var this_doing_server_validations = this.doElementValidation(e, ignore_elements, do_only_elements, notify, true);
        if (this_doing_server_validations) {
            doing_server_validations = true;
        }
    }
    
    return doing_server_validations;
}

function RequiredForm_doElementValidation(e,ignore_elements,do_only_elements,notify,is_group_validation)
{
    var doing_server_validations = false;        
    var required = e.getAttribute("required");
    if (required == "true" && ! ignore_elements[e.id]) {
        // ignore takes precedence over do_only
        if (do_only_elements && ! do_only_elements[e.id]) {
            return false;
        }
        // only process one of a set of radio/checkbox elements
        if (this.radio_elements_validated[e.name]) {
            return false;
        }
            
        // store all values for later group_validator pass, if any
        if (! is_group_validation) {
            for (var key in this.groups) {
                this.element_values[e.name] = getValue(this, e.name);
                break;
            }
        }
        
        // masking error when validate function is lost
        // because of multiple elems with the same id --
        // tbd: betterify the naming mechanism
        if (! is_group_validation) {
            e.is_form_validate = true;
            
            // client single validation
            this.doClientSingleValidationActions(e);

            // server single validation
            doing_server_validations = this.doServerSingleValidationActions(e, notify);
        }
        else if (is_group_validation && this.groups[e.group_id]) {
            e.is_form_validate = true;
            
            // client group validation
            this.doClientGroupValidationActions(e);

            // server group validation
            doing_server_validations = this.doServerGroupValidationActions(e, notify);

            // reset ignore_server_validations
            e.ignore_server_validations = false;
        }
    }
    
    return doing_server_validations;
}

function RequiredForm_doClientSingleValidationActions(e)
{
    if (e.validator) {
        e.validate(false,true);
    }
            
    // consequences of invalid input
    if (! e.isvalid) {
        this.doElementInvalidActions(e,false)
    }
}

function RequiredForm_doClientGroupValidationActions(e)
{
    // all group elements have to be valid to this point before
    // group client validation can proceed
    for (var elem_id in this.groups[e.group_id]["elements"]) {
        var elem = this.groups[e.group_id]["elements"][elem_id];
        var required = elem.getAttribute("required");
        if (required == "true" && ! elem.isvalid) {
            return;
        }
    }

    // no client validation for this elem, or it's already happened for
    // some other element, so bail
    var group = this.groups[e.group_id];    
    if (! group["validators"]["client"] || group["validations"]["client"] != false) {
        return;
    }
    
    // get validation results (e.g. { field_1 : true, field_2 : "error_text", ... } )
    var validation_results = eval(e.group_validator).apply(null,[ this.element_values ]);
        
    // mark validation as having happened
    this.groups[e.group_id]["validations"]["client"] = true;
                
    // go through all valid elements in group, tallying group errors, 
    // marking color, running other handlers, etc.
    for (var elem_id in group["elements"]) {
        var this_e = group["elements"][elem_id];
        var required = this_e.getAttribute("required");
        // only check the group elements that are ok to this point or are not required anymore
        if (required == "true" && this_e.isvalid) {
            this_e.doValidationActions(validation_results[elem_id] || false,false);
            if (! this_e.isvalid) {
                this.groups[e.group_id]["valid"] = false;
                this.doElementInvalidActions(this_e,true);
            }
        }
    }
    
    if (this.groups[e.group_id]["valid"] != false) {
        this.groups[e.group_id]["valid"] = true;
    }
}

function RequiredForm_doServerSingleValidationActions(e,notify)
{
    if (e.isvalid == true && e.server_validator && ! e.ignore_server_validations) {
        e.serverValidate( e.server_validator,
                          e.server_validator_callback,
                          false,
                          false,
                          notify
                        );
        return true;
    }
    return false;
}

function RequiredForm_doServerGroupValidationActions(e,notify)
{
    // all group elements have to be valid to this point before
    // group server validation can proceed
    for (var elem_id in this.groups[e.group_id]["elements"]) {
        var elem = this.groups[e.group_id]["elements"][elem_id];
        var required = elem.getAttribute("required");
        if (required == "true" && (! elem.isvalid || elem.ignore_server_validations)) {
            // don't do this group validation if any of its elements have 
            // ignore_server_validations set (mark validation as having happened)
            if (elem.ignore_server_validations) {
                this.groups[e.group_id]["validations"]["server"] = true;
            }
            return false;
        }
    }
    
    // no client validation for this elem, or it's already happened for
    // some other element, so bail
    var group = this.groups[e.group_id];    
    if (! group["validators"]["server"] || group["validations"]["server"] != false) {
        return false;
    }

    // do server validation
    e.serverValidate( e.group_server_validator,
                      e.group_server_validator_callback,
                      false,
                      true,
                      notify
                    );

    // mark validation as having happened
    this.groups[e.group_id]["validations"]["server"] = true;

    return true;
}

function RequiredForm_doElementInvalidActions(e,is_group_validation)
{
    var error_type = "";

    // do not show an error for this element
    if (e.getAttribute("showerror") == "false") {
        error_type = "ignore";
    }
    // radio or checkbox
    else if (e.isradio) {
        error_type = "empty";
        this.radio_elements_validated[e.name] = true;
    }
    // process error normally
    else {
        if (e.error) {
            error_type = "invalid_detailed";
        }
        else {
            // presumably, the group element that returns will contain 
            // a detailed text error that suffices as an error
            // for the entire group 
            if (is_group_validation) {
                error_type = "ignore";
            }
            // stand-alone element error
            else {
                error_type = isBlank(e.value) ? "empty" : "invalid";
            }
        }
    }
    
    // add element id to error data structure        
    this.failed_elements[error_type][e.id] = 1;
    
    this.isvalid = false;
}


// ******** Required (element) ******** //

function Required(id,args,do_validate)
{
    // check data
    var this_element = isRequiredDataValid(id,args);

    if (! this_element) {
        return null;
    }
  
    // set properties
    this.elem                                 = this_element;
    this.elem.id                              = id;
    this.elem.name                            = args["name"];
    this.elem.group_id                        = args["group_id"];
    this.elem.required                        = args["required"];
    this.elem.validator                       = args["validator"] || "isValidText(this.value)";
    this.elem.server_validator                = args["server_validator"];
    this.elem.server_validator_callback       = args["server_validator_callback"].replace(/\(.*\);?\s*$/,"");
    this.elem.group_validator                 = args["group_validator"].replace(/\(.*\);?\s*$/,"");
    this.elem.group_server_validator          = args["group_server_validator"].replace(/\(.*\);?\s*$/,"");
    this.elem.group_server_validator_callback = args["group_server_validator_callback"].replace(/\(.*\);?\s*$/,"");
    this.elem.ignore_server_validations       = false;
    this.elem.onpass                          = args["onpass"];
    this.elem.onfail                          = args["onfail"];
    this.elem.showvalid                       = args["showvalid"] || "true";
    this.elem.showerror                       = args["showerror"] || "true";
    this.elem.showinvalid                     = args["showinvalid"] || "true";
    this.elem.marklabel                       = args["marklabel"];
    this.elem.filter                          = args["filter"];
    this.elem.selector                        = args["selector"];
    // this.elem.triggers set by isRequiredDataValid

    // special default validator for radio merely makes
    // sure one of a group is checked
    if ( this.elem.getAttribute("type") && 
           ( this.elem.getAttribute("type").toLowerCase() == "radio" || 
             this.elem.getAttribute("type").toLowerCase() == "checkbox"
           )
       ) {
        this.elem.validator = "isValidCheckboxRadioInput(this)";
        this.elem.showinvalid = "false";
        // tag the first radio (should be in HTML rendering order)
        // with the isradio flag so that all info associated with 
        // the group (error text, etc.) is stored with this radio 
        if ( this.elem.form[this.elem.name].length && 
            this.elem.id == this.elem.form[this.elem.name][0].getAttribute("id")
           ) {
            this.elem.isradio = true;
        }
    }
  
    // set methods
    this.elem.validate                         = Required_validate;
    this.elem.doValidationActions              = Required_doValidationActions;
    this.elem.doServerValidationActions        = Required_doServerValidationActions
    this.elem.doElementServerValidationActions = Required_doElementServerValidationActions;
    this.elem.serverValidate                   = Required_serverValidate;
    this.elem.showPass                         = Required_showPass;
    this.elem.showFail                         = Required_showFail;
    this.elem.setTriggers                      = Required_setTriggers;
    this.elem.setRequired                      = Required_setRequired;

    // set internal properties
    var form = this.elem.form;
    //this.elem.isvalid = true;
    this.elem.error = null;
    this.elem.isvalid = null;

    this.elem.isdefaultvalue = isDefaultValue(form, this.elem.name);
    this.elem.ismarked = false;

    if (this.elem.required == "true") {
        // set when Required fires
        this.elem.setTriggers();
        // do initial validation
        if (do_validate) {
            this.elem.validate();
        }
    }
  
    if (this.elem.filter != "") {
        // demos: tbd
    }

    return this.elem;
}

function Required_setTriggers()
{
    for (var i = 0; i < this.triggers_array.length; i++) {
        var attach_func = new Function( "document.getElementById('" + this.id + "').validate();" );
        addEvent(this, this.triggers_array[i], attach_func);
    }
}

function Required_setFilter()
{
    if (this.filter != "" && this.triggers.length) {
        for (var i = 0; i < this.triggers.length; i++) {
            if (document.addEventListener) {
                var event_type = this.triggers[i];
                eval( 'document.addEventListener("' + event_type + '", ' + this.filter + ', true);' );
            }
            else {
                eval( 'document.' + this.triggers[i] + ' = ' + this.filter + ';' );
            }
        }
    }
}

function Required_setRequired(required)
{
    this.setAttribute("required", required ? "true" : "false");
    this.required = required ? "true" : "false";
    this.setTriggers();
    this.validate();
}

function Required_validate(skip_passfail_handlers,client_validation_only)
{
    // actually run single validator
    var valid_result = eval(this.validator);
    if (! skip_passfail_handlers) {
        skip_passfail_handlers = false;
    }

    // Single validator calls
    //
    // Call server validator, if it exists, and only if the client-side
    // validator returned true
    //
    // this branch will eventually run doValidationActions when the 
    // server returns
    if (! client_validation_only && valid_result == true && this.server_validator) {
        this.serverValidate( this.server_validator,
                             this.server_validator_callback,
                             skip_passfail_handlers,
                             false
                           );
    }
    // for client, run doValidationActions directly
    else {
        this.doValidationActions(valid_result,skip_passfail_handlers);
    }
}  

function Required_doValidationActions(valid_result,skip_passfail_handlers) 
{
    // save off old valid state
    this.last_isvalid = this.isvalid;
    
    this.is_form_validate = false;

    // true, false, 0, or 1 will trigger generic errors
    // note: using String() objects to set 0 or 1 won't work
    if ( typeof valid_result == "boolean" || 
         (typeof valid_result == "number" && (valid_result == 0 || valid_result == 1)) ||
         (typeof valid_result == "string" && valid_result.match(/^[01]$/))
       ) {
        this.isvalid = valid_result || false;
        this.error = null;
    }
    // validator returned special error text
    else {
        this.isvalid = false;
        this.error = valid_result;
    }
  
    // backwards compatibility
    this.valid = this.isvalid;
  
    // tally the default value of this element into the form
    var is_defaultvalue = isDefaultValue(this.form, this.name);
    if (this.isdefaultvalue != is_defaultvalue) {
        var form_delta = is_defaultvalue ? -1 : 1;
        this.form.tally += form_delta;
        // (debug)
        if (document.getElementById("tally")) {
            document.getElementById("tally").innerHTML = this.form.tally;
        }
    }
    this.isdefaultvalue = is_defaultvalue;

    // required has been set to false, so show the
    // valid UI
    if (this.required == "false") {
        this.showPass();
        return;
    } 

    // later, don't do the onpass/onfail handlers if 
    // neither the value nor the validity of the element 
    // have changed
    var value_or_validity_changed = ( this.last_value == getValue(this.form, this.name) &&
                                      this.isvalid == this.last_isvalid
                                    ) ? false : true;
  
    // reflect valid in UI (if applicable) and run
    // onpass/onfail handlers
    if (this.isvalid) {
        // only show the 'valid' color if we allow showvalid
        if (this.showvalid == "true") {
            this.showPass();
        }
        else {
            this.showFail();
        }
        // allow skipping of this handler, e.g. when a bunch 
        // of required elements are validated at the same time, 
        // but the handler should only be fired once
        if (this.onpass && skip_passfail_handlers != true && value_or_validity_changed) {
            eval(this.onpass);
        }
    }
    else {
        // only show the 'invalid' color if we allow showinvalid
        if (this.showinvalid == "true") {
            this.showFail();
        }
        else {
            this.showPass();
        }
        if (this.onfail && skip_passfail_handlers != true && value_or_validity_changed) {
            eval(this.onfail);
        }
    }

    // save off old value
    this.last_value   = getValue(this.form, this.name);
}

function Required_showPass()
{
    if (! this.marklabel) {
        if (this.getAttribute("selector") == "true") {
            var selector_body = document.getElementById(this.id + "_required_container");
            removeClassname(selector_body, "req_selector");
            addClassname(selector_body, "unreq_selector");
        }
        else {
            removeClassname(this, "req");
        }
    }
    // remove the invalid marker (see showFail())
    else if (this.marked) {
        var element_label = document.getElementById(this.id + "_label");
        if (element_label) {
            // need to match the marklabel string when removing it from
            // the label like this because of problems with other forms 
            // of matching like regexps and lastIndexOf
            //
            // demos: kind of kludgey, though -- work on a method using the
            // DOM to get rid of this marker element?
            var match_str = "";
            for (var i = element_label.innerHTML.length - 1; i >= 0; i--) {
                match_str = element_label.innerHTML.charAt(i) + match_str;
                if (match_str == this.marklabel) {
                    element_label.innerHTML = element_label.innerHTML.substr(0,i);
                }
            }
        }
    }
}

function Required_showFail()
{
    if (! this.marklabel) {
        if (this.getAttribute("selector") == "true") {
            var selector_body = document.getElementById(this.id + "_required_container");
            addClassname(selector_body, "req_selector");
            removeClassname(selector_body, "unreq_selector");
        }
        else {
            addClassname(this, "req");
        }
    }
    // allow user to specify a marker to apply to 
    // the end of the element's label field (of the
    // form <this.id>_label) when it's invalid
    else if (! this.ismarked) {
        var element_label = document.getElementById(this.id + "_label");
        if (element_label) {
            element_label.innerHTML += this.marklabel;
        }
        this.ismarked = true;
    }
}

function Required_serverValidate(webservice_type,validator_callback,skip_passfail_handlers,is_group_validation,notify)
{
    // callback function does required validation stuff (doServerValidationActions), and then
    // the optional callback function
    //
    // extra args beyond form data
    var args = { skip_passfail_handlers   : skip_passfail_handlers,
                 is_group_validation      : is_group_validation,
                 callback_function_string : validator_callback || "",
                 notify                   : notify,
                 elem_id                  : this.id
               };
    var webserv_args = { args              : args,
                         form              : this.form,
                         library           : webservice_type + "/validate",
                         callback          : Required_processWebServiceResults
                       };
    webserv_obj = new WebServiceAccessor(webserv_args);
    webserv_obj.getWebService();
}

// if webservice_accessor.js is not included in the enclosing page (before required.js), 
// this will fail and be caught
Required_processWebServiceResults = function (args)
{
    Required_doServerValidationActions(args);
    if (args["fdat"]["notify"]) {
        var form = document.getElementById(args["fdat"]["elem_id"]).form;
        form.finishValidation(args["fdat"]["notify"])
    }
    if (args["fdat"]["callback_function_string"]) {
        eval(args["fdat"]["callback_function_string"]).apply(null,[args]);
    }
}

function Required_doServerValidationActions(args)
{
    var elem = document.getElementById(args["fdat"]["elem_id"]);

    // single server validation
    if (! args["fdat"]["is_group_validation"]) {
        elem.doElementServerValidationActions(args);
    }
    // group server validation, loop through elements
    else {
        var group = elem.form.groups[elem.group_id];
        for (var group_elem_id in group["elements"]) {
            var group_elem = group["elements"][group_elem_id];
            // only check the group elements that are ok to this point
            if (group_elem.isvalid) {
                group_elem.doElementServerValidationActions(args);
                if (! group_elem.isvalid) {
                    elem.form.groups[elem.group_id]["valid"] = false;
                    elem.form.doElementInvalidActions(group_elem,true);
                }
            }
        }
    }
}

function Required_doElementServerValidationActions(args)
{
    var elem_raw_valid = args["validation"][this.id];
    var valid_result = false;
    if (elem_raw_valid == "1") {
        valid_result = true;
    }
    else if (typeof elem_raw_valid == "undefined") {
        valid_result = false;
    }
    else if (elem_raw_valid != "0") {
        valid_result = elem_raw_valid;
    }
    
    this.doValidationActions(valid_result,args["skip_passfail_handlers"]);
}

function RequiredForm_notify(failed_elements)
{
    var err_str = "";
    var pad = "    ";

    // empty fields
    var empty_element_keys = hash_keys(failed_elements["empty"]);
    if (empty_element_keys.length) {
        var error_texts = [ ];
        for (var elem_id in failed_elements["empty"]) {
            var elem = document.getElementById(elem_id);
            var title_text = elem.getAttribute("label") || elem.getAttribute("title");
            error_texts.push(title_text);
        }
        var error_header = (error_texts.length == 1) ? 
            xlate("The following field is required, but was not entered") : 
            xlate("The following fields are required, but were not entered");
        err_str += error_header + ":\n" + pad + error_texts.join("\n" + pad);
    }

    // invalid fields
    var invalid_element_keys = hash_keys(failed_elements["invalid"]);
    if (invalid_element_keys.length) {
        if (err_str) {
            err_str += "\n\n";
        }
        var error_texts = [ ];
        for (var elem_id in failed_elements["invalid"]) {
            var elem = document.getElementById(elem_id);
            var title_text = elem.getAttribute("label") || elem.getAttribute("title");
            error_texts.push(title_text);
        }
        var error_header = (error_texts.length == 1) ? 
            xlate("The following field failed validation") : 
            xlate("The following fields failed validation");
        err_str += error_header + ":\n" + pad + error_texts.join("\n" + pad);
    }
  
    // invalid fields with customized error text
    var detailed_element_keys = hash_keys(failed_elements["invalid_detailed"]);
    if (detailed_element_keys.length) {
        if (err_str) {
            err_str += "\n\n";
        }
        var error_texts = [ ];
        for (var elem_id in failed_elements["invalid_detailed"]) {
            var elem = document.getElementById(elem_id);
            var title_text = elem.error || elem.getAttribute("label") || elem.getAttribute("title");
            error_texts.push(title_text);
        }
        var error_header = "";
        if (error_texts.length == 1) {
            error_header = empty_element_keys.length || invalid_element_keys.length ? 
                xlate("The following additional error occurred") : xlate("The following error occurred");
        }
        else {
            error_header = empty_element_keys.length || invalid_element_keys.length ? 
                xlate("The following additional errors occurred") : xlate("The following errors occurred");
        }
        err_str += error_header + ":\n" + pad + error_texts.join("\n" + pad);
    }

    if (err_str) {
        alert(err_str);
        return true;
    }
    else {
        return false;
    }
}

function isRequiredDataValid(id,args)
{
    if (! id || id == "") {
        alert("required.js: the supplied id is invalid or the associated form element could not be found.");
        return false;
    }
    var element = document.getElementById(id);

    // if a string, convert triggers into an array
    var triggers = [ ];
    if (args["triggers"]) {
        if (isString(args["triggers"]) && args["triggers"] != "") {
            if (! args["triggers"].match(/,/)) {
                triggers = [ args["triggers"] ];
            }
            else {
                triggers = args["triggers"].split(/\s*,\s*/);
            }
        }
        else if (args["triggers"].length >= 0) {
            // check to make sure each trigger makes sense
            // with the element type of element 'id'.
            triggers = args["triggers"];
        }
        else {
            alert("required.js: the supplied 'triggers' argument for element id " + id + " must be a string or an array.");
        }
    }

    element.triggers_array = triggers;

    return element;
}


// ******** Common Validators ******** //

// default validator if no validator specified
// for required element
function isValidText(val)
{
    return (val != null && val.search(/\w/) != -1);
}

function isValidAlphaNumText(val)
{
    if (! isValidText(val)) {
        return false;
    }
  
    return (val.search(/^[a-zA-Z0-9]+$/) != -1);
}

// format is a string representation of a RegExp
function isValidMatchedText(val,format,re_attrs,allow_blank)
{
    if (isBlank(val) && allow_blank) {
        return true;
    }
    if (! format) {
        return false;
    }
    if (! re_attrs || re_attrs !== '' + re_attrs) {
        re_attrs = "";
    }
    var pat = new RegExp("^" + format + "$", re_attrs);
    var results = val.match(pat);
    return (results == null ? false : true);
}

function isValidFloat(val,allow_blank,max_precision)
{
    if (val != null && typeof val != "undefined" && (val + '').length > 0) {
        var is_number = isValidNumber(val);
        // we've already run unFormatVal on the number,
        // so doing it again is a bit inefficient, fix later
        if (is_number) {
            val = unFormatVal(val);
        }
        var max_precision_ok = (usingMaxPrecision(max_precision) && is_number) ? 
                               (parseFloat(val) == parseFloat(Number(val).toFixed(max_precision))) : 
                               true;
        return (is_number && max_precision_ok);
    }
    else if (allow_blank) {
        return true;
    }
    else {
        return false;
    }
}

function isValidPositiveFloat(val,allow_blank,max_precision)
{
    if (val && val != "" && Number(val) != 0) {
        var is_float = isValidFloat(val,false,max_precision);
        // we've already run unFormatVal on the number,
        // so doing it again is a bit inefficient, fix later
        if (is_float) {
            val = unFormatVal(val);
        }
        var max_precision_ok = (usingMaxPrecision(max_precision) && is_float) ? 
                               (parseFloat(val) == parseFloat(Number(val).toFixed(max_precision))) : 
                               true;
        return (is_float && val > 0 && max_precision_ok);
    }
    else if (allow_blank) {
        return true;
    }
    else {
        return false;
    }
}

function isValidFloatInRange(val,min,max,allow_blank,max_precision) {
    var is_float = isValidFloat(val,false,max_precision);
    if (is_float) {
        val = unFormatVal(val);
    }
    if (is_float) {
        return valueInRange(parseFloat(val),min,max);
    }
    else if (allow_blank && (val == 0 || ! val)) {
        return true;
    }
    else {
        return false;
    }
}

function isValidInteger(val,allow_blank)
{
    var is_float = isValidFloat(val,false);
    if (is_float) {
        val = unFormatVal(val);
    }
    if (is_float) {
        return (parseInt(val) == val);
    }
    else if (allow_blank && (val == 0 || ! val)) {
        return true;
    }
    else {
        return false;
    }
}

function isValidPositiveInteger(val,allow_blank)
{
    var is_positive_float = isValidPositiveFloat(val,false);
    if (is_positive_float) {
        val = unFormatVal(val);
    }
    if (is_positive_float) {
        return (parseInt(val) == val);
    }
    else if (allow_blank && (val == 0 || ! val)) {
        return true;
    }
    else {
        return false;
    }
}

function isValidIntegerInRange(val,min,max,allow_blank)
{
    var is_integer = isValidInteger(val,false);
    if (is_integer) {
        val = unFormatVal(val);
    }
    if (is_integer) {
        return valueInRange(parseInt(val),min,max);
    }
    else if (allow_blank && (val == 0 || ! val)) {
        return true;
    }
    else {
        return false;
    }
}

function isValidEmailAddress(val,allow_blank)
{
    if (isBlank(val) && allow_blank) {
        return true;
    }
        
    if (! val || ! isString(val)) {
        return false;
    }
    
    if (val.match(/^\s+$/)) {
        return false;
    }
  
    // do the real email validation 
    // (a rigorous check from javascript.internet.com)
    var emailPat     = /^(.+)@(.+)$/;
    var specialChars = "\\(\\)<>@,;:\\\\\\\"\\.\\[\\]";
    var validChars   = "\[^\\s" + specialChars + "\]";
    var quotedUser   = "(\"[^\"]*\")";
    var ipDomainPat  = /^\[(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\]$/;
    var atom         = validChars + '+';
    var word         = "(" + atom + "|" + quotedUser + ")"
    var userPat      = new RegExp("^" + word + "(\\." + word + ")*$")
    var domainPat    = new RegExp("^" + atom + "(\\." + atom +")*$")

    var matchArray = val.match(emailPat);
    if (matchArray == null) {
        return false;
    }
        
    var user = matchArray[1];
    var domain = matchArray[2];

    if (user.match(userPat) == null) {
        return false;
    }
        
    var IPArray = domain.match(ipDomainPat);
    if (IPArray != null) {
      for (var i = 1; i <= 4; i++) {
        if (IPArray[i] > 255)
          return false;
      }
      return true;
    }

    var domainArray = domain.match(domainPat);
    if (domainArray == null) {
        return false;
    }
        
    var atomPat = new RegExp(atom,"g");
    var domArr = domain.match(atomPat);
    var len = domArr.length;
    // note: removed original 3-letter tld check since 
    // longer tlds are now allowed
    if (domArr[domArr.length-1].length < 2) {
        return false;
    }
    
    if (len < 2) {
        return false;
    }
        
    return true;
}

function isValidPhoneNumber(val,allow_blank)
{
    if (isBlank(val) && allow_blank) {
        return true;
    }
    
    if (! val || ! isString(val)) {
        return false;
    }
        
    // given the number of international phone number standards,
    // this is meant to be pretty liberal
    return (val.search(/[^\s\d.\-\(\\)\++]/) == -1) ? true : false;
}

function isValidHHMMSS(val,allow_blank) {
                    
    if (isBlank(val) && allow_blank) {
        return true;
    }
    
    if (! val || ! isString(val)) {
        return false;
    }

    // We are looking for HH:MM:SS
    if (!val.match(/^\d\d:\d\d:\d\d$/)) {
        return false;
    }

    var times = val.split(':');
    var hour = parseInt(times[0]);
    var min = parseInt(times[1]);
    var sec = parseInt(times[2]);

    if (hour < 0 || hour > 24) {
        return false;
    }
    else if (min < 0 || min > 59) {
        return false;
    }
    else if (sec < 0 || sec > 59) {
        return false;
    }

    // and we are good
    return true;
}
        
    

// tbd: make rigorous like isValidEmailAddress above
function isValidURL(val,schemes,allow_blank)
{
    if (isBlank(val) && allow_blank) {
        return true;
    }
        
    if (! val || ! isString(val)) {
        return false;
    }
        
    // if schemes are specified, validate on those only
    var all_schemes = { mailto: 1, ftp: 1, gopher: 1, http: 1, https: 1 };
    var allowed_schemes = [ ];
    if (schemes) {
        for (var i = 0; i < schemes.length; i++) {
            if (all_schemes[schemes[i]]) {
                allowed_schemes.push(schemes[i]);
            }
        }
    }
    else {
        for (var scheme in all_schemes) {
            allowed_schemes.push(scheme);
        }
    }
  
    var scheme_string = "(" + allowed_schemes.join("|") + ")";
    
    var url_regexp = new RegExp("^(" + scheme_string + "://)?" +
      "([^/:\.]+\.[^/:\.]{2,})(:(\\d+))?/?([^\\?#]*)(\\?(.*))?(#(.*))?$");

    return (val.match(url_regexp) != null);
}

// simple default radio/checkbox button validator returns true
// if a single radio button, or one of a group (same name
// attribute for several radios), is checked
function isValidCheckboxRadioInput(this_input)
{
    if (this_input.form[this_input.name].length) {
        for (var i = 0; i < this_input.form[this_input.name].length; i++) {
            if (this_input.form[this_input.name][i].checked) {
                return true;
            }
        }
        return false;
    }
    else  {
        return this_input.checked;
    }
}

// ******** Validator Utilities ******** //

function usingMaxPrecision(max_precision)
{
    return ((max_precision && ! isNaN(max_precision)) || max_precision == 0);
}

function valueMatchesMaxPrecision(val,max_precision)
{
    return (parseFloat(val) == parseFloat(Number(val).toFixed(max_precision)));
}

function valueInRange(val,min,max)
{
    if (min <= val && val <= max) {
        return true;
    }
    else {
        return false;
    }
}

function isBlank(val)
{
    return (! val || val.match(/^\s*$/));
}

// When digit group separators or different decimal symbols
// are present, this function returns the val as a standard
// number parseable by js if it's a valid number, or false
// otherwise.
function isValidNumber(val)
{
    // do format checking 
    if (number_formats.length) {
        val = val + '';

        // validate number against all acceptable formats
        // (see validate_lang RequiredForm param above)
        var number_results = null;
        for (var i = 0; i < number_formats.length; i++) {
            var group_sep = number_formats[i]["digit_group_symbol"];
            var decimal   = number_formats[i]["decimal_symbol"];
    
            //  number format (en): /^\s*([-\\+]?)((\d{1,3}(?:(,)\d{3})*|\d*))((\.)?|(\.)\d*)\s*$/
            var number_re = new RegExp( getNumberRegExpString(group_sep,decimal) );
            number_results = number_re.exec(val);
            if (number_results != null) {
                return true;
            }
        }
    
        return false;
    }
    else {
        return (! isNaN(val));  
    }
}

function getNumberRegExpString(group_sep,decimal)
{
    group_sep = group_sep || "";
    decimal   = decimal || "";
    return "^\\s*" + 
           "([-\\+]?)(\\d{1,3}(?:(" + group_sep + ")\\d{3})*" + "|" + 
           "\\d*)" + "((" + decimal + ")?|(" + decimal + ")\\d*)" + 
           "\\s*$";
}

// formats a number in a human-readable locale string
function formatVal(val,lang_override)
{
    if (isNaN(val) || isNaN(parseFloat(val))) {
        return val;
    }
        
    // in strict mode, format val according to user langauge, otherwise
    // use def (mod30) mode
    var this_lang = lang_override || (number_formats.length == 1 ? lang : "def");
    var this_format = all_number_formats[this_lang];
  
    var group_sep = this_format["digit_group_symbol"].replace(/\\/, "");
    var decimal   = this_format["decimal_symbol"].replace(/\\/, "");
    val += "";
  
    // do number match on js number format
    var number_re = new RegExp( getNumberRegExpString("","\\.") );
    number_re.exec(val);
    var negative_sign = RegExp.$1 == "-" ? "-" : "";
    var integer_num = RegExp.$2;
    var decimal_num = RegExp.$4;
  
    // add digit grouping to integer
    var new_integer = "";
    var integer_array = integer_num.split("").reverse();
    for (var i = 1; i <= integer_array.length; i++) {
        var this_char = integer_array[i-1];
        new_integer += ( i % 3 == 0 && i < integer_array.length ) ? this_char + group_sep : this_char;
    }
    new_integer = new_integer.split("").reverse().join("");
  
    // replace canonical decimal with lang-specific decimal
    decimal_num = decimal_num.replace(/\./, decimal);

    return negative_sign + new_integer + decimal_num;
}

// based on the validate_lang property of RequriedForm,
// groks numbers formatted as locale-based strings and returns
// a real number javascript understands
function unFormatVal(val)
{
    // bail if the submitted number is valid to avoid 
    // inadvertent translation (e.g. 11.234 becoming 11234
    // because dot is a group separator in Germany
    var number_re_valid = new RegExp( getNumberRegExpString() );
    if (! val || ! lang || String(val).match(/^\s*$/) || String(val).match(number_re_valid)) {
        return parseFloat(val);
    }
        
    // loop through languages looking for unformatting
    if (number_formats.length) {
        val += "";
        for (var i = 0; i < number_formats.length; i++) {
            var group_sep = number_formats[i]["digit_group_symbol"];
            var decimal   = number_formats[i]["decimal_symbol"];
            var orig_val = val;

            // checking user language first, but go to def if 
            // decimal only format fails
            var number_no_group_re = new RegExp( getNumberRegExpString(null,decimal) );
            var number_re          = new RegExp( getNumberRegExpString(group_sep,decimal) );
            if (i == 0 && ! val.match(number_no_group_re)) {
                if (! val.match(number_re)) {
                    continue;
                }
            }

            // replace group and decimal chars to return 
            // normal number
            number_re.exec(val);
            var this_group_sep = RegExp.$3;
            var this_decimal   = RegExp.$5 || RegExp.$6;

            if (this_group_sep.match(new RegExp(group_sep))) {
                val = val.replace(new RegExp(group_sep,"g"), "");
            }
            if (this_decimal.match(new RegExp(decimal))) {
                val = val.replace(new RegExp(decimal,"g"), ".");
            }
                
            var changed_val = (orig_val != val);

            // if this format worked, don't check any more
            if (changed_val) {
                break;
            }
        }
    }

    return isNaN(val) ? val : parseFloat(val);
}

