X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/b37ceabb82336ee82757cd32efe353cfab8ec267..f5240829880f87e0cf581c6a296e436fdef0ef80:/examples/calendar/calendar-all-debug.js?ds=inline diff --git a/examples/calendar/calendar-all-debug.js b/examples/calendar/calendar-all-debug.js new file mode 100644 index 00000000..0b218ef8 --- /dev/null +++ b/examples/calendar/calendar-all-debug.js @@ -0,0 +1,5243 @@ +/*! + * Ext JS Library 3.3.0 + * Copyright(c) 2006-2010 Ext JS, Inc. + * licensing@extjs.com + * http://www.extjs.com/license + */ +Ext.ns('Ext.calendar'); + + (function() { + Ext.apply(Ext.calendar, { + Date: { + diffDays: function(start, end) { + day = 1000 * 60 * 60 * 24; + diff = end.clearTime(true).getTime() - start.clearTime(true).getTime(); + return Math.ceil(diff / day); + }, + + copyTime: function(fromDt, toDt) { + var dt = toDt.clone(); + dt.setHours( + fromDt.getHours(), + fromDt.getMinutes(), + fromDt.getSeconds(), + fromDt.getMilliseconds()); + + return dt; + }, + + compare: function(dt1, dt2, precise) { + if (precise !== true) { + dt1 = dt1.clone(); + dt1.setMilliseconds(0); + dt2 = dt2.clone(); + dt2.setMilliseconds(0); + } + return dt2.getTime() - dt1.getTime(); + }, + + // private helper fn + maxOrMin: function(max) { + var dt = (max ? 0: Number.MAX_VALUE), + i = 0, + args = arguments[1], + ln = args.length; + for (; i < ln; i++) { + dt = Math[max ? 'max': 'min'](dt, args[i].getTime()); + } + return new Date(dt); + }, + + max: function() { + return this.maxOrMin.apply(this, [true, arguments]); + }, + + min: function() { + return this.maxOrMin.apply(this, [false, arguments]); + } + } + }); +})();/** + * @class Ext.calendar.DayHeaderTemplate + * @extends Ext.XTemplate + *

This is the template used to render the all-day event container used in {@link Ext.calendar.DayView DayView} and + * {@link Ext.calendar.WeekView WeekView}. Internally the majority of the layout logic is deferred to an instance of + * {@link Ext.calendar.BoxLayoutTemplate}.

+ *

This template is automatically bound to the underlying event store by the + * calendar components and expects records of type {@link Ext.calendar.EventRecord}.

+ *

Note that this template would not normally be used directly. Instead you would use the {@link Ext.calendar.DayViewTemplate} + * that internally creates an instance of this template along with a {@link Ext.calendar.DayBodyTemplate}.

+ * @constructor + * @param {Object} config The config object + */ +Ext.calendar.DayHeaderTemplate = function(config){ + + Ext.apply(this, config); + + this.allDayTpl = new Ext.calendar.BoxLayoutTemplate(config); + this.allDayTpl.compile(); + + Ext.calendar.DayHeaderTemplate.superclass.constructor.call(this, + '
', + '', + '', + '', + '', + '', + '', + '', + '', + '
{allDayTpl}
', + '
' + ); +}; + +Ext.extend(Ext.calendar.DayHeaderTemplate, Ext.XTemplate, { + applyTemplate : function(o){ + return Ext.calendar.DayHeaderTemplate.superclass.applyTemplate.call(this, { + allDayTpl: this.allDayTpl.apply(o) + }); + } +}); + +Ext.calendar.DayHeaderTemplate.prototype.apply = Ext.calendar.DayHeaderTemplate.prototype.applyTemplate; +/** + * @class Ext.calendar.DayBodyTemplate + * @extends Ext.XTemplate + *

This is the template used to render the scrolling body container used in {@link Ext.calendar.DayView DayView} and + * {@link Ext.calendar.WeekView WeekView}. This template is automatically bound to the underlying event store by the + * calendar components and expects records of type {@link Ext.calendar.EventRecord}.

+ *

Note that this template would not normally be used directly. Instead you would use the {@link Ext.calendar.DayViewTemplate} + * that internally creates an instance of this template along with a {@link Ext.calendar.DayHeaderTemplate}.

+ * @constructor + * @param {Object} config The config object + */ +Ext.calendar.DayBodyTemplate = function(config){ + + Ext.apply(this, config); + + Ext.calendar.DayBodyTemplate.superclass.constructor.call(this, + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '
', + '
', + '
', + '', + '
', + '
', + '
', + '
', + '
', + '
', + '
', + '', + '
', + '
{.}
', + '
', + '
', + '
', + '
', + '
', + '
', + '
' + ); +}; + +Ext.extend(Ext.calendar.DayBodyTemplate, Ext.XTemplate, { + // private + applyTemplate : function(o){ + this.today = new Date().clearTime(); + this.dayCount = this.dayCount || 1; + + var i = 0, days = [], + dt = o.viewStart.clone(), + times; + + for(; iThis is the template used to render the all-day event container used in {@link Ext.calendar.DayView DayView} and + * {@link Ext.calendar.WeekView WeekView}. Internally this class simply defers to instances of {@link Ext.calerndar.DayHeaderTemplate} + * and {@link Ext.calerndar.DayBodyTemplate} to perform the actual rendering logic, but it also provides the overall calendar view + * container that contains them both. As such this is the template that should be used when rendering day or week views.

+ *

This template is automatically bound to the underlying event store by the + * calendar components and expects records of type {@link Ext.calendar.EventRecord}.

+ * @constructor + * @param {Object} config The config object + */ +Ext.calendar.DayViewTemplate = function(config){ + + Ext.apply(this, config); + + this.headerTpl = new Ext.calendar.DayHeaderTemplate(config); + this.headerTpl.compile(); + + this.bodyTpl = new Ext.calendar.DayBodyTemplate(config); + this.bodyTpl.compile(); + + Ext.calendar.DayViewTemplate.superclass.constructor.call(this, + '
', + '{headerTpl}', + '{bodyTpl}', + '
' + ); +}; + +Ext.extend(Ext.calendar.DayViewTemplate, Ext.XTemplate, { + // private + applyTemplate : function(o){ + return Ext.calendar.DayViewTemplate.superclass.applyTemplate.call(this, { + headerTpl: this.headerTpl.apply(o), + bodyTpl: this.bodyTpl.apply(o) + }); + } +}); + +Ext.calendar.DayViewTemplate.prototype.apply = Ext.calendar.DayViewTemplate.prototype.applyTemplate; +/** + * @class Ext.calendar.BoxLayoutTemplate + * @extends Ext.XTemplate + *

This is the template used to render calendar views based on small day boxes within a non-scrolling container (currently + * the {@link Ext.calendar.MonthView MonthView} and the all-day headers for {@link Ext.calendar.DayView DayView} and + * {@link Ext.calendar.WeekView WeekView}. This template is automatically bound to the underlying event store by the + * calendar components and expects records of type {@link Ext.calendar.EventRecord}.

+ * @constructor + * @param {Object} config The config object + */ +Ext.calendar.BoxLayoutTemplate = function(config){ + + Ext.apply(this, config); + + var weekLinkTpl = this.showWeekLinks ? '' : ''; + + Ext.calendar.BoxLayoutTemplate.superclass.constructor.call(this, + '', + '
', + weekLinkTpl, + '', + '', + '', + '', + '', + '', + '', + '', + '
 
', + '', + '', + '', + '', + '', + '', + '', + '', + '
{title}
', + '
', + '
', { + getRowTop: function(i, ln){ + return ((i-1)*(100/ln)); + }, + getRowHeight: function(ln){ + return 100/ln; + } + } + ); +}; + +Ext.extend(Ext.calendar.BoxLayoutTemplate, Ext.XTemplate, { + // private + applyTemplate : function(o){ + + Ext.apply(this, o); + + var w = 0, title = '', first = true, isToday = false, showMonth = false, prevMonth = false, nextMonth = false, + weeks = [[]], + today = new Date().clearTime(), + dt = this.viewStart.clone(), + thisMonth = this.startDate.getMonth(); + + for(; w < this.weekCount || this.weekCount == -1; w++){ + if(dt > this.viewEnd){ + break; + } + weeks[w] = []; + + for(var d = 0; d < this.dayCount; d++){ + isToday = dt.getTime() === today.getTime(); + showMonth = first || (dt.getDate() == 1); + prevMonth = (dt.getMonth() < thisMonth) && this.weekCount == -1; + nextMonth = (dt.getMonth() > thisMonth) && this.weekCount == -1; + + if(dt.getDay() == 1){ + // The ISO week format 'W' is relative to a Monday week start. If we + // make this check on Sunday the week number will be off. + weeks[w].weekNum = this.showWeekNumbers ? dt.format('W') : ' '; + weeks[w].weekLinkId = 'ext-cal-week-'+dt.format('Ymd'); + } + + if(showMonth){ + if(isToday){ + title = this.getTodayText(); + } + else{ + title = dt.format(this.dayCount == 1 ? 'l, F j, Y' : (first ? 'M j, Y' : 'M j')); + } + } + else{ + var dayFmt = (w == 0 && this.showHeader !== true) ? 'D j' : 'j'; + title = isToday ? this.getTodayText() : dt.format(dayFmt); + } + + weeks[w].push({ + title: title, + date: dt.clone(), + titleCls: 'ext-cal-dtitle ' + (isToday ? ' ext-cal-dtitle-today' : '') + + (w==0 ? ' ext-cal-dtitle-first' : '') + + (prevMonth ? ' ext-cal-dtitle-prev' : '') + + (nextMonth ? ' ext-cal-dtitle-next' : ''), + cellCls: 'ext-cal-day ' + (isToday ? ' ext-cal-day-today' : '') + + (d==0 ? ' ext-cal-day-first' : '') + + (prevMonth ? ' ext-cal-day-prev' : '') + + (nextMonth ? ' ext-cal-day-next' : '') + }); + dt = dt.add(Date.DAY, 1); + first = false; + } + } + + return Ext.calendar.BoxLayoutTemplate.superclass.applyTemplate.call(this, { + weeks: weeks + }); + }, + + // private + getTodayText : function(){ + var dt = new Date().format('l, F j, Y'), + todayText = this.showTodayText !== false ? this.todayText : '', + timeText = this.showTime !== false ? ' ' + + new Date().format('g:i a') + '' : '', + separator = todayText.length > 0 || timeText.length > 0 ? ' — ' : ''; + + if(this.dayCount == 1){ + return dt + separator + todayText + timeText; + } + fmt = this.weekCount == 1 ? 'D j' : 'j'; + return todayText.length > 0 ? todayText + timeText : new Date().format(fmt) + timeText; + } +}); + +Ext.calendar.BoxLayoutTemplate.prototype.apply = Ext.calendar.BoxLayoutTemplate.prototype.applyTemplate; +/** + * @class Ext.calendar.MonthViewTemplate + * @extends Ext.XTemplate + *

This is the template used to render the {@link Ext.calendar.MonthView MonthView}. Internally this class defers to an + * instance of {@link Ext.calerndar.BoxLayoutTemplate} to handle the inner layout rendering and adds containing elements around + * that to form the month view.

+ *

This template is automatically bound to the underlying event store by the + * calendar components and expects records of type {@link Ext.calendar.EventRecord}.

+ * @constructor + * @param {Object} config The config object + */ +Ext.calendar.MonthViewTemplate = function(config){ + + Ext.apply(this, config); + + this.weekTpl = new Ext.calendar.BoxLayoutTemplate(config); + this.weekTpl.compile(); + + var weekLinkTpl = this.showWeekLinks ? '' : ''; + + Ext.calendar.MonthViewTemplate.superclass.constructor.call(this, + '
', + '
', + weekLinkTpl, + '', + '', + '', + '', + '', + '', + '', + '', + '
{.:date("D")}
', + '
', + '
{weeks}
', + '
' + ); +}; + +Ext.extend(Ext.calendar.MonthViewTemplate, Ext.XTemplate, { + // private + applyTemplate : function(o){ + var days = [], + weeks = this.weekTpl.apply(o), + dt = o.viewStart; + + for(var i = 0; i < 7; i++){ + days.push(dt.add(Date.DAY, i)); + } + + var extraClasses = this.showHeader === true ? '' : 'ext-cal-noheader'; + if(this.showWeekLinks){ + extraClasses += ' ext-cal-week-links'; + } + + return Ext.calendar.MonthViewTemplate.superclass.applyTemplate.call(this, { + days: days, + weeks: weeks, + extraClasses: extraClasses + }); + } +}); + +Ext.calendar.MonthViewTemplate.prototype.apply = Ext.calendar.MonthViewTemplate.prototype.applyTemplate; +/** + * @class Ext.dd.ScrollManager + *

Provides automatic scrolling of overflow regions in the page during drag operations.

+ *

The ScrollManager configs will be used as the defaults for any scroll container registered with it, + * but you can also override most of the configs per scroll container by adding a + * ddScrollConfig object to the target element that contains these properties: {@link #hthresh}, + * {@link #vthresh}, {@link #increment} and {@link #frequency}. Example usage: + *


+var el = Ext.get('scroll-ct');
+el.ddScrollConfig = {
+    vthresh: 50,
+    hthresh: -1,
+    frequency: 100,
+    increment: 200
+};
+Ext.dd.ScrollManager.register(el);
+
+ * Note: This class uses "Point Mode" and is untested in "Intersect Mode". + * @singleton + */ +Ext.dd.ScrollManager = function() { + var ddm = Ext.dd.DragDropMgr, + els = {}, + dragEl = null, + proc = {}, + onStop = function(e) { + dragEl = null; + clearProc(); + }, + triggerRefresh = function() { + if (ddm.dragCurrent) { + ddm.refreshCache(ddm.dragCurrent.groups); + } + }, + doScroll = function() { + if (ddm.dragCurrent) { + var dds = Ext.dd.ScrollManager, + inc = proc.el.ddScrollConfig ? proc.el.ddScrollConfig.increment: dds.increment; + if (!dds.animate) { + if (proc.el.scroll(proc.dir, inc)) { + triggerRefresh(); + } + } else { + proc.el.scroll(proc.dir, inc, true, dds.animDuration, triggerRefresh); + } + } + }, + clearProc = function() { + if (proc.id) { + clearInterval(proc.id); + } + proc.id = 0; + proc.el = null; + proc.dir = ""; + }, + startProc = function(el, dir) { + clearProc(); + proc.el = el; + proc.dir = dir; + var freq = (el.ddScrollConfig && el.ddScrollConfig.frequency) ? + el.ddScrollConfig.frequency: Ext.dd.ScrollManager.frequency, + group = el.ddScrollConfig ? el.ddScrollConfig.ddGroup: undefined; + + if (group === undefined || ddm.dragCurrent.ddGroup == group) { + proc.id = setInterval(doScroll, freq); + } + }, + onFire = function(e, isDrop) { + if (isDrop || !ddm.dragCurrent) { + return; + } + var dds = Ext.dd.ScrollManager; + if (!dragEl || dragEl != ddm.dragCurrent) { + dragEl = ddm.dragCurrent; + // refresh regions on drag start + dds.refreshCache(); + } + + var xy = Ext.lib.Event.getXY(e), + pt = new Ext.lib.Point(xy[0], xy[1]), + id, + el, + r, + c; + for (id in els) { + if (els.hasOwnProperty(id)) { + el = els[id]; + r = el._region; + c = el.ddScrollConfig ? el.ddScrollConfig: dds; + if (r && r.contains(pt) && el.isScrollable()) { + if (r.bottom - pt.y <= c.vthresh) { + if (proc.el != el) { + startProc(el, "down"); + } + return; + } else if (r.right - pt.x <= c.hthresh) { + if (proc.el != el) { + startProc(el, "left"); + } + return; + } else if (pt.y - r.top <= c.vthresh) { + if (proc.el != el) { + startProc(el, "up"); + } + return; + } else if (pt.x - r.left <= c.hthresh) { + if (proc.el != el) { + startProc(el, "right"); + } + return; + } + } + } + } + clearProc(); + }; + + ddm.fireEvents = ddm.fireEvents.createSequence(onFire, ddm); + ddm.stopDrag = ddm.stopDrag.createSequence(onStop, ddm); + + return { + /** + * Registers new overflow element(s) to auto scroll + * @param {Mixed/Array} el The id of or the element to be scrolled or an array of either + */ + register: function(el) { + if (Ext.isArray(el)) { + var i = 0, + len = el.length; + for (; i < len; i++) { + this.register(el[i]); + } + } else { + el = Ext.get(el); + els[el.id] = el; + } + }, + + /** + * Unregisters overflow element(s) so they are no longer scrolled + * @param {Mixed/Array} el The id of or the element to be removed or an array of either + */ + unregister: function(el) { + if (Ext.isArray(el)) { + var i = 0, + len = el.length; + for (; i < len; i++) { + this.unregister(el[i]); + } + } else { + el = Ext.get(el); + delete els[el.id]; + } + }, + + /** + * The number of pixels from the top or bottom edge of a container the pointer needs to be to + * trigger scrolling (defaults to 25) + * @type Number + */ + vthresh: 25, + /** + * The number of pixels from the right or left edge of a container the pointer needs to be to + * trigger scrolling (defaults to 25) + * @type Number + */ + hthresh: 25, + + /** + * The number of pixels to scroll in each scroll increment (defaults to 50) + * @type Number + */ + increment: 100, + + /** + * The frequency of scrolls in milliseconds (defaults to 500) + * @type Number + */ + frequency: 500, + + /** + * True to animate the scroll (defaults to true) + * @type Boolean + */ + animate: true, + + /** + * The animation duration in seconds - + * MUST BE less than Ext.dd.ScrollManager.frequency! (defaults to .4) + * @type Number + */ + animDuration: 0.4, + + /** + * Manually trigger a cache refresh. + */ + refreshCache: function() { + var id; + for (id in els) { + if (els.hasOwnProperty(id)) { + if (typeof els[id] == 'object') { + // for people extending the object prototype + els[id]._region = els[id].getRegion(); + } + } + } + } + }; +}();/* + * @class Ext.calendar.StatusProxy + * A specialized drag proxy that supports a drop status icon, {@link Ext.Layer} styles and auto-repair. It also + * contains a calendar-specific drag status message containing details about the dragged event's target drop date range. + * This is the default drag proxy used by all calendar views. + * @constructor + * @param {Object} config + */ +Ext.calendar.StatusProxy = function(config) { + Ext.apply(this, config); + this.id = this.id || Ext.id(); + this.el = new Ext.Layer({ + dh: { + id: this.id, + cls: 'ext-dd-drag-proxy x-dd-drag-proxy ' + this.dropNotAllowed, + cn: [ + { + cls: 'x-dd-drop-icon' + }, + { + cls: 'ext-dd-ghost-ct', + cn: [ + { + cls: 'x-dd-drag-ghost' + }, + { + cls: 'ext-dd-msg' + } + ] + } + ] + }, + shadow: !config || config.shadow !== false + }); + this.ghost = Ext.get(this.el.dom.childNodes[1].childNodes[0]); + this.message = Ext.get(this.el.dom.childNodes[1].childNodes[1]); + this.dropStatus = this.dropNotAllowed; +}; + +Ext.extend(Ext.calendar.StatusProxy, Ext.dd.StatusProxy, { + /** + * @cfg {String} moveEventCls + * The CSS class to apply to the status element when an event is being dragged (defaults to 'ext-cal-dd-move'). + */ + moveEventCls: 'ext-cal-dd-move', + /** + * @cfg {String} addEventCls + * The CSS class to apply to the status element when drop is not allowed (defaults to 'ext-cal-dd-add'). + */ + addEventCls: 'ext-cal-dd-add', + + // inherit docs + update: function(html) { + if (typeof html == 'string') { + this.ghost.update(html); + } else { + this.ghost.update(''); + html.style.margin = '0'; + this.ghost.dom.appendChild(html); + } + var el = this.ghost.dom.firstChild; + if (el) { + Ext.fly(el).setStyle('float', 'none').setHeight('auto'); + Ext.getDom(el).id += '-ddproxy'; + } + }, + + /** + * Update the calendar-specific drag status message without altering the ghost element. + * @param {String} msg The new status message + */ + updateMsg: function(msg) { + this.message.update(msg); + } +});/* + * Internal drag zone implementation for the calendar components. This provides base functionality + * and is primarily for the month view -- DayViewDD adds day/week view-specific functionality. + */ +Ext.calendar.DragZone = Ext.extend(Ext.dd.DragZone, { + ddGroup: 'CalendarDD', + eventSelector: '.ext-cal-evt', + + constructor: function(el, config) { + if (!Ext.calendar._statusProxyInstance) { + Ext.calendar._statusProxyInstance = new Ext.calendar.StatusProxy(); + } + this.proxy = Ext.calendar._statusProxyInstance; + Ext.calendar.DragZone.superclass.constructor.call(this, el, config); + }, + + getDragData: function(e) { + // Check whether we are dragging on an event first + var t = e.getTarget(this.eventSelector, 3); + if (t) { + var rec = this.view.getEventRecordFromEl(t); + return { + type: 'eventdrag', + ddel: t, + eventStart: rec.data[Ext.calendar.EventMappings.StartDate.name], + eventEnd: rec.data[Ext.calendar.EventMappings.EndDate.name], + proxy: this.proxy + }; + } + + // If not dragging an event then we are dragging on + // the calendar to add a new event + t = this.view.getDayAt(e.getPageX(), e.getPageY()); + if (t.el) { + return { + type: 'caldrag', + start: t.date, + proxy: this.proxy + }; + } + return null; + }, + + onInitDrag: function(x, y) { + if (this.dragData.ddel) { + var ghost = this.dragData.ddel.cloneNode(true), + child = Ext.fly(ghost).child('dl'); + + Ext.fly(ghost).setWidth('auto'); + + if (child) { + // for IE/Opera + child.setHeight('auto'); + } + this.proxy.update(ghost); + this.onStartDrag(x, y); + } + else if (this.dragData.start) { + this.onStartDrag(x, y); + } + this.view.onInitDrag(); + return true; + }, + + afterRepair: function() { + if (Ext.enableFx && this.dragData.ddel) { + Ext.Element.fly(this.dragData.ddel).highlight(this.hlColor || 'c3daf9'); + } + this.dragging = false; + }, + + getRepairXY: function(e) { + if (this.dragData.ddel) { + return Ext.Element.fly(this.dragData.ddel).getXY(); + } + }, + + afterInvalidDrop: function(e, id) { + Ext.select('.ext-dd-shim').hide(); + } +}); + +/* + * Internal drop zone implementation for the calendar components. This provides base functionality + * and is primarily for the month view -- DayViewDD adds day/week view-specific functionality. + */ +Ext.calendar.DropZone = Ext.extend(Ext.dd.DropZone, { + ddGroup: 'CalendarDD', + eventSelector: '.ext-cal-evt', + + // private + shims: [], + + getTargetFromEvent: function(e) { + var dragOffset = this.dragOffset || 0, + y = e.getPageY() - dragOffset, + d = this.view.getDayAt(e.getPageX(), y); + + return d.el ? d: null; + }, + + onNodeOver: function(n, dd, e, data) { + var D = Ext.calendar.Date, + start = data.type == 'eventdrag' ? n.date: D.min(data.start, n.date), + end = data.type == 'eventdrag' ? n.date.add(Date.DAY, D.diffDays(data.eventStart, data.eventEnd)) : + D.max(data.start, n.date); + + if (!this.dragStartDate || !this.dragEndDate || (D.diffDays(start, this.dragStartDate) != 0) || (D.diffDays(end, this.dragEndDate) != 0)) { + this.dragStartDate = start; + this.dragEndDate = end.clearTime().add(Date.DAY, 1).add(Date.MILLI, -1); + this.shim(start, end); + + var range = start.format('n/j'); + if (D.diffDays(start, end) > 0) { + range += '-' + end.format('n/j'); + } + var msg = String.format(data.type == 'eventdrag' ? this.moveText: this.createText, range); + data.proxy.updateMsg(msg); + } + return this.dropAllowed; + }, + + shim: function(start, end) { + this.currWeek = -1; + var dt = start.clone(), + i = 0, + shim, + box, + cnt = Ext.calendar.Date.diffDays(dt, end) + 1; + + Ext.each(this.shims, + function(shim) { + if (shim) { + shim.isActive = false; + } + } + ); + + while (i++This is the {@link Ext.data.Record Record} specification for calendar event data used by the + * {@link Ext.calendar.CalendarPanel CalendarPanel}'s underlying store. It can be overridden as + * necessary to customize the fields supported by events, although the existing column names should + * not be altered. If your model fields are named differently you should update the mapping + * configs accordingly.

+ *

The only required fields when creating a new event record instance are StartDate and + * EndDate. All other fields are either optional are will be defaulted if blank.

+ *

Here is a basic example for how to create a new record of this type:


+rec = new Ext.calendar.EventRecord({
+    StartDate: '2101-01-12 12:00:00',
+    EndDate: '2101-01-12 13:30:00',
+    Title: 'My cool event',
+    Notes: 'Some notes'
+});
+
+ * If you have overridden any of the record's data mappings via the Ext.calendar.EventMappings object + * you may need to set the values using this alternate syntax to ensure that the fields match up correctly:

+var M = Ext.calendar.EventMappings;
+
+rec = new Ext.calendar.EventRecord();
+rec.data[M.StartDate.name] = '2101-01-12 12:00:00';
+rec.data[M.EndDate.name] = '2101-01-12 13:30:00';
+rec.data[M.Title.name] = 'My cool event';
+rec.data[M.Notes.name] = 'Some notes';
+
+ * @constructor + * @param {Object} data (Optional) An object, the properties of which provide values for the new Record's + * fields. If not specified the {@link Ext.data.Field#defaultValue defaultValue} + * for each field will be assigned. + * @param {Object} id (Optional) The id of the Record. The id is used by the + * {@link Ext.data.Store} object which owns the Record to index its collection + * of Records (therefore this id should be unique within each store). If an + * id is not specified a {@link #phantom} + * Record will be created with an {@link #Record.id automatically generated id}. + */ + (function() { + var M = Ext.calendar.EventMappings; + + Ext.calendar.EventRecord = Ext.data.Record.create([ + M.EventId, + M.CalendarId, + M.Title, + M.StartDate, + M.EndDate, + M.Location, + M.Notes, + M.Url, + M.IsAllDay, + M.Reminder, + M.IsNew + ]); + + /** + * Reconfigures the default record definition based on the current Ext.calendar.EventMappings object + */ + Ext.calendar.EventRecord.reconfigure = function() { + Ext.calendar.EventRecord = Ext.data.Record.create([ + M.EventId, + M.CalendarId, + M.Title, + M.StartDate, + M.EndDate, + M.Location, + M.Notes, + M.Url, + M.IsAllDay, + M.Reminder, + M.IsNew + ]); + }; +})(); +/* + * This is the view used internally by the panel that displays overflow events in the + * month view. Anytime a day cell cannot display all of its events, it automatically displays + * a link at the bottom to view all events for that day. When clicked, a panel pops up that + * uses this view to display the events for that day. + */ +Ext.calendar.MonthDayDetailView = Ext.extend(Ext.BoxComponent, { + initComponent: function() { + Ext.calendar.CalendarView.superclass.initComponent.call(this); + + this.addEvents({ + eventsrendered: true + }); + + if (!this.el) { + this.el = document.createElement('div'); + } + }, + + afterRender: function() { + this.tpl = this.getTemplate(); + + Ext.calendar.MonthDayDetailView.superclass.afterRender.call(this); + + this.el.on({ + 'click': this.view.onClick, + 'mouseover': this.view.onMouseOver, + 'mouseout': this.view.onMouseOut, + scope: this.view + }); + }, + + getTemplate: function() { + if (!this.tpl) { + this.tpl = new Ext.XTemplate( + '
', + '', + '', + '', + '', + '', + '', + '
{markup}
', + '
' + ); + } + this.tpl.compile(); + return this.tpl; + }, + + update: function(dt) { + this.date = dt; + this.refresh(); + }, + + refresh: function() { + if (!this.rendered) { + return; + } + var eventTpl = this.view.getEventTemplate(), + + templateData = []; + + evts = this.store.queryBy(function(rec) { + var thisDt = this.date.clearTime(true).getTime(), + recStart = rec.data[Ext.calendar.EventMappings.StartDate.name].clearTime(true).getTime(), + startsOnDate = (thisDt == recStart), + spansDate = false; + + if (!startsOnDate) { + var recEnd = rec.data[Ext.calendar.EventMappings.EndDate.name].clearTime(true).getTime(); + spansDate = recStart < thisDt && recEnd >= thisDt; + } + return startsOnDate || spansDate; + }, + this); + + evts.each(function(evt) { + var item = evt.data, + M = Ext.calendar.EventMappings; + + item._renderAsAllDay = item[M.IsAllDay.name] || Ext.calendar.Date.diffDays(item[M.StartDate.name], item[M.EndDate.name]) > 0; + item.spanLeft = Ext.calendar.Date.diffDays(item[M.StartDate.name], this.date) > 0; + item.spanRight = Ext.calendar.Date.diffDays(this.date, item[M.EndDate.name]) > 0; + item.spanCls = (item.spanLeft ? (item.spanRight ? 'ext-cal-ev-spanboth': + 'ext-cal-ev-spanleft') : (item.spanRight ? 'ext-cal-ev-spanright': '')); + + templateData.push({ + markup: eventTpl.apply(this.getTemplateEventData(item)) + }); + }, + this); + + this.tpl.overwrite(this.el, templateData); + this.fireEvent('eventsrendered', this, this.date, evts.getCount()); + }, + + getTemplateEventData: function(evt) { + var data = this.view.getTemplateEventData(evt); + data._elId = 'dtl-' + data._elId; + return data; + } +}); + +Ext.reg('monthdaydetailview', Ext.calendar.MonthDayDetailView); +/** + * @class Ext.calendar.CalendarPicker + * @extends Ext.form.ComboBox + *

A custom combo used for choosing from the list of available calendars to assign an event to.

+ *

This is pretty much a standard combo that is simply pre-configured for the options needed by the + * calendar components. The default configs are as follows:


+    fieldLabel: 'Calendar',
+    valueField: 'CalendarId',
+    displayField: 'Title',
+    triggerAction: 'all',
+    mode: 'local',
+    forceSelection: true,
+    width: 200
+
+ * @constructor + * @param {Object} config The config object + */ +Ext.calendar.CalendarPicker = Ext.extend(Ext.form.ComboBox, { + fieldLabel: 'Calendar', + valueField: 'CalendarId', + displayField: 'Title', + triggerAction: 'all', + mode: 'local', + forceSelection: true, + width: 200, + + // private + initComponent: function() { + Ext.calendar.CalendarPicker.superclass.initComponent.call(this); + this.tpl = this.tpl || + '
 
{' + this.displayField + '}
'; + }, + + // private + afterRender: function() { + Ext.calendar.CalendarPicker.superclass.afterRender.call(this); + + this.wrap = this.el.up('.x-form-field-wrap'); + this.wrap.addClass('ext-calendar-picker'); + + this.icon = Ext.DomHelper.append(this.wrap, { + tag: 'div', + cls: 'ext-cal-picker-icon ext-cal-picker-mainicon' + }); + }, + + // inherited docs + setValue: function(value) { + this.wrap.removeClass('ext-color-' + this.getValue()); + if (!value && this.store !== undefined) { + // always default to a valid calendar + value = this.store.getAt(0).data.CalendarId; + } + Ext.calendar.CalendarPicker.superclass.setValue.call(this, value); + this.wrap.addClass('ext-color-' + value); + } +}); + +Ext.reg('calendarpicker', Ext.calendar.CalendarPicker); +/* + * This is an internal helper class for the calendar views and should not be overridden. + * It is responsible for the base event rendering logic underlying all of the calendar views. + */ +Ext.calendar.WeekEventRenderer = function() { + + var getEventRow = function(id, week, index) { + var indexOffset = 1, + //skip row with date #'s + evtRow, + wkRow = Ext.get(id + '-wk-' + week); + if (wkRow) { + var table = wkRow.child('.ext-cal-evt-tbl', true); + evtRow = table.tBodies[0].childNodes[index + indexOffset]; + if (!evtRow) { + evtRow = Ext.DomHelper.append(table.tBodies[0], ''); + } + } + return Ext.get(evtRow); + }; + + return { + render: function(o) { + var w = 0, + grid = o.eventGrid, + dt = o.viewStart.clone(), + eventTpl = o.tpl, + max = o.maxEventsPerDay != undefined ? o.maxEventsPerDay: 999, + weekCount = o.weekCount < 1 ? 6: o.weekCount, + dayCount = o.weekCount == 1 ? o.dayCount: 7, + cellCfg; + + for (; w < weekCount; w++) { + if (!grid[w] || grid[w].length == 0) { + // no events or span cells for the entire week + if (weekCount == 1) { + row = getEventRow(o.id, w, 0); + cellCfg = { + tag: 'td', + cls: 'ext-cal-ev', + id: o.id + '-empty-0-day-' + dt.format('Ymd'), + html: ' ' + }; + if (dayCount > 1) { + cellCfg.colspan = dayCount; + } + Ext.DomHelper.append(row, cellCfg); + } + dt = dt.add(Date.DAY, 7); + } else { + var row, + d = 0, + wk = grid[w], + startOfWeek = dt.clone(), + endOfWeek = startOfWeek.add(Date.DAY, dayCount).add(Date.MILLI, -1); + + for (; d < dayCount; d++) { + if (wk[d]) { + var ev = emptyCells = skipped = 0, + day = wk[d], + ct = day.length, + evt; + + for (; ev < ct; ev++) { + if (!day[ev]) { + emptyCells++; + continue; + } + if (emptyCells > 0 && ev - emptyCells < max) { + row = getEventRow(o.id, w, ev - emptyCells); + cellCfg = { + tag: 'td', + cls: 'ext-cal-ev', + id: o.id + '-empty-' + ct + '-day-' + dt.format('Ymd') + }; + if (emptyCells > 1 && max - ev > emptyCells) { + cellCfg.rowspan = Math.min(emptyCells, max - ev); + } + Ext.DomHelper.append(row, cellCfg); + emptyCells = 0; + } + + if (ev >= max) { + skipped++; + continue; + } + evt = day[ev]; + + if (!evt.isSpan || evt.isSpanStart) { + //skip non-starting span cells + var item = evt.data || evt.event.data; + item._weekIndex = w; + item._renderAsAllDay = item[Ext.calendar.EventMappings.IsAllDay.name] || evt.isSpanStart; + item.spanLeft = item[Ext.calendar.EventMappings.StartDate.name].getTime() < startOfWeek.getTime(); + item.spanRight = item[Ext.calendar.EventMappings.EndDate.name].getTime() > endOfWeek.getTime(); + item.spanCls = (item.spanLeft ? (item.spanRight ? 'ext-cal-ev-spanboth': + 'ext-cal-ev-spanleft') : (item.spanRight ? 'ext-cal-ev-spanright': '')); + + row = getEventRow(o.id, w, ev); + cellCfg = { + tag: 'td', + cls: 'ext-cal-ev', + cn: eventTpl.apply(o.templateDataFn(item)) + }; + var diff = Ext.calendar.Date.diffDays(dt, item[Ext.calendar.EventMappings.EndDate.name]) + 1, + cspan = Math.min(diff, dayCount - d); + + if (cspan > 1) { + cellCfg.colspan = cspan; + } + Ext.DomHelper.append(row, cellCfg); + } + } + if (ev > max) { + row = getEventRow(o.id, w, max); + Ext.DomHelper.append(row, { + tag: 'td', + cls: 'ext-cal-ev-more', + id: 'ext-cal-ev-more-' + dt.format('Ymd'), + cn: { + tag: 'a', + html: '+' + skipped + ' more...' + } + }); + } + if (ct < o.evtMaxCount[w]) { + row = getEventRow(o.id, w, ct); + if (row) { + cellCfg = { + tag: 'td', + cls: 'ext-cal-ev', + id: o.id + '-empty-' + (ct + 1) + '-day-' + dt.format('Ymd') + }; + var rowspan = o.evtMaxCount[w] - ct; + if (rowspan > 1) { + cellCfg.rowspan = rowspan; + } + Ext.DomHelper.append(row, cellCfg); + } + } + } else { + row = getEventRow(o.id, w, 0); + if (row) { + cellCfg = { + tag: 'td', + cls: 'ext-cal-ev', + id: o.id + '-empty-day-' + dt.format('Ymd') + }; + if (o.evtMaxCount[w] > 1) { + cellCfg.rowSpan = o.evtMaxCount[w]; + } + Ext.DomHelper.append(row, cellCfg); + } + } + dt = dt.add(Date.DAY, 1); + } + } + } + } + }; +}(); +/** + * @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; + + 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'; + } +});/** + * @class Ext.calendar.MonthView + * @extends Ext.calendar.CalendarView + *

Displays a calendar view by month. This class does not usually need ot be used directly as you can + * use a {@link Ext.calendar.CalendarPanel CalendarPanel} to manage multiple calendar views at once including + * the month view.

+ * @constructor + * @param {Object} config The config object + */ +Ext.calendar.MonthView = Ext.extend(Ext.calendar.CalendarView, { + /** + * @cfg {Boolean} showTime + * True to display the current time in today's box in the calendar, false to not display it (defautls to true) + */ + showTime: true, + /** + * @cfg {Boolean} showTodayText + * True to display the {@link #todayText} string in today's box in the calendar, false to not display it (defautls to true) + */ + showTodayText: true, + /** + * @cfg {String} todayText + * The text to display in the current day's box in the calendar when {@link #showTodayText} is true (defaults to 'Today') + */ + todayText: 'Today', + /** + * @cfg {Boolean} showHeader + * True to display a header beneath the navigation bar containing the week names above each week's column, false not to + * show it and instead display the week names in the first row of days in the calendar (defaults to false). + */ + showHeader: false, + /** + * @cfg {Boolean} showWeekLinks + * True to display an extra column before the first day in the calendar that links to the {@link Ext.calendar.WeekView view} + * for each individual week, false to not show it (defaults to false). If true, the week links can also contain the week + * number depending on the value of {@link #showWeekNumbers}. + */ + showWeekLinks: false, + /** + * @cfg {Boolean} showWeekNumbers + * True to show the week number for each week in the calendar in the week link column, false to show nothing (defaults to false). + * Note that if {@link #showWeekLinks} is false this config will have no affect even if true. + */ + showWeekNumbers: false, + /** + * @cfg {String} weekLinkOverClass + * The CSS class name applied when the mouse moves over a week link element (only applies when {@link #showWeekLinks} is true, + * defaults to 'ext-week-link-over'). + */ + weekLinkOverClass: 'ext-week-link-over', + + //private properties -- do not override: + daySelector: '.ext-cal-day', + moreSelector: '.ext-cal-ev-more', + weekLinkSelector: '.ext-cal-week-link', + weekCount: -1, + // defaults to auto by month + dayCount: 7, + moreElIdDelimiter: '-more-', + weekLinkIdDelimiter: 'ext-cal-week-', + + // private + initComponent: function() { + Ext.calendar.MonthView.superclass.initComponent.call(this); + this.addEvents({ + /** + * @event dayclick + * Fires after the user clicks within the view container and not on an event element + * @param {Ext.calendar.MonthView} this + * @param {Date} dt The date/time that was clicked on + * @param {Boolean} allday True if the day clicked on represents an all-day box, else false. Clicks within the + * MonthView always return true for this param. + * @param {Ext.Element} el The Element that was clicked on + */ + dayclick: true, + /** + * @event weekclick + * Fires after the user clicks within a week link (when {@link #showWeekLinks is true) + * @param {Ext.calendar.MonthView} this + * @param {Date} dt The start date of the week that was clicked on + */ + weekclick: true, + // inherited docs + dayover: true, + // inherited docs + dayout: true + }); + }, + + // private + initDD: function() { + var cfg = { + view: this, + createText: this.ddCreateEventText, + moveText: this.ddMoveEventText, + ddGroup: 'MonthViewDD' + }; + + this.dragZone = new Ext.calendar.DragZone(this.el, cfg); + this.dropZone = new Ext.calendar.DropZone(this.el, cfg); + }, + + // private + onDestroy: function() { + Ext.destroy(this.ddSelector); + Ext.destroy(this.dragZone); + Ext.destroy(this.dropZone); + Ext.calendar.MonthView.superclass.onDestroy.call(this); + }, + + // private + afterRender: function() { + if (!this.tpl) { + this.tpl = new Ext.calendar.MonthViewTemplate({ + id: this.id, + showTodayText: this.showTodayText, + todayText: this.todayText, + showTime: this.showTime, + showHeader: this.showHeader, + showWeekLinks: this.showWeekLinks, + showWeekNumbers: this.showWeekNumbers + }); + } + this.tpl.compile(); + this.addClass('ext-cal-monthview ext-cal-ct'); + + Ext.calendar.MonthView.superclass.afterRender.call(this); + }, + + // private + onResize: function() { + if (this.monitorResize) { + this.maxEventsPerDay = this.getMaxEventsPerDay(); + this.refresh(); + } + }, + + // private + forceSize: function() { + // Compensate for the week link gutter width if visible + if (this.showWeekLinks && this.el && this.el.child) { + var hd = this.el.select('.ext-cal-hd-days-tbl'), + bgTbl = this.el.select('.ext-cal-bg-tbl'), + evTbl = this.el.select('.ext-cal-evt-tbl'), + wkLinkW = this.el.child('.ext-cal-week-link').getWidth(), + w = this.el.getWidth() - wkLinkW; + + hd.setWidth(w); + bgTbl.setWidth(w); + evTbl.setWidth(w); + } + Ext.calendar.MonthView.superclass.forceSize.call(this); + }, + + //private + initClock: function() { + if (Ext.fly(this.id + '-clock') !== null) { + this.prevClockDay = new Date().getDay(); + if (this.clockTask) { + Ext.TaskMgr.stop(this.clockTask); + } + this.clockTask = Ext.TaskMgr.start({ + run: function() { + var el = Ext.fly(this.id + '-clock'), + t = new Date(); + + if (t.getDay() == this.prevClockDay) { + if (el) { + el.update(t.format('g:i a')); + } + } + else { + this.prevClockDay = t.getDay(); + this.moveTo(t); + } + }, + scope: this, + interval: 1000 + }); + } + }, + + // inherited docs + getEventBodyMarkup: function() { + if (!this.eventBodyMarkup) { + this.eventBodyMarkup = ['{Title}', + '', + ' ', + '', + '', + ' ', + '', + '', + ' ', + '', + '', + ' ', + '' + ].join(''); + } + return this.eventBodyMarkup; + }, + + // inherited docs + getEventTemplate: function() { + if (!this.eventTpl) { + var tpl, + body = this.getEventBodyMarkup(); + + tpl = !(Ext.isIE || Ext.isOpera) ? + new Ext.XTemplate( + '
', + body, + '
' + ) + : new Ext.XTemplate( + '', + '
', + '
', + '
', + '', + '', + '
', + '', + body, + '', + '
', + '
', + '', + '
' + ); + tpl.compile(); + this.eventTpl = tpl; + } + return this.eventTpl; + }, + + // private + getTemplateEventData: function(evt) { + var M = Ext.calendar.EventMappings, + selector = this.getEventSelectorCls(evt[M.EventId.name]), + title = evt[M.Title.name]; + + return Ext.applyIf({ + _selectorCls: selector, + _colorCls: 'ext-color-' + (evt[M.CalendarId.name] ? + evt[M.CalendarId.name] : 'default') + (evt._renderAsAllDay ? '-ad': ''), + _elId: selector + '-' + evt._weekIndex, + _isRecurring: evt.Recurrence && evt.Recurrence != '', + _isReminder: evt[M.Reminder.name] && evt[M.Reminder.name] != '', + Title: (evt[M.IsAllDay.name] ? '': evt[M.StartDate.name].format('g:ia ')) + (!title || title.length == 0 ? '(No title)': title) + }, + evt); + }, + + // private + refresh: function() { + if (this.detailPanel) { + this.detailPanel.hide(); + } + Ext.calendar.MonthView.superclass.refresh.call(this); + + if (this.showTime !== false) { + this.initClock(); + } + }, + + // private + renderItems: function() { + Ext.calendar.WeekEventRenderer.render({ + eventGrid: this.allDayOnly ? this.allDayGrid: this.eventGrid, + viewStart: this.viewStart, + tpl: this.getEventTemplate(), + maxEventsPerDay: this.maxEventsPerDay, + id: this.id, + templateDataFn: this.getTemplateEventData.createDelegate(this), + evtMaxCount: this.evtMaxCount, + weekCount: this.weekCount, + dayCount: this.dayCount + }); + this.fireEvent('eventsrendered', this); + }, + + // private + getDayEl: function(dt) { + return Ext.get(this.getDayId(dt)); + }, + + // private + getDayId: function(dt) { + if (Ext.isDate(dt)) { + dt = dt.format('Ymd'); + } + return this.id + this.dayElIdDelimiter + dt; + }, + + // private + getWeekIndex: function(dt) { + var el = this.getDayEl(dt).up('.ext-cal-wk-ct'); + return parseInt(el.id.split('-wk-')[1], 10); + }, + + // private + getDaySize: function(contentOnly) { + var box = this.el.getBox(), + w = box.width / this.dayCount, + h = box.height / this.getWeekCount(); + + if (contentOnly) { + var hd = this.el.select('.ext-cal-dtitle').first().parent('tr'); + h = hd ? h - hd.getHeight(true) : h; + } + return { + height: h, + width: w + }; + }, + + // private + getEventHeight: function() { + if (!this.eventHeight) { + var evt = this.el.select('.ext-cal-evt').first(); + this.eventHeight = evt ? evt.parent('tr').getHeight() : 18; + } + return this.eventHeight; + }, + + // private + getMaxEventsPerDay: function() { + var dayHeight = this.getDaySize(true).height, + h = this.getEventHeight(), + max = Math.max(Math.floor((dayHeight - h) / h), 0); + + return max; + }, + + // private + getDayAt: function(x, y) { + var box = this.el.getBox(), + daySize = this.getDaySize(), + dayL = Math.floor(((x - box.x) / daySize.width)), + dayT = Math.floor(((y - box.y) / daySize.height)), + days = (dayT * 7) + dayL, + dt = this.viewStart.add(Date.DAY, days); + return { + date: dt, + el: this.getDayEl(dt) + }; + }, + + // inherited docs + moveNext: function() { + return this.moveMonths(1); + }, + + // inherited docs + movePrev: function() { + return this.moveMonths( - 1); + }, + + // private + onInitDrag: function() { + Ext.calendar.MonthView.superclass.onInitDrag.call(this); + Ext.select(this.daySelector).removeClass(this.dayOverClass); + if (this.detailPanel) { + this.detailPanel.hide(); + } + }, + + // private + onMoreClick: function(dt) { + if (!this.detailPanel) { + this.detailPanel = new Ext.Panel({ + id: this.id + '-details-panel', + title: dt.format('F j'), + layout: 'fit', + floating: true, + renderTo: Ext.getBody(), + tools: [{ + id: 'close', + handler: function(e, t, p) { + p.hide(); + } + }], + items: { + xtype: 'monthdaydetailview', + id: this.id + '-details-view', + date: dt, + view: this, + store: this.store, + listeners: { + 'eventsrendered': this.onDetailViewUpdated.createDelegate(this) + } + } + }); + } + else { + this.detailPanel.setTitle(dt.format('F j')); + } + this.detailPanel.getComponent(this.id + '-details-view').update(dt); + }, + + // private + onDetailViewUpdated: function(view, dt, numEvents) { + var p = this.detailPanel, + frameH = p.getFrameHeight(), + evtH = this.getEventHeight(), + bodyH = frameH + (numEvents * evtH) + 3, + dayEl = this.getDayEl(dt), + box = dayEl.getBox(); + + p.updateBox(box); + p.setHeight(bodyH); + p.setWidth(Math.max(box.width, 220)); + p.show(); + p.getPositionEl().alignTo(dayEl, 't-t?'); + }, + + // private + onHide: function() { + Ext.calendar.MonthView.superclass.onHide.call(this); + if (this.detailPanel) { + this.detailPanel.hide(); + } + }, + + // private + onClick: function(e, t) { + if (this.detailPanel) { + this.detailPanel.hide(); + } + if (Ext.calendar.MonthView.superclass.onClick.apply(this, arguments)) { + // The superclass handled the click already so exit + return; + } + if (this.dropZone) { + this.dropZone.clearShims(); + } + var el = e.getTarget(this.weekLinkSelector, 3), + dt, + parts; + if (el) { + dt = el.id.split(this.weekLinkIdDelimiter)[1]; + this.fireEvent('weekclick', this, Date.parseDate(dt, 'Ymd')); + return; + } + el = e.getTarget(this.moreSelector, 3); + if (el) { + dt = el.id.split(this.moreElIdDelimiter)[1]; + this.onMoreClick(Date.parseDate(dt, 'Ymd')); + return; + } + el = e.getTarget('td', 3); + if (el) { + if (el.id && el.id.indexOf(this.dayElIdDelimiter) > -1) { + parts = el.id.split(this.dayElIdDelimiter); + dt = parts[parts.length - 1]; + + this.fireEvent('dayclick', this, Date.parseDate(dt, 'Ymd'), false, Ext.get(this.getDayId(dt))); + return; + } + } + }, + + // private + handleDayMouseEvent: function(e, t, type) { + var el = e.getTarget(this.weekLinkSelector, 3, true); + if (el) { + el[type == 'over' ? 'addClass': 'removeClass'](this.weekLinkOverClass); + return; + } + Ext.calendar.MonthView.superclass.handleDayMouseEvent.apply(this, arguments); + } +}); + +Ext.reg('monthview', Ext.calendar.MonthView); +/** + * @class Ext.calendar.DayHeaderView + * @extends Ext.calendar.MonthView + *

This is the header area container within the day and week views where all-day events are displayed. + * Normally you should not need to use this class directly -- instead you should use {@link Ext.calendar.DayView DayView} + * which aggregates this class and the {@link Ext.calendar.DayBodyView DayBodyView} into the single unified view + * presented by {@link Ext.calendar.CalendarPanel CalendarPanel}.

+ * @constructor + * @param {Object} config The config object + */ +Ext.calendar.DayHeaderView = Ext.extend(Ext.calendar.MonthView, { + // private configs + weekCount: 1, + dayCount: 1, + allDayOnly: true, + monitorResize: false, + + /** + * @event dayclick + * Fires after the user clicks within the day view container and not on an event element + * @param {Ext.calendar.DayBodyView} this + * @param {Date} dt The date/time that was clicked on + * @param {Boolean} allday True if the day clicked on represents an all-day box, else false. Clicks within the + * DayHeaderView always return true for this param. + * @param {Ext.Element} el The Element that was clicked on + */ + + // private + afterRender: function() { + if (!this.tpl) { + this.tpl = new Ext.calendar.DayHeaderTemplate({ + id: this.id, + showTodayText: this.showTodayText, + todayText: this.todayText, + showTime: this.showTime + }); + } + this.tpl.compile(); + this.addClass('ext-cal-day-header'); + + Ext.calendar.DayHeaderView.superclass.afterRender.call(this); + }, + + // private + forceSize: Ext.emptyFn, + + // private + refresh: function() { + Ext.calendar.DayHeaderView.superclass.refresh.call(this); + this.recalcHeaderBox(); + }, + + // private + recalcHeaderBox: function() { + var tbl = this.el.child('.ext-cal-evt-tbl'), + h = tbl.getHeight(); + + this.el.setHeight(h + 7); + + if (Ext.isIE && Ext.isStrict) { + this.el.child('.ext-cal-hd-ad-inner').setHeight(h + 4); + } + if (Ext.isOpera) { + //TODO: figure out why Opera refuses to refresh height when + //the new height is lower than the previous one + // var ct = this.el.child('.ext-cal-hd-ct'); + // ct.repaint(); + } + }, + + // private + moveNext: function(noRefresh) { + this.moveDays(this.dayCount, noRefresh); + }, + + // private + movePrev: function(noRefresh) { + this.moveDays( - this.dayCount, noRefresh); + }, + + // private + onClick: function(e, t) { + var el = e.getTarget('td', 3), + parts, + dt; + if (el) { + if (el.id && el.id.indexOf(this.dayElIdDelimiter) > -1) { + parts = el.id.split(this.dayElIdDelimiter); + dt = parts[parts.length - 1]; + + this.fireEvent('dayclick', this, Date.parseDate(dt, 'Ymd'), true, Ext.get(this.getDayId(dt))); + return; + } + } + Ext.calendar.DayHeaderView.superclass.onClick.apply(this, arguments); + } +}); + +Ext.reg('dayheaderview', Ext.calendar.DayHeaderView); +/**S + * @class Ext.calendar.DayBodyView + * @extends Ext.calendar.CalendarView + *

This is the scrolling container within the day and week views where non-all-day events are displayed. + * Normally you should not need to use this class directly -- instead you should use {@link Ext.calendar.DayView DayView} + * which aggregates this class and the {@link Ext.calendar.DayHeaderView DayHeaderView} into the single unified view + * presented by {@link Ext.calendar.CalendarPanel CalendarPanel}.

+ * @constructor + * @param {Object} config The config object + */ +Ext.calendar.DayBodyView = Ext.extend(Ext.calendar.CalendarView, { + //private + dayColumnElIdDelimiter: '-day-col-', + + //private + initComponent: function() { + Ext.calendar.DayBodyView.superclass.initComponent.call(this); + + this.addEvents({ + /** + * @event eventresize + * Fires after the user drags the resize handle of an event to resize it + * @param {Ext.calendar.DayBodyView} this + * @param {Ext.calendar.EventRecord} rec The {@link Ext.calendar.EventRecord record} for the event that was resized + * containing the updated start and end dates + */ + eventresize: true, + /** + * @event dayclick + * Fires after the user clicks within the day view container and not on an event element + * @param {Ext.calendar.DayBodyView} this + * @param {Date} dt The date/time that was clicked on + * @param {Boolean} allday True if the day clicked on represents an all-day box, else false. Clicks within the + * DayBodyView always return false for this param. + * @param {Ext.Element} el The Element that was clicked on + */ + dayclick: true + }); + }, + + //private + initDD: function() { + var cfg = { + createText: this.ddCreateEventText, + moveText: this.ddMoveEventText, + resizeText: this.ddResizeEventText + }; + + this.el.ddScrollConfig = { + // scrolling is buggy in IE/Opera for some reason. A larger vthresh + // makes it at least functional if not perfect + vthresh: Ext.isIE || Ext.isOpera ? 100: 40, + hthresh: -1, + frequency: 50, + increment: 100, + ddGroup: 'DayViewDD' + }; + this.dragZone = new Ext.calendar.DayViewDragZone(this.el, Ext.apply({ + view: this, + containerScroll: true + }, + cfg)); + + this.dropZone = new Ext.calendar.DayViewDropZone(this.el, Ext.apply({ + view: this + }, + cfg)); + }, + + //private + refresh: function() { + var top = this.el.getScroll().top; + this.prepareData(); + this.renderTemplate(); + this.renderItems(); + + // skip this if the initial render scroll position has not yet been set. + // necessary since IE/Opera must be deferred, so the first refresh will + // override the initial position by default and always set it to 0. + if (this.scrollReady) { + this.scrollTo(top); + } + }, + + /** + * Scrolls the container to the specified vertical position. If the view is large enough that + * there is no scroll overflow then this method will have no affect. + * @param {Number} y The new vertical scroll position in pixels + * @param {Boolean} defer (optional)

True to slightly defer the call, false to execute immediately.

+ *

This method will automatically defer itself for IE and Opera (even if you pass false) otherwise + * the scroll position will not update in those browsers. You can optionally pass true, however, to + * force the defer in all browsers, or use your own custom conditions to determine whether this is needed.

+ *

Note that this method should not generally need to be called directly as scroll position is managed internally.

+ */ + scrollTo: function(y, defer) { + defer = defer || (Ext.isIE || Ext.isOpera); + if (defer) { + (function() { + this.el.scrollTo('top', y); + this.scrollReady = true; + }).defer(10, this); + } + else { + this.el.scrollTo('top', y); + this.scrollReady = true; + } + }, + + // private + afterRender: function() { + if (!this.tpl) { + this.tpl = new Ext.calendar.DayBodyTemplate({ + id: this.id, + dayCount: this.dayCount, + showTodayText: this.showTodayText, + todayText: this.todayText, + showTime: this.showTime + }); + } + this.tpl.compile(); + + this.addClass('ext-cal-body-ct'); + + Ext.calendar.DayBodyView.superclass.afterRender.call(this); + + // default scroll position to 7am: + this.scrollTo(7 * 42); + }, + + // private + forceSize: Ext.emptyFn, + + // private + onEventResize: function(rec, data) { + var D = Ext.calendar.Date, + start = Ext.calendar.EventMappings.StartDate.name, + end = Ext.calendar.EventMappings.EndDate.name; + + if (D.compare(rec.data[start], data.StartDate) === 0 && + D.compare(rec.data[end], data.EndDate) === 0) { + // no changes + return; + } + rec.set(start, data.StartDate); + rec.set(end, data.EndDate); + + this.fireEvent('eventresize', this, rec); + }, + + // inherited docs + getEventBodyMarkup: function() { + if (!this.eventBodyMarkup) { + this.eventBodyMarkup = ['{Title}', + '', + ' ', + '', + '', + ' ', + '' + // '', + // ' ', + // '', + // '', + // ' ', + // '' + ].join(''); + } + return this.eventBodyMarkup; + }, + + // inherited docs + getEventTemplate: function() { + if (!this.eventTpl) { + this.eventTpl = !(Ext.isIE || Ext.isOpera) ? + new Ext.XTemplate( + '
', + '
', this.getEventBodyMarkup(), '
', + '
 
', + '
' + ) + : new Ext.XTemplate( + '
', + '
 
', + '
', + '
', + this.getEventBodyMarkup(), + '
', + '
 
', + '
', + '
 
', + '
' + ); + this.eventTpl.compile(); + } + return this.eventTpl; + }, + + /** + *

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 all-day 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 + */ + getEventAllDayTemplate: function() { + if (!this.eventAllDayTpl) { + var tpl, + body = this.getEventBodyMarkup(); + + tpl = !(Ext.isIE || Ext.isOpera) ? + new Ext.XTemplate( + '
', + body, + '
' + ) + : new Ext.XTemplate( + '
', + '
', + '
', + '
', + body, + '
', + '
', + '
' + ); + tpl.compile(); + this.eventAllDayTpl = tpl; + } + return this.eventAllDayTpl; + }, + + // private + getTemplateEventData: function(evt) { + var selector = this.getEventSelectorCls(evt[Ext.calendar.EventMappings.EventId.name]), + data = {}, + M = Ext.calendar.EventMappings; + + this.getTemplateEventBox(evt); + + data._selectorCls = selector; + data._colorCls = 'ext-color-' + evt[M.CalendarId.name] + (evt._renderAsAllDay ? '-ad': ''); + data._elId = selector + (evt._weekIndex ? '-' + evt._weekIndex: ''); + data._isRecurring = evt.Recurrence && evt.Recurrence != ''; + data._isReminder = evt[M.Reminder.name] && evt[M.Reminder.name] != ''; + var title = evt[M.Title.name]; + data.Title = (evt[M.IsAllDay.name] ? '': evt[M.StartDate.name].format('g:ia ')) + (!title || title.length == 0 ? '(No title)': title); + + return Ext.applyIf(data, evt); + }, + + // private + getTemplateEventBox: function(evt) { + var heightFactor = 0.7, + start = evt[Ext.calendar.EventMappings.StartDate.name], + end = evt[Ext.calendar.EventMappings.EndDate.name], + startMins = start.getHours() * 60 + start.getMinutes(), + endMins = end.getHours() * 60 + end.getMinutes(), + diffMins = endMins - startMins; + + evt._left = 0; + evt._width = 100; + evt._top = Math.round(startMins * heightFactor) + 1; + evt._height = Math.max((diffMins * heightFactor) - 2, 15); + }, + + // private + renderItems: function() { + var day = 0, + evts = [], + ev, + d, + ct, + item, + i, + j, + l, + overlapCols, + prevCol, + colWidth, + evtWidth, + markup, + target; + for (; day < this.dayCount; day++) { + ev = emptyCells = skipped = 0; + d = this.eventGrid[0][day]; + ct = d ? d.length: 0; + + for (; ev < ct; ev++) { + evt = d[ev]; + if (!evt) { + continue; + } + item = evt.data || evt.event.data; + if (item._renderAsAllDay) { + continue; + } + Ext.apply(item, { + cls: 'ext-cal-ev', + _positioned: true + }); + evts.push({ + data: this.getTemplateEventData(item), + date: this.viewStart.add(Date.DAY, day) + }); + } + } + + // overlapping event pre-processing loop + i = j = overlapCols = prevCol = 0; + l = evts.length; + for (; i < l; i++) { + evt = evts[i].data; + evt2 = null; + prevCol = overlapCols; + for (j = 0; j < l; j++) { + if (i == j) { + continue; + } + evt2 = evts[j].data; + if (this.isOverlapping(evt, evt2)) { + evt._overlap = evt._overlap == undefined ? 1: evt._overlap + 1; + if (i < j) { + if (evt._overcol === undefined) { + evt._overcol = 0; + } + evt2._overcol = evt._overcol + 1; + overlapCols = Math.max(overlapCols, evt2._overcol); + } + } + } + } + + // rendering loop + for (i = 0; i < l; i++) { + evt = evts[i].data; + if (evt._overlap !== undefined) { + colWidth = 100 / (overlapCols + 1); + evtWidth = 100 - (colWidth * evt._overlap); + + evt._width = colWidth; + evt._left = colWidth * evt._overcol; + } + markup = this.getEventTemplate().apply(evt); + target = this.id + '-day-col-' + evts[i].date.format('Ymd'); + + Ext.DomHelper.append(target, markup); + } + + this.fireEvent('eventsrendered', this); + }, + + // private + getDayEl: function(dt) { + return Ext.get(this.getDayId(dt)); + }, + + // private + getDayId: function(dt) { + if (Ext.isDate(dt)) { + dt = dt.format('Ymd'); + } + return this.id + this.dayColumnElIdDelimiter + dt; + }, + + // private + getDaySize: function() { + var box = this.el.child('.ext-cal-day-col-inner').getBox(); + return { + height: box.height, + width: box.width + }; + }, + + // private + getDayAt: function(x, y) { + var sel = '.ext-cal-body-ct', + xoffset = this.el.child('.ext-cal-day-times').getWidth(), + viewBox = this.el.getBox(), + daySize = this.getDaySize(false), + relX = x - viewBox.x - xoffset, + dayIndex = Math.floor(relX / daySize.width), + // clicked col index + scroll = this.el.getScroll(), + row = this.el.child('.ext-cal-bg-row'), + // first avail row, just to calc size + rowH = row.getHeight() / 2, + // 30 minute increment since a row is 60 minutes + relY = y - viewBox.y - rowH + scroll.top, + rowIndex = Math.max(0, Math.ceil(relY / rowH)), + mins = rowIndex * 30, + dt = this.viewStart.add(Date.DAY, dayIndex).add(Date.MINUTE, mins), + el = this.getDayEl(dt), + timeX = x; + + if (el) { + timeX = el.getLeft(); + } + + return { + date: dt, + el: el, + // this is the box for the specific time block in the day that was clicked on: + timeBox: { + x: timeX, + y: (rowIndex * 21) + viewBox.y - scroll.top, + width: daySize.width, + height: rowH + } + }; + }, + + // private + onClick: function(e, t) { + if (this.dragPending || Ext.calendar.DayBodyView.superclass.onClick.apply(this, arguments)) { + // The superclass handled the click already so exit + return; + } + if (e.getTarget('.ext-cal-day-times', 3) !== null) { + // ignore clicks on the times-of-day gutter + return; + } + var el = e.getTarget('td', 3); + if (el) { + if (el.id && el.id.indexOf(this.dayElIdDelimiter) > -1) { + var dt = this.getDateFromId(el.id, this.dayElIdDelimiter); + this.fireEvent('dayclick', this, Date.parseDate(dt, 'Ymd'), true, Ext.get(this.getDayId(dt, true))); + return; + } + } + var day = this.getDayAt(e.xy[0], e.xy[1]); + if (day && day.date) { + this.fireEvent('dayclick', this, day.date, false, null); + } + } +}); + +Ext.reg('daybodyview', Ext.calendar.DayBodyView); +/** + * @class Ext.calendar.DayView + * @extends Ext.Container + *

Unlike other calendar views, is not actually a subclass of {@link Ext.calendar.CalendarView CalendarView}. + * Instead it is a {@link Ext.Container Container} subclass that internally creates and manages the layouts of + * a {@link Ext.calendar.DayHeaderView DayHeaderView} and a {@link Ext.calendar.DayBodyView DayBodyView}. As such + * DayView accepts any config values that are valid for DayHeaderView and DayBodyView and passes those through + * to the contained views. It also supports the interface required of any calendar view and in turn calls methods + * on the contained views as necessary.

+ * @constructor + * @param {Object} config The config object + */ +Ext.calendar.DayView = Ext.extend(Ext.Container, { + /** + * @cfg {Boolean} showTime + * True to display the current time in today's box in the calendar, false to not display it (defautls to true) + */ + showTime: true, + /** + * @cfg {Boolean} showTodayText + * True to display the {@link #todayText} string in today's box in the calendar, false to not display it (defautls to true) + */ + showTodayText: true, + /** + * @cfg {String} todayText + * The text to display in the current day's box in the calendar when {@link #showTodayText} is true (defaults to 'Today') + */ + todayText: 'Today', + /** + * @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 {Number} dayCount + * The number of days to display in the view (defaults to 1) + */ + dayCount: 1, + + // private + initComponent : function(){ + // rendering more than 7 days per view is not supported + this.dayCount = this.dayCount > 7 ? 7 : this.dayCount; + + var cfg = Ext.apply({}, this.initialConfig); + cfg.showTime = this.showTime; + cfg.showTodatText = this.showTodayText; + cfg.todayText = this.todayText; + cfg.dayCount = this.dayCount; + cfg.wekkCount = 1; + + var header = Ext.applyIf({ + xtype: 'dayheaderview', + id: this.id+'-hd' + }, cfg); + + var body = Ext.applyIf({ + xtype: 'daybodyview', + id: this.id+'-bd' + }, cfg); + + this.items = [header, body]; + this.addClass('ext-cal-dayview ext-cal-ct'); + + Ext.calendar.DayView.superclass.initComponent.call(this); + }, + + // private + afterRender : function(){ + Ext.calendar.DayView.superclass.afterRender.call(this); + + this.header = Ext.getCmp(this.id+'-hd'); + this.body = Ext.getCmp(this.id+'-bd'); + this.body.on('eventsrendered', this.forceSize, this); + }, + + // private + refresh : function(){ + this.header.refresh(); + this.body.refresh(); + }, + + // private + forceSize: function(){ + // The defer call is mainly for good ol' IE, but it doesn't hurt in + // general to make sure that the window resize is good and done first + // so that we can properly calculate sizes. + (function(){ + var ct = this.el.up('.x-panel-body'), + hd = this.el.child('.ext-cal-day-header'), + h = ct.getHeight() - hd.getHeight(); + + this.el.child('.ext-cal-body-ct').setHeight(h); + }).defer(10, this); + }, + + // private + onResize : function(){ + this.forceSize(); + }, + + // private + getViewBounds : function(){ + return this.header.getViewBounds(); + }, + + /** + * 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.header.getStartDate(); + }, + + /** + * 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(dt){ + this.header.setStartDate(dt, true); + this.body.setStartDate(dt, true); + }, + + // private + renderItems: function(){ + this.header.renderItems(); + this.body.renderItems(); + }, + + /** + * Returns true if the view is currently displaying today's date, else false. + * @return {Boolean} True or false + */ + isToday : function(){ + return this.header.isToday(); + }, + + /** + * Updates the view to contain the passed date + * @param {Date} dt The date to display + */ + moveTo : function(dt, noRefresh){ + this.header.moveTo(dt, noRefresh); + this.body.moveTo(dt, noRefresh); + }, + + /** + * Updates the view to the next consecutive date(s) + */ + moveNext : function(noRefresh){ + this.header.moveNext(noRefresh); + this.body.moveNext(noRefresh); + }, + + /** + * Updates the view to the previous consecutive date(s) + */ + movePrev : function(noRefresh){ + this.header.movePrev(noRefresh); + this.body.movePrev(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){ + this.header.moveDays(value, noRefresh); + this.body.moveDays(value, noRefresh); + }, + + /** + * Updates the view to show today + */ + moveToday : function(noRefresh){ + this.header.moveToday(noRefresh); + this.body.moveToday(noRefresh); + } +}); + +Ext.reg('dayview', Ext.calendar.DayView); +/** + * @class Ext.calendar.WeekView + * @extends Ext.calendar.DayView + *

Displays a calendar view by week. This class does not usually need ot be used directly as you can + * use a {@link Ext.calendar.CalendarPanel CalendarPanel} to manage multiple calendar views at once including + * the week view.

+ * @constructor + * @param {Object} config The config object + */ +Ext.calendar.WeekView = Ext.extend(Ext.calendar.DayView, { + /** + * @cfg {Number} dayCount + * The number of days to display in the view (defaults to 7) + */ + dayCount: 7 +}); + +Ext.reg('weekview', Ext.calendar.WeekView);/** + * @class Ext.calendar.DateRangeField + * @extends Ext.form.Field + *

A combination field that includes start and end dates and times, as well as an optional all-day checkbox.

+ * @constructor + * @param {Object} config The config object + */ +Ext.calendar.DateRangeField = Ext.extend(Ext.form.Field, { + /** + * @cfg {String} toText + * The text to display in between the date/time fields (defaults to 'to') + */ + toText: 'to', + /** + * @cfg {String} toText + * The text to display as the label for the all day checkbox (defaults to 'All day') + */ + allDayText: 'All day', + + // private + onRender: function(ct, position) { + if (!this.el) { + this.startDate = new Ext.form.DateField({ + id: this.id + '-start-date', + format: 'n/j/Y', + width: 100, + listeners: { + 'change': { + fn: function() { + this.checkDates('date', 'start'); + }, + scope: this + } + } + }); + this.startTime = new Ext.form.TimeField({ + id: this.id + '-start-time', + hidden: this.showTimes === false, + labelWidth: 0, + hideLabel: true, + width: 90, + listeners: { + 'select': { + fn: function() { + this.checkDates('time', 'start'); + }, + scope: this + } + } + }); + this.endTime = new Ext.form.TimeField({ + id: this.id + '-end-time', + hidden: this.showTimes === false, + labelWidth: 0, + hideLabel: true, + width: 90, + listeners: { + 'select': { + fn: function() { + this.checkDates('time', 'end'); + }, + scope: this + } + } + }); + this.endDate = new Ext.form.DateField({ + id: this.id + '-end-date', + format: 'n/j/Y', + hideLabel: true, + width: 100, + listeners: { + 'change': { + fn: function() { + this.checkDates('date', 'end'); + }, + scope: this + } + } + }); + this.allDay = new Ext.form.Checkbox({ + id: this.id + '-allday', + hidden: this.showTimes === false || this.showAllDay === false, + boxLabel: this.allDayText, + handler: function(chk, checked) { + this.startTime.setVisible(!checked); + this.endTime.setVisible(!checked); + }, + scope: this + }); + this.toLabel = new Ext.form.Label({ + xtype: 'label', + id: this.id + '-to-label', + text: this.toText + }); + + this.fieldCt = new Ext.Container({ + autoEl: { + id: this.id + }, + //make sure the container el has the field's id + cls: 'ext-dt-range', + renderTo: ct, + layout: 'table', + layoutConfig: { + columns: 6 + }, + defaults: { + hideParent: true + }, + items: [ + this.startDate, + this.startTime, + this.toLabel, + this.endTime, + this.endDate, + this.allDay + ] + }); + + this.fieldCt.ownerCt = this; + this.el = this.fieldCt.getEl(); + this.items = new Ext.util.MixedCollection(); + this.items.addAll([this.startDate, this.endDate, this.toLabel, this.startTime, this.endTime, this.allDay]); + } + Ext.calendar.DateRangeField.superclass.onRender.call(this, ct, position); + }, + + // private + checkDates: function(type, startend) { + var startField = Ext.getCmp(this.id + '-start-' + type), + endField = Ext.getCmp(this.id + '-end-' + type), + startValue = this.getDT('start'), + endValue = this.getDT('end'); + + if (startValue > endValue) { + if (startend == 'start') { + endField.setValue(startValue); + } else { + startField.setValue(endValue); + this.checkDates(type, 'start'); + } + } + if (type == 'date') { + this.checkDates('time', startend); + } + }, + + /** + * Returns an array containing the following values in order:
    + *
  • DateTime :
    The start date/time
  • + *
  • DateTime :
    The end date/time
  • + *
  • Boolean :
    True if the dates are all-day, false + * if the time values should be used
    + * @return {Array} The array of return values + */ + getValue: function() { + return [ + this.getDT('start'), + this.getDT('end'), + this.allDay.getValue() + ]; + }, + + // private getValue helper + getDT: function(startend) { + var time = this[startend + 'Time'].getValue(), + dt = this[startend + 'Date'].getValue(); + + if (Ext.isDate(dt)) { + dt = dt.format(this[startend + 'Date'].format); + } + else { + return null; + }; + if (time != '' && this[startend + 'Time'].isVisible()) { + return Date.parseDate(dt + ' ' + time, this[startend + 'Date'].format + ' ' + this[startend + 'Time'].format); + } + return Date.parseDate(dt, this[startend + 'Date'].format); + + }, + + /** + * Sets the values to use in the date range. + * @param {Array/Date/Object} v The value(s) to set into the field. Valid types are as follows:
      + *
    • Array :
      An array containing, in order, a start date, end date and all-day flag. + * This array should exactly match the return type as specified by {@link #getValue}.
    • + *
    • DateTime :
      A single Date object, which will be used for both the start and + * end dates in the range. The all-day flag will be defaulted to false.
    • + *
    • Object :
      An object containing properties for StartDate, EndDate and IsAllDay + * as defined in {@link Ext.calendar.EventMappings}.
      + */ + setValue: function(v) { + if (Ext.isArray(v)) { + this.setDT(v[0], 'start'); + this.setDT(v[1], 'end'); + this.allDay.setValue( !! v[2]); + } + else if (Ext.isDate(v)) { + this.setDT(v, 'start'); + this.setDT(v, 'end'); + this.allDay.setValue(false); + } + else if (v[Ext.calendar.EventMappings.StartDate.name]) { + //object + this.setDT(v[Ext.calendar.EventMappings.StartDate.name], 'start'); + if (!this.setDT(v[Ext.calendar.EventMappings.EndDate.name], 'end')) { + this.setDT(v[Ext.calendar.EventMappings.StartDate.name], 'end'); + } + this.allDay.setValue( !! v[Ext.calendar.EventMappings.IsAllDay.name]); + } + }, + + // private setValue helper + setDT: function(dt, startend) { + if (dt && Ext.isDate(dt)) { + this[startend + 'Date'].setValue(dt); + this[startend + 'Time'].setValue(dt.format(this[startend + 'Time'].format)); + return true; + } + }, + + // inherited docs + isDirty: function() { + var dirty = false; + if (this.rendered && !this.disabled) { + this.items.each(function(item) { + if (item.isDirty()) { + dirty = true; + return false; + } + }); + } + return dirty; + }, + + // private + onDisable: function() { + this.delegateFn('disable'); + }, + + // private + onEnable: function() { + this.delegateFn('enable'); + }, + + // inherited docs + reset: function() { + this.delegateFn('reset'); + }, + + // private + delegateFn: function(fn) { + this.items.each(function(item) { + if (item[fn]) { + item[fn](); + } + }); + }, + + // private + beforeDestroy: function() { + Ext.destroy(this.fieldCt); + Ext.calendar.DateRangeField.superclass.beforeDestroy.call(this); + }, + + /** + * @method getRawValue + * @hide + */ + getRawValue: Ext.emptyFn, + /** + * @method setRawValue + * @hide + */ + setRawValue: Ext.emptyFn +}); + +Ext.reg('daterangefield', Ext.calendar.DateRangeField); +/** + * @class Ext.calendar.ReminderField + * @extends Ext.form.ComboBox + *

      A custom combo used for choosing a reminder setting for an event.

      + *

      This is pretty much a standard combo that is simply pre-configured for the options needed by the + * calendar components. The default configs are as follows:

      
      +    width: 200,
      +    fieldLabel: 'Reminder',
      +    mode: 'local',
      +    triggerAction: 'all',
      +    forceSelection: true,
      +    displayField: 'desc',
      +    valueField: 'value'
      +
      + * @constructor + * @param {Object} config The config object + */ +Ext.calendar.ReminderField = Ext.extend(Ext.form.ComboBox, { + width: 200, + fieldLabel: 'Reminder', + mode: 'local', + triggerAction: 'all', + forceSelection: true, + displayField: 'desc', + valueField: 'value', + + // private + initComponent: function() { + Ext.calendar.ReminderField.superclass.initComponent.call(this); + + this.store = this.store || new Ext.data.ArrayStore({ + fields: ['value', 'desc'], + idIndex: 0, + data: [ + ['', 'None'], + ['0', 'At start time'], + ['5', '5 minutes before start'], + ['15', '15 minutes before start'], + ['30', '30 minutes before start'], + ['60', '1 hour before start'], + ['90', '1.5 hours before start'], + ['120', '2 hours before start'], + ['180', '3 hours before start'], + ['360', '6 hours before start'], + ['720', '12 hours before start'], + ['1440', '1 day before start'], + ['2880', '2 days before start'], + ['4320', '3 days before start'], + ['5760', '4 days before start'], + ['7200', '5 days before start'], + ['10080', '1 week before start'], + ['20160', '2 weeks before start'] + ] + }); + }, + + // inherited docs + initValue: function() { + if (this.value !== undefined) { + this.setValue(this.value); + } + else { + this.setValue(''); + } + this.originalValue = this.getValue(); + } +}); + +Ext.reg('reminderfield', Ext.calendar.ReminderField); +/** + * @class Ext.calendar.EventEditForm + * @extends Ext.form.FormPanel + *

      A custom form used for detailed editing of events.

      + *

      This is pretty much a standard form that is simply pre-configured for the options needed by the + * calendar components. It is also configured to automatically bind records of type {@link Ext.calendar.EventRecord} + * to and from the form.

      + *

      This form also provides custom events specific to the calendar so that other calendar components can be easily + * notified when an event has been edited via this component.

      + *

      The default configs are as follows:

      
      +    labelWidth: 65,
      +    title: 'Event Form',
      +    titleTextAdd: 'Add Event',
      +    titleTextEdit: 'Edit Event',
      +    bodyStyle: 'background:transparent;padding:20px 20px 10px;',
      +    border: false,
      +    buttonAlign: 'center',
      +    autoHeight: true,
      +    cls: 'ext-evt-edit-form',
      +
      + * @constructor + * @param {Object} config The config object + */ +Ext.calendar.EventEditForm = Ext.extend(Ext.form.FormPanel, { + labelWidth: 65, + title: 'Event Form', + titleTextAdd: 'Add Event', + titleTextEdit: 'Edit Event', + bodyStyle: 'background:transparent;padding:20px 20px 10px;', + border: false, + buttonAlign: 'center', + autoHeight: true, + // to allow for the notes field to autogrow + cls: 'ext-evt-edit-form', + + // private properties: + newId: 10000, + layout: 'column', + + // private + initComponent: function() { + + this.addEvents({ + /** + * @event eventadd + * Fires after a new event is added + * @param {Ext.calendar.EventEditForm} this + * @param {Ext.calendar.EventRecord} rec The new {@link Ext.calendar.EventRecord record} that was added + */ + eventadd: true, + /** + * @event eventupdate + * Fires after an existing event is updated + * @param {Ext.calendar.EventEditForm} this + * @param {Ext.calendar.EventRecord} rec The new {@link Ext.calendar.EventRecord record} that was updated + */ + eventupdate: true, + /** + * @event eventdelete + * Fires after an event is deleted + * @param {Ext.calendar.EventEditForm} this + * @param {Ext.calendar.EventRecord} rec The new {@link Ext.calendar.EventRecord record} that was deleted + */ + eventdelete: true, + /** + * @event eventcancel + * Fires after an event add/edit operation is canceled by the user and no store update took place + * @param {Ext.calendar.EventEditForm} this + * @param {Ext.calendar.EventRecord} rec The new {@link Ext.calendar.EventRecord record} that was canceled + */ + eventcancel: true + }); + + this.titleField = new Ext.form.TextField({ + fieldLabel: 'Title', + name: Ext.calendar.EventMappings.Title.name, + anchor: '90%' + }); + this.dateRangeField = new Ext.calendar.DateRangeField({ + fieldLabel: 'When', + anchor: '90%' + }); + this.reminderField = new Ext.calendar.ReminderField({ + name: 'Reminder' + }); + this.notesField = new Ext.form.TextArea({ + fieldLabel: 'Notes', + name: Ext.calendar.EventMappings.Notes.name, + grow: true, + growMax: 150, + anchor: '100%' + }); + this.locationField = new Ext.form.TextField({ + fieldLabel: 'Location', + name: Ext.calendar.EventMappings.Location.name, + anchor: '100%' + }); + this.urlField = new Ext.form.TextField({ + fieldLabel: 'Web Link', + name: Ext.calendar.EventMappings.Url.name, + anchor: '100%' + }); + + var leftFields = [this.titleField, this.dateRangeField, this.reminderField], + rightFields = [this.notesField, this.locationField, this.urlField]; + + if (this.calendarStore) { + this.calendarField = new Ext.calendar.CalendarPicker({ + store: this.calendarStore, + name: Ext.calendar.EventMappings.CalendarId.name + }); + leftFields.splice(2, 0, this.calendarField); + }; + + this.items = [{ + id: 'left-col', + columnWidth: 0.65, + layout: 'form', + border: false, + items: leftFields + }, + { + id: 'right-col', + columnWidth: 0.35, + layout: 'form', + border: false, + items: rightFields + }]; + + this.fbar = [{ + text: 'Save', + scope: this, + handler: this.onSave + }, + { + cls: 'ext-del-btn', + text: 'Delete', + scope: this, + handler: this.onDelete + }, + { + text: 'Cancel', + scope: this, + handler: this.onCancel + }]; + + Ext.calendar.EventEditForm.superclass.initComponent.call(this); + }, + + // inherited docs + loadRecord: function(rec) { + this.form.loadRecord.apply(this.form, arguments); + this.activeRecord = rec; + this.dateRangeField.setValue(rec.data); + if (this.calendarStore) { + this.form.setValues({ + 'calendar': rec.data[Ext.calendar.EventMappings.CalendarId.name] + }); + } + this.isAdd = !!rec.data[Ext.calendar.EventMappings.IsNew.name]; + if (this.isAdd) { + rec.markDirty(); + this.setTitle(this.titleTextAdd); + Ext.select('.ext-del-btn').setDisplayed(false); + } + else { + this.setTitle(this.titleTextEdit); + Ext.select('.ext-del-btn').setDisplayed(true); + } + this.titleField.focus(); + }, + + // inherited docs + updateRecord: function() { + var dates = this.dateRangeField.getValue(); + + this.form.updateRecord(this.activeRecord); + this.activeRecord.set(Ext.calendar.EventMappings.StartDate.name, dates[0]); + this.activeRecord.set(Ext.calendar.EventMappings.EndDate.name, dates[1]); + this.activeRecord.set(Ext.calendar.EventMappings.IsAllDay.name, dates[2]); + }, + + // private + onCancel: function() { + this.cleanup(true); + this.fireEvent('eventcancel', this, this.activeRecord); + }, + + // private + cleanup: function(hide) { + if (this.activeRecord && this.activeRecord.dirty) { + this.activeRecord.reject(); + } + delete this.activeRecord; + + if (this.form.isDirty()) { + this.form.reset(); + } + }, + + // private + onSave: function() { + if (!this.form.isValid()) { + return; + } + this.updateRecord(); + + if (!this.activeRecord.dirty) { + this.onCancel(); + return; + } + + this.fireEvent(this.isAdd ? 'eventadd': 'eventupdate', this, this.activeRecord); + }, + + // private + onDelete: function() { + this.fireEvent('eventdelete', this, this.activeRecord); + } +}); + +Ext.reg('eventeditform', Ext.calendar.EventEditForm); +/** + * @class Ext.calendar.EventEditWindow + * @extends Ext.Window + *

      A custom window containing a basic edit form used for quick editing of events.

      + *

      This window also provides custom events specific to the calendar so that other calendar components can be easily + * notified when an event has been edited via this component.

      + * @constructor + * @param {Object} config The config object + */ +Ext.calendar.EventEditWindow = function(config) { + var formPanelCfg = { + xtype: 'form', + labelWidth: 65, + frame: false, + bodyStyle: 'background:transparent;padding:5px 10px 10px;', + bodyBorder: false, + border: false, + items: [{ + id: 'title', + name: Ext.calendar.EventMappings.Title.name, + fieldLabel: 'Title', + xtype: 'textfield', + anchor: '100%' + }, + { + xtype: 'daterangefield', + id: 'date-range', + anchor: '100%', + fieldLabel: 'When' + }] + }; + + if (config.calendarStore) { + this.calendarStore = config.calendarStore; + delete config.calendarStore; + + formPanelCfg.items.push({ + xtype: 'calendarpicker', + id: 'calendar', + name: 'calendar', + anchor: '100%', + store: this.calendarStore + }); + } + + Ext.calendar.EventEditWindow.superclass.constructor.call(this, Ext.apply({ + titleTextAdd: 'Add Event', + titleTextEdit: 'Edit Event', + width: 600, + autocreate: true, + border: true, + closeAction: 'hide', + modal: false, + resizable: false, + buttonAlign: 'left', + savingMessage: 'Saving changes...', + deletingMessage: 'Deleting event...', + + fbar: [{ + xtype: 'tbtext', + text: 'Edit Details...' + }, + '->', { + text: 'Save', + disabled: false, + handler: this.onSave, + scope: this + }, + { + id: 'delete-btn', + text: 'Delete', + disabled: false, + handler: this.onDelete, + scope: this, + hideMode: 'offsets' + }, + { + text: 'Cancel', + disabled: false, + handler: this.onCancel, + scope: this + }], + items: formPanelCfg + }, + config)); +}; + +Ext.extend(Ext.calendar.EventEditWindow, Ext.Window, { + // private + newId: 10000, + + // private + initComponent: function() { + Ext.calendar.EventEditWindow.superclass.initComponent.call(this); + + this.formPanel = this.items.items[0]; + + this.addEvents({ + /** + * @event eventadd + * Fires after a new event is added + * @param {Ext.calendar.EventEditWindow} this + * @param {Ext.calendar.EventRecord} rec The new {@link Ext.calendar.EventRecord record} that was added + */ + eventadd: true, + /** + * @event eventupdate + * Fires after an existing event is updated + * @param {Ext.calendar.EventEditWindow} this + * @param {Ext.calendar.EventRecord} rec The new {@link Ext.calendar.EventRecord record} that was updated + */ + eventupdate: true, + /** + * @event eventdelete + * Fires after an event is deleted + * @param {Ext.calendar.EventEditWindow} this + * @param {Ext.calendar.EventRecord} rec The new {@link Ext.calendar.EventRecord record} that was deleted + */ + eventdelete: true, + /** + * @event eventcancel + * Fires after an event add/edit operation is canceled by the user and no store update took place + * @param {Ext.calendar.EventEditWindow} this + * @param {Ext.calendar.EventRecord} rec The new {@link Ext.calendar.EventRecord record} that was canceled + */ + eventcancel: true, + /** + * @event editdetails + * Fires when the user selects the option in this window to continue editing in the detailed edit form + * (by default, an instance of {@link Ext.calendar.EventEditForm}. Handling code should hide this window + * and transfer the current event record to the appropriate instance of the detailed form by showing it + * and calling {@link Ext.calendar.EventEditForm#loadRecord loadRecord}. + * @param {Ext.calendar.EventEditWindow} this + * @param {Ext.calendar.EventRecord} rec The {@link Ext.calendar.EventRecord record} that is currently being edited + */ + editdetails: true + }); + }, + + // private + afterRender: function() { + Ext.calendar.EventEditWindow.superclass.afterRender.call(this); + + this.el.addClass('ext-cal-event-win'); + + Ext.get('tblink').on('click', + function(e) { + e.stopEvent(); + this.updateRecord(); + this.fireEvent('editdetails', this, this.activeRecord); + }, + this); + }, + + /** + * Shows the window, rendering it first if necessary, or activates it and brings it to front if hidden. + * @param {Ext.data.Record/Object} o Either a {@link Ext.data.Record} if showing the form + * for an existing event in edit mode, or a plain object containing a StartDate property (and + * optionally an EndDate property) for showing the form in add mode. + * @param {String/Element} animateTarget (optional) The target element or id from which the window should + * animate while opening (defaults to null with no animation) + * @return {Ext.Window} this + */ + show: function(o, animateTarget) { + // Work around the CSS day cell height hack needed for initial render in IE8/strict: + var anim = (Ext.isIE8 && Ext.isStrict) ? null: animateTarget; + + Ext.calendar.EventEditWindow.superclass.show.call(this, anim, + function() { + Ext.getCmp('title').focus(false, 100); + }); + Ext.getCmp('delete-btn')[o.data && o.data[Ext.calendar.EventMappings.EventId.name] ? 'show': 'hide'](); + + var rec, + f = this.formPanel.form; + + if (o.data) { + rec = o; + this.isAdd = !!rec.data[Ext.calendar.EventMappings.IsNew.name]; + if (this.isAdd) { + // Enable adding the default record that was passed in + // if it's new even if the user makes no changes + rec.markDirty(); + this.setTitle(this.titleTextAdd); + } + else { + this.setTitle(this.titleTextEdit); + } + + f.loadRecord(rec); + } + else { + this.isAdd = true; + this.setTitle(this.titleTextAdd); + + var M = Ext.calendar.EventMappings, + eventId = M.EventId.name, + start = o[M.StartDate.name], + end = o[M.EndDate.name] || start.add('h', 1); + + rec = new Ext.calendar.EventRecord(); + rec.data[M.EventId.name] = this.newId++; + rec.data[M.StartDate.name] = start; + rec.data[M.EndDate.name] = end; + rec.data[M.IsAllDay.name] = !!o[M.IsAllDay.name] || start.getDate() != end.clone().add(Date.MILLI, 1).getDate(); + rec.data[M.IsNew.name] = true; + + f.reset(); + f.loadRecord(rec); + } + + if (this.calendarStore) { + Ext.getCmp('calendar').setValue(rec.data[Ext.calendar.EventMappings.CalendarId.name]); + } + Ext.getCmp('date-range').setValue(rec.data); + this.activeRecord = rec; + + return this; + }, + + // private + roundTime: function(dt, incr) { + incr = incr || 15; + var m = parseInt(dt.getMinutes(), 10); + return dt.add('mi', incr - (m % incr)); + }, + + // private + onCancel: function() { + this.cleanup(true); + this.fireEvent('eventcancel', this); + }, + + // private + cleanup: function(hide) { + if (this.activeRecord && this.activeRecord.dirty) { + this.activeRecord.reject(); + } + delete this.activeRecord; + + if (hide === true) { + // Work around the CSS day cell height hack needed for initial render in IE8/strict: + //var anim = afterDelete || (Ext.isIE8 && Ext.isStrict) ? null : this.animateTarget; + this.hide(); + } + }, + + // private + updateRecord: function() { + var f = this.formPanel.form, + dates = Ext.getCmp('date-range').getValue(), + M = Ext.calendar.EventMappings; + + f.updateRecord(this.activeRecord); + this.activeRecord.set(M.StartDate.name, dates[0]); + this.activeRecord.set(M.EndDate.name, dates[1]); + this.activeRecord.set(M.IsAllDay.name, dates[2]); + this.activeRecord.set(M.CalendarId.name, this.formPanel.form.findField('calendar').getValue()); + }, + + // private + onSave: function() { + if (!this.formPanel.form.isValid()) { + return; + } + this.updateRecord(); + + if (!this.activeRecord.dirty) { + this.onCancel(); + return; + } + + this.fireEvent(this.isAdd ? 'eventadd': 'eventupdate', this, this.activeRecord); + }, + + // private + onDelete: function() { + this.fireEvent('eventdelete', this, this.activeRecord); + } +});/** + * @class Ext.calendar.CalendarPanel + * @extends Ext.Panel + *

      This is the default container for Ext calendar views. It supports day, week and month views as well + * as a built-in event edit form. The only requirement for displaying a calendar is passing in a valid + * {@link #calendarStore} config containing records of type {@link Ext.calendar.EventRecord EventRecord}. In order + * to make the calendar interactive (enable editing, drag/drop, etc.) you can handle any of the various + * events fired by the underlying views and exposed through the CalendarPanel.

      + * {@link #layoutConfig} option if needed.

      + * @constructor + * @param {Object} config The config object + * @xtype calendarpanel + */ +Ext.calendar.CalendarPanel = Ext.extend(Ext.Panel, { + /** + * @cfg {Boolean} showDayView + * True to include the day view (and toolbar button), false to hide them (defaults to true). + */ + showDayView: true, + /** + * @cfg {Boolean} showWeekView + * True to include the week view (and toolbar button), false to hide them (defaults to true). + */ + showWeekView: true, + /** + * @cfg {Boolean} showMonthView + * True to include the month view (and toolbar button), false to hide them (defaults to true). + * If the day and week views are both hidden, the month view will show by default even if + * this config is false. + */ + showMonthView: true, + /** + * @cfg {Boolean} showNavBar + * True to display the calendar navigation toolbar, false to hide it (defaults to true). Note that + * if you hide the default navigation toolbar you'll have to provide an alternate means of navigating the calendar. + */ + showNavBar: true, + /** + * @cfg {String} todayText + * Alternate text to use for the 'Today' nav bar button. + */ + todayText: 'Today', + /** + * @cfg {Boolean} showTodayText + * True to show the value of {@link #todayText} instead of today's date in the calendar's current day box, + * false to display the day number(defaults to true). + */ + showTodayText: true, + /** + * @cfg {Boolean} showTime + * True to display the current time next to the date in the calendar's current day box, false to not show it + * (defaults to true). + */ + showTime: true, + /** + * @cfg {String} dayText + * Alternate text to use for the 'Day' nav bar button. + */ + dayText: 'Day', + /** + * @cfg {String} weekText + * Alternate text to use for the 'Week' nav bar button. + */ + weekText: 'Week', + /** + * @cfg {String} monthText + * Alternate text to use for the 'Month' nav bar button. + */ + monthText: 'Month', + + // private + layoutConfig: { + layoutOnCardChange: true, + deferredRender: true + }, + + // private property + startDate: new Date(), + + // private + initComponent: function() { + this.tbar = { + cls: 'ext-cal-toolbar', + border: true, + buttonAlign: 'center', + items: [{ + id: this.id + '-tb-prev', + handler: this.onPrevClick, + scope: this, + iconCls: 'x-tbar-page-prev' + }] + }; + + this.viewCount = 0; + + if (this.showDayView) { + this.tbar.items.push({ + id: this.id + '-tb-day', + text: this.dayText, + handler: this.onDayClick, + scope: this, + toggleGroup: 'tb-views' + }); + this.viewCount++; + } + if (this.showWeekView) { + this.tbar.items.push({ + id: this.id + '-tb-week', + text: this.weekText, + handler: this.onWeekClick, + scope: this, + toggleGroup: 'tb-views' + }); + this.viewCount++; + } + if (this.showMonthView || this.viewCount == 0) { + this.tbar.items.push({ + id: this.id + '-tb-month', + text: this.monthText, + handler: this.onMonthClick, + scope: this, + toggleGroup: 'tb-views' + }); + this.viewCount++; + this.showMonthView = true; + } + this.tbar.items.push({ + id: this.id + '-tb-next', + handler: this.onNextClick, + scope: this, + iconCls: 'x-tbar-page-next' + }); + this.tbar.items.push('->'); + + var idx = this.viewCount - 1; + this.activeItem = this.activeItem === undefined ? idx: (this.activeItem > idx ? idx: this.activeItem); + + if (this.showNavBar === false) { + delete this.tbar; + this.addClass('x-calendar-nonav'); + } + + Ext.calendar.CalendarPanel.superclass.initComponent.call(this); + + this.addEvents({ + /** + * @event eventadd + * Fires after a new event is added to the underlying store + * @param {Ext.calendar.CalendarPanel} this + * @param {Ext.calendar.EventRecord} rec The new {@link Ext.calendar.EventRecord record} that was added + */ + eventadd: true, + /** + * @event eventupdate + * Fires after an existing event is updated + * @param {Ext.calendar.CalendarPanel} this + * @param {Ext.calendar.EventRecord} rec The new {@link Ext.calendar.EventRecord record} that was updated + */ + eventupdate: true, + /** + * @event eventdelete + * Fires after an event is removed from the underlying store + * @param {Ext.calendar.CalendarPanel} this + * @param {Ext.calendar.EventRecord} rec The new {@link Ext.calendar.EventRecord record} that was removed + */ + eventdelete: true, + /** + * @event eventcancel + * Fires after an event add/edit operation is canceled by the user and no store update took place + * @param {Ext.calendar.CalendarPanel} this + * @param {Ext.calendar.EventRecord} rec The new {@link Ext.calendar.EventRecord record} that was canceled + */ + eventcancel: true, + /** + * @event viewchange + * Fires after a different calendar view is activated (but not when the event edit form is activated) + * @param {Ext.calendar.CalendarPanel} this + * @param {Ext.CalendarView} view The view being activated (any valid {@link Ext.calendar.CalendarView CalendarView} subclass) + * @param {Object} info Extra information about the newly activated view. This is a plain object + * with following properties:
        + *
      • activeDate :
        The currently-selected date
      • + *
      • viewStart :
        The first date in the new view range
      • + *
      • viewEnd :
        The last date in the new view range
      • + *
      + */ + viewchange: true + + // + // NOTE: CalendarPanel also relays the following events from contained views as if they originated from this: + // + /** + * @event eventsrendered + * Fires after events are finished rendering in the view + * @param {Ext.calendar.CalendarPanel} this + */ + /** + * @event eventclick + * Fires after the user clicks on an event element + * @param {Ext.calendar.CalendarPanel} 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 + */ + /** + * @event eventover + * Fires anytime the mouse is over an event element + * @param {Ext.calendar.CalendarPanel} 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 + */ + /** + * @event eventout + * Fires anytime the mouse exits an event element + * @param {Ext.calendar.CalendarPanel} 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 + */ + /** + * @event datechange + * Fires after the start date of the view changes + * @param {Ext.calendar.CalendarPanel} 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 + */ + /** + * @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.CalendarPanel} 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()). + */ + /** + * @event eventmove + * Fires after an event element is dragged by the user and dropped in a new position + * @param {Ext.calendar.CalendarPanel} this + * @param {Ext.calendar.EventRecord} rec The {@link Ext.calendar.EventRecord record} for the event that was moved with + * updated start and end dates + */ + /** + * @event initdrag + * Fires when a drag operation is initiated in the view + * @param {Ext.calendar.CalendarPanel} this + */ + /** + * @event eventresize + * Fires after the user drags the resize handle of an event to resize it + * @param {Ext.calendar.CalendarPanel} this + * @param {Ext.calendar.EventRecord} rec The {@link Ext.calendar.EventRecord record} for the event that was resized + * containing the updated start and end dates + */ + /** + * @event dayclick + * Fires after the user clicks within a day/week view container and not on an event element + * @param {Ext.calendar.CalendarPanel} this + * @param {Date} dt The date/time that was clicked on + * @param {Boolean} allday True if the day clicked on represents an all-day box, else false. + * @param {Ext.Element} el The Element that was clicked on + */ + }); + + this.layout = 'card'; + // do not allow override + if (this.showDayView) { + var day = Ext.apply({ + xtype: 'dayview', + title: this.dayText, + showToday: this.showToday, + showTodayText: this.showTodayText, + showTime: this.showTime + }, + this.dayViewCfg); + + day.id = this.id + '-day'; + day.store = day.store || this.eventStore; + this.initEventRelay(day); + this.add(day); + } + if (this.showWeekView) { + var wk = Ext.applyIf({ + xtype: 'weekview', + title: this.weekText, + showToday: this.showToday, + showTodayText: this.showTodayText, + showTime: this.showTime + }, + this.weekViewCfg); + + wk.id = this.id + '-week'; + wk.store = wk.store || this.eventStore; + this.initEventRelay(wk); + this.add(wk); + } + if (this.showMonthView) { + var month = Ext.applyIf({ + xtype: 'monthview', + title: this.monthText, + showToday: this.showToday, + showTodayText: this.showTodayText, + showTime: this.showTime, + listeners: { + 'weekclick': { + fn: function(vw, dt) { + this.showWeek(dt); + }, + scope: this + } + } + }, + this.monthViewCfg); + + month.id = this.id + '-month'; + month.store = month.store || this.eventStore; + this.initEventRelay(month); + this.add(month); + } + + this.add(Ext.applyIf({ + xtype: 'eventeditform', + id: this.id + '-edit', + calendarStore: this.calendarStore, + listeners: { + 'eventadd': { + scope: this, + fn: this.onEventAdd + }, + 'eventupdate': { + scope: this, + fn: this.onEventUpdate + }, + 'eventdelete': { + scope: this, + fn: this.onEventDelete + }, + 'eventcancel': { + scope: this, + fn: this.onEventCancel + } + } + }, + this.editViewCfg)); + }, + + // private + initEventRelay: function(cfg) { + cfg.listeners = cfg.listeners || {}; + cfg.listeners.afterrender = { + fn: function(c) { + // relay the view events so that app code only has to handle them in one place + this.relayEvents(c, ['eventsrendered', 'eventclick', 'eventover', 'eventout', 'dayclick', + 'eventmove', 'datechange', 'rangeselect', 'eventdelete', 'eventresize', 'initdrag']); + }, + scope: this, + single: true + }; + }, + + // private + afterRender: function() { + Ext.calendar.CalendarPanel.superclass.afterRender.call(this); + this.fireViewChange(); + }, + + // private + onLayout: function() { + Ext.calendar.CalendarPanel.superclass.onLayout.call(this); + if (!this.navInitComplete) { + this.updateNavState(); + this.navInitComplete = true; + } + }, + + // private + onEventAdd: function(form, rec) { + rec.data[Ext.calendar.EventMappings.IsNew.name] = false; + this.eventStore.add(rec); + this.hideEditForm(); + this.fireEvent('eventadd', this, rec); + }, + + // private + onEventUpdate: function(form, rec) { + rec.commit(); + this.hideEditForm(); + this.fireEvent('eventupdate', this, rec); + }, + + // private + onEventDelete: function(form, rec) { + this.eventStore.remove(rec); + this.hideEditForm(); + this.fireEvent('eventdelete', this, rec); + }, + + // private + onEventCancel: function(form, rec) { + this.hideEditForm(); + this.fireEvent('eventcancel', this, rec); + }, + + /** + * Shows the built-in event edit form for the passed in event record. This method automatically + * hides the calendar views and navigation toolbar. To return to the calendar, call {@link #hideEditForm}. + * @param {Ext.calendar.EventRecord} record The event record to edit + * @return {Ext.calendar.CalendarPanel} this + */ + showEditForm: function(rec) { + this.preEditView = this.layout.activeItem.id; + this.setActiveView(this.id + '-edit'); + this.layout.activeItem.loadRecord(rec); + return this; + }, + + /** + * Hides the built-in event edit form and returns to the previous calendar view. If the edit form is + * not currently visible this method has no effect. + * @return {Ext.calendar.CalendarPanel} this + */ + hideEditForm: function() { + if (this.preEditView) { + this.setActiveView(this.preEditView); + delete this.preEditView; + } + return this; + }, + + // private + setActiveView: function(id) { + var l = this.layout; + l.setActiveItem(id); + + if (id == this.id + '-edit') { + this.getTopToolbar().hide(); + this.doLayout(); + } + else { + l.activeItem.refresh(); + this.getTopToolbar().show(); + this.updateNavState(); + } + this.activeView = l.activeItem; + this.fireViewChange(); + }, + + // private + fireViewChange: function() { + var info = null, + view = this.layout.activeItem; + + if (view.getViewBounds) { + vb = view.getViewBounds(); + info = { + activeDate: view.getStartDate(), + viewStart: vb.start, + viewEnd: vb.end + }; + }; + this.fireEvent('viewchange', this, view, info); + }, + + // private + updateNavState: function() { + if (this.showNavBar !== false) { + var item = this.layout.activeItem, + suffix = item.id.split(this.id + '-')[1]; + + var btn = Ext.getCmp(this.id + '-tb-' + suffix); + btn.toggle(true); + } + }, + + /** + * Sets the start date for the currently-active calendar view. + * @param {Date} dt + */ + setStartDate: function(dt) { + this.layout.activeItem.setStartDate(dt, true); + this.updateNavState(); + this.fireViewChange(); + }, + + // private + showWeek: function(dt) { + this.setActiveView(this.id + '-week'); + this.setStartDate(dt); + }, + + // private + onPrevClick: function() { + this.startDate = this.layout.activeItem.movePrev(); + this.updateNavState(); + this.fireViewChange(); + }, + + // private + onNextClick: function() { + this.startDate = this.layout.activeItem.moveNext(); + this.updateNavState(); + this.fireViewChange(); + }, + + // private + onDayClick: function() { + this.setActiveView(this.id + '-day'); + }, + + // private + onWeekClick: function() { + this.setActiveView(this.id + '-week'); + }, + + // private + onMonthClick: function() { + this.setActiveView(this.id + '-month'); + }, + + /** + * Return the calendar view that is currently active, which will be a subclass of + * {@link Ext.calendar.CalendarView CalendarView}. + * @return {Ext.calendar.CalendarView} The active view + */ + getActiveView: function() { + return this.layout.activeItem; + } +}); + +Ext.reg('calendarpanel', Ext.calendar.CalendarPanel); \ No newline at end of file