﻿// Calendar: a Javascript class for Mootools that adds accessible and unobtrusive date pickers to your form elements <http://electricprism.com/aeron/calendar>
// Calendar RC4, Copyright (c) 2007 Aeron Glemann <http://electricprism.com/aeron>, MIT Style License.

//var Calendar = new Class({ options: { blocked: [], classes: [], days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], direction: 0, draggable: true, months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], navigation: 1, offset: 0, onHideStart: Class.empty, onHideComplete: Class.empty, onShowStart: Class.empty, onShowComplete: Class.empty, pad: 1, tweak: { x: 0, y: 0} }, initialize: function(F, B) { if (!F) { return false } this.setOptions(B); var D = ["calendar", "prev", "next", "month", "year", "today", "invalid", "valid", "inactive", "active", "hover", "hilite"]; var A = D.map(function(J, I) { if (this.options.classes[I]) { if (this.options.classes[I].length) { J = this.options.classes[I] } } return J }, this); this.classes = A.associate(D); this.calendar = new Element("div", { styles: { left: "-1000px", opacity: 0, position: "absolute", top: "-1000px", zIndex: 1000} }).addClass(this.classes.calendar).injectInside(document.body); if (window.ie6) { this.iframe = new Element("iframe", { styles: { left: "-1000px", position: "absolute", top: "-1000px", zIndex: 999} }).injectInside(document.body); this.iframe.style.filter = "progid:DXImageTransform.Microsoft.Alpha(style=0,opacity=0)" } this.fx = this.calendar.effect("opacity", { onStart: function() { if (this.calendar.getStyle("opacity") == 0) { if (window.ie6) { this.iframe.setStyle("display", "block") } this.calendar.setStyle("display", "block"); this.fireEvent("onShowStart", this.element) } else { this.fireEvent("onHideStart", this.element) } } .bind(this), onComplete: function() { if (this.calendar.getStyle("opacity") == 0) { this.calendar.setStyle("display", "none"); if (window.ie6) { this.iframe.setStyle("display", "none") } this.fireEvent("onHideComplete", this.element) } else { this.fireEvent("onShowComplete", this.element) } } .bind(this) }); if (window.Drag && this.options.draggable) { this.drag = new Drag.Move(this.calendar, { onDrag: function() { if (window.ie6) { this.iframe.setStyles({ left: this.calendar.style.left, top: this.calendar.style.top }) } } .bind(this) }) } this.calendars = []; var H = 0; var G = new Date(); G.setDate(G.getDate() + this.options.direction.toInt()); for (var C in F) { var E = { button: new Element("button", { type: "button" }), el: $(C), els: [], id: H++, month: G.getMonth(), visible: false, year: G.getFullYear() }; if (!this.element(C, F[C], E)) { continue } E.el.addClass(this.classes.calendar); E.button.addClass(this.classes.calendar).addEvent("click", function(I) { this.toggle(I) } .pass(E, this)).injectAfter(E.el); E.val = this.read(E); $extend(E, this.bounds(E)); $extend(E, this.values(E)); this.rebuild(E); this.calendars.push(E) } }, blocked: function(C) { var A = []; var D = new Date(C.year, C.month, 1).getDay(); var B = new Date(C.year, C.month + 1, 0).getDate(); this.options.blocked.each(function(I) { var G = I.split(" "); for (var J = 0; J <= 3; J++) { if (!G[J]) { G[J] = (J == 3) ? "" : "*" } G[J] = G[J].contains(",") ? G[J].split(",") : new Array(G[J]); var K = G[J].length - 1; for (var H = K; H >= 0; H--) { if (G[J][H].contains("-")) { var L = G[J][H].split("-"); for (var F = L[0]; F <= L[1]; F++) { if (!G[J].contains(F)) { G[J].push(F + "") } } G[J].splice(H, 1) } } } if (G[2].contains(C.year + "") || G[2].contains("*")) { if (G[1].contains(C.month + 1 + "") || G[1].contains("*")) { G[0].each(function(M) { if (M > 0) { A.push(M.toInt()) } }); if (G[3]) { for (var J = 0; J < B; J++) { var E = (J + D) % 7; if (G[3].contains(E + "")) { A.push(J + 1) } } } } } }, this); return A }, bounds: function(C) { var D = new Date(1000, 0, 1); var A = new Date(2999, 11, 31); var B = new Date().getDate() + this.options.direction.toInt(); if (this.options.direction > 0) { D = new Date(); D.setDate(B + this.options.pad * C.id) } if (this.options.direction < 0) { A = new Date(); A.setDate(B - this.options.pad * (this.calendars.length - C.id - 1)) } C.els.each(function(F) { if (F.getTag() == "select") { if (F.format.test("(y|Y)")) { var E = []; F.getChildren().each(function(J) { var I = this.unformat(J.value, F.format); if (!E.contains(I[0])) { E.push(I[0]) } }, this); E.sort(this.sort); if (E[0] > D.getFullYear()) { d = new Date(E[0], D.getMonth() + 1, 0); if (D.getDate() > d.getDate()) { D.setDate(d.getDate()) } D.setYear(E[0]) } if (E.getLast() < A.getFullYear()) { d = new Date(E.getLast(), A.getMonth() + 1, 0); if (A.getDate() > d.getDate()) { A.setDate(d.getDate()) } A.setYear(E.getLast()) } } if (F.format.test("(F|m|M|n)")) { var G = []; var H = []; F.getChildren().each(function(J) { var I = this.unformat(J.value, F.format); if ($type(I[0]) != "number" || I[0] == E[0]) { if (!G.contains(I[1])) { G.push(I[1]) } } if ($type(I[0]) != "number" || I[0] == E.getLast()) { if (!H.contains(I[1])) { H.push(I[1]) } } }, this); G.sort(this.sort); H.sort(this.sort); if (G[0] > D.getMonth()) { d = new Date(D.getFullYear(), G[0] + 1, 0); if (D.getDate() > d.getDate()) { D.setDate(d.getDate()) } D.setMonth(G[0]) } if (H.getLast() < A.getMonth()) { d = new Date(D.getFullYear(), H.getLast() + 1, 0); if (A.getDate() > d.getDate()) { A.setDate(d.getDate()) } A.setMonth(H.getLast()) } } } }, this); return { start: D, end: A} }, caption: function(G) { var A = { prev: { month: true, year: true }, next: { month: true, year: true} }; if (G.year == G.start.getFullYear()) { A.prev.year = false; if (G.month == G.start.getMonth() && this.options.navigation == 1) { A.prev.month = false } } if (G.year == G.end.getFullYear()) { A.next.year = false; if (G.month == G.end.getMonth() && this.options.navigation == 1) { A.next.month = false } } if ($type(G.months) == "array") { if (G.months.length == 1 && this.options.navigation == 2) { A.prev.month = A.next.month = false } } var B = new Element("caption"); var E = new Element("a").addClass(this.classes.prev).appendText("\x3c"); var D = new Element("a").addClass(this.classes.next).appendText("\x3e"); if (this.options.navigation == 2) { var F = new Element("span").addClass(this.classes.month).injectInside(B); if (A.prev.month) { E.clone().addEvent("click", function(H) { this.navigate(H, "m", -1) } .pass(G, this)).injectInside(F) } F.adopt(new Element("span").appendText(this.options.months[G.month])); if (A.next.month) { D.clone().addEvent("click", function(H) { this.navigate(H, "m", 1) } .pass(G, this)).injectInside(F) } var C = new Element("span").addClass(this.classes.year).injectInside(B); if (A.prev.year) { E.clone().addEvent("click", function(H) { this.navigate(H, "y", -1) } .pass(G, this)).injectInside(C) } C.adopt(new Element("span").appendText(G.year)); if (A.next.year) { D.clone().addEvent("click", function(H) { this.navigate(H, "y", 1) } .pass(G, this)).injectInside(C) } } else { if (A.prev.month && this.options.navigation) { E.clone().addEvent("click", function(H) { this.navigate(H, "m", -1) } .pass(G, this)).injectInside(B) } B.adopt(new Element("span").addClass(this.classes.month).appendText(this.options.months[G.month])); B.adopt(new Element("span").addClass(this.classes.year).appendText(G.year)); if (A.next.month && this.options.navigation) { D.clone().addEvent("click", function(H) { this.navigate(H, "m", 1) } .pass(G, this)).injectInside(B) } } return B }, changed: function(A) { A.val = this.read(A); $extend(A, this.values(A)); this.rebuild(A); if (!A.val) { return } if (A.val.getDate() < A.days[0]) { A.val.setDate(A.days[0]) } if (A.val.getDate() > A.days.getLast()) { A.val.setDate(A.days.getLast()) } A.els.each(function(B) { B.value = this.format(A.val, B.format) }, this); this.check(A); this.calendars.each(function(B) { if (B.visible) { this.display(B) } }, this) }, check: function(A) { this.calendars.each(function(D, B) { if (D.val) { var E = false; if (B < A.id) { var C = new Date(Date.parse(A.val)); C.setDate(C.getDate() - (this.options.pad * (A.id - B))); if (C < D.val) { E = true } } if (B > A.id) { var C = new Date(Date.parse(A.val)); C.setDate(C.getDate() + (this.options.pad * (B - A.id))); if (C > D.val) { E = true } } if (E) { if (D.start > C) { C = D.start } if (D.end < C) { C = D.end } D.month = C.getMonth(); D.year = C.getFullYear(); $extend(D, this.values(D)); D.val = D.days.contains(C.getDate()) ? C : null; this.write(D); if (D.visible) { this.display(D) } } } else { D.month = A.month; D.year = A.year } }, this) }, clicked: function(C, A, B) { B.val = (this.value(B) == A) ? null : new Date(B.year, B.month, A); this.write(B); if (!B.val) { B.val = this.read(B) } if (B.val) { this.check(B); this.toggle(B) } else { C.addClass(this.classes.valid); C.removeClass(this.classes.active) } }, display: function(J) { this.calendar.empty(); this.calendar.className = this.classes.calendar + " " + this.options.months[J.month].toLowerCase(); var K = new Element("div").injectInside(this.calendar); var R = new Element("table").injectInside(K).adopt(this.caption(J)); var Q = new Element("thead").injectInside(R); var B = new Element("tr").injectInside(Q); for (var P = 0; P <= 6; P++) { var E = this.options.days[(P + this.options.offset) % 7]; B.adopt(new Element("th", { title: E }).appendText(E.substr(0, 1))) } var A = new Element("tbody").injectInside(R); var B = new Element("tr").injectInside(A); var T = new Date(J.year, J.month, 1); var D = ((T.getDay() - this.options.offset) + 7) % 7; var I = new Date(J.year, J.month + 1, 0).getDate(); var L = new Date(J.year, J.month, 0).getDate(); var F = this.value(J); var N = J.days; var M = []; var G = []; this.calendars.each(function(X, W) { if (X != J && X.val) { if (J.year == X.val.getFullYear() && J.month == X.val.getMonth()) { M.push(X.val.getDate()) } if (J.val) { for (var V = 1; V <= I; V++) { T.setDate(V); if ((W < J.id && T > X.val && T < J.val) || (W > J.id && T > J.val && T < X.val)) { if (!G.contains(V)) { G.push(V) } } } } } }, this); var T = new Date(); var S = new Date(T.getFullYear(), T.getMonth(), T.getDate()).getTime(); for (var P = 1; P < 43; P++) { if ((P - 1) % 7 == 0) { B = new Element("tr").injectInside(A) } var H = new Element("td").injectInside(B); var O = P - D; var U = new Date(J.year, J.month, O); var C = ""; if (O === F) { C = this.classes.active } else { if (M.contains(O)) { C = this.classes.inactive } else { if (N.contains(O)) { C = this.classes.valid } else { if (O >= 1 && O <= I) { C = this.classes.invalid } } } } if (U.getTime() == S) { C = C + " " + this.classes.today } if (G.contains(O)) { C = C + " " + this.classes.hilite } H.addClass(C); if (N.contains(O)) { H.setProperty("title", this.format(U, "D M jS Y")); H.addEvents({ click: function(X, V, W) { this.clicked(X, V, W) } .pass([H, O, J], this), mouseover: function(W, V) { W.addClass(V) } .pass([H, this.classes.hover]), mouseout: function(W, V) { W.removeClass(V) } .pass([H, this.classes.hover]) }) } if (O < 1) { O = L + O } else { if (O > I) { O = O - I } } H.appendText(O) } }, element: function(B, C, D) { if ($type(C) == "object") { for (var A in C) { if (!this.element(A, C[A], D)) { return false } } return true } B = $(B); if (!B) { return false } B.format = C; if (B.getTag() == "select") { B.addEvent("change", function(E) { this.changed(E) } .pass(D, this)) } else { B.readOnly = true; B.addEvent("focus", function(E) { this.toggle(E) } .pass(D, this)) } D.els.push(B); return true }, format: function(C, K) { var I = ""; if (C) { var E = C.getDate(); var L = C.getDay(); var D = this.options.days[L]; var B = C.getMonth() + 1; var H = this.options.months[B - 1]; var J = C.getFullYear() + ""; for (var F = 0, G = K.length; F < G; F++) { var A = K.charAt(F); switch (A) { case "y": J = J.substr(2); case "Y": I += J; break; case "m": if (B < 10) { B = "0" + B } case "n": I += B; break; case "M": H = H.substr(0, 3); case "F": I += H; break; case "d": if (E < 10) { E = "0" + E } case "j": I += E; break; case "D": D = D.substr(0, 3); case "l": I += D; break; case "N": L += 1; case "w": I += L; break; case "S": if (E % 10 == 1 && E != "11") { I += "st" } else { if (E % 10 == 2 && E != "12") { I += "nd" } else { if (E % 10 == 3 && E != "13") { I += "rd" } else { I += "th" } } } break; default: I += A } } } return I }, navigate: function(C, B, D) { switch (B) { case "m": if ($type(C.months) == "array") { var A = C.months.indexOf(C.month) + D; if (A < 0 || A == C.months.length) { if (this.options.navigation == 1) { this.navigate(C, "y", D) } A = (A < 0) ? C.months.length - 1 : 0 } C.month = C.months[A] } else { var A = C.month + D; if (A < 0 || A == 12) { if (this.options.navigation == 1) { this.navigate(C, "y", D) } A = (A < 0) ? 11 : 0 } C.month = A } break; case "y": if ($type(C.years) == "array") { var A = C.years.indexOf(C.year) + D; C.year = C.years[A] } else { C.year += D } break } $extend(C, this.values(C)); if ($type(C.months) == "array") { var A = C.months.indexOf(C.month); if (A < 0) { C.month = C.months[0] } } this.display(C) }, read: function(C) { var A = [null, null, null]; C.els.each(function(F) { var E = this.unformat(F.value, F.format); E.each(function(H, G) { if ($type(H) == "number") { A[G] = H } }) }, this); if ($type(A[0]) == "number") { C.year = A[0] } if ($type(A[1]) == "number") { C.month = A[1] } var D = null; if (A.every(function(E) { return $type(E) == "number" })) { var B = new Date(A[0], A[1] + 1, 0).getDate(); if (A[2] > B) { A[2] = B } D = new Date(A[0], A[1], A[2]) } return (C.val == D) ? null : D }, rebuild: function(A) { A.els.each(function(B) { if (B.getTag() == "select" && B.format.test("^(d|j)$")) { var C = this.value(A); if (!C) { C = B.value.toInt() } B.empty(); A.days.each(function(D) { var E = new Element("option", { selected: (C == D), value: ((B.format == "d" && D < 10) ? "0" + D : D) }).appendText(D).injectInside(B) }, this) } }, this) }, sort: function(B, A) { return B - A }, toggle: function(C) { document.removeEvent("mousedown", this.fn); if (C.visible) { C.visible = false; C.button.removeClass(this.classes.active); this.fx.start(1, 0) } else { this.fn = function(I, H) { var I = new Event(I); var G = I.target; var F = false; while (G != document.body && G.nodeType == 1) { if (G == this.calendar) { F = true } this.calendars.each(function(J) { if (J.button == G || J.els.contains(G)) { F = true } }); if (F) { I.stop(); return false } else { G = G.parentNode } } this.toggle(H) } .create({ "arguments": C, bind: this, event: true }); document.addEvent("mousedown", this.fn); this.calendars.each(function(F) { if (F == C) { F.visible = true; F.button.addClass(this.classes.active) } else { F.visible = false; F.button.removeClass(this.classes.active) } }, this); var B = window.getSize().scrollSize; var E = C.button.getCoordinates(); var A = E.right + this.options.tweak.x; var D = E.top + this.options.tweak.y; if (!this.calendar.coord) { this.calendar.coord = this.calendar.getCoordinates() } if (A + this.calendar.coord.width > B.x) { A -= (A + this.calendar.coord.width - B.x) } if (D + this.calendar.coord.height > B.y) { D -= (D + this.calendar.coord.height - B.y) } this.calendar.setStyles({ left: A + "px", top: D + "px" }); if (window.ie6) { this.iframe.setStyles({ height: this.calendar.coord.height + "px", left: A + "px", top: D + "px", width: this.calendar.coord.width + "px" }) } this.display(C); this.fx.start(0, 1) } }, unformat: function(B, G) { G = G.escapeRegExp(); var I = { d: "([0-9]{2})", j: "([0-9]{1,2})", D: "(" + this.options.days.map(function(J) { return J.substr(0, 3) }).join("|") + ")", l: "(" + this.options.days.join("|") + ")", S: "(st|nd|rd|th)", F: "(" + this.options.months.join("|") + ")", m: "([0-9]{2})", M: "(" + this.options.months.map(function(J) { return J.substr(0, 3) }).join("|") + ")", n: "([0-9]{1,2})", Y: "([0-9]{4})", y: "([0-9]{2})" }; var E = []; var F = ""; for (var C = 0; C < G.length; C++) { var H = G.charAt(C); if (I[H]) { E.push(H); F += I[H] } else { F += H } } var D = B.match("^" + F + "$"); var A = new Array(3); if (D) { D = D.slice(1); E.each(function(K, J) { J = D[J]; switch (K) { case "y": J = "19" + J; case "Y": A[0] = J.toInt(); break; case "F": J = J.substr(0, 3); case "M": J = this.options.months.map(function(L) { return L.substr(0, 3) }).indexOf(J) + 1; case "m": case "n": A[1] = J.toInt() - 1; break; case "d": case "j": A[2] = J.toInt(); break } }, this) } return A }, value: function(B) { var A = null; if (B.val) { if (B.year == B.val.getFullYear() && B.month == B.val.getMonth()) { A = B.val.getDate() } } return A }, values: function(F) { var D, A, H; F.els.each(function(I) { if (I.getTag() == "select") { if (I.format.test("(y|Y)")) { D = []; I.getChildren().each(function(K) { var J = this.unformat(K.value, I.format); if (!D.contains(J[0])) { D.push(J[0]) } }, this); D.sort(this.sort) } if (I.format.test("(F|m|M|n)")) { A = []; I.getChildren().each(function(K) { var J = this.unformat(K.value, I.format); if ($type(J[0]) != "number" || J[0] == F.year) { if (!A.contains(J[1])) { A.push(J[1]) } } }, this); A.sort(this.sort) } if (I.format.test("(d|j)") && !I.format.test("^(d|j)$")) { H = []; I.getChildren().each(function(K) { var J = this.unformat(K.value, I.format); if (J[0] == F.year && J[1] == F.month) { if (!H.contains(J[2])) { H.push(J[2]) } } }, this) } } }, this); var G = 1; var E = new Date(F.year, F.month + 1, 0).getDate(); if (F.year == F.start.getFullYear()) { if (A == null && this.options.navigation == 2) { A = []; for (var C = 0; C < 12; C++) { if (C >= F.start.getMonth()) { A.push(C) } } } if (F.month == F.start.getMonth()) { G = F.start.getDate() } } if (F.year == F.end.getFullYear()) { if (A == null && this.options.navigation == 2) { A = []; for (var C = 0; C < 12; C++) { if (C <= F.end.getMonth()) { A.push(C) } } } if (F.month == F.end.getMonth()) { E = F.end.getDate() } } var B = this.blocked(F); if ($type(H) == "array") { H = H.filter(function(I) { if (I >= G && I <= E && !B.contains(I)) { return I } }) } else { H = []; for (var C = G; C <= E; C++) { if (!B.contains(C)) { H.push(C) } } } H.sort(this.sort); return { days: H, months: A, years: D} }, write: function(A) { this.rebuild(A); A.els.each(function(B) { B.value = this.format(A.val, B.format) }, this) } }); Calendar.implement(new Events, new Options);


// Calendar: a Javascript class for Mootools that adds accessible and unobtrusive date pickers to your form elements <http://electricprism.com/aeron/calendar>
// Calendar RC4, Copyright (c) 2007 Aeron Glemann <http://electricprism.com/aeron>, MIT Style License.

var Calendar = new Class({

    options: {
        blocked: [], // blocked dates 
        classes: [], // ['calendar', 'prev', 'next', 'month', 'year', 'today', 'invalid', 'valid', 'inactive', 'active', 'hover', 'hilite']
        days: ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'], // days of the week starting at sunday
        direction: 0, // -1 past, 0 past + future, 1 future
        draggable: true,
        months: ['Janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'],
        navigation: 1, // 0 = no nav; 1 = single nav for month; 2 = dual nav for month and year
        offset: 0, // first day of the week: 0 = sunday, 1 = monday, etc..
        onHideStart: Class.empty,
        onHideComplete: Class.empty,
        onShowStart: Class.empty,
        onShowComplete: Class.empty,
        pad: 1, // padding between multiple calendars
        tweak: { x: 0, y: 0} // tweak calendar positioning
    },

    // initialize: calendar constructor
    // @param obj (obj) a js object containing the form elements and format strings { id: 'format', id: 'format' etc }
    // @param props (obj) optional properties

    initialize: function(obj, options) {
        // basic error checking
        if (!obj) { return false; }

        this.setOptions(options);

        // create our classes array
        var keys = ['calendar', 'prev', 'next', 'month', 'year', 'today', 'invalid', 'valid', 'inactive', 'active', 'hover', 'hilite'];

        var values = keys.map(function(key, i) {
            if (this.options.classes[i]) {
                if (this.options.classes[i].length) { key = this.options.classes[i]; }
            }
            return key;
        }, this);

        this.classes = values.associate(keys);

        // create cal element with css styles required for proper cal functioning
        this.calendar = new Element('div', {
            'styles': { left: '-1000px', opacity: 0, position: 'absolute', top: '-1000px', zIndex: 1000 }
        }).addClass(this.classes.calendar).injectInside(document.body);

        // iex 6 needs a transparent iframe underneath the calendar in order to not allow select elements to render through
        if (window.ie6) {
            this.iframe = new Element('iframe', {
                'styles': { left: '-1000px', position: 'absolute', top: '-1000px', zIndex: 999 }
            }).injectInside(document.body);
            this.iframe.style.filter = 'progid:DXImageTransform.Microsoft.Alpha(style=0,opacity=0)';
        }

        // initialize fade method
        this.fx = this.calendar.effect('opacity', {
            onStart: function() {
                if (this.calendar.getStyle('opacity') == 0) { // show
                    if (window.ie6) { this.iframe.setStyle('display', 'block'); }
                    this.calendar.setStyle('display', 'block');
                    this.fireEvent('onShowStart', this.element);
                }
                else { // hide
                    this.fireEvent('onHideStart', this.element);
                }
            } .bind(this),
            onComplete: function() {
                if (this.calendar.getStyle('opacity') == 0) { // hidden
                    this.calendar.setStyle('display', 'none');
                    if (window.ie6) { this.iframe.setStyle('display', 'none'); }
                    this.fireEvent('onHideComplete', this.element);
                }
                else { // shown
                    this.fireEvent('onShowComplete', this.element);
                }
            } .bind(this)
        });

        // initialize drag method
        if (window.Drag && this.options.draggable) {
            this.drag = new Drag.Move(this.calendar, {
                onDrag: function() {
                    if (window.ie6) { this.iframe.setStyles({ left: this.calendar.style.left, top: this.calendar.style.top }); }
                } .bind(this)
            });
        }

        // create calendars array
        this.calendars = [];

        var id = 0;
        var d = new Date(); // today
		var minDate = new Date();
		minDate.setFullYear(2009,10,2)
		if(d<minDate){d=minDate;}
		
        d.setDate(d.getDate() + this.options.direction.toInt()); // correct today for directional offset

        for (var i in obj) {
            var cal = {
                button: new Element('button', { 'type': 'button' }),
                el: $(i),
                els: [],
                id: id++,
                month: d.getMonth(),
                visible: false,
                year: d.getFullYear()
            };

            // fix for bad element (naughty, naughty element!)
            if (!this.element(i, obj[i], cal)) { continue; }

            cal.el.addClass(this.classes.calendar);

            // create cal button
            cal.button.addClass(this.classes.calendar).addEvent('click', function(cal) { this.toggle(cal); } .pass(cal, this)).injectAfter(cal.el);

            // read in default value
            cal.val = this.read(cal);

            $extend(cal, this.bounds(cal)); // abs bounds of calendar

            $extend(cal, this.values(cal)); // valid days, months, years

            this.rebuild(cal);

            this.calendars.push(cal); // add to cals array		
        }
    },


    // blocked: returns an array of blocked days for the month / year
    // @param cal (obj)
    // @returns blocked days (array)

    blocked: function(cal) {
        var blocked = [];
        var offset = new Date(cal.year, cal.month, 1).getDay(); // day of the week (offset)
        var last = new Date(cal.year, cal.month + 1, 0).getDate(); // last day of this month

        this.options.blocked.each(function(date) {
            var values = date.split(' ');

            // preparation
            for (var i = 0; i <= 3; i++) {
                if (!values[i]) { values[i] = (i == 3) ? '' : '*'; } // make sure blocked date contains values for at least d, m and y
                values[i] = values[i].contains(',') ? values[i].split(',') : new Array(values[i]); // split multiple values
                var count = values[i].length - 1;
                for (var j = count; j >= 0; j--) {
                    if (values[i][j].contains('-')) { // a range
                        var val = values[i][j].split('-');
                        for (var k = val[0]; k <= val[1]; k++) {
                            if (!values[i].contains(k)) { values[i].push(k + ''); }
                        }
                        values[i].splice(j, 1);
                    }
                }
            }

            // execution
            if (values[2].contains(cal.year + '') || values[2].contains('*')) {
                if (values[1].contains(cal.month + 1 + '') || values[1].contains('*')) {
                    values[0].each(function(val) { // if blocked value indicates this month / year
                        if (val > 0) { blocked.push(val.toInt()); } // add date to blocked array
                    });

                    if (values[3]) { // optional value for day of week
                        for (var i = 0; i < last; i++) {
                            var day = (i + offset) % 7;

                            if (values[3].contains(day + '')) {
                                blocked.push(i + 1); // add every date that corresponds to the blocked day of the week to the blocked array
                            }
                        }
                    }
                }
            }
        }, this);

        return blocked;
    },


    // bounds: returns the start / end bounds of the calendar
    // @param cal (obj)
    // @returns obj	

    bounds: function(cal) {
        // 1. first we assume the calendar has no bounds (or a thousand years in either direction)

        // by default the calendar will accept a millennium in either direction
        var start = new Date(2009, 1, 10); // jan 1, 1000
        var end = new Date(2010, 2, 18); // dec 31, 2999

        // 2. but if the cal is one directional we adjust accordingly
        var currentDate = new Date();
        var day = currentDate.getDate() -1;
        var month = currentDate.getMonth() + 1;
        var year = currentDate.getFullYear();
        var date = new Date();
		var minDate = new Date();
		minDate.setFullYear(year,month,day)
		
		if(date<minDate){date=minDate.getDate() + this.options.direction.toInt();}
		else{date.getDate() + this.options.direction.toInt();}
		

        if (this.options.direction > 0) {
            start = new Date();
            var minDate = new Date();
			minDate.setFullYear(2009,10,1)
			
			if(start<minDate){start=minDate;}
		
			start.setDate(date + this.options.pad * cal.id);
        }

        if (this.options.direction < 0) {
            end = new Date();
            end.setDate(date - this.options.pad * (this.calendars.length - cal.id - 1));
        }

        // 3. then we can further filter the limits by using the pre-existing values in the selects
        cal.els.each(function(el) {
            if (el.getTag() == 'select') {
                if (el.format.test('(y|Y)')) { // search for a year select
                    var years = [];

                    el.getChildren().each(function(option) { // get options
                        var values = this.unformat(option.value, el.format);

                        if (!years.contains(values[0])) { years.push(values[0]); } // add to years array
                    }, this);

                    years.sort(this.sort);

                    if (years[0] > start.getFullYear()) {
                        d = new Date(years[0], start.getMonth() + 1, 0); // last day of new month

                        if (start.getDate() > d.getDate()) { start.setDate(d.getDate()); }

                        start.setYear(years[0]);
                    }

                    if (years.getLast() < end.getFullYear()) {
                        d = new Date(years.getLast(), end.getMonth() + 1, 0); // last day of new month

                        if (end.getDate() > d.getDate()) { end.setDate(d.getDate()); }

                        end.setYear(years.getLast());
                    }
                }

                if (el.format.test('(F|m|M|n)')) { // search for a month select
                    var months_start = [];
                    var months_end = [];

                    el.getChildren().each(function(option) { // get options
                        var values = this.unformat(option.value, el.format);

                        if ($type(values[0]) != 'number' || values[0] == years[0]) { // if it's a year / month combo for curr year, or simply a month select
                            if (!months_start.contains(values[1])) { months_start.push(values[1]); } // add to months array
                        }

                        if ($type(values[0]) != 'number' || values[0] == years.getLast()) { // if it's a year / month combo for curr year, or simply a month select
                            if (!months_end.contains(values[1])) { months_end.push(values[1]); } // add to months array
                        }
                    }, this);

                    months_start.sort(this.sort);
                    months_end.sort(this.sort);

                    if (months_start[0] > start.getMonth()) {
                        d = new Date(start.getFullYear(), months_start[0] + 1, 0); // last day of new month

                        if (start.getDate() > d.getDate()) { start.setDate(d.getDate()); }

                        start.setMonth(months_start[0]);
                    }

                    if (months_end.getLast() < end.getMonth()) {
                        d = new Date(start.getFullYear(), months_end.getLast() + 1, 0); // last day of new month

                        if (end.getDate() > d.getDate()) { end.setDate(d.getDate()); }

                        end.setMonth(months_end.getLast());
                    }
                }
            }
        }, this);

        return { 'start': start, 'end': end };
    },


    // caption: returns the caption element with header and navigation
    // @param cal (obj)
    // @returns caption (element)

    caption: function(cal) {
        // start by assuming navigation is allowed
        var navigation = {
            prev: { 'month': true, 'year': true },
            next: { 'month': true, 'year': true }
        };

        // if we're in an out of bounds year
        if (cal.year == cal.start.getFullYear()) {
            navigation.prev.year = false;
            if (cal.month == cal.start.getMonth() && this.options.navigation == 1) {
                navigation.prev.month = false;
            }
        }
        if (cal.year == cal.end.getFullYear()) {
            navigation.next.year = false;
            if (cal.month == cal.end.getMonth() && this.options.navigation == 1) {
                navigation.next.month = false;
            }
        }

        // special case of improved navigation but months array with only 1 month we can disable all month navigation
        if ($type(cal.months) == 'array') {
            if (cal.months.length == 1 && this.options.navigation == 2) {
                navigation.prev.month = navigation.next.month = false;
            }
        }

        var caption = new Element('caption');

        var prev = new Element('a').addClass(this.classes.prev).appendText('\x3c'); // <		
        var next = new Element('a').addClass(this.classes.next).appendText('\x3e'); // >

        if (this.options.navigation == 2) {
            var month = new Element('span').addClass(this.classes.month).injectInside(caption);

            if (navigation.prev.month) { prev.clone().addEvent('click', function(cal) { this.navigate(cal, 'm', -1); } .pass(cal, this)).injectInside(month); }

            month.adopt(new Element('span').appendText(this.options.months[cal.month]));

            if (navigation.next.month) { next.clone().addEvent('click', function(cal) { this.navigate(cal, 'm', 1); } .pass(cal, this)).injectInside(month); }

            var year = new Element('span').addClass(this.classes.year).injectInside(caption);

            if (navigation.prev.year) { prev.clone().addEvent('click', function(cal) { this.navigate(cal, 'y', -1); } .pass(cal, this)).injectInside(year); }

            year.adopt(new Element('span').appendText(cal.year));

            if (navigation.next.year) { next.clone().addEvent('click', function(cal) { this.navigate(cal, 'y', 1); } .pass(cal, this)).injectInside(year); }
        }
        else { // 1 or 0
            if (navigation.prev.month && this.options.navigation) { prev.clone().addEvent('click', function(cal) { this.navigate(cal, 'm', -1); } .pass(cal, this)).injectInside(caption); }

            caption.adopt(new Element('span').addClass(this.classes.month).appendText(this.options.months[cal.month]));

            caption.adopt(new Element('span').addClass(this.classes.year).appendText(cal.year));

            if (navigation.next.month && this.options.navigation) { next.clone().addEvent('click', function(cal) { this.navigate(cal, 'm', 1); } .pass(cal, this)).injectInside(caption); }

        }

        return caption;
    },


    // changed: run when a select value is changed
    // @param cal (obj)

    changed: function(cal) {
        cal.val = this.read(cal); // update calendar val from inputs	

        $extend(cal, this.values(cal)); // update bounds - based on curr month

        this.rebuild(cal); // rebuild days select

        if (!cal.val) { return; } // in case the same date was clicked the cal has no set date we should exit		

        if (cal.val.getDate() < cal.days[0]) { cal.val.setDate(cal.days[0]); }
        if (cal.val.getDate() > cal.days.getLast()) { cal.val.setDate(cal.days.getLast()); }

        cal.els.each(function(el) {	// then we can set the value to the field
            el.value = this.format(cal.val, el.format);
        }, this);

        this.check(cal); // checks other cals

        this.calendars.each(function(kal) { // update cal graphic if visible
            if (kal.visible) { this.display(kal); }
        }, this);
    },


    // check: checks other calendars to make sure no overlapping values
    // @param cal (obj)

    check: function(cal) {
        this.calendars.each(function(kal, i) {
            if (kal.val) { // if calendar has value set
                var change = false;

                if (i < cal.id) { // preceding calendar
                    var bound = new Date(Date.parse(cal.val));

                    bound.setDate(bound.getDate() - (this.options.pad * (cal.id - i)));

                    if (bound < kal.val) { change = true; }
                }
                if (i > cal.id) { // following calendar
                    var bound = new Date(Date.parse(cal.val));

                    bound.setDate(bound.getDate() + (this.options.pad * (i - cal.id)));

                    if (bound > kal.val) { change = true; }
                }

                if (change) {
                    if (kal.start > bound) { bound = kal.start; }
                    if (kal.end < bound) { bound = kal.end; }

                    kal.month = bound.getMonth();
                    kal.year = bound.getFullYear();

                    $extend(kal, this.values(kal));

                    // TODO - IN THE CASE OF SELECT MOVE TO NEAREST VALID VALUE
                    // IN THE CASE OF INPUT DISABLE

                    // if new date is not valid better unset cal value
                    // otherwise it would mean incrementally checking to find the nearest valid date which could be months / years away
                    kal.val = kal.days.contains(bound.getDate()) ? bound : null;

                    this.write(kal);

                    if (kal.visible) { this.display(kal); } // update cal graphic if visible
                }
            }
            else {
                kal.month = cal.month;
                kal.year = cal.year;
            }
        }, this);
    },


    // clicked: run when a valid day is clicked in the calendar
    // @param cal (obj)

    clicked: function(td, day, cal) {
        cal.val = (this.value(cal) == day) ? null : new Date(cal.year, cal.month, day+1); // set new value - if same then disable // KRISTOF DL : +1 gezet, want bug
var departure = (this.value(cal) == day) ? null : new Date(cal.year, cal.month, day+3);
        $('vertrekField').value=this.format(departure, 'd.m.Y') ;
        this.write(cal);

        // ok - in the special case that it's all selects and there's always a date no matter what (at least as far as the form is concerned)
        // we can't let the calendar undo a date selection - it's just not possible!!
        if (!cal.val) { cal.val = this.read(cal); }

        if (cal.val) {
            this.check(cal); // checks other cals						
            this.toggle(cal); // hide cal
        }
        else { // remove active class and replace with valid
            td.addClass(this.classes.valid);
            td.removeClass(this.classes.active);
        }
    },


    // display: create calendar element
    // @param cal (obj)

    display: function(cal) {
        // 1. header and navigation
        this.calendar.empty(); // init div

        this.calendar.className = this.classes.calendar + ' ' + this.options.months[cal.month].toLowerCase();

        var div = new Element('div').injectInside(this.calendar); // a wrapper div to help correct browser css problems with the caption element

        var table = new Element('table').injectInside(div).adopt(this.caption(cal));

        // 2. day names		
        var thead = new Element('thead').injectInside(table);

        var tr = new Element('tr').injectInside(thead);

        for (var i = 0; i <= 6; i++) {
            var th = this.options.days[(i + this.options.offset) % 7];

            tr.adopt(new Element('th', { 'title': th }).appendText(th.substr(0, 1)));
        }

        // 3. day numbers
        var tbody = new Element('tbody').injectInside(table);
        var tr = new Element('tr').injectInside(tbody);

        var d = new Date(cal.year, cal.month, 1);
        var offset = ((d.getDay() - this.options.offset) + 7) % 7; // day of the week (offset)
        var last = new Date(cal.year, cal.month + 1, 0).getDate(); // last day of this month
        var prev = new Date(cal.year, cal.month, 0).getDate(); // last day of previous month
        var active = this.value(cal); // active date (if set and within curr month)
        var valid = cal.days; // valid days for curr month
        var inactive = []; // active dates set by other calendars
        var hilited = [];
        this.calendars.each(function(kal, i) {
            if (kal != cal && kal.val) {
                if (cal.year == kal.val.getFullYear() && cal.month == kal.val.getMonth()) { inactive.push(kal.val.getDate()); }

                if (cal.val) {
                    for (var day = 1; day <= last; day++) {
                        d.setDate(day);

                        if ((i < cal.id && d > kal.val && d < cal.val) || (i > cal.id && d > cal.val && d < kal.val)) {
                            if (!hilited.contains(day)) { hilited.push(day); }
                        }
                    }
                }
            }
        }, this);
        var d = new Date();
        var today = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime(); // today obv 

        for (var i = 1; i < 43; i++) { // 1 to 42 (6 x 7 or 6 weeks)
            if ((i - 1) % 7 == 0) { tr = new Element('tr').injectInside(tbody); } // each week is it's own table row

            var td = new Element('td').injectInside(tr);

            var day = i - offset;
            var date = new Date(cal.year, cal.month, day);

            var cls = '';

            if (day === active) { cls = this.classes.active; } // active
            else if (inactive.contains(day)) { cls = this.classes.inactive; } // inactive
            else if (valid.contains(day)) { cls = this.classes.valid; } // valid
            else if (day >= 1 && day <= last) { cls = this.classes.invalid; } // invalid

            if (date.getTime() == today) { cls = cls + ' ' + this.classes.today; } // adds class for today

            if (hilited.contains(day)) { cls = cls + ' ' + this.classes.hilite; } // adds class if hilited

            td.addClass(cls);

            if (valid.contains(day)) { // if it's a valid - clickable - day we add interaction
                td.setProperty('title', this.format(date, 'D M jS Y'));

                td.addEvents({
                    'click': function(td, day, cal) {
                        this.clicked(td, day, cal);
                    } .pass([td, day, cal], this),
                    'mouseover': function(td, cls) {
                        td.addClass(cls);
                    } .pass([td, this.classes.hover]),
                    'mouseout': function(td, cls) {
                        td.removeClass(cls);
                    } .pass([td, this.classes.hover])
                });
            }

            // pad calendar with last days of prev month and first days of next month
            if (day < 1) { day = prev + day; }
            else if (day > last) { day = day - last; }

            td.appendText(day);
        }
    },


    // element: helper function
    // @param el (string) element id
    // @param f (string) format string
    // @param cal (obj)

    element: function(el, f, cal) {
        if ($type(f) == 'object') { // in the case of multiple inputs per calendar
            for (var i in f) {
                if (!this.element(i, f[i], cal)) { return false; }
            }

            return true;
        }

        el = $(el);

        if (!el) { return false; }

        el.format = f;

        if (el.getTag() == 'select') { // select elements allow the user to manually set the date via select option
            el.addEvent('change', function(cal) { this.changed(cal); } .pass(cal, this));
        }
        else { // input (type text) elements restrict the user to only setting the date via the calendar
            el.readOnly = true;
            el.addEvent('focus', function(cal) { this.toggle(cal); } .pass(cal, this));
        }

        cal.els.push(el);

        return true;
    },


    // format: formats a date object according to passed in instructions
    // @param date (obj)
    // @param f (string) any combination of punctuation / separators and d, j, D, l, S, m, n, F, M, y, Y
    // @returns string

    format: function(date, format) {
        format = 'd.m.Y';
        var str = '';

        if (date) {
            var j = date.getDate(); // 1 - 31
            var w = date.getDay(); // 0 - 6
            var l = this.options.days[w]; // Sunday - Saturday
            var n = date.getMonth() + 1; // 1 - 12
            var f = this.options.months[n - 1]; // January - December
            var y = date.getFullYear() + ''; // 19xx - 20xx

            for (var i = 0, len = format.length; i < len; i++) {
                var cha = format.charAt(i); // format char

                switch (cha) {
                    // year cases 
                    case 'y': // xx - xx
                        y = y.substr(2);
                    case 'Y': // 19xx - 20xx
                        str += y;
                        break;

                    // month cases 
                    case 'm': // 01 - 12
                        if (n < 10) { n = '0' + n; }
                    case 'n': // 1 - 12
                        str += n;
                        break;

                    case 'M': // Jan - Dec
                        f = f.substr(0, 3);
                    case 'F': // January - December
                        str += f;
                        break;

                    // day cases 
                    case 'd': // 01 - 31
                        if (j < 10) { j = '0' + j; }
                    case 'j': // 1 - 31
                        str += j;
                        break;

                    case 'D': // Sun - Sat
                        l = l.substr(0, 3);
                    case 'l': // Sunday - Saturday
                        str += l;
                        break;

                    case 'N': // 1 - 7
                        w += 1;
                    case 'w': // 0 - 6
                        str += w;
                        break;

                    case 'S': // st, nd, rd or th (works well with j)
                        if (j % 10 == 1 && j != '11') { str += 'st'; }
                        else if (j % 10 == 2 && j != '12') { str += 'nd'; }
                        else if (j % 10 == 3 && j != '13') { str += 'rd'; }
                        else { str += 'th'; }
                        break;

                    default:
                        str += cha;
                }
            }
        }

        return str; //  return format with values replaced
    },


    // navigate: calendar navigation
    // @param cal (obj)
    // @param type (str) m or y for month or year
    // @param n (int) + or - for next or prev

    navigate: function(cal, type, n) {
        switch (type) {
            case 'm': // month
                if ($type(cal.months) == 'array') {
                    var i = cal.months.indexOf(cal.month) + n; // index of current month

                    if (i < 0 || i == cal.months.length) { // out of range
                        if (this.options.navigation == 1) { // if type 1 nav we'll need to increment the year
                            this.navigate(cal, 'y', n);
                        }

                        i = (i < 0) ? cal.months.length - 1 : 0;
                    }

                    cal.month = cal.months[i];
                }
                else {
                    var i = cal.month + n;

                    if (i < 0 || i == 12) {
                        if (this.options.navigation == 1) {
                            this.navigate(cal, 'y', n);
                        }

                        i = (i < 0) ? 11 : 0;
                    }

                    cal.month = i;
                }
                break;

            case 'y': // year
                if ($type(cal.years) == 'array') {
                    var i = cal.years.indexOf(cal.year) + n;

                    cal.year = cal.years[i];
                }
                else {
                    cal.year += n;
                }
                break;
        }

        $extend(cal, this.values(cal));

        if ($type(cal.months) == 'array') { // if the calendar has a months select
            var i = cal.months.indexOf(cal.month); // and make sure the curr months exists for the new year

            if (i < 0) { cal.month = cal.months[0]; } // otherwise we'll reset the month
        }


        this.display(cal);
    },


    // read: compiles cal value based on array of inputs passed in
    // @param cal (obj)
    // @returns date (obj) or (null)

    read: function(cal) {
        var arr = [null, null, null];

        cal.els.each(function(el) {
            // returns an array which may contain empty values
            var values = this.unformat(el.value, el.format);

            values.each(function(val, i) {
                if ($type(val) == 'number') { arr[i] = val; }
            });
        }, this);

        // we can update the cals month and year values
        if ($type(arr[0]) == 'number') { cal.year = arr[0]; }
        if ($type(arr[1]) == 'number') { cal.month = arr[1]; }

        var val = null;

        if (arr.every(function(i) { return $type(i) == 'number'; })) { // if valid date
            var last = new Date(arr[0], arr[1] + 1, 0).getDate(); // last day of month

            if (arr[2] > last) { arr[2] = last; } // make sure we stay within the month (ex in case default day of select is 31 and month is feb)

            val = new Date(arr[0], arr[1], arr[2]);
        }

        return (cal.val == val) ? null : val; // if new date matches old return null (same date clicked twice = disable)
    },


    // rebuild: rebuilds days + months selects
    // @param cal (obj)

    rebuild: function(cal) {
        cal.els.each(function(el) {
            /*
            if (el.getTag() == 'select' && el.format.test('^(F|m|M|n)$')) { // special case for months-only select
            if (!cal.options) { cal.options = el.clone(); } // clone a copy of months select
			
				var val = (cal.val) ? cal.val.getMonth() : el.value.toInt();

				el.empty(); // initialize select

				cal.months.each(function(month) {
            // create an option element
            var option = new Element('option', {
            'selected': (val == month),
            'value': this.format(new Date(1, month, 1), el.format);
            }).appendText(day).injectInside(el);
            }, this);
            }
            */

            if (el.getTag() == 'select' && el.format.test('^(d|j)$')) { // special case for days-only select
                var d = this.value(cal);

                if (!d) { d = el.value.toInt(); } // if the calendar doesn't have a set value, try to use value from select

                el.empty(); // initialize select

                cal.days.each(function(day) {
                    // create an option element
                    var option = new Element('option', {
                        'selected': (d == day),
                        'value': ((el.format == 'd' && day < 10) ? '0' + day : day)
                    }).appendText(day).injectInside(el);
                }, this);
            }
        }, this);
    },


    // sort: helper function for numerical sorting

    sort: function(a, b) {
        return a - b;
    },


    // toggle: show / hide calendar 
    // @param cal (obj)

    toggle: function(cal) {
        document.removeEvent('mousedown', this.fn); // always remove the current mousedown script first

        if (cal.visible) { // simply hide curr cal						
            cal.visible = false;
            cal.button.removeClass(this.classes.active); // active

            this.fx.start(1, 0);
        }
        else { // otherwise show (may have to hide others)
            // hide cal on out-of-bounds click
            this.fn = function(e, cal) {
                var e = new Event(e);

                var el = e.target;

                var stop = false;

                while (el != document.body && el.nodeType == 1) {
                    if (el == this.calendar) { stop = true; }
                    this.calendars.each(function(kal) {
                        if (kal.button == el || kal.els.contains(el)) { stop = true; }
                    });

                    if (stop) {
                        e.stop();
                        return false;
                    }
                    else { el = el.parentNode; }
                }

                this.toggle(cal);
            } .create({ 'arguments': cal, 'bind': this, 'event': true });

            document.addEvent('mousedown', this.fn);

            this.calendars.each(function(kal) {
                if (kal == cal) {
                    kal.visible = true;
                    kal.button.addClass(this.classes.active); // css c-icon-active
                }
                else {
                    kal.visible = false;
                    kal.button.removeClass(this.classes.active); // css c-icon-active
                }
            }, this);

            var size = window.getSize().scrollSize;

            var coord = cal.button.getCoordinates();

            var x = coord.right + this.options.tweak.x;
            var y = coord.top + this.options.tweak.y;

            // make sure the calendar doesn't open off screen
            if (!this.calendar.coord) { this.calendar.coord = this.calendar.getCoordinates(); }

            if (x + this.calendar.coord.width > size.x) { x -= (x + this.calendar.coord.width - size.x); }
            if (y + this.calendar.coord.height > size.y) { y -= (y + this.calendar.coord.height - size.y); }

            this.calendar.setStyles({ left: x + 'px', top: y + 'px' });

            if (window.ie6) {
                this.iframe.setStyles({ height: this.calendar.coord.height + 'px', left: x + 'px', top: y + 'px', width: this.calendar.coord.width + 'px' });
            }

            this.display(cal);

            this.fx.start(0, 1);
        }
    },


    // unformat: takes a value from an input and parses the d, m and y elements
    // @param val (string)
    // @param f (string) any combination of punctuation / separators and d, j, D, l, S, m, n, F, M, y, Y
    // @returns array

    unformat: function(val, f) {
        f = f.escapeRegExp();

        var re = {
            d: '([0-9]{2})',
            j: '([0-9]{1,2})',
            D: '(' + this.options.days.map(function(day) { return day.substr(0, 3); }).join('|') + ')',
            l: '(' + this.options.days.join('|') + ')',
            S: '(st|nd|rd|th)',
            F: '(' + this.options.months.join('|') + ')',
            m: '([0-9]{2})',
            M: '(' + this.options.months.map(function(month) { return month.substr(0, 3); }).join('|') + ')',
            n: '([0-9]{1,2})',
            Y: '([0-9]{4})',
            y: '([0-9]{2})'
        }

        var arr = []; // array of indexes

        var g = '';

        // convert our format string to regexp
        for (var i = 0; i < f.length; i++) {
            var c = f.charAt(i);

            if (re[c]) {
                arr.push(c);

                g += re[c];
            }
            else {
                g += c;
            }
        }

        // match against date
        var matches = val.match('^' + g + '$');

        var dates = new Array(3);

        if (matches) {
            matches = matches.slice(1); // remove first match which is the date

            arr.each(function(c, i) {
                i = matches[i];

                switch (c) {
                    // year cases 
                    case 'y':
                        i = '19' + i; // 2 digit year assumes 19th century (same as JS)
                    case 'Y':
                        dates[0] = i.toInt();
                        break;

                    // month cases 
                    case 'F':
                        i = i.substr(0, 3);
                    case 'M':
                        i = this.options.months.map(function(month) { return month.substr(0, 3); }).indexOf(i) + 1;
                    case 'm':
                    case 'n':
                        dates[1] = i.toInt() - 1;
                        break;

                    // day cases 
                    case 'd':
                    case 'j':
                        dates[2] = i.toInt();
                        break;
                }
            }, this);
        }

        return dates;
    },


    // value: returns day value of calendar if set
    // @param cal (obj)
    // @returns day (int) or null

    value: function(cal) {
        var day = null;

        if (cal.val) {
            if (cal.year == cal.val.getFullYear() && cal.month == cal.val.getMonth()) { day = cal.val.getDate(); }
        }

        return day;
    },


    // values: returns the years, months (for curr year) and days (for curr month and year) for the calendar
    // @param cal (obj)
    // @returns obj	

    values: function(cal) {
        var years, months, days;

        cal.els.each(function(el) {
            if (el.getTag() == 'select') {
                if (el.format.test('(y|Y)')) { // search for a year select
                    years = [];

                    el.getChildren().each(function(option) { // get options
                        var values = this.unformat(option.value, el.format);

                        if (!years.contains(values[0])) { years.push(values[0]); } // add to years array
                    }, this);

                    years.sort(this.sort);
                }

                if (el.format.test('(F|m|M|n)')) { // search for a month select
                    months = []; // 0 - 11 should be

                    el.getChildren().each(function(option) { // get options
                        var values = this.unformat(option.value, el.format);

                        if ($type(values[0]) != 'number' || values[0] == cal.year) { // if it's a year / month combo for curr year, or simply a month select
                            if (!months.contains(values[1])) { months.push(values[1]); } // add to months array
                        }
                    }, this);

                    months.sort(this.sort);
                }

                if (el.format.test('(d|j)') && !el.format.test('^(d|j)$')) { // search for a day select, but NOT a days only select
                    days = []; // 1 - 31

                    el.getChildren().each(function(option) { // get options
                        var values = this.unformat(option.value, el.format);

                        // in the special case of days we dont want the value if its a days only select
                        // otherwise that will screw up the options rebuilding
                        // we will take the values if they are exact dates though
                        if (values[0] == cal.year && values[1] == cal.month) {
                            if (!days.contains(values[2])) { days.push(values[2]); } // add to days array
                        }
                    }, this);
                }
            }
        }, this);

        // we start with what would be the first and last days were there no restrictions
        var first = 1;
        var last = new Date(cal.year, cal.month + 1, 0).getDate(); // last day of the month

        // if we're in an out of bounds year
        if (cal.year == cal.start.getFullYear()) {
            // in the special case of improved navigation but no months array, we'll need to construct one
            if (months == null && this.options.navigation == 2) {
                months = [];

                for (var i = 0; i < 12; i++) {
                    if (i >= cal.start.getMonth()) { months.push(i); }
                }
            }

            // if we're in an out of bounds month
            if (cal.month == cal.start.getMonth()) {
                first = cal.start.getDate(); // first day equals day of bound
            }
        }
        if (cal.year == cal.end.getFullYear()) {
            // in the special case of improved navigation but no months array, we'll need to construct one
            if (months == null && this.options.navigation == 2) {
                months = [];

                for (var i = 0; i < 12; i++) {
                    if (i <= cal.end.getMonth()) { months.push(i); }
                }
            }

            if (cal.month == cal.end.getMonth()) {
                last = cal.end.getDate(); // last day equals day of bound
            }
        }

        // let's get our invalid days
        var blocked = this.blocked(cal);

        // finally we can prepare all the valid days in a neat little array
        if ($type(days) == 'array') { // somewhere there was a days select
            days = days.filter(function(day) {
                if (day >= first && day <= last && !blocked.contains(day)) { return day; }
            });
        }
        else { // no days select we'll need to construct a valid days array
            days = [];

            for (var i = first; i <= last; i++) {
                if (!blocked.contains(i)) { days.push(i); }
            }
        }

        days.sort(this.sort); // sorting our days will give us first and last of month

        return { 'days': days, 'months': months, 'years': years };
    },


    // write: sets calendars value to form elements
    // @param cal (obj)

    write: function(cal) {
        this.rebuild(cal);  // in the case of options, we'll need to make sure we have the correct number of days available

        cal.els.each(function(el) {	// then we can set the value to the field
            el.value = this.format(cal.val, el.format);
        }, this);
    }
});

Calendar.implement(new Events, new Options);
