build/ui.date.js

zebkit.package("ui.date", function(pkg, Class) {
    var ui           = zebkit.ui,
        compareDates = zebkit.util.compareDates,
        validateDate = zebkit.util.validateDate;

    /**
     * The package contains number of classes to implement
     * UI date related component like calendar, date field etc.
     *
     *     zebkit.require("ui", "ui.date", "layout", function(ui, date, layout) {
     *         var root = (new ui.zCanvas()).root;
     *         root.setFlowLayout("center", "center", "horizontal", 8);
     *
     *         // add calendar component
     *         root.add(new date.Calendar());
     *     });
     *
     * @class zebkit.ui.date
     * @access package
     */

    Date.prototype.daysInMonth = function() {
        return new Date(this.getFullYear(), this.getMonth() + 1, 0).getDate();
    };

    Date.prototype.firstWeekDay = function() {
        return new Date(this.getFullYear(), this.getMonth(), 1).getDay();
    };

    Date.prototype.prevMonth = function() {
        return new Date(this.getFullYear(), this.getMonth() - 1, 1);
    };

    Date.prototype.nextMonth = function() {
        return new Date(this.getFullYear(), this.getMonth() + 1, 1);
    };

    Date.prototype.isValid = function() {
        // invalid dates have time set
        // to NaN, NaN never equals each other
        return this.getTime() === this.getTime();
    };

    Date.prototype.getMonthName = function() {
        return pkg.MONTHS[this.getMonth()].name;
    };

    Date.prototype.getMonthNick = function() {
        return pkg.MONTHS[this.getMonth()].nickname;
    };

    Date.prototype.getWeekdayName = function() {
        return pkg.WEEKS[this.getDay()].name;
    };

    Date.prototype.getWeekdayNick = function() {
        return pkg.WEEKS[this.getDay()].nickname;
    };

    Date.prototype.getMonth2 = function() {
        return this.getMonth() + 1;
    };

    /**
     *  Shows the given month and year days. This is one of the core class that
     *  is widely used with other date UI component.
     *  @constructor
     *  @param {Date} [date] year to specify month and year to be shown
     *  @param {Integer}  [month] a month to be shown
     *  @param {Integer}  [year] an year to be shown
     *  @class zebkit.ui.date.MonthDaysGrid
     *  @extends zebkit.ui.grid.Grid
     */

    /**
     * Fire when the specified month of the given year has been shown with the
     * component
     *
     *       monthDays.on("monthShown", function(src, prevMonth, prevYear) {
     *           ...
     *       });
     *
     * @event monthShown
     * @param  {zebkit.ui.date.MonthDaysGrid} src a source of the event
     * @param  {Integer} prevMonth a previous shown month
     * @param  {Integer} prevYear a previous shown year
     */

    /**
     * Fire when the given date has been selected
     *
     *       monthDays.on("dateSelected", function(src, item, b) {
     *           ...
     *       });
     *
     * @event dateSelected
     * @param  {zebkit.ui.date.MonthDaysGrid} src a source of the event
     * @param  {zebkit.ui.date.MonthDaysGrid.DayPan} item a selected item
     * @param  {Boolean} b true if fate has been selected, false if the date has
     * been de-selected
     */
    pkg.MonthDaysGrid = Class(ui.grid.Grid, [
        function() {
            this.tags = {};
            this.$super(6, 7);

            this.setSelectMode(new this.clazz.DaySelectMode(this));
            this.setNavigationMode("cell");

            // pre-fill model with data
            for(var i = 0; i < this.model.rows * this.model.cols; i++) {
                this.model.puti(i, new this.clazz.DayPan());
            }

            this.add("top", new this.clazz.GridCaption());

            if (arguments.length > 0) {
                this.setMonthToShow.apply(this, arguments);
            }
        },

        function $clazz() {
            this.DaySelectMode = Class(zebkit.ui.grid.CellSelectMode, [
                function(target) {
                    this.$super(target);
                    this.selectedItems = {};
                },

                function $prototype() {
                    this.posChanged = function(pos, prevOffset, prevLine, prevCol) {};

                    this.isSelected = function (row, col) {
                        if (this.target.hasMonthShown() && row >= 0 && col >= 0) {
                            var idx = this.$idx(this.target.model.get(row, col));
                            return idx !== null ? this.selectedItems[idx] !== undefined
                                                : false;
                        } else {
                            return false;
                        }
                    };

                    this.clearSelect = function() {
                        for(var row = 0; row < this.target.model.rows; row++) {
                            for(var col = 0; col < this.target.model.cols; col++) {
                                if (this.isSelected(row, col)) {
                                    var idx  = this.$idx(this.target.model.get(row, col));
                                    delete this.selectedItems[idx];
                                    this.fireSelected(row, col, false);
                                }
                            }
                        }
                    };

                    this.select = function(row, col, b) {
                        var model = this.target.model;
                        if (row >= 0 && col >= 0) {
                            this.clearSelect();
                            var item = this.target.model.get(row, col);
                            if (this.isSelected(row, col) !== b && this.target.isDaySelectable(item)) {
                                this.selectedItems[this.$idx(item)] = {
                                    day   : item.day,
                                    month : item.month,
                                    year  : item.year
                                };
                                this.fireSelected(row, col, b);
                                this.target.repaint();
                            }
                        }
                    };

                    this.uninstall = function() {
                        this.selectedItems = {};
                    };

                    this.$idx = function(item) {
                        if (item.day >= 0 && item.month >= 0 && item.year >= 0) {
                            return 10000000 * item.day + 100000 * item.month + item.year;
                        } else {
                            return null;
                        }
                    };

                    this.getFirstSelected = function() {
                        for (var selected in this.selectedItems) {
                            var item = this.selectedItems[selected];
                            return new Date(item.year, item.month, item.day);
                        }
                        return null;
                    };
                },

                function fireSelected(row, col, b) {
                    this.$super(row, col, b);
                    var day = this.target.model.get(row, col);
                    this.target.dateSelected(day, b);
                }
            ]);

            /**
             * Day panel element class.
             * @constructor
             * @class zebkit.ui.date.MonthDaysGrid.DayPan
             * @extends {zebkit.ui.Panel}
             */
            this.DayPan = Class(ui.Panel, [
                function() {
                    this.tags  = [];
                    this.icon  = new this.clazz.IconPan();
                    this.label = new this.clazz.Label();
                    this.$super();
                    this.add(this.icon);
                    this.add(this.label);
                },

                function $clazz() {
                    this.Label   = Class(ui.Label, [
                        function() {
                            this.$super(new zebkit.draw.DecoratedTextRender(""));
                        },

                        function setTextDecorations() {
                            this.view.setDecorations.apply(this.view, arguments);
                            this.repaint();
                            return this;
                        },

                        function setTextDecorationsColor(c) {
                            this.view.lineColor = c;
                            this.repaint();
                            return this;
                        }
                    ]);

                    this.IconPan = Class(ui.ViewPan, []);
                },

                function $prototype() {
                    /**
                     * Assigned tags list.
                     * @attribute tags
                     * @type {Array}
                     * @readOnly
                     * @private
                     */
                    this.tags = null;

                    /**
                     * A month day
                     * @attribute day
                     * @type {Integer}
                     * @default -1
                     * @readOnly
                     */
                    this.day = -1;

                    /**
                     * A month.
                     * @attribute month
                     * @type {Integer}
                     * @default -1
                     * @readOnly
                     */
                    this.month = -1;

                    /**
                     * An year.
                     * @attribute year
                     * @type {Integer}
                     * @default -1
                     * @readOnly
                     */
                    this.year = -1;

                    /**
                     * Set the specified related to the panel date.
                     * @param {Integer} day   a month day
                     * @param {Integer} month a month
                     * @param {Integer} year  an year
                     * @method setValue
                     * @chainable
                     */
                    this.setValue = function(day, month, year) {
                        this.day   = day;
                        this.month = month;
                        this.year  = year;
                        this.label.setValue("" + day);
                        return this;
                    };

                    /**
                     * Add the specified tag to the given day panel
                     * @param  {String} tag a tag name
                     * @return {Boolean} true if the tag has been added
                     * @method tag
                     */
                    this.tag = function(tag) {
                        if (tag === null || tag === undefined) {
                            throw new Error("Undefined tag name");
                        }

                        if (this.tags.indexOf(tag) < 0) {
                            this.tags.push(tag);
                            return true;
                        } else {
                            return false;
                        }
                    };
                    /**
                     * Remove the specified tag or all tags for the given day panel
                     * @param  {String} [tag] a tag name to be removed. All assigned
                     * tags will be removed if the argument has not been specified.
                     * @return {Boolean} true if the tag or tags have been removed
                     * @method untag
                     */
                    this.untag = function(tag) {
                        if (arguments.length === 0) {
                            var len = this.tags.length;
                            if (len > 0) {
                                this.tags.length = 0;
                                this.tags        = [];
                                this.properties(this.clazz);
                            }
                            return len > 0;
                        } else  {
                            var i = this.tags.indexOf(tag);
                            if (i >= 0) {
                                this.tags.splice(i, 1);
                                return true;
                            } else {
                                return false;
                            }
                        }
                    };

                    /**
                     * Check if the day panel has the given tag;
                     * @param  {String}  a tag to be checked
                     * @return {Boolean} true if the given tag has been assigned to the day panel
                     * @method hasTag
                     */
                    this.hasTag = function(tag) {
                        return this.tags.indexOf(tag) >= 0;
                    };

                    /**
                     * Set text color.
                     * @param {String} c a color
                     * @method setColor
                     * @chainable
                     */
                    this.setColor = function(c) {
                        this.label.setColor(c);
                        return this;
                    };

                    /**
                     * Set text font
                     * @param {String | zebkit.Font} f a font
                     * @method setFont
                     * @chainable
                     */
                    this.setFont = function(c) {
                        this.label.setFont(c);
                        return this;
                    };

                    this.setTextDecorations = function() {
                        this.label.setTextDecorations.apply(this.label, arguments);
                        return this;
                    };

                    this.setTextDecorationsColor = function() {
                        this.label.setTextDecorationsColor.apply(this.label, arguments);
                        return this;
                    };
                }
            ]);

            this.GridCaption = Class(ui.grid.GridCaption, [
                function $clazz() {
                    this.Label = Class(ui.Label, [
                        function setNickname(name) {
                            return this.setValue(name);
                        }
                    ]);
                },

                function $prototype() {
                    this.setNamesOfWeekDays = function(daysOfWeek) {
                        for(var i = 0; i < daysOfWeek.length; i++) {
                            this.setLabel(i, new this.clazz.Label().properties(daysOfWeek[i]));
                        }
                    };
                }
            ]);
        },

        /**
         * @for zebkit.ui.date.MonthDaysGrid
         */
        function $prototype() {
            /**
             * Custom tagger. The attribute is function that is called to tag
             * the given day panel item.
             * @attribute tagger
             * @type {Function}
             * @default null
             * @readOnly
             */
            this.tagger = null;

            /**
             * Shown month
             * @attribute month
             * @type {Integer}
             * @default -1
             * @readOnly
             */
            this.month = -1;

            /**
             * Shown month year
             * @attribute year
             * @type {Integer}
             * @default -1
             * @readOnly
             */
            this.year = -1;

            /**
             * Minimum possible selection date
             * @attribute minDate
             * @type {Date}
             * @default null
             * @readOnly
             */
            this.minDate = null;

            /**
             * Maximum possible selection date
             * @attribute maxDate
             * @type {Date}
             * @default null
             * @readOnly
             */
            this.maxDate = null;

            /**
             * Re-tag the grid model. The process assign the following tags to grid model items:
             *
             *    -  "prevMonth" - for model item that belong to previous month of the year
             *    -  "nextMonth" - for model item that belong to the next month of the year
             *    -  "notSelectable" - for model items that cannot be selected
             *    -  "today"         - for model item that matches current date
             *
             * @chainable
             * @method retagModel
             */
            this.retagModel = function() {
                var curDate = new Date(),
                    month   = curDate.getMonth(),
                    year    = curDate.getFullYear(),
                    day     = curDate.getDate();

                for(var i = 0; i < this.model.rows * this.model.cols; i++) {
                    var item = this.model.geti(i);
                    item.untag();

                    if (item.year < this.year || (item.year === this.year && item.month < this.month)) {
                        item.tag("prevMonth");
                    } else if (item.year > this.year || (item.year === this.year && item.month > this.month)) {
                        item.tag("nextMonth");
                    } else {
                        item.tag("shownMonth");
                    }

                    if (this.isDaySelectable(item) === false) {
                        item.tag("notSelectable");
                    }

                    if (this.tagger !== null) {
                        this.tagger(item);
                    }

                    if (item.day === day && item.month === month && item.year === year) {
                        item.tag("today");
                    }

                    for(var j = 0; j < item.tags.length; j++) {
                        var k = item.tags[j];
                        if (this.tags[k] !== undefined && this.tags[k] !== null) {
                            item.properties(this.tags[k]);
                        }
                    }
                }

                this.vrp();
            };

            /**
             * Set date range possible for selection
             * @param {Date} min a minimal possible selection date or null
             * @param {Date} max a maximal possible selection date or null
             * @method setDateRange
             * @chainable
             */
            this.setDateRange = function(min, max) {
                if (min !== null) {
                    validateDate(min);
                }

                if (max !== null) {
                    validateDate(max);
                }

                var b       = false,
                    prevMin = this.minDate,
                    prevMax = this.maxDate;

                if (min !== this.minDate && compareDates(this.minDate, min) !== 0) {
                    b = true;
                    this.minDate = min;
                }

                if (max !== this.maxDate && compareDates(this.maxDate, max) !== 0) {
                    b = true;
                    this.maxDate = max;
                }

                if (compareDates(this.minDate, this.maxDate) === 1) {
                    this.maxDate = prevMax;
                    this.minDate = prevMin;
                    throw new RangeError("Date range is not valid : [" + min + ", " + max + "]");
                }

                if (b) {
                    this.clearSelect();
                    this.retagModel();
                    if (this.dateRangeUpdated !== undefined) {
                        this.dateRangeUpdated(prevMin, prevMax);
                    }
                }

                return this;
            };

            /**
             * Test if the month days have a month shown.
             * @method hasMonthShown
             * @return {Boolean} true if a month is shown
             */
            this.hasMonthShown = function() {
                return this.month >= 0 && this.year;
            };

            /**
             * Set maximal possible selection date
             * @param {Date} date a maximal possible selection date or null
             * @method setMaxDate
             * @chainable
             */
            this.setMaxDate = function(date) {
                this.setDateRange(this.minDate, date);
                return this;
            };

            /**
             * Set minimal possible selection date
             * @param {Date} date a minimal possible selection date or null
             * @method setMinDate
             * @chainable
             */
            this.setMinDate = function(date) {
                this.setDateRange(date, this.maxDate);
                return this;
            };

            /**
             * Test if the given month can be shown
             * @param  {Integer} month a month
             * @param  {Integer} year  a year
             * @return {Boolean} true if the month can be shown
             * @method  canMonthBeShown
             */
            this.canMonthBeShown = function(month, year) {
                return true;
            };

            /**
             * Set a month and an year with the given arguments. If no parameters
             * have been passed the current date year and month will be set.
             * @param {Date} [date] a date object.
             * @param {Integer} [month] a month.
             * @param {Integer} [year] a full year.
             * @method setMonthToShow
             * @chainable
             */
            this.setMonthToShow = function(month, year) {
                if (arguments.length === 1) {
                    if (month instanceof Date) {
                        year  = month.getFullYear();
                        month = month.getMonth();
                    } else {
                        year = (new Date()).getFullYear();
                    }
                } else if (arguments.length === 0) {
                    var cd = new Date();
                    year  = cd.getFullYear();
                    month = cd.getMonth();
                }

                validateDate(month, year);

                if (this.canMonthBeShown(month, year) && this.month !== month || this.year !== year) {
                    var prevYear  = this.year,
                        prevMonth = this.month;

                    this.month = month;
                    this.year  = year;

                    var date         = new Date(this.year, this.month),
                        firstWeekDay = date.firstWeekDay(),
                        pdate        = date.prevMonth(),
                        ndate        = date.nextMonth(),
                        pdays        = pdate.daysInMonth(),
                        i            = 0,
                        d            = 0;

                    // if current month starts from the first cell
                    // shift one week ahead to shown number of
                    // previous month days
                    if (firstWeekDay === 0) {
                        firstWeekDay += 7;
                    }

                    for(; i < firstWeekDay; i++) {
                        this.model.geti(i).setValue(
                            pdays - firstWeekDay + i + 1,
                            pdate.getMonth(),
                            pdate.getFullYear()
                        );
                    }

                    for(d = 1; d <= date.daysInMonth(); i++, d++) {
                        this.model.geti(i).setValue(d, month, year);
                    }

                    for(d = 1; i < this.model.rows * this.model.cols; i++, d++) {
                        this.model.geti(i).setValue(d, ndate.getMonth(), ndate.getFullYear());
                    }

                    this.retagModel();
                    if (this.monthShown !== undefined) {
                        this.monthShown(prevMonth, prevYear);
                    }
                    this.fire("monthShown", [this, prevMonth, prevYear]);
                }
                return this;
            };

            /**
             * Set the current date month shown.
             * @chainable
             * @method setCurrentMonthToShow
             */
            this.setCurrentMonthToShow = function() {
                this.setMonthToShow(new Date());
                return this;
            };

            /**
             * Set the next date month to show
             * @chainable
             * @method setNextMonthToShow
             */
            this.setNextMonthToShow = function() {
                if (this.month < 0) {
                    this.setMonthToShow(0, 1900);
                } else {
                    var d = new Date(this.year,
                                     this.month).nextMonth();
                    this.setMonthToShow(d.getMonth(), d.getFullYear());
                }
                return this;
            };

            /**
             * Set the previous date month to show.
             * @chainable
             * @method setPrevMonthToShow
             */
            this.setPrevMonthToShow = function() {
                if (this.month < 0) {
                    this.setMonthToShow(0, 1900);
                } else {
                    var d = new Date(this.year,
                                     this.month).prevMonth();
                    this.setMonthToShow(d.getMonth(), d.getFullYear());
                }
                return this;
            };

            /**
             * Set the next year with the current month to show.
             * @chainable
             * @method setNextYearToShow
             */
            this.setNextYearToShow = function() {
                this.setMonthToShow(this.month, this.year + 1);
                return this;
            };

            /**
             * Set the next year with the current month to show.
             * @chainable
             * @method setSelectedToShow
             */
            this.setSelectedToShow = function() {
                var selected = this.selectMode.getFirstSelected();
                if (selected !== null) {
                    this.setMonthToShow(selected);
                }
                return this;
            };

            this.getFirstSelected = function() {
                return this.selectMode.getFirstSelected();
            };

            /**
             * Set the previous year with the current month to show.
             * @chainable
             * @method setNextYearToShow
             */
            this.setPrevYearToShow = function () {
                this.setMonthToShow(this.month, this.year - 1);
                return this;
            };

            /**
             * Test if the given day is selectable
             * @param  {zebkit.ui.date.DayPan} item a day panel
             * @return {Boolean} true if the given day is selectable
             * @method isDaySelectable
             */
            this.isDaySelectable = function(item) {
                return  (this.minDate === null || compareDates(item.day, item.month, item.year, this.minDate) >= 0) &&
                        (this.maxDate === null || compareDates(item.day, item.month, item.year, this.maxDate) <= 0);
            };

            this.pointerMoved = function(e) {
                var p = this.cellByLocation(e.x, e.y);
                if (p !== null) {
                    this.position.setRowCol(p.row, p.col);
                } else {
                    this.position.setOffset(null);
                }
            };

            this.pointerExited = function(e) {
                this.position.setOffset(null);
            };

            /**
             * Set days tagger function.
             * @param {Function} tagger a tagger function
             * @method setTagger
             * @chainable
             */
            this.setTagger = function(tagger) {
                if (this.tagger !== tagger) {
                    this.tagger = tagger;
                    this.retagModel();
                }
                return this;
            };

            /**
             * Set the visual properties for the tags.
             * @param {Object} tags set visual properties for the given tags
             * @chainable
             * @method setTagsDecoration
             * @example
             *
             *     //  render previous month day with gray color
             *     //  and next month with light gray color
             *     daysGrid.setTagsDecoration({
             *         "prevMonth" : {
             *             "color" : "lightGray"
             *          },
             *
             *          "nextMonth" : {
             *              "color" : "lightGray"
             *          }
             *      });
             */
            this.setTagsDecoration = function(tags) {
                this.tags = zebkit.clone(tags);
                this.vrp();
                return this;
            };

            /**
             * Add visual properties for the given tags set
             * @param {Object} tags [description]
             * @chainable
             * @method addTagsDecoration
             */
            this.addTagsDecoration = function(tags) {
                for(var k in tags) {
                    this.tags[k] = zebkit.clone(tags[k]);
                }
                this.vrp();
                return this;
            };

            /**
             * Set decoration for the given tag.
             * @param {String} tag a tag name
             * @param {Object} p  a tag decoration
             * @chainable
             * @method setTagDecoration
             */
            this.setTagDecoration = function(tag, p) {
                if (p === null) {
                    delete this.tags[tag];
                } else {
                    this.tags[tag] = zebkit.clone(p);
                }
                this.vrp();
                return this;
            };

            /**
             * Day selected callback function.
             * @param  {zebkit.ui.date.MonthDaysGrid.Daypan} item a day panel
             * @param  {Boolean} b  a selection cell state
             * @method dateSelected
             * @protected
             */
            this.dateSelected = function(item, b) {
                this.fire("dateSelected", [ this, item, b] );
            };

            /**
             * Select the specified date and show appropriate month of
             * selected date year.
             * @param  {Integer} day   a day
             * @param  {Integer} month a month
             * @param  {Integer} year  an year
             * @chainable
             * @method selectDate
             */
            this.selectDate = function(day, month, year) {
                if (day === null) {
                    this.clearSelect();
                } else {
                    if (arguments.length === 0) {
                        day = new Date();
                    }


                    if (day instanceof Date) {
                        month = day.getMonth();
                        year  = day.getFullYear();
                        day   = day.getDate();
                    }

                    this.setMonthToShow(month, year);

                    for(var r = 0; r < this.model.rows; r++) {
                        for(var c = 0; c < this.model.cols; c++) {
                            var item = this.model.get(r, c);
                            if (item.year === year && item.day === day && item.month === month) {
                                this.select(r, c, true);
                                break;
                            }
                        }
                    }
                }
                return this;
            };
        },

        function keyPressed(e) {
            if (e.code === "Enter") {
                this.select(this.position.currentLine, this.position.currentCol, true);
            }
            this.$super(e);
        },

        function pointerClicked(e) {
            this.$super(e);
            var p = this.cellByLocation(e.x, e.y);
            if (p !== null) {
                this.select(p.row, p.col, true);
            }
        },

        function rPsMetric() {
            this.$super();

            var max = 0, cols = this.getGridCols(), i = 0;
            for(i = 0; i < cols; i++) {
                if (this.colWidths[i] > max) {
                    max = this.colWidths[i];
                }
            }

            for(i = 0; i < cols; i++) {
                this.colWidths[i] = max;
            }
        },

        function $getPosMarker() {
            var item = this.model.geti(this.position.offset);
            return this.isDaySelectable(item) === false ? this.views.notSelectableMarker
                                                        : this.$super();
        }

    ]).events("monthShown", "dateSelected");

    /**
     * Calendar component. This component is supposed to be used to select a single date
     * with the help of provided UI.
     *
     *
     *      var c = new zebkit.ui.date.Calendar();
     *      var z = new zebkit.ui.zCanvas();
     *      z.root.setFlowLayout();
     *      z.root.add(c);
     *
     * @param  {Date} [date] a date to be set
     * @class zebkit.ui.date.Calendar
     * @extends zebkit.ui.Panel
     * @uses zebkit.ui.FireEventRepeatedly
     * @constructor
     */

    /**
     * Fire when the date has been selected or de-selected.
     *
     *       calendar.on("dateSelected", function(src, date, b) {
     *           ...
     *       });
     *
     * @event dateSelected
     * @param  {zebkit.ui.date.Calendar} src a source of the event
     * @param  {Date} date a date that has been selected.
     * @param  {Boolean} b a selection state.
     */
    pkg.Calendar = new Class(ui.Panel, [
        function(date) {
            if (arguments.length === 0) {
                date = new Date();
            }

            var $this = this;
            this.$super(new zebkit.layout.BorderLayout());

            this.monthDays = new pkg.MonthDaysGrid([
                function monthShown(prevMonth, prevYear) {
                    $this.comboMonth.select(this.month);
                    $this.yearField.setValue("" + this.year);

                    var v = $this.getValue();
                    $this.byPath("#dotButton").setEnabled(v !== null &&
                                                          (this.year  !== v.getFullYear() ||
                                                           this.month !== v.getMonth()       ));

                    $this.repaint();
                    $this.fire("monthShown", [$this, prevMonth, prevYear]);
                },

                function dateSelected(item, b) {
                    if (b === true) {
                        if (item.tags.length > 0) {
                            if (item.hasTag("nextMonth") === true) {
                                $this.monthDays.setNextMonthToShow();
                            } else if (item.hasTag("prevMonth") === true) {
                                $this.monthDays.setPrevMonthToShow();
                            }
                        }
                        $this.dateSelected(new Date(item.year, item.month, item.day), b);
                    } else {
                        $this.dateSelected(new Date(item.year, item.month, item.day), b);
                    }

                    this.$super(item, b);
                }
            ]);

            this.comboMonth = new this.clazz.MonthsCombo();
            this.comboMonth.setMaxPadHeight(200);
            this.comboMonth.on("selected", function(src) {
                var m = src.getSelectedMonth();
                $this.monthDays.setMonthToShow(m.value, $this.monthDays.year);
            });

            this.yearField = new this.clazz.YearField("", [
                function keyPressed(e) {
                    switch (e.code) {
                        case "ArrowUp"  : $this.monthDays.setNextYearToShow(); break;
                        case "ArrowDown": $this.monthDays.setPrevYearToShow(); break;
                        default: return this.$super(e);
                    }
                }
            ]);

            var topPan = new this.clazz.InfoPan({
                layout: new zebkit.layout.BorderLayout(),
                kids  : {
                    right: new ui.Panel({
                        layout : new zebkit.layout.FlowLayout("center", "center"),
                        kids   : [
                            this.comboMonth,
                            new ui.Panel({
                                layout : new zebkit.layout.BorderLayout(),
                                kids   : {
                                    center : this.yearField,
                                    right  : new ui.Panel({
                                        layout: new zebkit.layout.FlowLayout("center", "center", "vertical", 1),
                                        kids  : [
                                            new this.clazz.TopArrowButton(),
                                            new this.clazz.BottomArrowButton()
                                        ]
                                    })
                                }
                            })
                        ]
                    }),

                    left: new ui.Panel({
                        layout : new zebkit.layout.FlowLayout("center", "center", "horizontal", 3),
                        kids   : [
                            new this.clazz.LeftArrowButton(),
                            new this.clazz.DotButton(),
                            new this.clazz.RightArrowButton()
                        ]
                    })
                }
            });

            this.add("top", topPan);
            this.add("center", this.monthDays);

            this.byPath("#dotButton").on(function() {
                $this.monthDays.setSelectedToShow();
            });

            this.byPath("#leftButton").on(function() {
                $this.monthDays.setPrevMonthToShow();
            });

            this.byPath("#rightButton").on(function() {
                $this.monthDays.setNextMonthToShow();
            });

            this.byPath("#topButton").on(function() {
                $this.monthDays.setNextYearToShow();
            });

            this.byPath("#bottomButton").on(function() {
                $this.monthDays.setPrevYearToShow();
            });

            this.monthDays.selectDate(date);
        },

        function $clazz() {
            this.LeftArrowButton   = Class(ui.ArrowButton, []);
            this.TopArrowButton    = Class(ui.ArrowButton, []);
            this.BottomArrowButton = Class(ui.ArrowButton, []);
            this.RightArrowButton  = Class(ui.ArrowButton, []);
            this.Link              = Class(ui.Link, []);

            this.DotButton = Class(ui.EvStatePan, ui.FireEventRepeatedly, []).events("fired");

            /**
             * Combo box component to render and host selectable list of months.
             * @constructor
             * @extends zebkit.ui.Combo
             * @class  zebkit.ui.date.Calendar.MonthsCombo
             */
            this.MonthsCombo = Class(ui.Combo, [
                function() {
                    this.$super(new this.clazz.CompList(true));
                    this.button.removeMe();
                },

                function $clazz() {
                    this.Label = Class(ui.Label, [
                        function(month) {
                            this.$super(month.name);
                            this.month = {
                                name     : month.name,
                                value    : month.value,
                                nickname : month.nickname
                            };
                        }
                    ]);

                    this.CompList = Class(ui.CompList, []);
                },

                function $prototype() {
                    /**
                     * Set the list of months.
                     * @param {Array} array of months. It is expected every item in the array is:
                     *
                     *      {
                     *          name     : "<name of month>",
                     *          nickname : "<short name of month>",
                     *          value    : an integer number of the month
                     *      }
                     *
                     * @method setMonths
                     * @chainable
                     */
                    this.setMonths = function(months) {
                        this.list.model.removeAll();
                        for(var i = 0; i < months.length; i++) {
                            this.list.model.add(new this.clazz.Label(months[i]));
                        }
                        return this;
                    };

                    /**
                     * Get selected month item.
                     * @return {object} a selected month as a the following structure:
                     *
                     *      {
                     *          name     : "<name of month>",
                     *          nickname : "<short name of month>",
                     *          value    : an integer number of the month
                     *      }
                     *
                     * @method getSelectedMonth
                     */
                    this.getSelectedMonth = function() {
                        var s = this.list.getSelected();
                        if (s === null) {
                            return null;
                        } else {
                            return {
                                name     : s.month.name,
                                value    : s.month.value,
                                nickname : s.month.nickname
                            };
                        }
                    };

                    this.padShown = function(b) {
                        if (b === true) {
                            this.list.makeSelectedVisible();
                            //this.list.position.setOffset(0);
                        }
                    };
                }
            ]);

            this.InfoPan = Class(ui.Panel, []);

            this.YearField = Class(ui.TextField, []);
        },

        /**
         * @for zebkit.ui.date.Calendar
         */
        function $prototype() {
            /**
             * Combo box component to show list of months.
             * @attribute comboMonth
             * @type {zebkit.ui.date.Calendar.Combo}
             * @readOnly
             */
            this.comboMonth = null;

            /**
             * Month days
             * @attribute monthDays
             * @type {zebkit.ui.date.MonthDaysGrid}
             * @readOnly
             */
            this.monthDays = null;

            /**
             * Year selection field.
             * @attribute yearField
             * @type {zebkit.ui.date.Calendar.YearField}
             * @readOnly
             */
            this.yearField = null;

            /**
             * Get a selected date.
             * @return {Date} a selected date
             * @method getValue
             */
            this.getValue = function() {
                return this.monthDays.getFirstSelected();
            };

            /**
             * Set the given date as selected.
             * @return {Date} date a date to be selected
             * @method setValue
             * @chainable
             */
            this.setValue = function() {
                this.monthDays.selectDate.apply(this.monthDays, arguments);
                return this;
            };

            /**
             * Set the date range. Date selection is possible only withing the range.
             * @param minDate a minimal possible date.
             * @param maxDate a maximal possible date.
             * @method setDateRange
             * @chainable
             */
            this.setDateRange = function() {
                this.monthDays.setDateRange.apply(this.monthDays, arguments);
                return this;
            };

            /**
             * Set minimal possible date
             * @param minDate a minimal possible date.
             * @method setMinDate
             * @chainable
             */
            this.setMinDate = function() {
                this.monthDays.setMinDate.apply(this.monthDays, arguments);
                return this;
            };

            /**
             * Set maximal possible date
             * @param mazDate a maximal possible date.
             * @method setMaxDate
             * @chainable
             */
            this.setMaxDate = function() {
                this.monthDays.setMaxDate.apply(this.monthDays, arguments);
                return this;
            };

            /**
             * Method that is called every time a new date has been selected
             * @param  {Date} date a date that has been selected
             * @param  {Boolean} b indicates if the date has been selected or de-selected
             * @method dateSelected
             * @protected
             */
            this.dateSelected = function(date, b) {
                this.byPath("#dotButton").setEnabled(false);
                this.fire("dateSelected", [this, date]);
            };
        }
    ]).events("dateSelected", "monthShown");


    /**
     * Text field component to keep formated with the specified pattern date. The pattern
     * can consists of from the placeholders where every placeholder is a formatted value
     * of a property of Date object:
     *
     *      ${N,K,<date_object_property>}
     *
     *     N - length of the field
     *     K - character to fulfill
     *
     *  For example "${2,0,date}/${2,0,month2}/${4,0,fullYear}" pattern represents "Day/Month/Year"
     *  date format ("22/12/1997").
     *
     * @param  {String} [format] format string
     * @constructor
     * @class zebkit.ui.date.DateTextField
     * @extends zebkit.ui.TextField
     */
    pkg.DateTextField = Class(ui.TextField, [
        function(format) {
            if (arguments.length === 0) {
                format = "${2,0,date}/${2,0,month2}/${4,0,fullYear}";
            }

            this.$super();
            this.setFormat(format);


            // this.on("updated", function(src) {

            //     var parser = /([0-9][0-9])\s*[\/\-]\s*([0-9][0-9])\s*[\/\-]\s*([0-9][0-9][0-9][0-9])/;
            //     var v = src.getValue();
            //     var m = v.match(parser);
            //     if (m !== null) {
            //         src.setColor("black");
            //     } else {
            //         var prev = src.date;
            //         src.date = null;
            //         if (src.date !== null) {
            //             src.fire("dateSelected", [src, prev, false]);
            //         }

            //         src.setColor("red");
            //     }
            // });

            // this.setEditable(true);
        },

        function $prototype() {
            this.notDefined = "-";

            this.$maxWidth = 0;

            /**
             * Date value
             * @attribute date
             * @type {Date}
             * @readOnly
             * @default null
             */
            this.date = null;

            this.$format = function(d) {
                return zebkit.util.format(this.format, d !== null ? d :{}, this.notDefined);
            };
        },

        /**
         * Set the pattern to be used to format date
         * @param {String} format a format pattern
         * @method setFormat
         * @chainable
         */
        function setFormat(format) {
            if (format === null || format === undefined) {
                throw new Error("Format is not defined " + this.clazz.$name);
            }

            if (this.format !== format) {
                this.format = format;
                this.$getSuper("setValue").call(this, this.$format(this.date));
            }

            return this;
        },

        function setValue(d) {
            if (d !== null) {
                validateDate(d);
            }

            if (compareDates(this.date, d) !== 0) {
                this.date = d;
                this.$super(this.$format(this.date));
            }
        },

        function calcPreferredSize(target) {
            var ps = this.$super(target);
            ps.width = this.$maxWidth;
            return ps;
        },

        function focused () {
            if (this.hasFocus()) {
                this.selectAll();
            } else {
                this.clearSelection();
            }
            this.$super();
        },

        function recalc() {
            this.$super();
            var s = this.$format(new Date());
            this.$maxWidth = this.getFont().stringWidth(s);
            this.$maxWidth += Math.floor(this.$maxWidth / 10);
        }
    ]).events("dateSelected");

    /**
     * Popup calendar interface. The interface should be mixed to components that
     * need to show popup calendar.
     * @class zebkit.ui.date.PopupCalendarMix
     * @interface zebkit.ui.date.PopupCalendarMix
     */
    pkg.PopupCalendarMix = zebkit.Interface([
        function childKeyPressed(e) {
            if (e.code === "Enter") {
                this.showCalendar(e.source);
            } else if (e.code === "Backspace") {
                //
            }
        },

        /**
         * Get calendar component.
         * @return {zebkit.ui.date.Calendar} a calendar
         * @method getCalendar
         */
        function getCalendar() {
            if (this.clazz.$name === undefined) {
                throw new Error();
            }

            if (this.calendar === undefined || this.calendar === null) {
                var $this = this;

                this.$freezeCalendar = false;

                this.calendar = new pkg.Calendar([
                    function $clazz() {
                        this.MonthsCombo.$name = "INNER";
                    },

                    function winActivated(e) {
                        if (e.isActive === false) {
                            $this.hideCalendar();
                        }
                    },

                    function childKeyPressed(e){
                        if (e.code === "Escape") {
                            $this.hideCalendar();
                        }
                    },

                    function dateSelected(date, b) {
                        this.$super(date, b);
                        if (date !== null && b) {
                            if ($this.$freezeCalendar === false) {
                                if ($this.dateSelected !== undefined) {
                                    $this.dateSelected.call($this, date, b);
                                }
                                $this.hideCalendar();
                            }
                        }
                    }
                ]);
            }
            return this.calendar;
        },

        /**
         * Show calendar as popup window for the given anchor component
         * @param  {zebkit.ui.Panel} anchor an anchor component.
         * @chainable
         * @method showCalendar
         */
        function showCalendar(anchor) {
            try {
                this.$freezeCalendar = true;
                this.hideCalendar();

                var calendar = this.getCalendar();
                this.$anchor = anchor;

                var c = this.getCanvas(),
                    w = c.getLayer("win"),
                    p = zebkit.layout.toParentOrigin(0, 0, anchor, c);

                calendar.toPreferredSize();

                p.y = p.y + anchor.height;
                if (p.y + calendar.height > w.height - w.getBottom()) {
                    p.y = p.y - calendar.height - anchor.height - 1;
                }

                if (p.x + calendar.width > w.width - w.getRight()) {
                    p.x -= (p.x + calendar.width - w.width + w.getRight());
                }

                calendar.setLocation(p.x, p.y);
                ui.showWindow(this, "mdi", calendar);

                ui.activateWindow(calendar);
                calendar.monthDays.requestFocus();

                if (this.calendarShown !== undefined) {
                    this.calendarShown(this.calendar);
                }
            } finally {
                this.$freezeCalendar = false;
            }

            return this;
        },

        /**
         * Hide calendar that has been shown as popup window.
         * @chainable
         * @method hideCalendar
         */
        function hideCalendar() {
            if (this.calendar !== undefined && this.calendar !== null) {
                var calendar = this.getCalendar();
                if (calendar.parent !== null) {
                    calendar.removeMe();
                    if (this.calendarHidden !== undefined) {
                        this.calendarHidden();
                    }
                    this.$anchor.requestFocus();
                    this.$anchor = null;
                }
            }
            return this;
        }
    ]);

    /**
     * Input field to specify a date.
     * @param  {String} [format] a date format
     * @constructor
     * @class zebkit.ui.date.DateInputField
     * @extends {zebkit.ui.Panel}
     * @uses zebkit.ui.date.PopupCalendarMix
     */

    /**
     * Fire when a date has been selected or de-selected.
     *
     *       dateInputField.on("dateSelected", function(src, date, b) {
     *           ...
     *       });
     *
     * @event dateSelected
     * @param  {zebkit.ui.date.DateInputField} src a source of the event
     * @param  {Date} date a new date that has been selected.
     * @param  {Boolean} b a selection state
     */
    pkg.DateInputField = Class(ui.Panel, pkg.PopupCalendarMix, [
        function (format) {
            this.$super(new zebkit.layout.FlowLayout());

            var $this = this;
            this.dateField = arguments.length === 0 ? new this.clazz.DateTextField()
                                                    : new this.clazz.DateTextField(format);
            this.add(this.dateField);
            this.add(new this.clazz.Button("...")).on(function(src) {
                $this.showCalendar($this.dateField);
            });

            // sync calendar and input field dates
            this.dateField.setValue(this.getValue());
        },

        function $clazz() {
            this.Button = Class(ui.Button, []);

            this.Calendar = Class(pkg.Calendar, [
                function $clazz() {
                    this.MonthsCombo = Class(pkg.Calendar.MonthsCombo, []);
                }
            ]);

            this.DateTextField = Class(pkg.DateTextField, []);
        },

        function $prototype() {
            this.dateSelected = function(date, b) {
                this.dateField.setValue(date);
                this.fire("dateSelected", [this, date, b]);
            };

            /**
             * Set the selected date.
             * @param {Date} d a date
             * @method setValue
             * @chainable
             */
            this.setValue = function(d) {
                this.getCalendar().selectValue(d);
                return this;
            };

            /**
             * Get the selected date.
             * @return {Date} a selected date.
             * @method getValue
             * @chainable
             */
            this.getValue = function() {
                return this.getCalendar().getValue();
            };
        }
    ]).events("dateSelected");

    /**
     * Input field to enter a date range.
     * @constructor
     * @class zebkit.ui.date.DateRangeInput
     * @uses zebkit.ui.date.PopupCalendarMix
     * @extends {zebkit.ui.Panel}
     */

    /**
     * Fire when a new date range has been selected.
     *
     *       dateRangeInput.on("dateRangeSelected", function(src, prevRange) {
     *           ...
     *       });
     *
     * @event dateRangeSelected
     * @param  {zebkit.ui.date.DateRangeInput} src a source of the event
     * @param  {Object} prevRange a previous range. The object has the
     * following structure:
     *
     *      {
     *         min : {Date},
     *         max : {Date}
     *      }
     *
     */
    pkg.DateRangeInput = Class(ui.Panel, pkg.PopupCalendarMix, [
        function() {
            this.$super();

            var $this = this,
                la    = new this.clazz.LeftArrowButton(),
                ra    = new this.clazz.RightArrowButton();

            this.minDateField = new this.clazz.MinDateTextField([
                function keyPressed(e) {
                    if (e.code === "ArrowRight" && this.position.offset === this.getMaxOffset()) {
                        $this.maxDateField.position.setOffset(0);
                        $this.maxDateField.requestFocus();
                    }
                    this.$super(e);
                }
            ]);

            this.maxDateField = new this.clazz.MaxDateTextField([
                function keyPressed(e) {
                    if (e.code === "ArrowLeft" && this.position.offset === 0) {
                        $this.minDateField.requestFocus();
                    }
                    this.$super(e);
                }
            ]);

            var cal = this.getCalendar();
            cal.monthDays.setTagger((function(item) {
                if (compareDates(item.day, item.month, item.year, $this.minDateField.date) === 0) {
                    item.tag("startDate");
                } else if (compareDates(item.day, item.month, item.year, $this.maxDateField.date) === 0) {
                    item.tag("endDate");
                }
            }));

            if (this.clazz.tags !== undefined &&  this.clazz.tags !== null) {
                cal.monthDays.addTagsDecoration(this.clazz.tags);
            }

            this.add(new this.clazz.DateInputPan(la, this.minDateField));
            this.add(new this.clazz.Line());
            this.add(new this.clazz.DateInputPan(this.maxDateField, ra));

            la.on(function () {
                $this.getCalendar().monthDays.setMaxDate($this.maxDateField.date);
                $this.showCalendar($this.minDateField);
            });

            ra.on(function () {
                $this.getCalendar().monthDays.setMinDate($this.minDateField.date);
                $this.showCalendar($this.maxDateField);
            });
        },

        function $clazz() {
            this.MinDateTextField  = Class(pkg.DateTextField, []);
            this.MaxDateTextField  = Class(pkg.DateTextField, []);
            this.LeftArrowButton   = Class(ui.ArrowButton, []);
            this.RightArrowButton  = Class(ui.ArrowButton, []);

            this.DateInputPan = Class(ui.Panel, [
                function() {
                    this.$super();
                    for(var i = 0; i < arguments.length; i++) {
                        this.add(arguments[i]);
                    }
                }
            ]);

            this.Line = Class(ui.Line, []);
        },

        function calendarShown(calendar) {
            if (this.$anchor === this.minDateField) {
                calendar.setValue(this.minDateField.date);
                calendar.monthDays.setDateRange(null, this.maxDateField.date);
            } else {
                calendar.setValue(this.maxDateField.date);
                calendar.monthDays.setDateRange(this.minDateField.date, null);
            }
        },

        function dateSelected(date, b) {
            if (b === true) {
                this.setValue(this.$anchor === this.minDateField ? date : this.minDateField.date,
                              this.$anchor === this.maxDateField ? date : this.maxDateField.date);
            }
        },

        /**
         * Set the given date range.
         * @param {Date} d1 a minimal possible date
         * @param {Date} d2 a maximal possible date
         * @method setValue
         */
        function setValue(d1, d2) {
            if (compareDates(d1, d2) === 1) {
                throw new RangeError();
            }

            if (compareDates(d1, this.minDateField.date) !== 0 ||
                compareDates(d2, this.maxDateField.date) !== 0   )
            {
                var prev = this.getValue();
                this.minDateField.setValue(d1);
                this.maxDateField.setValue(d2);

                this.getCalendar().monthDays.retagModel();

                this.fire("dateRangeSelected", [this, prev]);
                if (this.dateRangeSelected !== undefined) {
                    this.dateRangeSelected(prev);
                }
            }
        },

        /**
         * Get current date range.
         * @return {Object} a date range object:
         *
         *     {
         *         min : {Date},
         *         max : {Date}
         *      }
         *
         * @method getValue
         */
        function getValue() {
            return  {
                min : this.minDateField.date,
                max : this.maxDateField.date
            };
        }
    ]).events("dateRangeSelected");
}, true);