/* FormUtils package, version 0.4
* (c) April 2007 Elmar Keij
*
* Description:
*
* Credits:
*
* License:
*
* Usage - automatic:
*
* Usage - manual:
*
*/

// create the package object
var FormUtils = new base2.Package(this, {
    name:    "FormUtils",
    version: "0.5b",
    imports: "DOM, A1B.Util",
    exports: "FormTransformer, FormValidator",
    
    extractValues: function (form) {
        var compStack = new Object(); // key=value pairs
        if (!form) return compStack;
        
        var el;
        for (var i = 0; i < form.elements.length; i++ ) {
            el = form.elements[i];
              if (el.disabled || !el.name) {
                  // Don't submit disabled elements.
                  // Don't submit elements without name.
                  continue;
              }
              if (!el.type) {
                  // It seems that this element doesn't have a type set,
                  // so skip it.
                  continue;
              }
              switch(el.type.toLowerCase()) {
                  case 'text':
                  case 'password':
                  case 'textarea':
                  case 'hidden':
                  case 'submit':
                      compStack[el.name] = el.value != "" ? el.value : "-";
                      break;
                  case 'select-one':
                      var opt;
                      var value;
                      if (el.selectedIndex >= 0) {
                          opt = el.options[el.selectedIndex];
                          var hasValueAttr = Element.hasAttribute(opt, "value");
                          value = hasValueAttr && el.value != "" ? opt.text : "-";
                      }
                      compStack[el.name] = value;
                      break;
                  case 'select-multiple':
                      var values = new Array();
                      var value;
                      for (var j = 0; j < el.length; j++) {
                          var hasValueAttr = Element.hasAttribute(el.options[j], "value");
                          if (el.options[j].selected) {
                              value = hasValueAttr && el.value != "" ? el.options[j].text : "-";
                              values.push(value);
                          }
                          compStack[el.name] = values;
                      }
                      break;
                  case 'checkbox':
                  case 'radio':
                      if (el.checked){
                          var label = Element.querySelectorAll(el.parentNode, ("label[for="+ el.id +"]")).item(0);
                          compStack[el.name] = label.innerHTML;
                      }
                      break;
                  default:
                      // file, button, reset
                      break;
              }
        }
        return compStack;
    }    
});

// evaluate the imported namespace
eval(this.imports); 

var MSIE = detect("MSIE");

// define package contents
var FormTransformer = Base.extend({
    form : null,
    
    constructor: function(myform){
        this.form = myform;
    },
    
    registerElements : function(es) {
        for(var i = 0; i < es.length; i++){
            var e = es.item(i);
            var value = e.getAttribute("transform");
            this._monitor(e, this._normalize(value));
        }
    },
    
    _normalize : function(v){
        var re = /([A-Za-z0-9_':,\s\[\]])+/g;
        var result = v.match(re);
        return result;
    },
    
    _monitor : function(observer, list){      
        var observables = new Object();
        
        //the eval in this loop is expensive, but i do not see a way around it except explicit parsing
        // build list of observables
        for(var i = 0; i < list.length; i++){
            var splits = String2.split(list[i], ":", 2);
            //remove whitespace and get observable
            var observable = splits[0].replace(/\s/, "");
            var vals = eval(splits[1]);
            // cast to array
            vals = vals.constructor === Array ? vals : [vals];
            observables[observable] = vals;
        }
        
        // trigger initial
        this._changeHandler(observer, observables, null)
        
        //the eventlisteners in this loop are expensive, test if this is due changehandler or just the function
        // register observers
        for(name in observables){
            var observable = this.form[name];
            observable = observable.type == undefined ? observable : [observable];
            //alert("observable: " + observable)
            for(var j = 0; j < observable.length; j++){
                var o = observable[j];
                Element.addEventListener(o, "change", bind(this._changeHandler, this, observer, observables), false);
                // the onchange event isn't thrown for radio buttons in IE until they lose their focus
                if(MSIE && o.type == "radio"){
                    Element.addEventListener(o, "click", function(event){event.target.blur();}, false)
                }
            }
        } 
    },
    
    _changeHandler : function(observer, observables){
        var transform = true;
        //alert("arguments: " + arguments.length + "\n" + arguments[0].id + "\n" + arguments[1].id +"\n"+ arguments[2].id +"\n"+ arguments[3]);
        for(name in observables){
            var observable = this.form[name];
            var vals = observables[name];
            var getval = observable.type == undefined ? this._getRadioValue : this._getValue;
            transform = transform && Array2.contains(vals, getval(observable));
        }
        
        transform ? ElementVisibility.show(observer, this.form) : ElementVisibility.hide(observer, this.form);
    },
    
    _getRadioValue : function(elements){
        for(var i = 0; i < elements.length; i++){
            var element = elements[i];
            if(element.checked){
                return element.value
            }
        }
    },
    
    _getValue : function(element){
        // if element not a form element           
        if(element.type == undefined)
            throw "Type Error: " + element + " is not a form element";
        return element.value;
    }
});

var FormValidator = Base.extend({
    form : null,
    _valid : null,
    
    constructor: function(myform){
        this.form = myform;
        this.submitted = false;
        this.realAction = this.form.action;
        this._valid = new Object();
        
        Element.addEventListener(this.form, 'keypress', bind(this.keyPress, this), false);
        
        Element.addEventListener(this.form, 'submit', bind(this.formSubmitted, this), false);
        this.form.setAttribute("action", "javascript:(function(){}).apply()");
        
        Element.addEventListener(this.form, 'reset', bind(this.formReset, this), false);
        
        for(var i = 0; i < this.form.elements.length; i++) {
            var element = this.form.elements[i];
            this._valid[element.id] = null;            
            if(element.type == "text" || element.type == "textarea")
                Element.addEventListener(element, 'keyup', bind(this.inputChanged, this), false);
            if(element.type == "select-one" || element.type == "select-multiple")
                Element.addEventListener(element, 'change', bind(this.inputChanged, this), false);
            if(element.type == "text" || element.type == "textarea" || element.type == "select-one" || element.type == "select-multiple")
                Element.addEventListener(element, 'blur', bind(this.inputChanged, this), false);
        }
    },
    
    isValid: function(e){
        return this._valid[e.id] == null ? false : this._valid[e.id];
    },
    
    setValid: function(e, valid){
        this._valid[e.id] = valid;
        this.toggleValid(e);
    },
    
    toggleValid: function(e) {
        if (this._valid[e.id] == null){
            HTMLElement.removeClass(e, "ok");
            HTMLElement.removeClass(e, "error");
        } 
        else if (this._valid[e.id]) {
            HTMLElement.removeClass(e, "error");
            HTMLElement.addClass(e, "ok");
        } 
        else {
            HTMLElement.removeClass(e, "ok");
            HTMLElement.addClass(e, "error");
        }
    },
    
    inputChanged : function (event) {
        this.validateInput(event.target);
    },
    
    formSubmitted : function() {
        var formValid = this.validateElements(this.form.elements);
        if(formValid)
            this.form.setAttribute("action", this.realAction);
    },
    
    formReset: function(event) {
        var elements = this.form.elements;
        for (var i = 0; i < elements.length; i++) {
            var element = elements[i];
            element.setValid(null);
        }
    },
    keyPress: function(event){
        if(event.which == 13){
            Event.preventDefault(event);
        }
    },
    
    validateElements : function(elements){
        var valid = true;
        for (var i = 0; i < elements.length; i++) {
            var element = elements.item ? elements.item(i) : elements[i];
            if (!this.isValid(element) &&
                element.tagName != "FIELDSET" &&
                element.type != "reset" &&
                element.type != "submit" &&
                element.type != "button" &&
                element.type != "radio" &&
                ElementVisibility.visible(element))
            {
                this.validateInput(element);
                valid = valid && this.isValid(element);
            }
        }
        return valid;
    },
    
    validateInput : function(element){
        var t_required = "t_required";
        
        if (HTMLElement.hasClass(element, "t_required")) {
            if(element.type == "select-one" || element.type == "select-multiple")
                this.setValid(element, !this.isEmptySelect(element));
            else
                this.setValid(element, !this.isEmpty(element.value) && this.checkType(element));
        }
        else { 
            this.setValid(element, this.checkType(element));
        }
    },
    
    // private helper function
    checkType: function(element) {
        // declare types
        var t_alpha        = "t_alpha";
        var t_numeric      = "t_numeric";
        var t_alphanumeric = "t_alphanumeric";
        var t_money        = "t_money";
        var t_date         = "t_date";
        var t_email        = "t_email";
        var t_tphone       = "t_tphone";
        var t_phone        = "t_phone";
        var t_mobile       = "t_mobile";
        var t_bank         = "t_bank";
        var t_zip          = "t_zip";
        var value          = String2.trim(element.value);
        
        if (HTMLElement.hasClass(element, "t_alpha"))
            return this.isAlpha(value);
        
        if (HTMLElement.hasClass(element, "t_numeric"))
            return this.isNumeric(value);
        
        if (HTMLElement.hasClass(element, "t_alphanumeric"))
            return this.isAlphaNumeric(value);
        
        if (HTMLElement.hasClass(element, "t_money"))
            return this.isMoney(value);
        
        if (HTMLElement.hasClass(element, "t_date"))
            return this.isDate(value);
        
        if (HTMLElement.hasClass(element, "t_email"))
            return this.isEmail(value);
        
        if (HTMLElement.hasClass(element, "t_phone"))
            return this.isDutchPhoneNumber(value);
        
        if (HTMLElement.hasClass(element, "t_mobile"))
            return this.isMobileNumber(value);
        
        if (HTMLElement.hasClass(element, "t_bank"))
            return this.isBankAccount(value);
        
        if (HTMLElement.hasClass(element, "t_zip"))
            return this.isZipCode(value);
        
        // if element is not of a compatible type it is valid by default
        return true;
    },
    
    // returns true if the string is empty
    isEmpty : function(str){
        return (str == null) || (str.length == 0);
    },
    
    // returns true if the select has a disabled value
    isEmptySelect : function(e){
        if (e.type == "select-multiple") {
            return e.selectedIndex == -1;
        }    
        else {
            for (var i = 0; i < e.options.length; i++){
                var option = e.options[i];
                if(e.value == option.value && option.disabled)
                    return true;
            }
            return false;
        }
    },
    
    // returns true if the string only contains characters A-Z or a-z
    isAlpha : function(str){
        if(this.isEmpty(str)) return true;
        var re = /^[a-zA-Z]+$/
        return re.test(str);
    },
    
    // returns true if the string only contains characters 0-9
    isNumeric : function(str){
        if(this.isEmpty(str)) return true;
        var re = /^\d+$/
        return re.test(str);
    },
    
    // returns true if the string only contains characters A-Z, a-z or 0-9
    isAlphaNumeric : function(str){
        if(this.isEmpty(str)) return true;
        var re = /^[a-zA-Z0-9]+$/
        return re.test(str);
    },
    
    isMoney : function(str){
        if(this.isEmpty(str)) return true;
        var re = /^\u20AC?[1-9](\d+|(\d{0,2}\.\d{3})+)?(,(-|\d{2}))?$/
        return re.test(str);
    },
    
    // returns true if the string is a valid date formatted as...
    // dd mm yyyy, dd/mm/yyyy, dd.mm.yyyy, dd-mm-yyyy
    isDate : function(str){
        var re = /^(\d{1,2})[\s\.\/-](\d{1,2})[\s\.\/-](\d{4})$/
        if(this.isEmpty(str)) return true;
        if (!re.test(str)) return false;
        var result = str.match(re);
        var d = parseInt(result[1].replace(/0(?=\d)/, ""));
        var m = parseInt(result[2].replace(/0(?=\d)/, ""));
        var y = parseInt(result[3]);
        if(m < 1 || m > 12 || y < 1900 || y > 2100) return false;
        if(m == 2){
            var days = ((y % 4) == 0) ? 29 : 28;
        }
        else if(m == 4 || m == 6 || m == 9 || m == 11){
            var days = 30;
        }
        else{
            var days = 31;
        }
        return (d >= 1 && d <= days);
    },
    
    // returns true if the string is a valid email
    //The email address regular expression accepts email addresses in the format username @ domain name. There are a couple of other valid email address formats (eg. username @ [ip address]) but this regular expression only accepts the more common format.
    isEmail : function(str){
        if(this.isEmpty(str)) return true;
        var re = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*\.(\w{2}|(com|net|org|edu|int|mil|gov|arpa|biz|aero|name|coop|info|pro|museum|asia|cat|jobs|mobi|tel|travel))$/
        return re.test(str);
    },
    
    isTotalPhoneNumber : function(str){
        if(this.isEmpty(str)) return true;
        return this.isDutchPhoneNumber(str) || this.isMobileNumber(str) || this.isIntPhoneNumber(str);
    },
    
    // The only supported country numbers are Netherlands(31), Belgium(32), France(33), Germany(49) and Luxemburg(352)
    // valid formats: +31(0)318123456, 0031(0)318123456, +31 (0)318123456, 0031 (0)318123456, +31 (0)318 123456, 0031 (0)318 123456
    //                +31(0)612345678, 0031(0)612345678, +31 (0)612345678, 0031 (0)612345678, +31 (0)6 12345678, 0031 (0)6 12345678
    isIntPhoneNumber : function(str){
        if(this.isEmpty(str)) return true;
        var re = /^(\+|00)(31|32|33|49|352)\s?(\(0\))?(6\s?\d{8}|\d{2}\s?\d{7}|\d{3}\s?\d{6})$/
        return re.test(str);
    },
    
    // Dutch phone number notation; valid formats are:
    // 0318123456, 0318 123456, 0318-123456, 0201234567, 020 1234567, 020-1234567
    isDutchPhoneNumber : function(str){
        if(this.isEmpty(str)) return true;
        var re = /^0([1-5,7,8]\d(\s|-)?\d{7}|[1-5,7,8]\d{2}(\s|-)?\d{6})$/
        return re.test(str);
    },
    
    // Local mobile number; valid formats are:
    // 0612345678, 06 12345678
    isMobileNumber : function(str){
        if(this.isEmpty(str)) return true;
        var re = /^06(\s|-)?\d{8}$/
        return re.test(str);
    },
    
    // returns true if the string is a valid date formatted as...
    // mm dd yyyy, mm/dd/yyyy, mm.dd.yyyy, mm-dd-yyyy
    isUSDate : function(str){
        var re = /^(\d{1,2})[\s\.\/-](\d{1,2})[\s\.\/-](\d{4})$/
        if (!re.test(str) || this.isEmpty(str)) return false;
        var result = str.match(re);
        var m = parseInt(result[1].replace(/0(?=\d)/, ""));
        var d = parseInt(result[2].replace(/0(?=\d)/, ""));
        var y = parseInt(result[3]);
        if(m < 1 || m > 12 || y < 1900 || y > 2100) return false;
        if(m == 2){
            var days = ((y % 4) == 0) ? 29 : 28;
        }
        else if(m == 4 || m == 6 || m == 9 || m == 11){
            var days = 30;
        }
        else{
            var days = 31;
        }
        return (d >= 1 && d <= days);
    },
    
    // Dutch Bank account
    isBankAccount : function(str){
        if(this.isEmpty(str)) return true;
        var _elf = 0;
        // remove preceding 0's'
        var newStr = '';
        var con = true;
        for (var i=0; i < str.length; i++){
            if (str.charAt(i) != '0')
                con = false;
            if (str.charAt(i) != '0' || !con)
                newStr += str.charAt(i);
        }
        str = newStr;
        var elfproef = function(s){
            var dot = /\./g
            var clean = str.replace(dot, "");
            for(var i = 0; i < clean.length; i++){
                _elf = _elf + clean[i] * (clean.length - i);
            }
            return _elf % 11 == 0;
        };
        var re = /^\d{9}|\d{2}\.\d{2}\.\d{2}\.\d{3}$/
        var postbank = /^\d{1,7}$/
        return postbank.test(str) || (re.test(str) && elfproef(str));
    },
    
    isZipCode : function(str){
        if(this.isEmpty(str)) return true;
        var re = /^\d{4}\s?[A-Z,a-z]{2}$/
        return re.test(str);
    },
    
    // returns true if "str1" is the same as the "str2"
    isMatch : function(str1, str2){
        return str1 == str2;
    },
    
    // returns true if the string contains only whitespace
    // cannot check a password type input for whitespace
    isWhitespace : function(str){ // NOT USED IN FORM VALIDATION
        var re = /\s/g
        return re.test(str);
    },
    
    // removes any whitespace from the string and returns the result
    // the value of "replacement" will be used to replace the whitespace (optional)
    stripWhitespace : function(str, replacement){// NOT USED IN FORM VALIDATION
        if (replacement == null) replacement = '';
        var result = str;
        var re = /\s/g
        if(str.search(re) != -1){
            result = str.replace(re, replacement);
        }
        return result;
    },
    
    // returns true if the string's length equals "len"
    isLength : function(str, len){
        return str.length == len;
    },
    
    // returns true if the string's length is between "min" and "max"
    isLengthBetween : function(str, min, max){
        return (str.length >= min)&&(str.length <= max);
    }    
});

// evaluate the exported namespace (this initialises the Package)
eval(this.exports);
