Ext.namespace('Zarafa.calendar.ui');
/**
* @class Zarafa.calendar.ui.AbstractCalendarView
* @extends Zarafa.core.ui.View
*
* Base class for calendar views. It supports containing multiple {@link Zarafa.hierarchy.data.MAPIFolderRecord folders},
* and can display the different folders side-by-side using {@link Zarafa.calendar.ui.CalendarTabView tabs}.
*
* TODO refactor the use of separate startDate, dueDate parameters in the events to use the DateRange object.
*/
Zarafa.calendar.ui.AbstractCalendarView = Ext.extend(Zarafa.core.ui.View, {
/**
* @cfg {Zarafa.calendar.ui.DateRangeSelectionModel} rangeSelectionModel The selection model
* used for selecting a particular {@link Zarafa.core.DateRange daterange}.
*/
rangeSelectionModel : undefined,
/**
* @cfg {Zarafa.calendar.ui.AppointmentSelectionModel} selectionModel The selection model
* used for selecting {@link Zarafa.core.data.IPMRecord records}.
*/
selectionModel : undefined,
/**
* @cfg {Zarafa.calendar.CalendarContextModel} contextModel A reference to the context model of the parent view
*/
contextModel : undefined,
/**
* The array of {@link Zarafa.hierarchy.data.MAPIFolderRecord folders} which are opened
* by this {@link Zarafa.core.ui.View view}.
* @property
* @type Array
*/
folders : undefined,
/**
* The currently selected {@link Zarafa.hierarchy.data.MAPIFolderRecord folder} within
* this {@link Zarafa.core.ui.View view}. If set, this must be an element
* from the {@link #folders} array.
* @property
* @type Zarafa.hierarchy.data.MAPIFolderRecord
*/
selectedFolder : undefined,
/**
* The array of {@link Zarafa.calendar.ui.CalendarTabView tabs} which
* are visible within this {@link Zarafa.core.ui.View view}. Each
* {@link Zarafa.hierarchy.data.MAPIFolderRecord folder} from the {@link #folders} array
* will have one {@link Zarafa.calendar.ui.CalendarTabView tab} assigned
* to it.
* @property
* @type Zarafa.calendar.ui.CalendarTabView
*/
tabs : undefined,
/**
* The {@link Zarafa.calendar.ui.SelectionRangeView view} element which displays
* the currently selected period in this calendar {@link Zarafa.core.ui.View view}.
* @property
* @type Zarafa.calendar.ui.SelectionRangeView
*/
selectionView : undefined,
/**
* The {@link Zarafa.calendar.ui.TextEditView view} which is enabled when the
* user starts typing after selecting a period. Using this view we add support
* for the quick-appointments which are placed into the calendar.
* @property
* @type Zarafa.calendar.ui.TextEditView
*/
textEditView : undefined,
/**
* The array of {@link Zarafa.calendar.ui.AppointmentView appointments} which
* are visible within this calendar view.
* @property
* @type Array
*/
appointments : undefined,
/**
* The offset all elements must have from the left side of the {@link #container}. This
* offset is configured before {@link #layout} by {@link #setLeftMargin}.
* @property
* @type Number
*/
leftOffset : 0,
/**
* The total with for the tab to use. This must at least be the result of {@link #getMinimumWidth}.
* This is configured before {@link #layout} by {@link #setWidth}.
* @property
* @type Number
*/
width: 0,
/**
* @cfg {Number} appointmentBodyLeftMargin The left margin which must be applied to
* the {@link Zarafa.calendar.ui.AppointmentView appointments} which are renderd within this
* calendar when the 'useMargin' argument to {@link #dateRangeToBodyBounds} is true.
*/
appointmentBodyLeftMargin : 0,
/**
* @cfg {Number} appointmentBodyRightMargin The right margin which must be applied to
* the {@link Zarafa.calendar.ui.AppointmentView appointments} which are rendered within this
* calendar when the 'useMargin' argument to {@link #dateRangeToBodyBounds} is true.
*/
appointmentBodyRightMargin : 0,
* @cfg {Number} appointmentHeaderLeftMargins The left margin which must be applied to
* the {@link Zarafa.calendar.ui.AppointmentView appointments} which are rendered within
* the header of this calendar when the 'useMargin' argument to {@link #dateRangeToHeaderBounds} is true.
*/
appointmentHeaderLeftMargin : 6,
* @cfg {Number} appointmentHeaderRightMargins The right margin which must be applied to
* the {@link Zarafa.calendar.ui.AppointmentView appointments} which are rendered within
* the header of this calendar when the 'useMargin' argument to {@link #dateRangeToHeaderBounds} is true.
*/
appointmentHeaderRightMargin : 6,
/**
* The body part of the calendar. This contains the view in which all {@link #appointments}
* are displayed. This field is created using {@link #createDiv} during {@link #render}.
* @property
* @type Ext.Element
*/
body : undefined,
* The header part of the calendar. This contains extra information (like the day, or weekday).
* This field is created using {@link #createDiv} during {@link #render}.
* @property
* @type Ext.Element
*/
header : undefined,
* The left border of the {@link #header}.
* This field is created using {@link #createDiv} during {@link #render}.
* @property
* @type Ext.Element
*/
headerBorderLeft : undefined,
* The right border of the {@link #header}.
* This field is created using {@link #createDiv} during {@link #render}.
* @property
* @type Ext.Element
*/
headerBorderRight : undefined,
/**
* The left border of the {@link #body}.
* This field is created using {@link #createDiv} during {@link #render}.
* @property
* @type Ext.Element
*/
borderLeft : undefined,
/**
* The right border of the {@link #body}.
* This field is created using {@link #createDiv} during {@link #render}.
* @property
* @type Ext.Element
*/
borderRight : undefined,
/**
* The bottom border of the {@link #body}.
* This field is created using {@link #createDiv} during {@link #render}.
* @property
* @type Ext.Element
*/
borderBottom : undefined,
/**
* The tab area in which the {@link #tabs} will be rendered.
* This field is created using {@link #createDiv} during {@link #render}.
* @property
* @type Ext.Element
*/
tabArea : undefined,
/**
* @cfg {Boolean} enableDD true to enable drag & drop in both body and header
*/
enableDD : false,
/**
* @cfg {Boolean} enableBodyDD true to enable drag and drop in the body
*/
enableBodyDD : false,
* @cfg {Boolean} enableHeaderDD true to enable drag and drop in the header
*/
enableHeaderDD : false,
/**
* @cfg {Boolean} enableBodyDrag true to enable just drag in the body
*/
enableBodyDrag : false,
* @cfg {Boolean} enableHeaderDrag true to enable just drag in the header
*/
enableHeaderDrag : false,
/**
* @cfg {Boolean} enableBodyDrop true to enable just drop in the body
*/
enableBodyDrop : false,
* @cfg {Boolean} enableHeaderDrop true to enable just drop in the body
*/
enableHeaderDrop : false,
/**
* @cfg {String} ddGroup The DD group this TreePanel belongs to
*/
ddGroup : 'AppointmentDD',
/**
* The DragZone which will be installed on the {@link #body}. This is initialized
* when either {@link #enableBodyDD} or {@link #enableBodyDrag} is enabled. It can optionally
* be configured using {@link #bodyDragConfig}.
* @property
* @type Zarafa.calendar.ui.CalendarViewDragZone
* @private
*/
bodyDragZone : undefined,
/**
* @cfg {Object} bodyDragConfig Configuration object which will be used to initialize
* the {@link #bodyDragZone} when either {@link #enableBodyDD} or {@link #enableBodyDrag} is enabled.
*/
bodyDragConfig : undefined,
/**
* The DropZone which will be installed on the {@link #body}. This is initialized
* when either {@link #enableBodyDD} or {@link #enableBodyDrop} is enabled. It can optionally
* be configured using {@link #bodyDropConfig}.
* @property
* @type Zarafa.calendar.ui.CalendarViewDropZone
* @private
*/
bodyDropZone : undefined,
/**
* @cfg {Object} bodyDropConfig Configuration object which will be used to initialize
* the {@link #bodyDropZone} when either {@link #enableBodyDD} or {@link #enableBodyDrop} is enabled.
*/
bodyDropConfig : undefined,
* The DragZone which will be installed on the {@link #header}. This is initialized
* when either {@link #enableHeaderDD} or {@link #enableHeaderDrag} is enabled. It can optionally
* be configured using {@link #headerDragConfig}.
* @property
* @type Zarafa.calendar.ui.CalendarViewDragZone
* @private
*/
headerDragZone : undefined,
* @cfg {Object} headerDragConfig Configuration object which will be used to initialize
* the {@link #headerDragZone} when either {@link #enableHeaderDD} or {@link #enableHeaderDrag} is enabled.
*/
headerDragConfig : undefined,
* The DropZone which will be installed on the {@link #header}. This is initialized
* when either {@link #enableHeaderDD} or {@link #enableHeaderDrop} is enabled. It can optionally
* be configured using {@link #headerDropConfig}.
* @property
* @type Zarafa.calendar.ui.CalendarViewDropZone
* @private
*/
headerDropZone : undefined,
* @cfg {Object} headerDropConfig Configuration object which will be used to initialize
* the {@link #headerDropZone} when either {@link #enableHeaderDD} or {@link #enableHeaderDrop} is enabled.
*/
headerDropConfig : undefined,
/**
* @cfg {String} groupId The unique identifier for the {@link Zarafa.core.MultiFolderContextModel#getGroupings group}
* which refers to this calendar and the group of {@link #folders}.
*/
groupId : undefined,
/**
* @cfg {Boolean} active Flag whether this view is the currently active one inside the {@link Zarafa.calenaar.CalendarMultiView}
* There is one active group, but each group has a selected calendar in it
*/
active : false,
/**
* The active tab stroke which will show stroke on top of the active calendar tabs
* This field is created using {@link #createDiv} during {@link #render}.
* @property
* @type Ext.Element
*/
activeTabStroke : undefined,
/**
* @constructor
* @param {Object} config configuration object
*/
constructor : function(config)
{
config = config || {};
Ext.applyIf(config, {
baseCls : 'zarafa-calendar'
});
// define drag/drop events
this.addEvents(
/**
* @event appointmentcalendardrop
* Fires when an appointment is dragged from this calendar component onto another.
* @param {Zarafa.calendar.ui.AbstractCalendarDaysView} calendar The calendar which fired the event
* @param {Zarafa.calendar.ui.AbstractCalendarDaysView} source The calendar from where the appointment was dragged
* @param {Zarafa.calendar.AppointmentRecord} appointment the appointment record that was dropped
* @param {Zarafa.core.DateRange} dateRange The new daterange for the appointment
* @param {Ext.EventObject} event The original event object
*/
'appointmentcalendardrop',
/**
* @event appointmentmouseover
* Fires when the mouse is being mover over an appointment.
* @param {Zarafa.calendar.ui.AbstractCalendarDaysView} calendar The calendar which fired the event
* @param {Zarafa.calendar.AppointmentRecord} record The appointment over which the mouse is moving
* @param {Ext.EventObject} event The original event object
*/
'appointmentmouseover',
/**
* @event appointmentmouseover
* Fires when the mouse is being mover away from an appointment.
* @param {Zarafa.calendar.ui.AbstractCalendarDaysView} calendar The calendar which fired the event
* @param {Zarafa.calendar.AppointmentRecord} record The appointment over which the mouse has moved out
* @param {Ext.EventObject} event The original event object
*/
'appointmentmouseout',
/**
* @event appointmentmove
* Fires when an appointment has been moved.
* @param {Zarafa.calendar.ui.AbstractCalendarDaysView} calendar The calendar which fired the event
* @param {Zarafa.calendar.AppointmentRecord} appointment the appointment record that was moved
* @param {Zarafa.core.DateRange} dateRange The new daterange for the appointment
* @param {Ext.EventObject} event The original event object
*/
'appointmentmove',
/**
* @event appointmentresize
* Fires when an appointment has been resized. Resizing in this context means that either the start
* date or the due date has been changed.
* @param {Zarafa.calendar.ui.AbstractCalendarDaysView} calendar The calendar which fired the event
* @param {Zarafa.calendar.AppointmentRecord} appointment the appointment record that was resized
* @param {Zarafa.core.DateRange} dateRange The new daterange for the appointment
* @param {Ext.EventObject} event The original event object
*/
'appointmentresize',
/**
* @event appointmentcreate
* Fires when an appointment has been created.
* @param {Zarafa.calendar.ui.AbstractCalendarDaysView} calendar The calendar which fired the event
* @param {Zarafa.core.DateRange} appointment date range.
* @param {String} text appointment description text
*/
'appointmentcreate',
/**
* @event appointmentinitdrag
* Fired when the user has started dragging an appointment
* This means that the user held the mouse down over an appointment
* either for a long enough period, or started dragging with the mouse down
* This event is passed up to the {@link Zarafa.calendar.ui.CalendarPanel}
*
* @param {Zarafa.calendar.ui.CalendarMultiView} multiview The Calendar MultiView which fired the event
* @param {Ext.EventObject} event The mouse event
* @param {Zarafa.calendar.ui.AppointmentView} The appointment on which the event occurred
*/
'appointmentinitdrag',
/**
* @event appointmentenddrag
* Fired when the user drops a previously dragged appointment
* i.e. released the mouse.
*
* @param {Zarafa.calendar.ui.CalendarMultiView} multiview The Calendar MultiView which fired the event
* @param {Ext.EventObject} event The mouse event
* @param {Zarafa.calendar.ui.AppointmentView} The appointment on which the event occurred
*/
'appointmentenddrag',
* @event contextmenu
* Fires when the user right-clicks on an appointment
* @param {Zarafa.calendar.ui.AbstractCalendarDaysView} calendar Calendar view
* @param {Ext.EventObject} event right-click event.
* @param {Ext.Record} appointment record
* @param {Zarafa.core.DateRange} range The Datarange on which the contextmenu
* was invoked. This is only provided when the event didn't occur on a record.
*/
'contextmenu',
/**
* @event dblclick
* Fires when the user double-clicks on on an appointment.
* @param {Zarafa.calendar.ui.AbstractCalendarDaysView} calendar Calendar view
* @param {Ext.EventObject} event right-click event.
* @param {Ext.data.Record} record appointment record.
*/
'dblclick',
/**
* @event dayclick
* Fires when a user clicks on the header of a day, or on the expand button in the box view.
* @param {Zarafa.core.ui.View} source event source
* @param {Date} date date of the day.
*/
'dayclick',
/**
* @event activate
* Fires when the user clicks the tab body, indicating that the corresponding tab must be activated.
* @param {Zarafa.core.ui.View} source event source
* @param {MAPIFolder} folder The activated folder.
*/
'activate',
/**
* @event merge
* Fires when the user clicks the left arrow in a tab, indicating that the corresponding folder should be merged left.
* @param {Zarafa.core.ui.View} source event source
* @param {MAPIFolder} folder The folder which is being merged
*/
'merge',
/**
* @event merge
* Fires when the user clicks the right arrow in a tab, indicating that the corresponding folder should be separated and placed into a new view.
* @param {Zarafa.core.ui.View} source event source
* @param {MAPIFolder} folder The folder which is being separated
*/
'separate',
/**
* @event close
* Fires when the user clicks the close icon in a tab.
* @param {Zarafa.core.ui.View} source event source
* @param {MAPIFolder} folder The folder which is being closed
*/
'close'
);
Zarafa.calendar.ui.AbstractCalendarView.superclass.constructor.call(this, config);
},
/**
* Initialises the view.
* @protected
*/
init : function()
{
// super.init
Zarafa.calendar.ui.AbstractCalendarView.superclass.init.call(this);
this.folders = [];
this.tabs = {};
this.appointments = [];
// create a selection view
this.selectionView = this.createAppointmentProxy();
this.selectionView.setVisible(false);
this.addChildView(this.selectionView);
// create a text edit view
this.textEditView = new Zarafa.calendar.ui.TextEditView();
this.mon(this.textEditView, 'textentered', this.onTextEntered, this);
this.addChildView(this.textEditView);
// hook selection model
this.mon(this.selectionModel, 'appointmentselect', this.onAppointmentSelect, this);
this.mon(this.selectionModel, 'appointmentdeselect', this.onAppointmentDeselect, this);
this.mon(this.selectionModel, 'selectionclear', this.onAppointmentSelectionClear, this);
this.mon(this.rangeSelectionModel, 'selectionchange', this.onRangeSelectionChange, this);
},
/**
* Adds a MAPI folder to this calendar view. A new tab view is automatically created and added to
* the tab strip at the top of the view.
* @param {Zarafa.hierarchy.data.MAPIFolderRecord} folder MAPI folder to add to this view.
*/
addFolder : function(folder)
{
// Add the folder to the list.
this.folders.push(folder);
// Create a new Tab view to visually represent the new folder in the tab strip at the
// top of the calendar view.
var tab = new Zarafa.calendar.ui.CalendarTabView({
parentView: this,
folder : folder
});
// Hook event handlers to the newly created tab view.
this.mon(tab, 'merge', this.onTabMerge, this);
this.mon(tab, 'separate', this.onTabSeparate, this);
this.mon(tab, 'close', this.onTabClose, this);
this.mon(tab, 'click', this.onTabClick, this);
// If this view has been rendered, render the tab. If the view has not been rendered yet,
// the tab will be rendered automatically in the render() method through Zarafa.core.ui.View::renderChildren().
if (this.rendered) {
tab.render(this.container);
}
// Finally add the tab view to the local tab view hash.
this.tabs[folder.get('entryid')] = tab;
this.setSelectedFolder(folder);
},
/**
* Removes a MAPI folder from this calendar view. Automatically removes and destroys the associated tab view.
* @param {Zarafa.hierarchy.data.MAPIFolderRecord} folder MAPI folder to remove from this view.
*/
removeFolder : function(folder)
{
return this.removeFolderById(folder.get('entryid'));
},
/**
* Removes a MAPI folder from this calendar view. Automatically removes and destroys the associated tab view.
* @param {string} id MAPI ID of the folder to remove from this view.
*/
removeFolderById : function(id)
{
// Remove the folder from the list.
this.folders.remove(this.getFolderById(id));
// Get the tab view and unhook the event handlers.
var tab = this.tabs[id];
this.mun(tab, 'merge', this.onTabMerge, this);
this.mun(tab, 'separate', this.onTabSeparate, this);
this.mun(tab, 'close', this.onTabClose, this);
this.mun(tab, 'click', this.onTabClick, this);
// Remove the tab view as a child view and destroy.
this.removeChildView(tab, true);
// Remove the tab view from the tab view hash.
delete this.tabs[id];
},
/**
* Gets a list of MAPI folder IDs currently in this view.
* @return {String}[] MAPI folders IDs currently in this view.
*/
getFolderIds : function()
{
var ret = [];
for (var i = 0, folder; folder = this.folders[i]; i++) {
ret[i] = folder.get('entryid');
}
return ret;
},
/**
* Gets a MAPI folder by its MAPI ID.
* @param {string} id folder MAPI ID.
* @return {Zarafa.hierarchy.data.MAPIFolderRecord} folder object if the folder was found, or undefined otherwise.
*/
getFolderById : function(id)
{
// Find folder object to go with the folder ID.
for (var i=0, folder; folder = this.folders[i]; i++) {
if (Zarafa.core.EntryId.compareEntryIds(folder.get('entryid'), id)) {
return folder;
}
}
},
/**
* Gets a list of MAPI folders currently in this view. Is filled by the onLoad event.
* @return {Zarafa.hierarchy.data.MAPIFolderRecord[]} MAPI folders currently in this view.
*/
getFolders : function()
{
return this.folders;
},
/**
* Updates the currently {@link #selectedFolder selected folder}. If {@link #rendered} it will
* also {@link #layout relayout} the view.
* @param {Zarafa.hierarchy.data.MAPIFolderRecord} folder The folder which is currently selected
*/
setSelectedFolder : function(folder)
{
this.selectedFolder = folder;
if (this.rendered) {
this.layout();
}
},
/**
* Gets the currently selected folder.
* @return {Zarafa.hierarchy.data.MAPIFolderRecord} currently selected MAPI folder.
*/
getSelectedFolder : function()
{
// Check if selected folder is available in the folders array. This could be false when
// settings haven't been updated properly.
if ( this.folders.indexOf(this.selectedFolder) < 0 ){
// Set the first folder in the list as the selected one
this.setSelectedFolder(this.folders[0]);
}
return this.selectedFolder;
},
/**
* Checks if this view is currently showing the given MAPI folder.
* @param {Zarafa.hierarchy.data.MAPIFolderRecord} folder MAPI folders currently in this view.
* @return true iff this view contains the given folder.
*/
containsFolder : function(folder)
{
return this.containsFolderId(folder.get('entryid'));
},
/**
* Checks if this view is currently showing the given MAPI folder.
* @param {string} id folder MAPI ID.
* @return true iff this view contains the given folder.
*/
containsFolderId : function(id)
{
return Ext.isDefined(this.getFolderById(id));
},
/**
* Sorts the calendar folders based on the order of the input folder list. It is assumed that the folders argument
* is a superset of the folder list currently active in this view.
* @param {Zarafa.hierarchy.data.MAPIFolderRecord[]} folders Folder list.
*/
sortFolders : function(folders)
{
var sortedFolders = [];
for (var i = 0, folder; folder = folders[i]; i++) {
if (this.containsFolderId(folder.get('entryid'))) {
sortedFolders.push(folder);
}
}
this.folders = sortedFolders;
},
/**
* Returns the current date range for this calendar view. This value is retrieved
* from the parent view.
* @return {Zarafa.core.DateRange} The daterange
*/
getDateRange : function()
{
return this.parentView.getDateRange();
},
/**
* Returns the begin date for this calendar value. This value is retrieved
* from the parent view.
* @return {Date} calendar start date.
*/
getStartDate : function()
{
return this.getDateRange().getStartDate();
},
/**
* Returns the due date for this calendar value. This value is retrieved
* from the parent view.
* @return {Date} calendar due date.
*/
getDueDate : function()
{
return this.getDateRange().getDueDate();
},
* The {@link Zarafa.calendar.ui.CalendarMultiView CalendarMultiView} view has a header area that automatically
* resizes when its child views require more space. In the days view for instance, appointments that span
* more than 24 hours are laid out in the header.
* @return {Number} height in pixels the calendar view needs to properly lay out its header.
*/
getDesiredHeaderHeight : function()
{
return 0;
},
/**
* The {@link Zarafa.calendar.ui.CalendarMultiView CalendarMultiView} view has a tab area that is visible when there
* are multiple calendars open.
* @return {Number} height in pixels of the tab area of the calendar view.
*/
getTabHeight : function()
{
return this.parentView.getTabHeight();
},
/**
* Finds an appointment view by record.
* @param {Ext.data.Record} record to look for
* @return {Zarafa.calendar.ui.AppointmentView} an appointment view iff found, undefined otherwise
*/
findAppointment : function(record)
{
for (var i=0, appointment; appointment=this.appointments[i]; i++) {
if (appointment.getRecord().equals(record)) {
return appointment;
}
}
// not found
return undefined;
},
/**
* Tests if an appointment view exists on this calendar view that represents the given record.
* @param {Ext.data.Record} record to look for
* @return {Boolean} true iff an appointment view exists that represents the given record.
*/
containsAppointment : function(record)
{
return Ext.isDefined(this.findAppointment(record));
},
/**
* Create a new {@link Zarafa.calendar.ui.AppointmentView Appointment} object
* for the given {@link Zarafa.core.data.IPMRecord record}.
* Subclasses must override this function to make the appointments visible
* in this view.
* @param {Zarafa.core.data.IPMRecord} record The record for which the AppointmentView
* must be created.
* @return {Zarafa.calendar.ui.AppointmentView} The new appointmentview
* @protected
*/
createAppointment : Ext.emptyFn,
/**
* Create an Appointment Proxy object which can represent the selected text
* Must be implemented by the subclasses.
* @return Zarafa.calendar.ui.AbstractDateRangeView
* @protected
*/
createAppointmentProxy : Ext.emptyFn,
/**
* Adds a new appointment to the view.
* @param {Ext.data.Record} record a record with the appointment data
* @param {Boolean} layout (optional) if true layout() will be called after the appointment was added. Defaults to true.
* @return {Boolean} True if an appointment was added, false otherwise.
*/
addAppointment : function(record, layout)
{
// Don't add anything that is not an appointment
if ( Zarafa.core.MessageClass.getDefaultFolderTypeFromMessageClass(record.get('message_class')) !== 'calendar' ){
return false;
}
var appointment = this.createAppointment(record);
appointment.render(this.container);
this.appointments.push(appointment);
if (layout !== false) {
this.layout();
}
return true;
},
/**
* Removes an appointment from the view.
* @param {Ext.data.Record} record appointment record
* @param {Boolean} layout (optional) if true layout() will be called after the appointment was added. Defaults to true.
* @return {Boolean} true if an appointment was removed, false otherwise (the appointment was not found in this view)
*/
removeAppointment : function(record, layout)
{
var appointment;
if (appointment = this.findAppointment(record)) {
this.appointments.remove(appointment);
this.removeChildView(appointment, true);
if (layout !== false) {
this.layout();
}
return true;
}
return false;
},
/**
* Removes all appointments from the view and destroys them.
* @param {Boolean} destroy if true, destroy the appointments. Defaults to true.
* @param {Boolean} layout (optional) if true layout() will be called after the appointment was added. Defaults to true.
* @return {Zarafa.calendar.ui.AppointmentView[]} a list of appointments that were removed.
*/
clearAppointments : function(destroy, layout)
{
for (var i=0, appointment; appointment=this.appointments[i]; i++) {
this.removeChildView(appointment, destroy);
}
// Save the appointments list so we can return it from the function.
var ret = this.appointments;
// Clear the appointments list.
this.appointments = [];
// Optionally layout the view.
if (layout !== false) {
this.layout();
}
return ret;
},
/**
* Gets a list of appointment records currently on the view.
* @return {Zarafa.core.data.IPMRecord[]} records.
*/
getAppointmentRecords : function()
{
var ret = [];
for (var i=0, appointment; appointment=this.appointments[i]; i++) {
ret.push(appointment.getRecord());
}
return ret;
},
/**
* Converts a date range ([startDate, dueDate>) to zero or more (left, right, top, bottom) bounds objects.
* This method is used to lay out appointments (and proxies) on the calendar body.
* @param {Zarafa.core.DateRange} dateRange date range
* @param {Number} column (optional) when several appointments are overlapping a column may be assigned
* @param {Number} columnCount (optional) the number of overlapping appointments in an overlap dependency graph
* @param {Boolean} useMargin (optional) True to apply margins to the appointments to prevent them from
* filling up the entire width of a daybox (This applies {@link #appointmentBodyLeftMargin} and
* {@link #appointmentBodyRightMargin}).
*/
dateRangeToBodyBounds : function(dateRange, column, columnCount, useMargin)
{
return [];
},
* Converts a date range ([startDate, dueDate>) a (left, right, top, bottom) box.
* This method is used to lay out appointments (and proxies) on the calendar header.
* @param {Zarafa.core.DateRange} dateRange date range
* @param {Number} column (optional) when several appointments are overlapping a column may be assigned
* @param {Number} columnCount (optional) the number of overlapping appointments in an overlap dependency graph
* @param {Boolean} useMargin (optional) True to apply margins to the appointments to prevent them
* from filling up the entire width of the header (This applies {@link #appointmentHeaderLeftMargin} and
* {@link #appointmentHeaderRightMargin}).
*/
dateRangeToHeaderBounds : function(dateRange, row, rowCount, useMargin)
{
return {
left : 0,
right : 0,
top : 0,
bottom : 0
};
},
* Tests if the date range should be laid out in the header. Should be overridden by child classes. For example,
* the days view lays out appointments in the header if they span > 24 hours, but the box view never lays out
* appointments on the header.
* @return {Boolean} true iff the date range should be laid out on the calendar header.
*/
isHeaderRange : function(dateRange)
{
return false;
},
/**
* Sets the tab minimum offset from the left. Called by the parent
* {@link Zarafa.calendar.ui.CalendarMultiView CalendarMultiView} before layout.
* @param {Number} left The offset from the left
*/
setLeftMargin : function(left)
{
this.leftOffset = left;
},
/**
* Sets the tab width. Called by the parent
* {@link Zarafa.calendar.ui.CalendarMultiView CalendarMultiView} before layout.
* @param {Number} width tab width.
*/
setWidth : function(width)
{
this.width = width;
},
/**
* Sets whether this view can merge left. If the view is the leftmost on screen, this will be set to true by the
* parent multiview. This information is used to hide/show the merge icon on the tabs in the view.
* @param {Boolean} canMerge
*/
setCanMerge : function(canMerge)
{
this.canMerge = canMerge;
},
/**
* Sets whether this view can be closed. If the view is the only one on screen, this will be set to true by the
* parent multiview. This information is used to hide/show the close icon on the tabs in the view.
* @param {Boolean} canClose
*/
setCanClose : function(canClose)
{
this.canClose = canClose;
},
/**
* Converts a location in page coordinates to a corresponding date.
* @param {Number} x horizontal component of the location
* @param {Number} y vertical component of the location
* @return {Date} a Date object that corresponds to the given location
*/
screenLocationToDate : Ext.emptyFn,
/**
* Converts a location in page coordinates to a corresponding daterange.
* @param {Number} x horizontal component of the location
* @param {Number} y vertical component of the location
* @return {Zarafa.core.DateRange} A DateRange object that corresponds to the given location
*/
screenLocationToDateRange : Ext.emptyFn,
/**
* The border width in pixels. Retrieved from the parent view.
* If this calendar component is the only calendar on the view it will return 0.
* @return {Number} border width in pixels
* @private
*/
getBorderWidth : function()
{
return this.parentView.getBorderWidth();
},
/**
* Zoom level. Retrieved from the parent view.
* @return {Number} zoom level in minutes.
* @private
*/
getZoomLevel : function()
{
return this.parentView.zoomLevel;
},
/**
* @return {Ext.Element} the calendar body element
*/
getCalendarBody : function()
{
return this.body;
},
* @return {Ext.Element} the calendar header element
*/
getCalendarHeader : function()
{
return this.header;
},
/**
* Compares two appointments. Used for sorting appointments by start date. If the start dates of both
* appointments are equal, further distinction is made by due date and entryId. Note that the tie
* breaker here is entryId because the quick sort used by JavaScript is not stable, so the user might
* experience appointments with equal start and due dates swapping places on updates without it.
*
* @param {Zarafa.calendar.ui.AppointmentView} a appointment A
* @param {Zarafa.calendar.ui.AppointmentView} b appointment B
* @return {Number} 1 if appointment A starts before appointment B, 0 if they are equal, -1 if appointment A
* starts after appointment B.
*/
appointmentCompare : function(a, b)
{
var dateComp = a.getDateRange().compare(b.getDateRange());
if (dateComp === 0) {
return a.getRecord().get('entryid')>b.getRecord().get('entryid')?1:-1;
} else {
return dateComp;
}
},
/**
* Performs greedy coloring for a set of overlapping appointments. Each appointment is assigned a slot
* and the total slot count (number of required slots to lay out the set) is assigned to the appointment
* in the form of the 'slotCount' property.
* <p>
* The algorithm can lay out appointments by time (as in the body) or by whole days (as in the header).
* In the latter case if two appointments start or end on the same day they will always overlap, regardless
* of the time within that day.
* @param {Boolean} wholeDays if true the overlap test is for whole days rather than time
*/
doGreedyColoring : function(appointments, wholeDays)
{
// sort appointments by start date
appointments.sort(this.appointmentCompare);
// create the to do list. We'll remove fitted appointments from this list and continue until it is empty
var todoList = [];
// fill the to do list with start/due values (numbers). In the case of fitting by day we'll fill it
// with day numbers. In the normal case it will be filled with milliseconds since epoch
if (wholeDays) {
Ext.each(appointments, function(appointment) {
var calendarStartTime = this.getDateRange().getStartTime();
todoList.push({
appointment : appointment,
start : Math.floor((appointment.getDateRange().getStartTime() - calendarStartTime) / Date.dayInMillis),
due : Math.ceil((appointment.getDateRange().getDueTime() - calendarStartTime) / Date.dayInMillis)
});
}, this);
} else {
Ext.each(appointments, function(appointment) {
todoList.push({
appointment : appointment,
start : appointment.getAdjustedDateRange().getStartTime(),
due : appointment.getAdjustedDateRange().getDueTime()
});
});
}
// simple greedy lay out
var slot = 0;
while (todoList.length > 0) {
// first element can always go into the empty slot
var first = todoList.shift();
first.appointment.slot = slot;
// the slot now spans start-due
var due = first.due;
// look for appointments that are outside the start-due range, and can therefore
// go into the current slot
for (var i = 0; i < todoList.length; i++) {
if (todoList[i].start >= due) {
// add the selected item to the current slot
todoList[i].appointment.slot = slot;
// update the span to include the new appointment
due = todoList[i].due;
// remove the selected item from the to do list
todoList.splice(i, 1);
i--;
}
}
// next slot
slot++;
}
Ext.each(appointments, function(appointment) { appointment.slotCount = slot; } );
},
/**
* Renders the view. This class provides a this.header and this.body div that can be used
* to attach elements to the header and body areas respectively.
* <p>
* This class also provides a border with a tab (shown when there are more than one calendar
* in the parent view).
* @param {Ext.Element} container The Ext.Element into which the view must be rendered.
*/
render : function(container)
{
// FIXME: This should _never_ be true...
if (this.rendered) {
return;
}
// create tab, header and body divs
this.createDiv(this.parentView.scrollable, 'body');
this.createDiv(this.parentView.header, 'header', 'zarafa-calendar-header');
// border divs
this.createDiv(this.parentView.header, 'headerBorderLeft');
this.createDiv(this.parentView.header, 'headerBorderRight');
this.createDiv(this.parentView.scrollable, 'borderLeft');
this.createDiv(this.parentView.scrollable, 'borderRight');
this.createDiv(this.parentView.bottom, 'borderBottom');
this.createDiv(this.parentView.tab, 'tabArea');
this.createDiv(this.tabArea, 'activeTabStroke');
Zarafa.calendar.ui.AbstractCalendarView.superclass.render.call(this, container);
this.renderChildren();
this.mon(this.selectionView, 'keypress', this.onKeyPress, this);
this.mon(this.selectionView, 'keydown', this.onKeyPress, this);
if (this.enableDD || this.enableBodyDD || this.enableBodyDrag) {
if (!this.bodyDragZone && this.body) {
this.bodyDragZone = new Zarafa.calendar.ui.CalendarViewDragZone(this, this.bodyDragConfig || {
ddGroup : this.ddGroup || 'AppointmentDD',
headerMode : false
});
}
}
if (this.enableDD || this.enableHeaderDD || this.enableHeaderDrag) {
if (!this.headerDragZone && this.header) {
this.headerDragZone = new Zarafa.calendar.ui.CalendarViewDragZone(this, this.headerDragConfig || {
ddGroup : this.ddGroup || 'AppointmentDD',
headerMode : true
});
}
}
if (this.enableDD || this.enableBodyDD || this.enableBodyDrop) {
if (!this.bodyDropZone && this.body) {
this.bodyDropZone = new Zarafa.calendar.ui.CalendarViewDropZone(this, this.bodyDropConfig || {
ddGroup : this.ddGroup || 'AppointmentDD',
headerMode : false,
selectingSnapMode : Zarafa.calendar.data.SnapModes.ZOOMLEVEL,
draggingSnapMode : Zarafa.calendar.data.SnapModes.ZOOMLEVEL
});
this.bodyDropZone.proxy = this.selectionView;
}
}
if (this.enableDD || this.enableHeaderDD || this.enableHeaderDrop) {
if (!this.headerDropZone && this.header) {
this.headerDropZone = new Zarafa.calendar.ui.CalendarViewDropZone(this, this.headerDropConfig || {
ddGroup : this.ddGroup || 'AppointmentDD',
headerMode : true,
selectingSnapMode : Zarafa.calendar.data.SnapModes.DAY,
draggingSnapMode : Zarafa.calendar.data.SnapModes.DAY
});
this.headerDropZone.proxy = this.selectionView;
}
}
},
/**
* Event handler which is triggered when the user pressed a key after selection a
* time range with the {@link #rangeSelectionModel}. If the first character is
* {@link Ext.form.VTypes.alphanum alphanumeric} then the {@link #textEditView} will be enabled for creating
* a quick-appointment.
* @param {Object} event The event object for this event
* @private
*/
onKeyPress : function(event)
{
// If the key is not alphanumeric (a-z, A-Z, 0-9) ignore the event
if (!Ext.form.VTypes.alphanum(String.fromCharCode(event.getKey()))) {
return;
}
if (this.rangeSelectionModel.isActive() && this.rangeSelectionModel.getCalendarView() == this && !this.textEditView.isVisible())
{
var range = this.rangeSelectionModel.getDateRange();
// Clear the range selection. Note that this causes the 'rangeSelectionChange' to fire, which in
// turn causes the textEditView to be made invisible. Therefore we do this before making the
// textEditView visible through the select(); method.
this.rangeSelectionModel.clearSelections();
// Show the text edit view.
this.textEditView.setDateRange(range);
this.textEditView.select();
}
},
/**
* Event handler which is raised when the user has used the {@link #textEditView} to write
* the subject for a quick-appointment. If this subject is not empty, the {@link #appointmentcreate}
* event will be fired for creating a new appointment.
* @param {Zarafa.calendar.ui.TextEditView} view The view which raised the event.
* @param {String} text The text which was entered into the view.
* @private
*/
onTextEntered : function(view, text)
{
// Don't fire event on empty text
if (Ext.isEmpty(text.trim())) {
return;
}
this.fireEvent('appointmentcreate', this, view.getDateRange(), text);
},
/**
* Handles layout of the individual divs that make up the border around the calendar view.
* @private
*/
layoutBorder : function()
{
var selectedFolder = this.getSelectedFolder();
var colorScheme = this.contextModel.getColorScheme(selectedFolder.get('entryid'));
if (this.parentView.showBorder) {
var borderWidth = this.getBorderWidth();
// border divs in the header area
var headerHeight = this.parentView.getHeaderAreaHeight();
this.headerBorderLeft.dom.className = this.getClassName('border', 'left');
this.headerBorderLeft.setLeftTop(this.leftOffset, 0);
this.headerBorderLeft.setSize(borderWidth, headerHeight);
this.headerBorderLeft.applyStyles({
'background-color' : colorScheme.header,
'border-color' : colorScheme.header
});
this.headerBorderLeft.show();
this.headerBorderRight.dom.className = this.getClassName('border', 'right');
this.headerBorderRight.setLeftTop(this.leftOffset + this.width - borderWidth, 0);
this.headerBorderRight.setSize(borderWidth, headerHeight);
this.headerBorderRight.applyStyles({
'background-color' : colorScheme.header,
'border-color' : colorScheme.header
});
this.headerBorderRight.show();
// border divs in the body area
var bodyHeight = this.parentView.getHourHeight() * this.parentView.numHours;
this.borderLeft.dom.className = this.getClassName('border', 'left');
this.borderLeft.setLeftTop(this.leftOffset, 0);
this.borderLeft.setSize(borderWidth, bodyHeight);
this.borderLeft.applyStyles({
'background-color' : colorScheme.header,
'border-color' : colorScheme.header
});
this.borderLeft.show();
this.borderRight.dom.className = this.getClassName('border', 'right');
this.borderRight.setLeftTop(this.leftOffset + this.width - borderWidth, 0);
this.borderRight.setSize(borderWidth, bodyHeight);
this.borderRight.applyStyles({
'background-color' : colorScheme.header,
'border-color' : colorScheme.header
});
this.borderRight.show();
// border div in the bottom area
this.borderBottom.dom.className = this.getClassName('border', 'bottom');
this.borderBottom.setLeftTop(this.leftOffset, 0);
this.borderBottom.setSize(this.width, borderWidth);
this.borderBottom.applyStyles({
'background-color' : colorScheme.header,
'border-color' : colorScheme.header
});
this.borderBottom.show();
} else {
this.headerBorderLeft.hide();
this.headerBorderRight.hide();
this.borderLeft.hide();
this.borderRight.hide();
this.borderBottom.hide();
}
},
/**
* Handles layout of the individual {@link #tabs}. This will also obtain the desired width
* for each individual tab, as to calculate how much width each tab could be assigned with.
* @private
*/
layoutTabs : function()
{
var tabs = [];
var tabsThatNeedResizing = [];
var tabsThatNeedNoResizing = [];
// Update the settings of the tabs and calculate the total desired width.
// Layout the tabs.
var totalDesiredWidth = 0;
for (var i=0, folder; folder=this.folders[i]; i++) {
var tab = this.tabs[folder.get('entryid')];
tab.setSelected(this.selectedFolder == folder, this.active);
tab.setShowMergeIcon(this.canMerge);
tab.setShowSeparateIcon(this.folders.length > 1);
tab.setShowCloseIcon(this.canClose);
tabs.push(tab);
tab.desiredWidth = tab.getDesiredWidth();
tab.lrmargins = tab.tabContents.getMargins('lr');
totalDesiredWidth += tab.desiredWidth + tab.lrmargins;
}
// If the width of all the tabs together is more than the width of this control,
// the tabs will have to be shrunk.
if ( totalDesiredWidth > this.width ){
for ( i=0; i<tabs.length; i++ ){
if ( (tabs[i].getDesiredWidth() + tabs[i].tabContents.getMargins('lr')) < this.width/tabs.length ){
tabsThatNeedNoResizing.push(tabs[i]);
}else{
tabsThatNeedResizing.push(tabs[i]);
}
}
var totalAvailableWidthForTabsThatNeedResizing = this.width;
var totalWidthOfTabsThatNeedResizing = 0;
for ( i=0; i<tabsThatNeedNoResizing.length; i++ ){
totalAvailableWidthForTabsThatNeedResizing -= tabsThatNeedNoResizing[i].desiredWidth + tabsThatNeedNoResizing[i].lrmargins;
}
for ( i=0; i<tabsThatNeedResizing.length; i++ ){
totalWidthOfTabsThatNeedResizing += tabsThatNeedResizing[i].desiredWidth + tabsThatNeedResizing[i].lrmargins;
}
if ( tabsThatNeedResizing.length === 1 ){
tabsThatNeedResizing[0].desiredWidth = totalAvailableWidthForTabsThatNeedResizing - tabsThatNeedResizing[0].lrmargins;
} else {
while ( totalWidthOfTabsThatNeedResizing > totalAvailableWidthForTabsThatNeedResizing ){
// find the largest two tabs
var tab0 = tabsThatNeedResizing[0].desiredWidth > tabsThatNeedResizing[1].desiredWidth ? tabsThatNeedResizing[0] : tabsThatNeedResizing[1];
var tab1 = tabsThatNeedResizing[0].desiredWidth > tabsThatNeedResizing[1].desiredWidth ? tabsThatNeedResizing[1] : tabsThatNeedResizing[0];
for ( i=2; i<tabsThatNeedResizing.length; i++ ){
if ( tabs[i].desiredWidth > tab0.desiredWidth ){
tab0 = tabs[i];
} else if ( tabs[i].desiredWidth > tab1.desiredWidth ){
tab1 = tabs[i];
}
}
if ( tab0.desiredWidth - tab1.desiredWidth > totalWidthOfTabsThatNeedResizing - totalAvailableWidthForTabsThatNeedResizing ){
// Easy one: just shrink the biggest tab and we're good to go.
tab0.desiredWidth -= totalWidthOfTabsThatNeedResizing - totalAvailableWidthForTabsThatNeedResizing + tab0.lrmargins;
totalWidthOfTabsThatNeedResizing = totalAvailableWidthForTabsThatNeedResizing;
} else {
// Just make the largest tab smaller than the second largest tab and start all over (iterating until we are small enough)
totalWidthOfTabsThatNeedResizing -= (tab0.desiredWidth-tab1.desiredWidth) + 1;
tab0.desiredWidth = tab1.desiredWidth - 1;
}
}
}
}
// Layout the tabs.
for (var i=0, folder; folder=this.folders[i]; i++) {
var tab = this.tabs[folder.get('entryid')];
var width = tab.desiredWidth;
// Check if we don't get conflicts with a possible min-width set in the css-files
tab.tabContents.dom.style.removeProperty('min-width');
var cssMinWidth = parseInt(tab.tabContents.getStyle('min-width')) + tab.tabContents.getPadding('lr');
if ( cssMinWidth > width ){
tab.tabContents.setStyle('min-width', 0);
if ( totalDesiredWidth <= this.width ){
width = cssMinWidth;
}
}
tab.setWidth(width);
}
},
/**
* Lays out the view.
* @protected
*/
onLayout : function()
{
// Update the themeCls, this will be used when assigning
// the CSS class names to all objects.
if ( this.getSelectedFolder() ) {
var colorScheme = this.contextModel.getColorScheme(this.getSelectedFolder().get('entryid'));
if(colorScheme) {
this.calendarColorScheme = colorScheme;
}
}else{
return Zarafa.calendar.ui.AbstractCalendarView.superclass.onLayout.call(this);
}
// layout border and tab
this.layoutBorder();
// layout body and header containers
var dayStripHeight = this.parentView.getHourHeight() * this.parentView.numHours;
this.body.dom.className = this.getClassName('container', 'body');
this.body.setLeftTop(this.leftOffset + this.getBorderWidth(), 0);
this.body.setSize(this.width - this.getBorderWidth() * 2, dayStripHeight);
this.header.dom.className = this.getClassName('container', 'header');
this.header.setLeftTop(this.leftOffset + this.getBorderWidth(), 0);
this.header.setSize(this.width - this.getBorderWidth() * 2, this.parentView.getHeaderAreaHeight());
this.tabArea.dom.className = this.getClassName('container', 'tabarea');
this.tabArea.setLeftTop(this.leftOffset, 0);
this.tabArea.setSize(this.width, this.parentView.getTabAreaHeight());
this.activeTabStroke.dom.className = this.getClassName('tabarea', 'stroke');
this.activeTabStroke.setSize(this.width, this.parentView.tabStrokeHeight);
if (this.parentView.showBorder) {
this.tabArea.show();
} else {
this.tabArea.hide();
}
// Determine the color scheme of the appointments
for (var i=0, appointment; appointment=this.appointments[i]; i++) {
var folderId = appointment.getRecord().get('parent_entryid');
appointment.calendarColorScheme = this.contextModel.getColorScheme(folderId);
var folder = this.getFolderById(folderId);
appointment.setActive(folder == this.selectedFolder);
}
Zarafa.calendar.ui.AbstractCalendarView.superclass.onLayout.call(this);
this.layoutTabs();
},
/**
* This will call {@link Zarafa.core.ui.View#layout layout} on all the
* {@link #children}.
* @protected
*/
onAfterLayout : function()
{
this.layoutChildren();
},
/**
* Called by the {@link #parentView} when the {@link Zarafa.core.data.IPMStore#beforeload beforeload} event
* has been fired from the appointment {@link Zarafa.calendar.ui.CalendarMultiView#store store}.
* @param {Zarafa.core.data.IPMStore} store store that fired the event.
* @param {Object} options the options (parameters) with which the load was invoked.
*/
beforeAppointmentsLoad : function(store, options)
{
// Destroy all appointments.
this.clearAppointments(true);
},
/**
* Called by the {@link #parentView} when the {@link Zarafa.core.data.IPMStore#load load} event
* has been fired from the appointment {@link Zarafa.calendar.ui.CalendarMultiView#store store}.
* @param {Zarafa.core.data.IPMStore} store store that fired the event.
* @param {Zarafa.core.data.IPMRecord[]} records loaded record set
* @param {Object} options the options (parameters) with which the load was invoked.
*/
onAppointmentsLoad : function(store, records, options)
{
// Destroy all appointments.
this.clearAppointments(true);
// Create an appointment for each record.
Ext.each(records, function(record) {
if (this.containsFolderId(record.get('parent_entryid')) && record.isValid()) {
this.addAppointment(record, false);
}
}, this);
// Keeping the selection can be achieved properly by using 'recordselectionchange'
// event of respective context instance in case of any GridView.
// But we need to keep selection manually for this particular calendar view.
var selectedRecords = this.contextModel.selectedRecords;
if (!Ext.isEmpty(selectedRecords)) {
this.selectionModel.selectRecord(selectedRecords[0], false);
}
},
/**
* Called by the {@link #parentView} when the {@link Zarafa.core.data.IPMStore#add add} event
* has been fired from the appointment {@link Zarafa.calendar.ui.CalendarMultiView#store store}.
* Checks if the record belongs to this view (by checking the parent id)
* and adds a new AppointmentView to the calendar if required.
* @param {Ext.data.Store} store data store
* @param {Ext.data.Record} record that was added
* @param {String} operation mutation operation key. Equals 'add'
*/
onAppointmentAdd : function(store, record, operation)
{
if (this.containsFolderId(record.get('parent_entryid')) && !this.containsAppointment(record) && record.isValid()) {
this.addAppointment(record);
}
},
/**
* Called by the {@link #parentView} when the {@link Zarafa.core.data.IPMStore#remove remove} event
* has been fired from the appointment {@link Zarafa.calendar.ui.CalendarMultiView#store store}.
* Checks if an AppointmentView displaying the record exists on this calendar and removes it.
* @param {Ext.data.Store} store data store
* @param {Ext.data.Record} record that was added
* @param {String} operation mutation operation key. Equals 'remove'
*/
onAppointmentRemove : function(store, record, operation)
{
if (this.containsAppointment(record)) {
this.removeAppointment(record);
}
},
/**
* Called by the {@link #parentView} when the {@link Zarafa.core.data.IPMStore#remove remove} event
* has been fired from the appointment {@link Zarafa.calendar.ui.CalendarMultiView#store store}.
* This event is usually fired twice, once to signal that the data of a record has been changed (operation=='update'),
* and once to signal that the data has been successfully committed to the store (operation=='commit').
* The 'commit' operation means that the changed record has been written to the database back-end.
* @param {Ext.data.Store} store data store
* @param {Ext.data.Record} record that was added
* @param {String} operation mutation operation key. Equals 'update'
*/
onAppointmentUpdate : function(store, record, operation)
{
var appointment = this.findAppointment(record);
if (!appointment) {
// Appointment doesn't exist. We don't care about the update.
return;
}
var dateRange = appointment.getDateRange();
if (dateRange.getStartDate() !== record.get('startdate') || dateRange.getDueDate() !== record.get('duedate')) {
// If the appointment start and/or duedate have been changed, then we must do a layout
// of all appointments (appointments can now overlap which could cause the appointments
// to be resized).
appointment.updateDateRange(record);
this.layout();
} else {
// The appointment presence nor size in this calendar has changed,
// we can simply update the appointment with the new data.
appointment.layout();
}
},
/**
* Event handler which is fired when the {@link #selectionModel} fires the
* {@link Zarafa.calendar.ui.AppointmentSelectionModel#appointmentselect appointmentselect} event.
* This will lookup which {@link Zarafa.calendar.ui.AppointmentView appointment} belongs to the
* given {@link Zarafa.core.data.IPMRecord record} and will use
* {@link Zarafa.calendar.ui.AppointmentView#setSelected setSelected} to mark the appointment
* as selected.
* @param {Zarafa.calendar.ui.AppointmentSelectionModel} selectionModel The selection model which
* fired the event.
* @param {Zarafa.core.data.IPMRecord} record The record which was selected.
* @private
*/
onAppointmentSelect : function(selectionModel, record)
{
var appointment = this.findAppointment(record);
// If the text edit view is still visible, hide it.
if (this.textEditView.isVisible()) {
this.textEditView.hide();
}
if (appointment) {
appointment.setSelected(true);
}
},
/**
* Event handler which is fired when the {@link #selectionModel} fires the
* {@link Zarafa.calendar.ui.AppointmentSelectionModel#appointmentdeselect appointmentdeselect} event.
* This will lookup which {@link Zarafa.calendar.ui.AppointmentView appointment} belongs to the
* given {@link Zarafa.core.data.IPMRecord record} and will use
* {@link Zarafa.calendar.ui.AppointmentView#setSelected setSelected} to mark the appointment
* as unselected.
* @param {Zarafa.calendar.ui.AppointmentSelectionModel} selectionModel The selection model which
* fired the event.
* @param {Zarafa.core.data.IPMRecord} record The record which was deselected.
* @private
*/
onAppointmentDeselect : function(selectionModel, record)
{
var appointment = this.findAppointment(record);
// If the text edit view is still visible, hide it.
if (this.textEditView.isVisible()) {
this.textEditView.hide();
}
if (appointment) {
appointment.setSelected(false);
}
},
/**
* Event handler which is fired when the {@link #selectionModel} fires the
* {@link Zarafa.calendar.ui.AppointmentSelectionModel#selectionclear selectionclear} event.
* This will call {@link Zarafa.calendar.ui.AppointmentView#setSelected setSelected} for
* each {@link #appointments appointment} to mark them as unselected.
* @param {Zarafa.calendar.ui.AppointmentSelectionModel} selectionModel The selection model which
* fired the event.
* @private
*/
onAppointmentSelectionClear : function(selectionModel)
{
// If the text edit view is still visible, hide it.
if (this.textEditView.isVisible()) {
this.textEditView.hide();
}
for (var i = 0, len = this.appointments.length; i < len; i++) {
this.appointments[i].setSelected(false);
}
},
/**
* Fires when the selection range has changed. This means the selection range my have been cleared or
* set.
* @param {Zarafa.calendar.ui.DateRangeSelectionModel} selectionModel range selection model firing the event.
* @param {Zarafa.core.DateRange} dateRange selected date range, or undefined if not active.
* @param {Zarafa.calendar.ui.AbstractCalendarView} calendarView the calendar view the selection was made on.
* @param {Boolean} active true iff the selection is active (set).
* @private
*/
onRangeSelectionChange : function(selectionModel, dateRange, calendarView, active)
{
// Make sure the selection view has been created.
if (!this.selectionView) {
return;
}
// If the text edit view is still visible, hide it.
if (this.textEditView.isVisible()) {
this.textEditView.hide();
this.textEditView.inputText = '';
}
// Check if the selected range is active and belongs to this calendar view, and
// show the selection if so.
if (active && calendarView == this) {
this.selectionView.setDateRange(dateRange);
this.selectionView.setVisible(true);
} else {
// Otherwise hide the selection view
if (this.selectionView.isVisible()) {
this.selectionView.setVisible(false);
}
}
},
/**
* Handles the 'merge' event of a tab, which is fired when the user clicks the left arrow (merge) button.
* Fires the 'merge' event.
* @param {MAPIFolder} folder MAPI folder.
* @private
*/
onTabMerge : function(folder)
{
this.fireEvent('merge', this, folder);
},
/**
* Handles the 'merge' event of a tab, which is fired when the user clicks the right arrow (separate) button.
* Fires the 'separate' event.
* @param {MAPIFolder} folder MAPI folder.
* @private
*/
onTabSeparate : function(folder)
{
this.fireEvent('separate', this, folder);
},
/**
* Handles the 'close' event of a tab, which is fired when the user clicks the tab itself.
* Fires the 'close' event.
* @param {MAPIFolder} folder MAPI folder.
* @private
*/
onTabClose : function(folder)
{
this.fireEvent('close', this, folder);
},
/**
* Handles the 'click' event of a tab, which is fired when the user clicks the tab itself.
* @param {MAPIFolder} folder MAPI folder.
* @private
*/
onTabClick : function(folder)
{
this.setSelectedFolder(folder);
this.fireEvent('activate', this, folder);
},
/**
* Called by the {@link Zarafa.calendar.ui.CalendarViewDragZone} to determine the text
* of the {@link Zarafa.calendar.ui.AppointmentView appointment} which is being dragged.
* @return {String} The string which must be shown while dragging the appointment
*/
getDragDropText : function()
{
var count = this.selectionModel.getCount();
return String.format(ngettext('{0} selected item', '{0} selected items', count), count);
},
/**
* Event handler for the D&D proxy, which is called when the appointment was dropped.
* This will fire the {@link #appointmentcalendardrop} event.
* @param {Ext.EventObject} event The event object
* @param {Zarafa.calendar.ui.AbstractCalendarView} calendar The calendar from where the appointment was dragged
* @param {Zarafa.calendar.ui.AppointmentView} appointment The appointment which was dropped
* @param {Zarafa.core.DateRange} dateRange The daterange for the appointment
* @private
*/
onDrop : function(event, calendar, appointment, dateRange)
{
//Make sure this view gets the focus by calling the onTabClick event handler
this.onTabClick(this.getSelectedFolder());
this.fireEvent('appointmentcalendardrop', this, calendar, appointment.getRecord(), dateRange, event);
this.selectionModel.clearSelections();
},
/**
* Event handler for the D&D proxy, which is called when the appointment was moved.
* This will fire the {@link #appointmentmove} event.
* @param {Ext.EventObject} event The event object
* @param {Zarafa.calendar.ui.AppointmentView} appointment The moved appointment
* @param {Zarafa.core.DateRange} dateRange The daterange for the appointment
* @private
*/
onMove : function(event, appointment, dateRange)
{
this.fireEvent('appointmentmove', this, appointment.getRecord(), dateRange, event);
this.selectionModel.clearSelections();
},
/**
* Event handler for the D&D proxy, which is called when the appointment was resized.
* This will fire the {@link #appointmentresize} event.
* @param {Ext.EventObject} event The event object
* @param {Zarafa.calendar.ui.AppointmentView} appointment The resized appointment
* @param {Zarafa.core.DateRange} dateRange The daterange for the appointment
* @private
*/
onResize : function(event, appointment, dateRange)
{
this.fireEvent('appointmentresize', this, appointment.getRecord(), dateRange, event);
this.selectionModel.clearSelections();
},
/**
* Generic implementation for handling mousemove events on the calendar, this needs to be
* implemented by subclasses to correctly delegate the event to the proper location in
* the calendar.
* @param {Ext.EventObject} event The event object
*/
onMouseMove : Ext.emptyFn,
/**
* Event handler for the D&D proxy, which is called when the mousedown event has been fired.
* Depending if the Ctrl key was presed the previously selected record will remain selected
* or will be deselected.
* @param {Object} event The event object
* @param {Zarafa.calendar.ui.AppointmentView} appointment The appointment object
* @private
*/
onMouseDown : function(event, appointment)
{
// Make sure this view gets the focus by calling the onTabClick event handler.
// Only call onTabClick when we actually change the selected folder group, to
// avoid firing off an unrequired 'activate' event.
if (this.groupId != this.contextModel.active_group) {
this.onTabClick(this.getSelectedFolder());
}
if (appointment) {
var record = appointment.getRecord();
if (event.ctrlKey) {
if (this.selectionModel.isSelected(record)) {
this.selectionModel.deselectRecord(record);
} else {
this.selectionModel.selectRecord(record, true);
}
} else {
this.selectionModel.selectRecord(record, false);
}
this.rangeSelectionModel.clearSelections();
} else {
this.selectionModel.clearSelections();
var xy = event.getXY();
var range = this.screenLocationToDateRange(xy[0], xy[1]);
// Manage selection into selection model if range is defined
if (range) {
this.rangeSelectionModel.set(range, this);
}
}
},
/**
* Event handler for the D&D proxy, which is called when the mouseup event has been fired.
* If an {@link Zarafa.calendar.ui.AppointmentView appointment} was selected and moved this will mark
* the given appointment as selected.
* @param {Object} event The event object
* @param {Zarafa.calendar.ui.AppointmentView} appointment The selected appointment
* @private
*/
onMouseUp : function(event, appointment)
{
if (appointment && (appointment.isAllDay() ? appointment.eventOverHeader(event) : appointment.eventOverBody(event))) {
this.selectionModel.deselectRecord(appointment.getRecord());
}
},
/**
* Method called from {@link Zarafa.calendar.ui.CalendarViewDragZone#onInitDrag} when the user starts dragging
*
* @param {Ext.EventObject} event The mouse event
* @param {Zarafa.calendar.ui.AppointmentView} appointment The appointment on which the event occurred
* @private
*/
onInitDrag : function(event, appointment)
{
this.fireEvent('appointmentinitdrag', this, event, appointment);
},
/**
* Method called from {@link Zarafa.calendar.ui.CalendarViewDragZone#onInitDrag} when the user releases an appointment they had been dragging.
*
* @param {Ext.EventObject} event The mouse event
* @param {Zarafa.calendar.ui.AppointmentView} The appointment on which the event occurred
* @private
*/
onEndDrag : function(event, appointment)
{
this.fireEvent('appointmentenddrag', this, event, appointment);
},
/**
* Event handler for the D&D proxy, which is called when an given {@link Zarafa.core.DateRange dateRange}
* has been selected. This will update the {@link #rangeSelectionModel}
* @param {Object} event The event object
* @param {Zarafa.core.DateRange} dateRange The dateRange which was selected.
* @private
*/
onSelect : function(event, dateRange)
{
this.rangeSelectionModel.set(dateRange, this);
},
/**
* Destroy all elements which were created by this calendar view. Note that all
* cleaned up by the superclass.
* @override
*/
destroy : function()
{
Ext.destroy(this.bodyDragZone, this.bodyDropZone);
Ext.destroy(this.headerDragZone, this.headerDropZone);
Zarafa.calendar.ui.AbstractCalendarView.superclass.destroy.call(this);
},
/**
* Set this view to the currently active one in the {@link Zarafa.calendar.CalendarMultiView}
* @param {Boolean} active
*/
setActive : function(active)
{
this.active = active;
}
});