The source code

    /*!
     * Ext JS Library 3.3.1
     * Copyright(c) 2006-2010 Sencha Inc.
     * licensing@sencha.com
     * http://www.sencha.com/license
     */
    /**
     * @class Ext.calendar.CalendarView
     * @extends Ext.BoxComponent
     * This is an abstract class that serves as the base for other calendar views. This class is not
     * intended to be directly instantiated.
     * When extending this class to create a custom calendar view, you must provide an implementation
     * for the renderItems method, as there is no default implementation for rendering events
     * The rendering logic is totally dependent on how the UI structures its data, which
     * is determined by the underlying UI template (this base class does not have a template).
     * @constructor
     * @param {Object} config The config object
     */
    Ext.calendar.CalendarView = Ext.extend(Ext.BoxComponent, {
        /**
         * @cfg {Number} startDay
         * The 0-based index for the day on which the calendar week begins (0=Sunday, which is the default)
         */
        startDay: 0,
        /**
         * @cfg {Boolean} spansHavePriority
         * Allows switching between two different modes of rendering events that span multiple days. When true,
         * span events are always sorted first, possibly at the expense of start dates being out of order (e.g.,
         * a span event that starts at 11am one day and spans into the next day would display before a non-spanning
         * event that starts at 10am, even though they would not be in date order). This can lead to more compact
         * layouts when there are many overlapping events. If false (the default), events will always sort by start date
         * first which can result in a less compact, but chronologically consistent layout.
         */
        spansHavePriority: false,
        /**
         * @cfg {Boolean} trackMouseOver
         * Whether or not the view tracks and responds to the browser mouseover event on contained elements (defaults to
         * true). If you don't need mouseover event highlighting you can disable this.
         */
        trackMouseOver: true,
        /**
         * @cfg {Boolean} enableFx
         * Determines whether or not visual effects for CRUD actions are enabled (defaults to true). If this is false
         * it will override any values for {@link #enableAddFx}, {@link #enableUpdateFx} or {@link enableRemoveFx} and
         * all animations will be disabled.
         */
        enableFx: true,
        /**
         * @cfg {Boolean} enableAddFx
         * True to enable a visual effect on adding a new event (the default), false to disable it. Note that if
         * {@link #enableFx} is false it will override this value. The specific effect that runs is defined in the
         * {@link #doAddFx} method.
         */
        enableAddFx: true,
        /**
         * @cfg {Boolean} enableUpdateFx
         * True to enable a visual effect on updating an event, false to disable it (the default). Note that if
         * {@link #enableFx} is false it will override this value. The specific effect that runs is defined in the
         * {@link #doUpdateFx} method.
         */
        enableUpdateFx: false,
        /**
         * @cfg {Boolean} enableRemoveFx
         * True to enable a visual effect on removing an event (the default), false to disable it. Note that if
         * {@link #enableFx} is false it will override this value. The specific effect that runs is defined in the
         * {@link #doRemoveFx} method.
         */
        enableRemoveFx: true,
        /**
         * @cfg {Boolean} enableDD
         * True to enable drag and drop in the calendar view (the default), false to disable it
         */
        enableDD: true,
        /**
         * @cfg {Boolean} monitorResize
         * True to monitor the browser's resize event (the default), false to ignore it. If the calendar view is rendered
         * into a fixed-size container this can be set to false. However, if the view can change dimensions (e.g., it's in
         * fit layout in a viewport or some other resizable container) it is very important that this config is true so that
         * any resize event propagates properly to all subcomponents and layouts get recalculated properly.
         */
        monitorResize: true,
        /**
         * @cfg {String} ddCreateEventText
         * The text to display inside the drag proxy while dragging over the calendar to create a new event (defaults to
         * 'Create event for {0}' where {0} is a date range supplied by the view)
         */
        ddCreateEventText: 'Create event for {0}',
        /**
         * @cfg {String} ddMoveEventText
         * The text to display inside the drag proxy while dragging an event to reposition it (defaults to
         * 'Move event to {0}' where {0} is the updated event start date/time supplied by the view)
         */
        ddMoveEventText: 'Move event to {0}',
        /**
         * @cfg {String} ddResizeEventText
         * The string displayed to the user in the drag proxy while dragging the resize handle of an event (defaults to
         * 'Update event to {0}' where {0} is the updated event start-end range supplied by the view). Note that
         * this text is only used in views
         * that allow resizing of events.
         */
        ddResizeEventText: 'Update event to {0}',

        //private properties -- do not override:
        weekCount: 1,
        dayCount: 1,
        eventSelector: '.ext-cal-evt',
        eventOverClass: 'ext-evt-over',
        eventElIdDelimiter: '-evt-',
        dayElIdDelimiter: '-day-',

        /**
         * Returns a string of HTML template markup to be used as the body portion of the event template created
         * by {@link #getEventTemplate}. This provdes the flexibility to customize what's in the body without
         * having to override the entire XTemplate. This string can include any valid {@link Ext.Template} code, and
         * any data tokens accessible to the containing event template can be referenced in this string.
         * @return {String} The body template string
         */
        getEventBodyMarkup: Ext.emptyFn,
        // must be implemented by a subclass
        /**
         * Returns the XTemplate that is bound to the calendar's event store (it expects records of type
         * {@link Ext.calendar.EventRecord}) to populate the calendar views with events. Internally this method
         * by default generates different markup for browsers that support CSS border radius and those that don't.
         * This method can be overridden as needed to customize the markup generated.
         * Note that this method calls {@link #getEventBodyMarkup} to retrieve the body markup for events separately
         * from the surrounding container markup.  This provdes the flexibility to customize what's in the body without
         * having to override the entire XTemplate. If you do override this method, you should make sure that your
         * overridden version also does the same.
         * @return {Ext.XTemplate} The event XTemplate
         */
        getEventTemplate: Ext.emptyFn,
        // must be implemented by a subclass
        // private
        initComponent: function() {
            this.setStartDate(this.startDate || new Date());

            Ext.calendar.CalendarView.superclass.initComponent.call(this);

            this.addEvents({
                /**
                 * @event eventsrendered
                 * Fires after events are finished rendering in the view
                 * @param {Ext.calendar.CalendarView} this
                 */
                eventsrendered: true,
                /**
                 * @event eventclick
                 * Fires after the user clicks on an event element
                 * @param {Ext.calendar.CalendarView} this
                 * @param {Ext.calendar.EventRecord} rec The {@link Ext.calendar.EventRecord record} for the event that was clicked on
                 * @param {HTMLNode} el The DOM node that was clicked on
                 */
                eventclick: true,
                /**
                 * @event eventover
                 * Fires anytime the mouse is over an event element
                 * @param {Ext.calendar.CalendarView} this
                 * @param {Ext.calendar.EventRecord} rec The {@link Ext.calendar.EventRecord record} for the event that the cursor is over
                 * @param {HTMLNode} el The DOM node that is being moused over
                 */
                eventover: true,
                /**
                 * @event eventout
                 * Fires anytime the mouse exits an event element
                 * @param {Ext.calendar.CalendarView} this
                 * @param {Ext.calendar.EventRecord} rec The {@link Ext.calendar.EventRecord record} for the event that the cursor exited
                 * @param {HTMLNode} el The DOM node that was exited
                 */
                eventout: true,
                /**
                 * @event datechange
                 * Fires after the start date of the view changes
                 * @param {Ext.calendar.CalendarView} this
                 * @param {Date} startDate The start date of the view (as explained in {@link #getStartDate}
                 * @param {Date} viewStart The first displayed date in the view
                 * @param {Date} viewEnd The last displayed date in the view
                 */
                datechange: true,
                /**
                 * @event rangeselect
                 * Fires after the user drags on the calendar to select a range of dates/times in which to create an event
                 * @param {Ext.calendar.CalendarView} this
                 * @param {Object} dates An object containing the start (StartDate property) and end (EndDate property) dates selected
                 * @param {Function} callback A callback function that MUST be called after the event handling is complete so that
                 * the view is properly cleaned up (shim elements are persisted in the view while the user is prompted to handle the
                 * range selection). The callback is already created in the proper scope, so it simply needs to be executed as a standard
                 * function call (e.g., callback()).
                 */
                rangeselect: true,
                /**
                 * @event eventmove
                 * Fires after an event element is dragged by the user and dropped in a new position
                 * @param {Ext.calendar.CalendarView} this
                 * @param {Ext.calendar.EventRecord} rec The {@link Ext.calendar.EventRecord record} for the event that was moved with
                 * updated start and end dates
                 */
                eventmove: true,
                /**
                 * @event initdrag
                 * Fires when a drag operation is initiated in the view
                 * @param {Ext.calendar.CalendarView} this
                 */
                initdrag: true,
                /**
                 * @event dayover
                 * Fires while the mouse is over a day element
                 * @param {Ext.calendar.CalendarView} this
                 * @param {Date} dt The date that is being moused over
                 * @param {Ext.Element} el The day Element that is being moused over
                 */
                dayover: true,
                /**
                 * @event dayout
                 * Fires when the mouse exits a day element
                 * @param {Ext.calendar.CalendarView} this
                 * @param {Date} dt The date that is exited
                 * @param {Ext.Element} el The day Element that is exited
                 */
                dayout: true
                /*
                 * @event eventdelete
                 * Fires after an event element is deleted by the user. Not currently implemented directly at the view level -- currently
                 * deletes only happen from one of the forms.
                 * @param {Ext.calendar.CalendarView} this
                 * @param {Ext.calendar.EventRecord} rec The {@link Ext.calendar.EventRecord record} for the event that was deleted
                 */
                //eventdelete: true
            });
        },

        // private
        afterRender: function() {
            Ext.calendar.CalendarView.superclass.afterRender.call(this);

            this.renderTemplate();

            if (this.store) {
                this.setStore(this.store, true);
            }

            this.el.on({
                'mouseover': this.onMouseOver,
                'mouseout': this.onMouseOut,
                'click': this.onClick,
                'resize': this.onResize,
                scope: this
            });

            this.el.unselectable();

            if (this.enableDD && this.initDD) {
                this.initDD();
            }

            this.on('eventsrendered', this.forceSize);
            this.forceSize.defer(100, this);

        },

        // private
        forceSize: function() {
            if (this.el && this.el.child) {
                var hd = this.el.child('.ext-cal-hd-ct'),
                bd = this.el.child('.ext-cal-body-ct');

                if (bd == null || hd == null) return;

                var headerHeight = hd.getHeight(),
                sz = this.el.parent().getSize();

                bd.setHeight(sz.height - headerHeight);
            }
        },

        refresh: function() {
            this.prepareData();
            this.renderTemplate();
            this.renderItems();
        },

        getWeekCount: function() {
            var days = Ext.calendar.Date.diffDays(this.viewStart, this.viewEnd);
            return Math.ceil(days / this.dayCount);
        },

        // private
        prepareData: function() {
            var lastInMonth = this.startDate.getLastDateOfMonth(),
            w = 0,
            row = 0,
            dt = this.viewStart.clone(),
            weeks = this.weekCount < 1 ? 6: this.weekCount;

            this.eventGrid = [[]];
            this.allDayGrid = [[]];
            this.evtMaxCount = [];

            var evtsInView = this.store.queryBy(function(rec) {
                return this.isEventVisible(rec.data);
            },
            this);

            for (; w < weeks; w++) {
                this.evtMaxCount[w] = 0;
                if (this.weekCount == -1 && dt > lastInMonth) {
                    //current week is fully in next month so skip
                    break;
                }
                this.eventGrid[w] = this.eventGrid[w] || [];
                this.allDayGrid[w] = this.allDayGrid[w] || [];

                for (d = 0; d < this.dayCount; d++) {
                    if (evtsInView.getCount() > 0) {
                        var evts = evtsInView.filterBy(function(rec) {
                            var startsOnDate = (dt.getTime() == rec.data[Ext.calendar.EventMappings.StartDate.name].clearTime(true).getTime());
                            var spansFromPrevView = (w == 0 && d == 0 && (dt > rec.data[Ext.calendar.EventMappings.StartDate.name]));
                            return startsOnDate || spansFromPrevView;
                        },
                        this);

                        this.sortEventRecordsForDay(evts);
                        this.prepareEventGrid(evts, w, d);
                    }
                    dt = dt.add(Date.DAY, 1);
                }
            }
            this.currentWeekCount = w;
        },

        // private
        prepareEventGrid: function(evts, w, d) {
            var row = 0,
            dt = this.viewStart.clone(),
            max = this.maxEventsPerDay ? this.maxEventsPerDay: 999;

            evts.each(function(evt) {
                var M = Ext.calendar.EventMappings,
                days = Ext.calendar.Date.diffDays(
                Ext.calendar.Date.max(this.viewStart, evt.data[M.StartDate.name]),
                Ext.calendar.Date.min(this.viewEnd, evt.data[M.EndDate.name])) + 1;

                if (days > 1 || Ext.calendar.Date.diffDays(evt.data[M.StartDate.name], evt.data[M.EndDate.name]) > 1) {
                    this.prepareEventGridSpans(evt, this.eventGrid, w, d, days);
                    this.prepareEventGridSpans(evt, this.allDayGrid, w, d, days, true);
                } else {
                    row = this.findEmptyRowIndex(w, d);
                    this.eventGrid[w][d] = this.eventGrid[w][d] || [];
                    this.eventGrid[w][d][row] = evt;

                    if (evt.data[M.IsAllDay.name]) {
                        row = this.findEmptyRowIndex(w, d, true);
                        this.allDayGrid[w][d] = this.allDayGrid[w][d] || [];
                        this.allDayGrid[w][d][row] = evt;
                    }
                }

                if (this.evtMaxCount[w] < this.eventGrid[w][d].length) {
                    this.evtMaxCount[w] = Math.min(max + 1, this.eventGrid[w][d].length);
                }
                return true;
            },
            this);
        },

        // private
        prepareEventGridSpans: function(evt, grid, w, d, days, allday) {
            // this event spans multiple days/weeks, so we have to preprocess
            // the events and store special span events as placeholders so that
            // the render routine can build the necessary TD spans correctly.
            var w1 = w,
            d1 = d,
            row = this.findEmptyRowIndex(w, d, allday),
            dt = this.viewStart.clone();

            var start = {
                event: evt,
                isSpan: true,
                isSpanStart: true,
                spanLeft: false,
                spanRight: (d == 6)
            };
            grid[w][d] = grid[w][d] || [];
            grid[w][d][row] = start;

            while (--days) {
                dt = dt.add(Date.DAY, 1);
                if (dt > this.viewEnd) {
                    break;
                }
                if (++d1 > 6) {
                    // reset counters to the next week
                    d1 = 0;
                    w1++;
                    row = this.findEmptyRowIndex(w1, 0);
                }
                grid[w1] = grid[w1] || [];
                grid[w1][d1] = grid[w1][d1] || [];

                grid[w1][d1][row] = {
                    event: evt,
                    isSpan: true,
                    isSpanStart: (d1 == 0),
                    spanLeft: (w1 > w) && (d1 % 7 == 0),
                    spanRight: (d1 == 6) && (days > 1)
                };
            }
        },

        // private
        findEmptyRowIndex: function(w, d, allday) {
            var grid = allday ? this.allDayGrid: this.eventGrid,
            day = grid[w] ? grid[w][d] || [] : [],
            i = 0,
            ln = day.length;

            for (; i < ln; i++) {
                if (day[i] == null) {
                    return i;
                }
            }
            return ln;
        },

        // private
        renderTemplate: function() {
            if (this.tpl) {
                this.tpl.overwrite(this.el, this.getParams());
                this.lastRenderStart = this.viewStart.clone();
                this.lastRenderEnd = this.viewEnd.clone();
            }
        },

        disableStoreEvents: function() {
            this.monitorStoreEvents = false;
        },

        enableStoreEvents: function(refresh) {
            this.monitorStoreEvents = true;
            if (refresh === true) {
                this.refresh();
            }
        },

        // private
        onResize: function() {
            this.refresh();
        },

        // private
        onInitDrag: function() {
            this.fireEvent('initdrag', this);
        },

        // private
        onEventDrop: function(rec, dt) {
            if (Ext.calendar.Date.compare(rec.data[Ext.calendar.EventMappings.StartDate.name], dt) === 0) {
                // no changes
                return;
            }
            var diff = dt.getTime() - rec.data[Ext.calendar.EventMappings.StartDate.name].getTime();
            rec.set(Ext.calendar.EventMappings.StartDate.name, dt);
            rec.set(Ext.calendar.EventMappings.EndDate.name, rec.data[Ext.calendar.EventMappings.EndDate.name].add(Date.MILLI, diff));

            this.fireEvent('eventmove', this, rec);
        },

        // private
        onCalendarEndDrag: function(start, end, onComplete) {
            // set this flag for other event handlers that might conflict while we're waiting
            this.dragPending = true;

            // have to wait for the user to save or cancel before finalizing the dd interation
            var o = {};
            o[Ext.calendar.EventMappings.StartDate.name] = start;
            o[Ext.calendar.EventMappings.EndDate.name] = end;

            this.fireEvent('rangeselect', this, o, this.onCalendarEndDragComplete.createDelegate(this, [onComplete]));
        },

        // private
        onCalendarEndDragComplete: function(onComplete) {
            // callback for the drop zone to clean up
            onComplete();
            // clear flag for other events to resume normally
            this.dragPending = false;
        },

        // private
        onUpdate: function(ds, rec, operation) {
            if (this.monitorStoreEvents === false) {
                return;
            }
            if (operation == Ext.data.Record.COMMIT) {
                this.refresh();
                if (this.enableFx && this.enableUpdateFx) {
                    this.doUpdateFx(this.getEventEls(rec.data[Ext.calendar.EventMappings.EventId.name]), {
                        scope: this
                    });
                }
            }
        },

        doUpdateFx: function(els, o) {
            this.highlightEvent(els, null, o);
        },

        // private
        onAdd: function(ds, records, index) {
            if (this.monitorStoreEvents === false) {
                return;
            }
            var rec = records[0];
            this.tempEventId = rec.id;
            this.refresh();

            if (this.enableFx && this.enableAddFx) {
                this.doAddFx(this.getEventEls(rec.data[Ext.calendar.EventMappings.EventId.name]), {
                    scope: this
                });
            };
        },

        doAddFx: function(els, o) {
            els.fadeIn(Ext.apply(o, {
                duration: 2
            }));
        },

        // private
        onRemove: function(ds, rec) {
            if (this.monitorStoreEvents === false) {
                return;
            }
            if (this.enableFx && this.enableRemoveFx) {
                this.doRemoveFx(this.getEventEls(rec.data[Ext.calendar.EventMappings.EventId.name]), {
                    remove: true,
                    scope: this,
                    callback: this.refresh
                });
            }
            else {
                this.getEventEls(rec.data[Ext.calendar.EventMappings.EventId.name]).remove();
                this.refresh();
            }
        },

        doRemoveFx: function(els, o) {
            els.fadeOut(o);
        },

        /**
         * Visually highlights an event using {@link Ext.Fx#highlight} config options.
         * If {@link #highlightEventActions} is false this method will have no effect.
         * @param {Ext.CompositeElement} els The element(s) to highlight
         * @param {Object} color (optional) The highlight color. Should be a 6 char hex
         * color without the leading # (defaults to yellow: 'ffff9c')
         * @param {Object} o (optional) Object literal with any of the {@link Ext.Fx} config
         * options. See {@link Ext.Fx#highlight} for usage examples.
         */
        highlightEvent: function(els, color, o) {
            if (this.enableFx) {
                var c;
                ! (Ext.isIE || Ext.isOpera) ?
                els.highlight(color, o) :
                // Fun IE/Opera handling:
                els.each(function(el) {
                    el.highlight(color, Ext.applyIf({
                        attr: 'color'
                    },
                    o));
                    c = el.child('.ext-cal-evm');
                    if (c) {
                        c.highlight(color, o);
                    }
                },
                this);
            }
        },

        /**
         * Retrieve an Event object's id from its corresponding node in the DOM.
         * @param {String/Element/HTMLElement} el An {@link Ext.Element}, DOM node or id
         */
        getEventIdFromEl: function(el) {
            el = Ext.get(el);
            var id = el.id.split(this.eventElIdDelimiter)[1];
            if (id.indexOf('-') > -1) {
                //This id has the index of the week it is rendered in as the suffix.
                //This allows events that span across weeks to still have reproducibly-unique DOM ids.
                id = id.split('-')[0];
            }
            return id;
        },

        // private
        getEventId: function(eventId) {
            if (eventId === undefined && this.tempEventId) {
                eventId = this.tempEventId;
            }
            return eventId;
        },

        /**
         *
         * @param {String} eventId
         * @param {Boolean} forSelect
         * @return {String} The selector class
         */
        getEventSelectorCls: function(eventId, forSelect) {
            var prefix = forSelect ? '.': '';
            return prefix + this.id + this.eventElIdDelimiter + this.getEventId(eventId);
        },

        /**
         *
         * @param {String} eventId
         * @return {Ext.CompositeElement} The matching CompositeElement of nodes
         * that comprise the rendered event.  Any event that spans across a view
         * boundary will contain more than one internal Element.
         */
        getEventEls: function(eventId) {
            var els = Ext.select(this.getEventSelectorCls(this.getEventId(eventId), true), false, this.el.id);
            return new Ext.CompositeElement(els);
        },

        /**
         * Returns true if the view is currently displaying today's date, else false.
         * @return {Boolean} True or false
         */
        isToday: function() {
            var today = new Date().clearTime().getTime();
            return this.viewStart.getTime() <= today && this.viewEnd.getTime() >= today;
        },

        // private
        onDataChanged: function(store) {
            this.refresh();
        },

        // private
        isEventVisible: function(evt) {
            var start = this.viewStart.getTime(),
            end = this.viewEnd.getTime(),
            M = Ext.calendar.EventMappings,
            evStart = (evt.data ? evt.data[M.StartDate.name] : evt[M.StartDate.name]).getTime(),
            evEnd = (evt.data ? evt.data[M.EndDate.name] : evt[M.EndDate.name]).add(Date.SECOND, -1).getTime(),

            startsInRange = (evStart >= start && evStart <= end),
            endsInRange = (evEnd >= start && evEnd <= end),
            spansRange = (evStart < start && evEnd > end);

            return (startsInRange || endsInRange || spansRange);
        },

        // private
        isOverlapping: function(evt1, evt2) {
            var ev1 = evt1.data ? evt1.data: evt1,
            ev2 = evt2.data ? evt2.data: evt2,
            M = Ext.calendar.EventMappings,
            start1 = ev1[M.StartDate.name].getTime(),
            end1 = ev1[M.EndDate.name].add(Date.SECOND, -1).getTime(),
            start2 = ev2[M.StartDate.name].getTime(),
            end2 = ev2[M.EndDate.name].add(Date.SECOND, -1).getTime();

            if (end1 < start1) {
                end1 = start1;
            }
            if (end2 < start2) {
                end2 = start2;
            }

            var ev1startsInEv2 = (start1 >= start2 && start1 <= end2),
            ev1EndsInEv2 = (end1 >= start2 && end1 <= end2),
            ev1SpansEv2 = (start1 < start2 && end1 > end2);

            return (ev1startsInEv2 || ev1EndsInEv2 || ev1SpansEv2);
        },

        getDayEl: function(dt) {
            return Ext.get(this.getDayId(dt));
        },

        getDayId: function(dt) {
            if (Ext.isDate(dt)) {
                dt = dt.format('Ymd');
            }
            return this.id + this.dayElIdDelimiter + dt;
        },

        /**
         * Returns the start date of the view, as set by {@link #setStartDate}. Note that this may not
         * be the first date displayed in the rendered calendar -- to get the start and end dates displayed
         * to the user use {@link #getViewBounds}.
         * @return {Date} The start date
         */
        getStartDate: function() {
            return this.startDate;
        },

        /**
         * Sets the start date used to calculate the view boundaries to display. The displayed view will be the
         * earliest and latest dates that match the view requirements and contain the date passed to this function.
         * @param {Date} dt The date used to calculate the new view boundaries
         */
        setStartDate: function(start, refresh) {
            this.startDate = start.clearTime();
            this.setViewBounds(start);
            this.store.load({
                params: {
                    start: this.viewStart.format('m-d-Y'),
                    end: this.viewEnd.format('m-d-Y')
                }
            });
            if (refresh === true) {
                this.refresh();
            }
            this.fireEvent('datechange', this, this.startDate, this.viewStart, this.viewEnd);
        },

        // private
        setViewBounds: function(startDate) {
            var start = startDate || this.startDate,
            offset = start.getDay() - this.startDay;

            switch (this.weekCount) {
            case 0:
            case 1:
                this.viewStart = this.dayCount < 7 ? start: start.add(Date.DAY, -offset).clearTime(true);
                this.viewEnd = this.viewStart.add(Date.DAY, this.dayCount || 7).add(Date.SECOND, -1);
                return;

            case - 1:
                // auto by month
                start = start.getFirstDateOfMonth();
                offset = start.getDay() - this.startDay;
                if (offset < 0) {
                    offset += 7;
                }
                this.viewStart = start.add(Date.DAY, -offset).clearTime(true);

                // start from current month start, not view start:
                var end = start.add(Date.MONTH, 1).add(Date.SECOND, -1);
                // fill out to the end of the week:
                this.viewEnd = end.add(Date.DAY, 6 - end.getDay());
                return;

            default:
                this.viewStart = start.add(Date.DAY, -offset).clearTime(true);
                this.viewEnd = this.viewStart.add(Date.DAY, this.weekCount * 7).add(Date.SECOND, -1);
            }
        },

        // private
        getViewBounds: function() {
            return {
                start: this.viewStart,
                end: this.viewEnd
            };
        },

        /* private
         * Sort events for a single day for display in the calendar.  This sorts allday
         * events first, then non-allday events are sorted either based on event start
         * priority or span priority based on the value of {@link #spansHavePriority}
         * (defaults to event start priority).
         * @param {MixedCollection} evts A {@link Ext.util.MixedCollection MixedCollection}
         * of {@link #Ext.calendar.EventRecord EventRecord} objects
         */
        sortEventRecordsForDay: function(evts) {
            if (evts.length < 2) {
                return;
            }
            evts.sort('ASC',
            function(evtA, evtB) {
                var a = evtA.data,
                b = evtB.data,
                M = Ext.calendar.EventMappings;

                // Always sort all day events before anything else
                if (a[M.IsAllDay.name]) {
                    return - 1;
                }
                else if (b[M.IsAllDay.name]) {
                    return 1;
                }
                if (this.spansHavePriority) {
                    // This logic always weights span events higher than non-span events
                    // (at the possible expense of start time order). This seems to
                    // be the approach used by Google calendar and can lead to a more
                    // visually appealing layout in complex cases, but event order is
                    // not guaranteed to be consistent.
                    var diff = Ext.calendar.Date.diffDays;
                    if (diff(a[M.StartDate.name], a[M.EndDate.name]) > 0) {
                        if (diff(b[M.StartDate.name], b[M.EndDate.name]) > 0) {
                            // Both events are multi-day
                            if (a[M.StartDate.name].getTime() == b[M.StartDate.name].getTime()) {
                                // If both events start at the same time, sort the one
                                // that ends later (potentially longer span bar) first
                                return b[M.EndDate.name].getTime() - a[M.EndDate.name].getTime();
                            }
                            return a[M.StartDate.name].getTime() - b[M.StartDate.name].getTime();
                        }
                        return - 1;
                    }
                    else if (diff(b[M.StartDate.name], b[M.EndDate.name]) > 0) {
                        return 1;
                    }
                    return a[M.StartDate.name].getTime() - b[M.StartDate.name].getTime();
                }
                else {
                    // Doing this allows span and non-span events to intermingle but
                    // remain sorted sequentially by start time. This seems more proper
                    // but can make for a less visually-compact layout when there are
                    // many such events mixed together closely on the calendar.
                    return a[M.StartDate.name].getTime() - b[M.StartDate.name].getTime();
                }
            }.createDelegate(this));
        },

        /**
         * Updates the view to contain the passed date
         * @param {Date} dt The date to display
         */
        moveTo: function(dt, noRefresh) {
            if (Ext.isDate(dt)) {
                this.setStartDate(dt);
                if (noRefresh !== false) {
                    this.refresh();
                }
                return this.startDate;
            }
            return dt;
        },

        /**
         * Updates the view to the next consecutive date(s)
         */
        moveNext: function(noRefresh) {
            return this.moveTo(this.viewEnd.add(Date.DAY, 1));
        },

        /**
         * Updates the view to the previous consecutive date(s)
         */
        movePrev: function(noRefresh) {
            var days = Ext.calendar.Date.diffDays(this.viewStart, this.viewEnd) + 1;
            return this.moveDays( - days, noRefresh);
        },

        /**
         * Shifts the view by the passed number of months relative to the currently set date
         * @param {Number} value The number of months (positive or negative) by which to shift the view
         */
        moveMonths: function(value, noRefresh) {
            return this.moveTo(this.startDate.add(Date.MONTH, value), noRefresh);
        },

        /**
         * Shifts the view by the passed number of weeks relative to the currently set date
         * @param {Number} value The number of weeks (positive or negative) by which to shift the view
         */
        moveWeeks: function(value, noRefresh) {
            return this.moveTo(this.startDate.add(Date.DAY, value * 7), noRefresh);
        },

        /**
         * Shifts the view by the passed number of days relative to the currently set date
         * @param {Number} value The number of days (positive or negative) by which to shift the view
         */
        moveDays: function(value, noRefresh) {
            return this.moveTo(this.startDate.add(Date.DAY, value), noRefresh);
        },

        /**
         * Updates the view to show today
         */
        moveToday: function(noRefresh) {
            return this.moveTo(new Date(), noRefresh);
        },

        /**
         * Sets the event store used by the calendar to display {@link Ext.calendar.EventRecord events}.
         * @param {Ext.data.Store} store
         */
        setStore: function(store, initial) {
            if (!initial && this.store) {
                this.store.un("datachanged", this.onDataChanged, this);
                this.store.un("add", this.onAdd, this);
                this.store.un("remove", this.onRemove, this);
                this.store.un("update", this.onUpdate, this);
                this.store.un("clear", this.refresh, this);
            }
            if (store) {
                store.on("datachanged", this.onDataChanged, this);
                store.on("add", this.onAdd, this);
                store.on("remove", this.onRemove, this);
                store.on("update", this.onUpdate, this);
                store.on("clear", this.refresh, this);
            }
            this.store = store;
            if (store && store.getCount() > 0) {
                this.refresh();
            }
        },

        getEventRecord: function(id) {
            var idx = this.store.find(Ext.calendar.EventMappings.EventId.name, id);
            return this.store.getAt(idx);
        },

        getEventRecordFromEl: function(el) {
            return this.getEventRecord(this.getEventIdFromEl(el));
        },

        // private
        getParams: function() {
            return {
                viewStart: this.viewStart,
                viewEnd: this.viewEnd,
                startDate: this.startDate,
                dayCount: this.dayCount,
                weekCount: this.weekCount,
                title: this.getTitle()
            };
        },

        getTitle: function() {
            return this.startDate.format('F Y');
        },

        /*
         * Shared click handling.  Each specific view also provides view-specific
         * click handling that calls this first.  This method returns true if it
         * can handle the click (and so the subclass should ignore it) else false.
         */
        onClick: function(e, t) {
            var el = e.getTarget(this.eventSelector, 5);
            if (el) {
                var id = this.getEventIdFromEl(el);
                this.fireEvent('eventclick', this, this.getEventRecord(id), el);
                return true;
            }
        },

        // private
        onMouseOver: function(e, t) {
            if (this.trackMouseOver !== false && (this.dragZone == undefined || !this.dragZone.dragging)) {
                if (!this.handleEventMouseEvent(e, t, 'over')) {
                    this.handleDayMouseEvent(e, t, 'over');
                }
            }
        },

        // private
        onMouseOut: function(e, t) {
            if (this.trackMouseOver !== false && (this.dragZone == undefined || !this.dragZone.dragging)) {
                if (!this.handleEventMouseEvent(e, t, 'out')) {
                    this.handleDayMouseEvent(e, t, 'out');
                }
            }
        },

        // private
        handleEventMouseEvent: function(e, t, type) {
            var el = e.getTarget(this.eventSelector, 5, true),
                rel,
                els,
                evtId;
            if (el) {
                rel = Ext.get(e.getRelatedTarget());
                if (el == rel || el.contains(rel)) {
                    return true;
                }

                evtId = this.getEventIdFromEl(el);

                if (this.eventOverClass != '') {
                    els = this.getEventEls(evtId);
                    els[type == 'over' ? 'addClass': 'removeClass'](this.eventOverClass);
                }
                this.fireEvent('event' + type, this, this.getEventRecord(evtId), el);
                return true;
            }
            return false;
        },

        // private
        getDateFromId: function(id, delim) {
            var parts = id.split(delim);
            return parts[parts.length - 1];
        },

        // private
        handleDayMouseEvent: function(e, t, type) {
            t = e.getTarget('td', 3);
            if (t) {
                if (t.id && t.id.indexOf(this.dayElIdDelimiter) > -1) {
                    var dt = this.getDateFromId(t.id, this.dayElIdDelimiter),
                    rel = Ext.get(e.getRelatedTarget()),
                    relTD,
                    relDate;

                    if (rel) {
                        relTD = rel.is('td') ? rel: rel.up('td', 3);
                        relDate = relTD && relTD.id ? this.getDateFromId(relTD.id, this.dayElIdDelimiter) : '';
                    }
                    if (!rel || dt != relDate) {
                        var el = this.getDayEl(dt);
                        if (el && this.dayOverClass != '') {
                            el[type == 'over' ? 'addClass': 'removeClass'](this.dayOverClass);
                        }
                        this.fireEvent('day' + type, this, Date.parseDate(dt, "Ymd"), el);
                    }
                }
            }
        },

        // private
        renderItems: function() {
            throw 'This method must be implemented by a subclass';
        }
    });