/*********************
  form.js

  Form helper routines - disableSubmitOnEnterFormWide

  Form helper routines - radio and select
  Form validation routines
    Elements are marked for validation by adding special css Classes.
    Validation happens automatically onblur.  If there is an element 
    with the id controlname_Error, an error message will apepar there.
    If a control fails validation, its elem.valid property will be set to false.
**********************/

var form = new Object; // namespace

/*
  formValReq - the control must have a non-blank value
  formValNum - the control must have a numeric value
  formValInt - the control must have a integer value
  formValAlpha - the control must have a alphabetical value
  formValName - alpha, spaces, apostrophes, periods, and ampersands
  formValDate - parseable as a Date
  formValZip  - zip code, 5 or 5-4
  formValUrl  - looks like a URL
*/


/***********************************
       Submission Control
***********************************/
form.disableSubmitOnEnterFormWide = function(formElem) {
    formElem = $(formElem);
    formElem.getElements().each(function (elem) {
        Event.observe(elem, "keypress", form.ignoreEnter);
    });
};
form.ignoreEnter = function(evt) {
    var key = evt.keyCode || evt.which;
    //alert('In ignoreEnter, have key ' + key + ' and KEY RETURN is ' + Event.KEY_RETURN);
    if (key == Event.KEY_RETURN) {
        Event.stop(evt);
        return false;
    } else {
        return true;
    }
};



/***********************************
   Radio and Select Helpers
************************************/

// Returns the selected state of the option with the 
// specified value in the given select.
form.getSelectOption = function(id, value) {
    var sel = $(id);
    var opts = sel.options;

    // Find and select the desired option
    for (var i = 0; i < opts.length;  i++) {
	if (opts[i].value == value) {
            return opts[i].selected;
	}
    }

    return undefined;
}

// Returns the value of the first selected option in the given select.
form.getSelectFirstSelected = function(id) {
    var sel = $(id);
    var opts = sel.options;

    // Find and select the desired option
    for (var i = 0; i < opts.length;  i++) {
	if (opts[i].selected) {
            return opts[i].value
	}
    }

    return undefined;
}


// Returns the number of options selected in the given select.
form.getSelectedCount = function(id) {
    var sel = $(id);
    var opts = sel.options;
    var count = 0;
    
    // Find and select the desired option
    for (var i = 0; i < opts.length;  i++) {
        count += opts[i].selected ? 1 : 0;
    }

    return count;
}

// Sets the Select control with the given ID to the option with the given value
form.setSelect = function(id, value) {
    var sel = $(id);
    var opts = sel.options;

    // Unselect everything 
    for (var i = 0; i < opts.length;  i++) {
	opts[i].selected = false;
     }

    // Find and select the desired option
    for (var i = 0; i < opts.length;  i++) {
	if (opts[i].value == value) {
	    opts[i].selected = true;
	    return true;
	}
    }

    // Not found...
    return false;
};

// Returns the value of the selected radio button, given the id of button in the group.
form.getRadioValue = function(elemArg) {
    elemArg = $(elemArg);
    var name = elemArg.name;
    var frm = elemArg.form;
    var rads = frm.elements[name]; // should be an array
    for (var i = 0; i < rads.length; i++) {
        if (rads[i].checked) { return rads[i].value; }
    }
    return undefined;
};


/*******************************************
               Validation
********************************************/


/*****
  Desiderata:
  - validate as a predicate on-demand
  - sepearate validation from error message display
  - make it easy to know if an entire form is entirely valid
  - make it easy to know if a list of controls is all valid 
  - allow dynamic attachment of validators
  - continue to use css classes to mark validation rules
  - allow on-the-fly addition of custom validators
*****/

// Given a control ID, wire up the validators that the control requests in its CSS classes.
form.attachValidation = function(id) {
    var type = $(id).type;
    Event.observe($(id), 'blur', form.validateControlOnEvent);
    // If it's text, also attach to onkeypress
    if (type == 'text' || type == 'password') {
        Event.observe($(id), 'keypress', form.validateControlOnEventWithDelay);
    }
};

// Stop validating a control (presumably because you're about to destroy it). 
form.detachValidation = function(id) {
    var type = $(id).type;
    Event.stopObserving($(id), 'blur', form.validateControlOnEvent);
    if (type == 'text' || type=='password') {
        Event.stopObserving($(id), 'keypress', form.validateControlOnEventWithDelay);
    }
};

// Validate the control, and show the error message in response to user events, 
// but only if this event hasn't happened in an a short period.
form.validationIntID = undefined;

form.validateControlOnEventWithDelay = function(evt) {
    var elem = Event.element(evt);
    if (form.validationIntID) {
        clearTimeout(form.validationIntID);
    }
    form.validationIntID = setTimeout('form.delayedValidation("' + elem.id + '",true);', 1000);
};
form.delayedValidation = function(id, showError) {
    clearTimeout(form.validationIntID);
    form.validationIntID = undefined;
    form.validateControl(id, showError);
    form.updateButtonsForField(id);
};

// Validate the control, and show the error message in response to user events.
form.validateControlOnEvent = function(evt) {
    var elem = Event.element(evt);
    form.validateControl(elem.id, true);
    form.updateButtonsForField(elem.id);
}

// Validate one control, optionally showing the error.
form.validateControl = function(id, showError) {

    if (form.validationIntID) {
        clearTimeout(form.validationIntID);
        form.validationIntID = undefined;
    }

    if (showError == undefined) { showError = true; }

    // Loop over CSS classes, looking for known validation classes
    // if it fails validation, remember the class.
    var valid = true;
    var failClass = '';
    var elem = $(id);
    elem.classNames().each(function(cls) {

        var validator = form.valsByClass[cls];
        if (valid && validator) {
            valid = validator(elem, showError); // Second param only used by valMatch
            if (! valid) { failClass = cls; }   
        }
    });

    // Show/clear error message if requested.
    if (showError) {
        if (valid) {
            form.clearError($(id));
        } else {
            form.showError($(id), failClass);
        }
    }

    return valid;
};

// Given an array of field IDs, validate them all.
form.validateArray = function(fields, showError) {
    fields = $A(fields);
    var allvalid = true;
    fields.each(function(field) {
        //alert('About to validate ' + field);
        allvalid = allvalid && form.validateControl(field, showError);
    });
    return allvalid;
};

// TODO - remove this.
form.isValid = function(id) { return form.validateControl(id, false); };

// Cry foul.
form.showError = function(ctlElem, valClass) {
    //alert('In showError, have ctlElem ' + ctlElem + ' and valClass ' + valClass);
    
    var err = $(ctlElem.id + '_Error');
    var msg = form.errorsByClass[valClass];
    if (err) {
        err.innerHTML = msg;
        if (err.tagName.toLowerCase() != 'td') { err.show(); }
    } else {
        alert(msg);
    }

};

// Un-cry foul.
form.clearError = function(ctlElem) {
    //alert('In clearError');
    var err = $(ctlElem.id + '_Error');
    if (err) {
        err.innerHTML = '&nbsp;';
        if (err.tagName.toLowerCase() != 'td') { err.hide(); }
    }

};

// Add a custom validator 
form.addCustomValidator = function (className, errorMsg, validator) {
    form.valsByClass[className]  = validator;
    form.errorsByClass[className] = errorMsg;
}


// Setup
form.injectValidators = function () {
    // Perl-style: accumulate elems of interest using their IDs as keys to this hash.
    var elems = new Object;

    // Loop over known validator classes and find any elems that have at least one.
    Object.keys(form.valsByClass).each(function(valClass) {
        var ctls = document.getElementsByClassName(valClass);
        for(var i=0; i < ctls.length; i++) {
            elems[ctls[i].id] = 1;
        }
    });

    // Now that we have a unique collection, loop over it.
    Object.keys(elems).each(function(id) {
        form.attachValidation(id);
    });
    
};
onloadHooks.addHook(form.injectValidators);

/*******************************************
     Handy-Dandy Global Button Disablement
********************************************/
form.field2buttons = new Object; // Map field IDs to arrays of buttons they should affect.
form.button2fields = new Object; // Map button IDs to lists of fields that affect them.

form.monitorButton = function(buttonID, fields, immediateValidation) {
    fields = $A(fields).flatten();
    buttonID = $(buttonID).id;
    button = $(buttonID);
    if (immediateValidation == undefined) { immediateValidation = true; }

    // Add the info to the lists.
    form.button2fields[buttonID] = fields;
    fields.each(function(field) {
        var list = form.field2buttons[field];
        if (list) {
            list[list.length] = button;
        } else {
            list = [ button ];
        }
        form.field2buttons[field] = list;
    });

    // Go ahead and validate now
    if (immediateValidation) {
        form.updateButtonEnable(buttonID);
    }
};
form.unmonitorButton = function(buttonID, fields) {
    fields = $A(fields).flatten();
    buttonID = $(buttonID).id;
    button = $(buttonID);

    // Remove the info from the lists.
    fields.each(function(fn) {
        form.button2fields[buttonID] = form.button2fields[buttonID].without(fn);
        form.field2buttons[fn] = form.field2buttons[fn].without(button);
    });
};

form.updateButtonEnable = function(buttonID) {
    var fields = form.button2fields[buttonID];
    var allvalid = form.validateArray(fields, false);
    $(buttonID).disabled = ! allvalid;
    if (! allvalid) { help.abortHover(buttonID); }
};
form.updateButtonsForField = function(field) {
    var buttons = form.field2buttons[field];
    if (buttons) {
        buttons.each(function(button) {
            form.updateButtonEnable(button.id);
        });
    }
};


/*******************************************
               Validators
********************************************/

/***
  Validator static data
***/
form.errorsByClass = {
    'formValReq'   : 'This field is required.',
    //'formValNum'   : '',
    'formValPhone' : 'This field should be a phone number, including area code, in the form (123) 456-7890.',
    'formValEmail' : 'This field must be a valid email address, like "jane@example.edu".',
    'formValInt'   : 'This field must be a whole number, like 5 or 27.',
    'formValAlpha' : 'This field should only have letters in it.',
    'formValName'  : 'This field should only have letters, spaces, periods, apostrophes, and ampersands.',
    'formValDate'  : 'This field should be a valid date in MM/DD/YYYY format.',
    'formValMatch' : 'The fields do not match.',
    'formValZip'   : 'This field should have either 5 or 9 digits with an optional dash.',
    'formValUrl'   : 'This field should look like a web address, like "http://www.yahoo.com"'

};

form.regexByClass = {
    //'formValNum'   : '',
    'formValEmail' : /^[^\s\@\"\']+[\@]([\w\-]+\.)+(\w+)$/,
    'formValPhone' : /^\s*((1\-)?(\(?\d{3}\)?(\-|\s)?)\d{3}(\-|\s)?\d{4})\s*$/,
    'formValInt'   : /^\d+$/,
    'formValZip'   : /^\d{5}(\-?)(\d{4})?$/,
    'formValUrl'   : /^http(s?):\/\/[\d\w]+(\.[\d\w]+)+(\.((com)|(org)|(edu)|(gov)|(us)|(net)|(info)))(\/.*)?$/,
    'formValAlpha' : /^[A-Za-z]+$/,
    'formValName'  : /^[\w\s\.\'\&]+$/,
    'formValDate'  : /^\d\d?\/\d\d?\/\d{4}$/
};
//                   x  J  F  M  A  M  J  J  A  S  O  N  D
form.daysPerMonth = [0, 31,29,31,30,31,30,31,31,30,31,30,31];


// General validator for regex-based vaildations.
form.validateRegex = function(elem, valClass) {
    //alert('In validateRegex, have elem ' + elem);
    if (! elem.value) { return true; } // hmmmm....???
    //alert('In validatebyRegex, have regex for vC ' + valClass + ' as  ' + form.regexByClass[ valClass ]);
    return form.regexByClass[ valClass ].test(elem.value);
};

form.validateRequired = function(elem) { return (elem.value && elem.value.length != 0); };

form.validateAlpha    = function(elem) { return form.validateRegex(elem, 'formValAlpha' ); };
form.validateName     = function(elem) { return form.validateRegex(elem, 'formValName'  ); };
form.validateInteger  = function(elem) { return form.validateRegex(elem, 'formValInt'   ); };
form.validateEmail    = function(elem) { return form.validateRegex(elem, 'formValEmail' ); };
form.validatePhone    = function(elem) { return form.validateRegex(elem, 'formValPhone' ); };
form.validateZip      = function(elem) { return form.validateRegex(elem, 'formValZip'   ); };
form.validateUrl      = function(elem) { return form.validateRegex(elem, 'formValUrl'   ); };

// Implements formValDate
form.validateDate = function(elem) {
    if (! elem.value) { return true; }

    // First see if it passes regex
    if (! form.regexByClass[ 'formValDate' ].test(elem.value)) {
        return false;
    }

    // Date contructor will happily parse a bogus date.... so will Date.parse.
    var parts = elem.value.match(/^(\d\d?)\/(\d\d?)\/(\d{4})$/);
    if (0 < parts[1] && parts[1] < 13 && // Month between 1 and 12 inclusive
        0 < parts[2] && parts[2] <= form.daysPerMonth[parts[1]] &&
        1900 < parts[3] && parts[3] < 2100) { // Year between 1901 and 2099 inclusive
        return true;        
    } else {
        return false;
    }
};

// Checks that this control matches another control.
// Match pairs are set by adding a class of the form 'match-XXXX'.
form.validateMatch = function(elem, showError) {
    //alert('In valMatch');

    // Find the match class tag
    var re = /match.+/;
    var tag = elem.classNames().find(function(cls) { return re.test(cls); });

    // Find all controls in this match ring, and confirm they all match
    var allmatch = true;
    var value = elem.value;
    var matches =  document.getElementsByClassName(tag);
    matches.each(function(elem) {
        allmatch = allmatch && (elem.value == value);
    });

    if (showError) {
        matches.each(function(elem) {
            if (allmatch) {
                form.clearError(elem);
            } else {
                form.showError(elem, 'formValMatch');
            }
        });

    }

    return allmatch;
}


// Callbacks by class name
form.valsByClass = {
    'formValReq'   : form.validateRequired,
    //'formValNum'   : form.validateNumeric,
    'formValEmail' : form.validateEmail,
    'formValPhone' : form.validatePhone,
    'formValAlpha' : form.validateAlpha,
    'formValName'  : form.validateName,
    'formValInt'   : form.validateInteger,
    'formValZip'   : form.validateZip,
    'formValUrl'   : form.validateUrl,
    'formValDate'  : form.validateDate,
    'formValMatch' : form.validateMatch
};
