//MooTools More, <http://mootools.net/more>. Copyright (c) 2006-2009 Aaron Newton <http://clientcide.com/>, Valerio Proietti <http://mad4milk.net> & the MooTools team <http://mootools.net/developers>, MIT Style License.

/*
---

script: More.js

description: MooTools More

license: MIT-style license

authors:
- Guillermo Rauch
- Thomas Aylott
- Scott Kyle

requires:
- core:1.2.4/MooTools

provides: [MooTools.More]

...
*/

MooTools.More = {
    'version': '1.2.4.4',
    'build': '6f6057dc645fdb7547689183b2311063bd653ddf'
};

/*
---

script: MooTools.Lang.js

description: Provides methods for localization.

license: MIT-style license

authors:
- Aaron Newton

requires:
- core:1.2.4/Events
- /MooTools.More

provides: [MooTools.Lang]

...
*/

(function(){

    var data = {
        language: 'en-US',
        languages: {
            'en-US': {}
        },
        cascades: ['en-US']
    };
    
    var cascaded;

    MooTools.lang = new Events();

    $extend(MooTools.lang, {

        setLanguage: function(lang){
            if (!data.languages[lang]) return this;
            data.language = lang;
            this.load();
            this.fireEvent('langChange', lang);
            return this;
        },

        load: function() {
            var langs = this.cascade(this.getCurrentLanguage());
            cascaded = {};
            $each(langs, function(set, setName){
                cascaded[setName] = this.lambda(set);
            }, this);
        },

        getCurrentLanguage: function(){
            return data.language;
        },

        addLanguage: function(lang){
            data.languages[lang] = data.languages[lang] || {};
            return this;
        },

        cascade: function(lang){
            var cascades = (data.languages[lang] || {}).cascades || [];
            cascades.combine(data.cascades);
            cascades.erase(lang).push(lang);
            var langs = cascades.map(function(lng){
                return data.languages[lng];
            }, this);
            return $merge.apply(this, langs);
        },

        lambda: function(set) {
            (set || {}).get = function(key, args){
                return $lambda(set[key]).apply(this, $splat(args));
            };
            return set;
        },

        get: function(set, key, args){
            if (cascaded && cascaded[set]) return (key ? cascaded[set].get(key, args) : cascaded[set]);
        },

        set: function(lang, set, members){
            this.addLanguage(lang);
            langData = data.languages[lang];
            if (!langData[set]) langData[set] = {};
            $extend(langData[set], members);
            if (lang == this.getCurrentLanguage()){
                this.load();
                this.fireEvent('langChange', lang);
            }
            return this;
        },

        list: function(){
            return Hash.getKeys(data.languages);
        }

    });

})();

/*
---

script: Class.Binds.js

description: Automagically binds specified methods in a class to the instance of the class.

license: MIT-style license

authors:
- Aaron Newton

requires:
- core:1.2.4/Class
- /MooTools.More

provides: [Class.Binds]

...
*/

Class.Mutators.Binds = function(binds){
    return binds;
};

Class.Mutators.initialize = function(initialize){
    return function(){
        $splat(this.Binds).each(function(name){
            var original = this[name];
            if (original) this[name] = original.bind(this);
        }, this);
        return initialize.apply(this, arguments);
    };
};


/*
---

script: Date.js

description: Extends the Date native object to include methods useful in managing dates.

license: MIT-style license

authors:
- Aaron Newton
- Nicholas Barthelemy - https://svn.nbarthelemy.com/date-js/
- Harald Kirshner - mail [at] digitarald.de; http://digitarald.de
- Scott Kyle - scott [at] appden.com; http://appden.com

requires:
- core:1.2.4/Array
- core:1.2.4/String
- core:1.2.4/Number
- core:1.2.4/Lang
- core:1.2.4/Date.English.US
- /MooTools.More

provides: [Date]

...
*/

(function(){

var Date = this.Date;

if (!Date.now) Date.now = $time;

Date.Methods = {
    ms: 'Milliseconds',
    year: 'FullYear',
    min: 'Minutes',
    mo: 'Month',
    sec: 'Seconds',
    hr: 'Hours'
};

['Date', 'Day', 'FullYear', 'Hours', 'Milliseconds', 'Minutes', 'Month', 'Seconds', 'Time', 'TimezoneOffset',
    'Week', 'Timezone', 'GMTOffset', 'DayOfYear', 'LastMonth', 'LastDayOfMonth', 'UTCDate', 'UTCDay', 'UTCFullYear',
    'AMPM', 'Ordinal', 'UTCHours', 'UTCMilliseconds', 'UTCMinutes', 'UTCMonth', 'UTCSeconds'].each(function(method){
    Date.Methods[method.toLowerCase()] = method;
});

var pad = function(what, length){
    return new Array(length - String(what).length + 1).join('0') + what;
};

Date.implement({

    set: function(prop, value){
        switch ($type(prop)){
            case 'object':
                for (var p in prop) this.set(p, prop[p]);
                break;
            case 'string':
                prop = prop.toLowerCase();
                var m = Date.Methods;
                if (m[prop]) this['set' + m[prop]](value);
        }
        return this;
    },

    get: function(prop){
        prop = prop.toLowerCase();
        var m = Date.Methods;
        if (m[prop]) return this['get' + m[prop]]();
        return null;
    },

    clone: function(){
        return new Date(this.get('time'));
    },

    increment: function(interval, times){
        interval = interval || 'day';
        times = $pick(times, 1);

        switch (interval){
            case 'year':
                return this.increment('month', times * 12);
            case 'month':
                var d = this.get('date');
                this.set('date', 1).set('mo', this.get('mo') + times);
                return this.set('date', d.min(this.get('lastdayofmonth')));
            case 'week':
                return this.increment('day', times * 7);
            case 'day':
                return this.set('date', this.get('date') + times);
        }

        if (!Date.units[interval]) throw new Error(interval + ' is not a supported interval');

        return this.set('time', this.get('time') + times * Date.units[interval]());
    },

    decrement: function(interval, times){
        return this.increment(interval, -1 * $pick(times, 1));
    },

    isLeapYear: function(){
        return Date.isLeapYear(this.get('year'));
    },

    clearTime: function(){
        return this.set({hr: 0, min: 0, sec: 0, ms: 0});
    },

    diff: function(date, resolution){
        if ($type(date) == 'string') date = Date.parse(date);
        
        return ((date - this) / Date.units[resolution || 'day'](3, 3)).toInt(); // non-leap year, 30-day month
    },

    getLastDayOfMonth: function(){
        return Date.daysInMonth(this.get('mo'), this.get('year'));
    },

    getDayOfYear: function(){
        return (Date.UTC(this.get('year'), this.get('mo'), this.get('date') + 1) 
            - Date.UTC(this.get('year'), 0, 1)) / Date.units.day();
    },

    getWeek: function(){
        return (this.get('dayofyear') / 7).ceil();
    },
    
    getOrdinal: function(day){
        return Date.getMsg('ordinal', day || this.get('date'));
    },

    getTimezone: function(){
        return this.toString()
            .replace(/^.*? ([A-Z]{3}).[0-9]{4}.*$/, '$1')
            .replace(/^.*?\(([A-Z])[a-z]+ ([A-Z])[a-z]+ ([A-Z])[a-z]+\)$/, '$1$2$3');
    },

    getGMTOffset: function(){
        var off = this.get('timezoneOffset');
        return ((off > 0) ? '-' : '+') + pad((off.abs() / 60).floor(), 2) + pad(off % 60, 2);
    },

    setAMPM: function(ampm){
        ampm = ampm.toUpperCase();
        var hr = this.get('hr');
        if (hr > 11 && ampm == 'AM') return this.decrement('hour', 12);
        else if (hr < 12 && ampm == 'PM') return this.increment('hour', 12);
        return this;
    },

    getAMPM: function(){
        return (this.get('hr') < 12) ? 'AM' : 'PM';
    },

    parse: function(str){
        this.set('time', Date.parse(str));
        return this;
    },

    isValid: function(date) {
        return !!(date || this).valueOf();
    },

    format: function(f){
        if (!this.isValid()) return 'invalid date';
        f = f || '%x %X';
        f = formats[f.toLowerCase()] || f; // replace short-hand with actual format
        var d = this;
        return f.replace(/%([a-z%])/gi,
            function($0, $1){
                switch ($1){
                    case 'a': return Date.getMsg('days')[d.get('day')].substr(0, 3);
                    case 'A': return Date.getMsg('days')[d.get('day')];
                    case 'b': return Date.getMsg('months')[d.get('month')].substr(0, 3);
                    case 'B': return Date.getMsg('months')[d.get('month')];
                    case 'c': return d.toString();
                    case 'd': return pad(d.get('date'), 2);
                    case 'H': return pad(d.get('hr'), 2);
                    case 'I': return ((d.get('hr') % 12) || 12);
                    case 'j': return pad(d.get('dayofyear'), 3);
                    case 'm': return pad((d.get('mo') + 1), 2);
                    case 'M': return pad(d.get('min'), 2);
                    case 'o': return d.get('ordinal');
                    case 'p': return Date.getMsg(d.get('ampm'));
                    case 'S': return pad(d.get('seconds'), 2);
                    case 'U': return pad(d.get('week'), 2);
                    case 'w': return d.get('day');
                    case 'x': return d.format(Date.getMsg('shortDate'));
                    case 'X': return d.format(Date.getMsg('shortTime'));
                    case 'y': return d.get('year').toString().substr(2);
                    case 'Y': return d.get('year');
                    case 'T': return d.get('GMTOffset');
                    case 'Z': return d.get('Timezone');
                }
                return $1;
            }
        );
    },

    toISOString: function(){
        return this.format('iso8601');
    }

});

Date.alias('toISOString', 'toJSON');
Date.alias('diff', 'compare');
Date.alias('format', 'strftime');

var formats = {
    db: '%Y-%m-%d %H:%M:%S',
    compact: '%Y%m%dT%H%M%S',
    iso8601: '%Y-%m-%dT%H:%M:%S%T',
    rfc822: '%a, %d %b %Y %H:%M:%S %Z',
    'short': '%d %b %H:%M',
    'long': '%B %d, %Y %H:%M'
};

var parsePatterns = [];
var nativeParse = Date.parse;

var parseWord = function(type, word, num){
    var ret = -1;
    var translated = Date.getMsg(type + 's');

    switch ($type(word)){
        case 'object':
            ret = translated[word.get(type)];
            break;
        case 'number':
            ret = translated[month - 1];
            if (!ret) throw new Error('Invalid ' + type + ' index: ' + index);
            break;
        case 'string':
            var match = translated.filter(function(name){
                return this.test(name);
            }, new RegExp('^' + word, 'i'));
            if (!match.length)    throw new Error('Invalid ' + type + ' string');
            if (match.length > 1) throw new Error('Ambiguous ' + type);
            ret = match[0];
    }

    return (num) ? translated.indexOf(ret) : ret;
};

Date.extend({

    getMsg: function(key, args) {
        return MooTools.lang.get('Date', key, args);
    },

    units: {
        ms: $lambda(1),
        second: $lambda(1000),
        minute: $lambda(60000),
        hour: $lambda(3600000),
        day: $lambda(86400000),
        week: $lambda(608400000),
        month: function(month, year){
            var d = new Date;
            return Date.daysInMonth($pick(month, d.get('mo')), $pick(year, d.get('year'))) * 86400000;
        },
        year: function(year){
            year = year || new Date().get('year');
            return Date.isLeapYear(year) ? 31622400000 : 31536000000;
        }
    },

    daysInMonth: function(month, year){
        return [31, Date.isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
    },

    isLeapYear: function(year){
        return ((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0);
    },

    parse: function(from){
        var t = $type(from);
        if (t == 'number') return new Date(from);
        if (t != 'string') return from;
        from = from.clean();
        if (!from.length) return null;

        var parsed;
        parsePatterns.some(function(pattern){
            var bits = pattern.re.exec(from);
            return (bits) ? (parsed = pattern.handler(bits)) : false;
        });

        return parsed || new Date(nativeParse(from));
    },

    parseDay: function(day, num){
        return parseWord('day', day, num);
    },

    parseMonth: function(month, num){
        return parseWord('month', month, num);
    },

    parseUTC: function(value){
        var localDate = new Date(value);
        var utcSeconds = Date.UTC(
            localDate.get('year'),
            localDate.get('mo'),
            localDate.get('date'),
            localDate.get('hr'),
            localDate.get('min'),
            localDate.get('sec')
        );
        return new Date(utcSeconds);
    },

    orderIndex: function(unit){
        return Date.getMsg('dateOrder').indexOf(unit) + 1;
    },

    defineFormat: function(name, format){
        formats[name] = format;
    },

    defineFormats: function(formats){
        for (var name in formats) Date.defineFormat(name, formats[name]);
    },

    parsePatterns: parsePatterns, // this is deprecated
    
    defineParser: function(pattern){
        parsePatterns.push((pattern.re && pattern.handler) ? pattern : build(pattern));
    },
    
    defineParsers: function(){
        Array.flatten(arguments).each(Date.defineParser);
    },
    
    define2DigitYearStart: function(year){
        startYear = year % 100;
        startCentury = year - startYear;
    }

});

var startCentury = 1900;
var startYear = 70;

var regexOf = function(type){
    return new RegExp('(?:' + Date.getMsg(type).map(function(name){
        return name.substr(0, 3);
    }).join('|') + ')[a-z]*');
};

var replacers = function(key){
    switch(key){
        case 'x': // iso8601 covers yyyy-mm-dd, so just check if month is first
            return ((Date.orderIndex('month') == 1) ? '%m[.-/]%d' : '%d[.-/]%m') + '([.-/]%y)?';
        case 'X':
            return '%H([.:]%M)?([.:]%S([.:]%s)?)? ?%p? ?%T?';
    }
    return null;
};

var keys = {
    d: /[0-2]?[0-9]|3[01]/,
    H: /[01]?[0-9]|2[0-3]/,
    I: /0?[1-9]|1[0-2]/,
    M: /[0-5]?\d/,
    s: /\d+/,
    o: /[a-z]*/,
    p: /[ap]\.?m\.?/,
    y: /\d{2}|\d{4}/,
    Y: /\d{4}/,
    T: /Z|[+-]\d{2}(?::?\d{2})?/
};

keys.m = keys.I;
keys.S = keys.M;

var currentLanguage;

var recompile = function(language){
    currentLanguage = language;
    
    keys.a = keys.A = regexOf('days');
    keys.b = keys.B = regexOf('months');
    
    parsePatterns.each(function(pattern, i){
        if (pattern.format) parsePatterns[i] = build(pattern.format);
    });
};

var build = function(format){
    if (!currentLanguage) return {format: format};
    
    var parsed = [];
    var re = (format.source || format) // allow format to be regex
     .replace(/%([a-z])/gi,
        function($0, $1){
            return replacers($1) || $0;
        }
    ).replace(/\((?!\?)/g, '(?:') // make all groups non-capturing
     .replace(/ (?!\?|\*)/g, ',? ') // be forgiving with spaces and commas
     .replace(/%([a-z%])/gi,
        function($0, $1){
            var p = keys[$1];
            if (!p) return $1;
            parsed.push($1);
            return '(' + p.source + ')';
        }
    ).replace(/\[a-z\]/gi, '[a-z\\u00c0-\\uffff]'); // handle unicode words

    return {
        format: format,
        re: new RegExp('^' + re + '$', 'i'),
        handler: function(bits){
            bits = bits.slice(1).associate(parsed);
            var date = new Date().clearTime();
            if ('d' in bits) handle.call(date, 'd', 1);
            if ('m' in bits || 'b' in bits || 'B' in bits) handle.call(date, 'm', 1);
            for (var key in bits) handle.call(date, key, bits[key]);
            return date;
        }
    };
};

var handle = function(key, value){
    if (!value) return this;

    switch(key){
        case 'a': case 'A': return this.set('day', Date.parseDay(value, true));
        case 'b': case 'B': return this.set('mo', Date.parseMonth(value, true));
        case 'd': return this.set('date', value);
        case 'H': case 'I': return this.set('hr', value);
        case 'm': return this.set('mo', value - 1);
        case 'M': return this.set('min', value);
        case 'p': return this.set('ampm', value.replace(/\./g, ''));
        case 'S': return this.set('sec', value);
        case 's': return this.set('ms', ('0.' + value) * 1000);
        case 'w': return this.set('day', value);
        case 'Y': return this.set('year', value);
        case 'y':
            value = +value;
            if (value < 100) value += startCentury + (value < startYear ? 100 : 0);
            return this.set('year', value);
        case 'T':
            if (value == 'Z') value = '+00';
            var offset = value.match(/([+-])(\d{2}):?(\d{2})?/);
            offset = (offset[1] + '1') * (offset[2] * 60 + (+offset[3] || 0)) + this.getTimezoneOffset();
            return this.set('time', this - offset * 60000);
    }

    return this;
};

Date.defineParsers(
    '%Y([-./]%m([-./]%d((T| )%X)?)?)?', // "1999-12-31", "1999-12-31 11:59pm", "1999-12-31 23:59:59", ISO8601
    '%Y%m%d(T%H(%M%S?)?)?', // "19991231", "19991231T1159", compact
    '%x( %X)?', // "12/31", "12.31.99", "12-31-1999", "12/31/2008 11:59 PM"
    '%d%o( %b( %Y)?)?( %X)?', // "31st", "31st December", "31 Dec 1999", "31 Dec 1999 11:59pm"
    '%b( %d%o)?( %Y)?( %X)?', // Same as above with month and day switched
    '%Y %b( %d%o( %X)?)?', // Same as above with year coming first
    '%o %b %d %X %T %Y' // "Thu Oct 22 08:11:23 +0000 2009"
);

MooTools.lang.addEvent('langChange', function(language){
    if (MooTools.lang.get('Date')) recompile(language);
}).fireEvent('langChange', MooTools.lang.getCurrentLanguage());

})();

/*
---

script: Element.Forms.js

description: Extends the Element native object to include methods useful in managing inputs.

license: MIT-style license

authors:
- Aaron Newton

requires:
- core:1.2.4/Element
- /MooTools.More

provides: [Element.Forms]

...
*/

Element.implement({

    tidy: function(){
        this.set('value', this.get('value').tidy());
    },

    getTextInRange: function(start, end){
        return this.get('value').substring(start, end);
    },

    getSelectedText: function(){
        if (this.setSelectionRange) return this.getTextInRange(this.getSelectionStart(), this.getSelectionEnd());
        return document.selection.createRange().text;
    },

    getSelectedRange: function() {
        if ($defined(this.selectionStart)) return {start: this.selectionStart, end: this.selectionEnd};
        var pos = {start: 0, end: 0};
        var range = this.getDocument().selection.createRange();
        if (!range || range.parentElement() != this) return pos;
        var dup = range.duplicate();
        if (this.type == 'text') {
            pos.start = 0 - dup.moveStart('character', -100000);
            pos.end = pos.start + range.text.length;
        } else {
            var value = this.get('value');
            var offset = value.length;
            dup.moveToElementText(this);
            dup.setEndPoint('StartToEnd', range);
            if(dup.text.length) offset -= value.match(/[\n\r]*$/)[0].length;
            pos.end = offset - dup.text.length;
            dup.setEndPoint('StartToStart', range);
            pos.start = offset - dup.text.length;
        }
        return pos;
    },

    getSelectionStart: function(){
        return this.getSelectedRange().start;
    },

    getSelectionEnd: function(){
        return this.getSelectedRange().end;
    },

    setCaretPosition: function(pos){
        if (pos == 'end') pos = this.get('value').length;
        this.selectRange(pos, pos);
        return this;
    },

    getCaretPosition: function(){
        return this.getSelectedRange().start;
    },

    selectRange: function(start, end){
        if (this.setSelectionRange) {
            this.focus();
            this.setSelectionRange(start, end);
        } else {
            var value = this.get('value');
            var diff = value.substr(start, end - start).replace(/\r/g, '').length;
            start = value.substr(0, start).replace(/\r/g, '').length;
            var range = this.createTextRange();
            range.collapse(true);
            range.moveEnd('character', start + diff);
            range.moveStart('character', start);
            range.select();
        }
        return this;
    },

    insertAtCursor: function(value, select){
        var pos = this.getSelectedRange();
        var text = this.get('value');
        this.set('value', text.substring(0, pos.start) + value + text.substring(pos.end, text.length));
        if ($pick(select, true)) this.selectRange(pos.start, pos.start + value.length);
        else this.setCaretPosition(pos.start + value.length);
        return this;
    },

    insertAroundCursor: function(options, select){
        options = $extend({
            before: '',
            defaultMiddle: '',
            after: ''
        }, options);
        var value = this.getSelectedText() || options.defaultMiddle;
        var pos = this.getSelectedRange();
        var text = this.get('value');
        if (pos.start == pos.end){
            this.set('value', text.substring(0, pos.start) + options.before + value + options.after + text.substring(pos.end, text.length));
            this.selectRange(pos.start + options.before.length, pos.end + options.before.length + value.length);
        } else {
            var current = text.substring(pos.start, pos.end);
            this.set('value', text.substring(0, pos.start) + options.before + current + options.after + text.substring(pos.end, text.length));
            var selStart = pos.start + options.before.length;
            if ($pick(select, true)) this.selectRange(selStart, selStart + current.length);
            else this.setCaretPosition(selStart + text.length);
        }
        return this;
    }

});

/*
---

script: Element.Shortcuts.js

description: Extends the Element native object to include some shortcut methods.

license: MIT-style license

authors:
- Aaron Newton

requires:
- core:1.2.4/Element.Style
- /MooTools.More

provides: [Element.Shortcuts]

...
*/

Element.implement({

    isDisplayed: function(){
        return this.getStyle('display') != 'none';
    },

    isVisible: function(){
        var w = this.offsetWidth,
            h = this.offsetHeight;
        return (w == 0 && h == 0) ? false : (w > 0 && h > 0) ? true : this.isDisplayed();
    },

    toggle: function(){
        return this[this.isDisplayed() ? 'hide' : 'show']();
    },

    hide: function(){
        var d;
        try {
            //IE fails here if the element is not in the dom
            d = this.getStyle('display');
        } catch(e){}
        return this.store('originalDisplay', d || '').setStyle('display', 'none');
    },

    show: function(display){
        display = display || this.retrieve('originalDisplay') || 'block';
        return this.setStyle('display', (display == 'none') ? 'block' : display);
    },

    swapClass: function(remove, add){
        return this.removeClass(remove).addClass(add);
    }

});


/*
---

script: Form.Validator.js

description: A css-class based form validation system.

license: MIT-style license

authors:
- Aaron Newton

requires:
- core:1.2.4/Options
- core:1.2.4/Events
- core:1.2.4/Selectors
- core:1.2.4/Element.Event
- core:1.2.4/Element.Style
- core:1.2.4/JSON
- /Lang- /Class.Binds
- /Date Element.Forms
- /Form.Validator.English
- /Element.Shortcuts

provides: [Form.Validator, InputValidator, FormValidator.BaseValidators]

...
*/
if (!window.Form) window.Form = {};

var InputValidator = new Class({

    Implements: [Options],

    options: {
        errorMsg: 'Validation failed.',
        test: function(field){return true;}
    },

    initialize: function(className, options){
        this.setOptions(options);
        this.className = className;
    },

    test: function(field, props){
        if (document.id(field)) return this.options.test(document.id(field), props||this.getProps(field));
        else return false;
    },

    getError: function(field, props){
        var err = this.options.errorMsg;
        if ($type(err) == 'function') err = err(document.id(field), props||this.getProps(field));
        return err;
    },

    getProps: function(field){
        if (!document.id(field)) return {};
        return field.get('validatorProps');
    }

});

Element.Properties.validatorProps = {

    set: function(props){
        return this.eliminate('validatorProps').store('validatorProps', props);
    },

    get: function(props){
        if (props) this.set(props);
        if (this.retrieve('validatorProps')) return this.retrieve('validatorProps');
        if (this.getProperty('validatorProps')){
            try {
                this.store('validatorProps', JSON.decode(this.getProperty('validatorProps')));
            }catch(e){
                return {};
            }
        } else {
            var vals = this.get('class').split(' ').filter(function(cls){
                return cls.test(':');
            });
            if (!vals.length){
                this.store('validatorProps', {});
            } else {
                props = {};
                vals.each(function(cls){
                    var split = cls.split(':');
                    if (split[1]) {
                        try {
                            props[split[0]] = JSON.decode(split[1]);
                        } catch(e) {}
                    }
                });
                this.store('validatorProps', props);
            }
        }
        return this.retrieve('validatorProps');
    }

};

Form.Validator = new Class({

    Implements:[Options, Events],

    Binds: ['onSubmit'],

    options: {/*
        onFormValidate: $empty(isValid, form, event),
        onElementValidate: $empty(isValid, field, className, warn),
        onElementPass: $empty(field),
        onElementFail: $empty(field, validatorsFailed) */
        fieldSelectors: 'input, select, textarea',
        ignoreHidden: true,
        ignoreDisabled: true,
        useTitles: false,
        evaluateOnSubmit: true,
        evaluateFieldsOnBlur: true,
        evaluateFieldsOnChange: true,
        serial: true,
        stopOnFailure: true,
        warningPrefix: function(){
            return Form.Validator.getMsg('warningPrefix') || 'Warning: ';
        },
        errorPrefix: function(){
            return Form.Validator.getMsg('errorPrefix') || 'Error: ';
        }
    },

    initialize: function(form, options){
        this.setOptions(options);
        this.element = document.id(form);
        this.element.store('validator', this);
        this.warningPrefix = $lambda(this.options.warningPrefix)();
        this.errorPrefix = $lambda(this.options.errorPrefix)();
        if (this.options.evaluateOnSubmit) this.element.addEvent('submit', this.onSubmit);
        if (this.options.evaluateFieldsOnBlur || this.options.evaluateFieldsOnChange) this.watchFields(this.getFields());
    },

    toElement: function(){
        return this.element;
    },

    getFields: function(){
        return (this.fields = this.element.getElements(this.options.fieldSelectors));
    },

    watchFields: function(fields){
        fields.each(function(el){
            if (this.options.evaluateFieldsOnBlur)
                el.addEvent('blur', this.validationMonitor.pass([el, false], this));
            if (this.options.evaluateFieldsOnChange)
                el.addEvent('change', this.validationMonitor.pass([el, true], this));
        }, this);
    },

    validationMonitor: function(){
        $clear(this.timer);
        this.timer = this.validateField.delay(50, this, arguments);
    },

    onSubmit: function(event){
        if (!this.validate(event) && event) event.preventDefault();
        else this.reset();
    },

    reset: function(){
        this.getFields().each(this.resetField, this);
        return this;
    },

    validate: function(event){
        var result = this.getFields().map(function(field){
            return this.validateField(field, true);
        }, this).every(function(v){ return v;});
        this.fireEvent('formValidate', [result, this.element, event]);
        if (this.options.stopOnFailure && !result && event) event.preventDefault();
        return result;
    },

    validateField: function(field, force){
        if (this.paused) return true;
        field = document.id(field);
        var passed = !field.hasClass('validation-failed');
        var failed, warned;
        if (this.options.serial && !force){
            failed = this.element.getElement('.validation-failed');
            warned = this.element.getElement('.warning');
        }
        if (field && (!failed || force || field.hasClass('validation-failed') || (failed && !this.options.serial))){
            var validators = field.className.split(' ').some(function(cn){
                return this.getValidator(cn);
            }, this);
            var validatorsFailed = [];
            field.className.split(' ').each(function(className){
                if (className && !this.test(className, field)) validatorsFailed.include(className);
            }, this);
            passed = validatorsFailed.length === 0;
            if (validators && !field.hasClass('warnOnly')){
                if (passed){
                    field.addClass('validation-passed').removeClass('validation-failed');
                    this.fireEvent('elementPass', field);
                } else {
                    field.addClass('validation-failed').removeClass('validation-passed');
                    this.fireEvent('elementFail', [field, validatorsFailed]);
                }
            }
            if (!warned){
                var warnings = field.className.split(' ').some(function(cn){
                    if (cn.test('^warn-') || field.hasClass('warnOnly'))
                        return this.getValidator(cn.replace(/^warn-/,''));
                    else return null;
                }, this);
                field.removeClass('warning');
                var warnResult = field.className.split(' ').map(function(cn){
                    if (cn.test('^warn-') || field.hasClass('warnOnly'))
                        return this.test(cn.replace(/^warn-/,''), field, true);
                    else return null;
                }, this);
            }
        }
        return passed;
    },

    test: function(className, field, warn){
        field = document.id(field);
        if((this.options.ignoreHidden && !field.isVisible()) || (this.options.ignoreDisabled && field.get('disabled'))) return true;
        var validator = this.getValidator(className);
        if (field.hasClass('ignoreValidation')) return true;
        warn = $pick(warn, false);
        if (field.hasClass('warnOnly')) warn = true;
        var isValid = validator ? validator.test(field) : true;
        if (validator && field.isVisible()) this.fireEvent('elementValidate', [isValid, field, className, warn]);
        if (warn) return true;
        return isValid;
    },

    resetField: function(field){
        field = document.id(field);
        if (field){
            field.className.split(' ').each(function(className){
                if (className.test('^warn-')) className = className.replace(/^warn-/, '');
                field.removeClass('validation-failed');
                field.removeClass('warning');
                field.removeClass('validation-passed');
            }, this);
        }
        return this;
    },

    stop: function(){
        this.paused = true;
        return this;
    },

    start: function(){
        this.paused = false;
        return this;
    },

    ignoreField: function(field, warn){
        field = document.id(field);
        if (field){
            this.enforceField(field);
            if (warn) field.addClass('warnOnly');
            else field.addClass('ignoreValidation');
        }
        return this;
    },

    enforceField: function(field){
        field = document.id(field);
        if (field) field.removeClass('warnOnly').removeClass('ignoreValidation');
        return this;
    }

});

Form.Validator.getMsg = function(key){
    return MooTools.lang.get('Form.Validator', key);
};

Form.Validator.adders = {

    validators:{},

    add : function(className, options){
        this.validators[className] = new InputValidator(className, options);
        //if this is a class (this method is used by instances of Form.Validator and the Form.Validator namespace)
        //extend these validators into it
        //this allows validators to be global and/or per instance
        if (!this.initialize){
            this.implement({
                validators: this.validators
            });
        }
    },

    addAllThese : function(validators){
        $A(validators).each(function(validator){
            this.add(validator[0], validator[1]);
        }, this);
    },

    getValidator: function(className){
        return this.validators[className.split(':')[0]];
    }

};

$extend(Form.Validator, Form.Validator.adders);

Form.Validator.implement(Form.Validator.adders);

Form.Validator.add('IsEmpty', {

    errorMsg: false,
    test: function(element){
        if (element.type == 'select-one' || element.type == 'select')
            return !(element.selectedIndex >= 0 && element.options[element.selectedIndex].value != '');
        else
            return ((element.get('value') == null) || (element.get('value').length == 0));
    }

});

Form.Validator.addAllThese([

    ['required', {
        errorMsg: function(){
            return Form.Validator.getMsg('required');
        },
        test: function(element){
            return !Form.Validator.getValidator('IsEmpty').test(element);
        }
    }],

    ['minLength', {
        errorMsg: function(element, props){
            if ($type(props.minLength))
                return Form.Validator.getMsg('minLength').substitute({minLength:props.minLength,length:element.get('value').length });
            else return '';
        },
        test: function(element, props){
            if ($type(props.minLength)) return (element.get('value').length >= $pick(props.minLength, 0));
            else return true;
        }
    }],

    ['maxLength', {
        errorMsg: function(element, props){
            //props is {maxLength:10}
            if ($type(props.maxLength))
                return Form.Validator.getMsg('maxLength').substitute({maxLength:props.maxLength,length:element.get('value').length });
            else return '';
        },
        test: function(element, props){
            //if the value is <= than the maxLength value, element passes test
            return (element.get('value').length <= $pick(props.maxLength, 10000));
        }
    }],

    ['validate-integer', {
        errorMsg: Form.Validator.getMsg.pass('integer'),
        test: function(element){
            return Form.Validator.getValidator('IsEmpty').test(element) || (/^(-?[1-9]\d*|0)$/).test(element.get('value'));
        }
    }],

    ['validate-numeric', {
        errorMsg: Form.Validator.getMsg.pass('numeric'),
        test: function(element){
            return Form.Validator.getValidator('IsEmpty').test(element) ||
                (/^-?(?:0$0(?=\d*\.)|[1-9]|0)\d*(\.\d+)?$/).test(element.get('value'));
        }
    }],

    ['validate-digits', {
        errorMsg: Form.Validator.getMsg.pass('digits'),
        test: function(element){
            return Form.Validator.getValidator('IsEmpty').test(element) || (/^[\d() .:\-\+#]+$/.test(element.get('value')));
        }
    }],

    ['validate-alpha', {
        errorMsg: Form.Validator.getMsg.pass('alpha'),
        test: function(element){
            return Form.Validator.getValidator('IsEmpty').test(element) ||  (/^[a-zA-Z]+$/).test(element.get('value'));
        }
    }],

    ['validate-alphanum', {
        errorMsg: Form.Validator.getMsg.pass('alphanum'),
        test: function(element){
            return Form.Validator.getValidator('IsEmpty').test(element) || !(/\W/).test(element.get('value'));
        }
    }],

    ['validate-date', {
        errorMsg: function(element, props){
            if (Date.parse){
                var format = props.dateFormat || '%x';
                return Form.Validator.getMsg('dateSuchAs').substitute({date: new Date().format(format)});
            } else {
                return Form.Validator.getMsg('dateInFormatMDY');
            }
        },
        test: function(element, props){
            if (Form.Validator.getValidator('IsEmpty').test(element)) return true;
            var d;
            if (Date.parse){
                var format = props.dateFormat || '%x';
                d = Date.parse(element.get('value'));
                var formatted = d.format(format);
                if (formatted != 'invalid date') element.set('value', formatted);
                return !isNaN(d);
            } else {
                var regex = /^(\d{2})\/(\d{2})\/(\d{4})$/;
                if (!regex.test(element.get('value'))) return false;
                d = new Date(element.get('value').replace(regex, '$1/$2/$3'));
                return (parseInt(RegExp.$1, 10) == (1 + d.getMonth())) &&
                    (parseInt(RegExp.$2, 10) == d.getDate()) &&
                    (parseInt(RegExp.$3, 10) == d.getFullYear());
            }
        }
    }],

    ['validate-email', {
        errorMsg: Form.Validator.getMsg.pass('email'),
        test: function(element){
            return Form.Validator.getValidator('IsEmpty').test(element) || (/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i).test(element.get('value'));
        }
    }],

    ['validate-url', {
        errorMsg: Form.Validator.getMsg.pass('url'),
        test: function(element){
            return Form.Validator.getValidator('IsEmpty').test(element) || (/^(https?|ftp|rmtp|mms):\/\/(([A-Z0-9][A-Z0-9_-]*)(\.[A-Z0-9][A-Z0-9_-]*)+)(:(\d+))?\/?/i).test(element.get('value'));
        }
    }],

    ['validate-currency-dollar', {
        errorMsg: Form.Validator.getMsg.pass('currencyDollar'),
        test: function(element){
            // [$]1[##][,###]+[.##]
            // [$]1###+[.##]
            // [$]0.##
            // [$].##
            return Form.Validator.getValidator('IsEmpty').test(element) ||  (/^\$?\-?([1-9]{1}[0-9]{0,2}(\,[0-9]{3})*(\.[0-9]{0,2})?|[1-9]{1}\d*(\.[0-9]{0,2})?|0(\.[0-9]{0,2})?|(\.[0-9]{1,2})?)$/).test(element.get('value'));
        }
    }],

    ['validate-one-required', {
        errorMsg: Form.Validator.getMsg.pass('oneRequired'),
        test: function(element, props){
            var p = document.id(props['validate-one-required']) || element.getParent();
            return p.getElements('input').some(function(el){
                if (['checkbox', 'radio'].contains(el.get('type'))) return el.get('checked');
                return el.get('value');
            });
        }
    }]

]);

Element.Properties.validator = {

    set: function(options){
        var validator = this.retrieve('validator');
        if (validator) validator.setOptions(options);
        return this.store('validator:options');
    },

    get: function(options){
        if (options || !this.retrieve('validator')){
            if (options || !this.retrieve('validator:options')) this.set('validator', options);
            this.store('validator', new Form.Validator(this, this.retrieve('validator:options')));
        }
        return this.retrieve('validator');
    }

};

Element.implement({

    validate: function(options){
        this.set('validator', options);
        return this.get('validator', options).validate();
    }

});
//legacy
var FormValidator = Form.Validator;

/*
---

script: Form.Validator.Inline.js

description: Extends Form.Validator to add inline messages.

license: MIT-style license

authors:
- Aaron Newton

requires:
- /Form.Validator

provides: [Form.Validator.Inline]

...
*/

Form.Validator.Inline = new Class({

    Extends: Form.Validator,

    options: {
        scrollToErrorsOnSubmit: true,
        scrollFxOptions: {
            transition: 'quad:out',
            offset: {
                y: -20
            }
        }
    },

    initialize: function(form, options){
        this.parent(form, options);
        this.addEvent('onElementValidate', function(isValid, field, className, warn){
            var validator = this.getValidator(className);
            if (!isValid && validator.getError(field)){
                if (warn) field.addClass('warning');
                var advice = this.makeAdvice(className, field, validator.getError(field), warn);
                this.insertAdvice(advice, field);
                this.showAdvice(className, field);
            } else {
                this.hideAdvice(className, field);
            }
        });
    },

    makeAdvice: function(className, field, error, warn){
        var errorMsg = (warn)?this.warningPrefix:this.errorPrefix;
            errorMsg += (this.options.useTitles) ? field.title || error:error;
        var cssClass = (warn) ? 'warning-advice' : 'validation-advice';
        var advice = this.getAdvice(className, field);
        if(advice) {
            advice = advice.set('html', errorMsg);
        } else {
            advice = new Element('div', {
                html: errorMsg,
                styles: { display: 'none' },
                id: 'advice-' + className + '-' + this.getFieldId(field)
            }).addClass(cssClass);
        }
        field.store('advice-' + className, advice);
        return advice;
    },

    getFieldId : function(field){
        return field.id ? field.id : field.id = 'input_' + field.name;
    },

    showAdvice: function(className, field){
        var advice = this.getAdvice(className, field);
        if (advice && !field.retrieve(this.getPropName(className))
                && (advice.getStyle('display') == 'none'
                || advice.getStyle('visiblity') == 'hidden'
                || advice.getStyle('opacity') == 0)){
            field.store(this.getPropName(className), true);
            if (advice.reveal) advice.reveal();
            else advice.setStyle('display', 'block');
        }
    },

    hideAdvice: function(className, field){
        var advice = this.getAdvice(className, field);
        if (advice && field.retrieve(this.getPropName(className))){
            field.store(this.getPropName(className), false);
            //if Fx.Reveal.js is present, transition the advice out
            if (advice.dissolve) advice.dissolve();
            else advice.setStyle('display', 'none');
        }
    },

    getPropName: function(className){
        return 'advice' + className;
    },

    resetField: function(field){
        field = document.id(field);
        if (!field) return this;
        this.parent(field);
        field.className.split(' ').each(function(className){
            this.hideAdvice(className, field);
        }, this);
        return this;
    },

    getAllAdviceMessages: function(field, force){
        var advice = [];
        if (field.hasClass('ignoreValidation') && !force) return advice;
        var validators = field.className.split(' ').some(function(cn){
            var warner = cn.test('^warn-') || field.hasClass('warnOnly');
            if (warner) cn = cn.replace(/^warn-/, '');
            var validator = this.getValidator(cn);
            if (!validator) return;
            advice.push({
                message: validator.getError(field),
                warnOnly: warner,
                passed: validator.test(),
                validator: validator
            });
        }, this);
        return advice;
    },

    getAdvice: function(className, field){
        return field.retrieve('advice-' + className);
    },

    insertAdvice: function(advice, field){
        //Check for error position prop
        var props = field.get('validatorProps');
        //Build advice
        if (!props.msgPos || !document.id(props.msgPos)){
            if(field.type.toLowerCase() == 'radio') field.getParent().adopt(advice);
            else advice.inject(document.id(field), 'before');
        } else {
            document.id(props.msgPos).grab(advice);
        }
    },

    validateField: function(field, force){
        var result = this.parent(field, force);
        if (this.options.scrollToErrorsOnSubmit && !result){
            var failed = document.id(this).getElement('.validation-failed');
            var par = document.id(this).getParent();
            while (par != document.body && par.getScrollSize().y == par.getSize().y){
                par = par.getParent();
            }
            var fx = par.retrieve('fvScroller');
            if (!fx && window.Fx && Fx.Scroll){
                fx = new Fx.Scroll(par, this.options.scrollFxOptions);
                par.store('fvScroller', fx);
            }
            if (failed){
                if (fx) fx.toElement(failed);
                else par.scrollTo(par.getScroll().x, failed.getPosition(par).y - 20);
            }
        }
        return result;
    }

});


/*
---

script: Date.English.US.js

description: Date messages for US English.

license: MIT-style license

authors:
- Aaron Newton

requires:
- /Lang
- /Date

provides: [Date.English.US]

...
*/

MooTools.lang.set('en-US', 'Date', {

    months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
    days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
    //culture's date order: MM/DD/YYYY
    dateOrder: ['month', 'date', 'year'],
    shortDate: '%m/%d/%Y',
    shortTime: '%I:%M%p',
    AM: 'AM',
    PM: 'PM',

    /* Date.Extras */
    ordinal: function(dayOfMonth){
        //1st, 2nd, 3rd, etc.
        return (dayOfMonth > 3 && dayOfMonth < 21) ? 'th' : ['th', 'st', 'nd', 'rd', 'th'][Math.min(dayOfMonth % 10, 4)];
    },

    lessThanMinuteAgo: 'less than a minute ago',
    minuteAgo: 'about a minute ago',
    minutesAgo: '{delta} minutes ago',
    hourAgo: 'about an hour ago',
    hoursAgo: 'about {delta} hours ago',
    dayAgo: '1 day ago',
    daysAgo: '{delta} days ago',
    weekAgo: '1 week ago',
    weeksAgo: '{delta} weeks ago',
    monthAgo: '1 month ago',
    monthsAgo: '{delta} months ago',
    yearAgo: '1 year ago',
    yearsAgo: '{delta} years ago',
    lessThanMinuteUntil: 'less than a minute from now',
    minuteUntil: 'about a minute from now',
    minutesUntil: '{delta} minutes from now',
    hourUntil: 'about an hour from now',
    hoursUntil: 'about {delta} hours from now',
    dayUntil: '1 day from now',
    daysUntil: '{delta} days from now',
    weekUntil: '1 week from now',
    weeksUntil: '{delta} weeks from now',
    monthUntil: '1 month from now',
    monthsUntil: '{delta} months from now',
    yearUntil: '1 year from now',
    yearsUntil: '{delta} years from now'

});


/*
---

script: Form.Validator.English.js

description: Form Validator messages for English.

license: MIT-style license

authors:
- Aaron Newton

requires:
- /Lang
- /Form.Validator

provides: [Form.Validator.English]

...
*/

MooTools.lang.set('en-US', 'Form.Validator', {

    required:'This field is required.',
    minLength:'Please enter at least {minLength} characters (you entered {length} characters).',
    maxLength:'Please enter no more than {maxLength} characters (you entered {length} characters).',
    integer:'Please enter an integer in this field. Numbers with decimals (e.g. 1.25) are not permitted.',
    numeric:'Please enter only numeric values in this field (i.e. "1" or "1.1" or "-1" or "-1.1").',
    digits:'Please use numbers and punctuation only in this field (for example, a phone number with dashes or dots is permitted).',
    alpha:'Please use letters only (a-z) with in this field. No spaces or other characters are allowed.',
    alphanum:'Please use only letters (a-z) or numbers (0-9) only in this field. No spaces or other characters are allowed.',
    dateSuchAs:'Please enter a valid date such as {date}',
    dateInFormatMDY:'Please enter a valid date such as MM/DD/YYYY (i.e. "12/31/1999")',
    email:'Please enter a valid email address. For example "fred@domain.com".',
    url:'Please enter a valid URL such as http://www.google.com.',
    currencyDollar:'Please enter a valid $ amount. For example $100.00 .',
    oneRequired:'Please enter something for at least one of these inputs.',
    errorPrefix: 'Error: ',
    warningPrefix: 'Warning: ',

    //Form.Validator.Extras

    noSpace: 'There can be no spaces in this input.',
    reqChkByNode: 'No items are selected.',
    requiredChk: 'This field is required.',
    reqChkByName: 'Please select a {label}.',
    match: 'This field needs to match the {matchName} field',
    startDate: 'the start date',
    endDate: 'the end date',
    currendDate: 'the current date',
    afterDate: 'The date should be the same or after {label}.',
    beforeDate: 'The date should be the same or before {label}.',
    startMonth: 'Please select a start month',
    sameMonth: 'These two dates must be in the same month - you must change one or the other.',
    creditcard: 'The credit card number entered is invalid. Please check the number and try again. {length} digits entered.'

});
