/*
* #dependsFile client/zarafa/calendar/data/SnapModes.js
* #dependsFile client/zarafa/calendar/data/DragStates.js
*/
Ext.namespace('Zarafa.calendar.ui');
/**
* @class Zarafa.calendar.ui.CalendarViewDropZone
* @extends Ext.dd.DropZone
*
* A special DropZone which supports dragging appointments over the calendar.
* The calendar is represented using a special {@link Zarafa.calendar.ui.AbstractDateRangeView}
* which can visualize the daterange which is selected or will be occupied by the selected
* appointment.
*/
Zarafa.calendar.ui.CalendarViewDropZone = Ext.extend(Ext.dd.DropZone, {
/**
* @cfg {String} The CSS class returned to the drag source when drop is allowed
* while the user has the Ctrl-key pressed (defaults to "x-dd-drop-ok-add").
*/
dropAllowedAdd : 'x-dd-drop-ok-add',
* @cfg {Boolean} headerMode True of this DropZone is installed on the header of
* the calendar, or in the body. This determines if the {@link Zarafa.calendar.ui.AbstractCalendarView#header}
* or {@link Zarafa.calendar.ui.AbstractCalendarView#body} will be used to connect the event handlers.
*/
headerMode : false,
/**
* @cfg {Zarafa.calendar.data.SnapModes} selectingSnapMode The snapmode for selections and resizing.
* If this is {@link Zarafa.calendar.data.SnapModes#ZOOMLEVEL} then appointments or selections that
* are resized are snapped to the time which matches the
* {@link Zarafa.calendar.ui.AbstractCalendarView#getZoomLevel zoomlevel}. When this option is
* {@link Zarafa.calendar.data.SnapModes#DAY} the time is snapped to the entire day.
*/
selectingSnapMode : Zarafa.calendar.data.SnapModes.ZOOMLEVEL,
/**
* @cfg {Zarafa.calendar.data.SnapModes} draggingSnapMode The snapmode for appointments when dragging.
* If this is {@link Zarafa.calendar.data.SnapModes#ZOOMLEVEL} then appointments are snapped to the
* time which matches the {@link Zarafa.calendar.ui.AbstractCalendarView#getZoomLevel zoomlevel}.
* When this option is {@link Zarafa.calendar.data.SnapModes#DAY} the appointments are snapped
* to the entire day.
*/
draggingSnapMode : Zarafa.calendar.data.SnapModes.ZOOMLEVEL,
/**
* The proxy which must be used to display the selected range.
* @property
* @type Zarafa.calendar.ui.AbstractDateRangeView
* @private
*/
proxy : undefined,
/**
* The current Drag & Drop State. This influences the way how the dragged appointment
* is being treated (dragging, resizing, selecting).
* @property
* @type Zarafa.calendar.data.DragStates
* @private
*/
state : Zarafa.calendar.data.DragStates.NONE,
/**
* The daterange which reflects the size of the selected range. This equals
* the DateRange in the {@link #proxy}.
* @property
* @type Zarafa.core.DateRange
* @private
*/
dateRange : undefined,
/**
* The {@link Date} object which represents the exact start date on which the user
* {@link #onNodeEnter entered} this region.
* @property
* @type Date
* @private
*/
initDate : undefined,
/**
* The {@link Zarafa.core.DateRange DateRange} of the selection area on which the
* user {@link #onNodeEnter entered} this region.
* @property
* @type Zarafa.core.DateRange
* @private
*/
initDateRange : undefined,
/**
* @constructor
* @param {Zarafa.calendar.ui.AbstractCalendarView} calendar The calendar on which this Dropzone is installed
* @param {Object} config Configuration object
*/
constructor : function(calendar, config)
{
config = config || {};
this.calendar = calendar;
var element = this.calendar.body;
if (config.headerMode === true) {
element = this.calendar.header;
}
Ext.applyIf(config, {
ddGroup : 'AppointmentDD'
});
Zarafa.calendar.ui.CalendarViewDropZone.superclass.constructor.call(this, element, config);
},
/**
* Returns a custom data object associated with the DOM node that is the target of the event.
* This will return the element on which this DropZone has been installed.
* @param {Ext.EventObject} The event
* @returns {Object} The custom data
* @protected
*/
getTargetFromEvent: function(e)
{
// Check if the cursor is over the visible part of the
// element on which this dropzone is installed. For the
// header this is simple as there is no scrollbar active.
// For the body, we must check if the cursor is over the
// hidden part of the body (in which case, the body is
// not considered the target).
var element = this.el;
if (this.headerMode !== true) {
var parentElement = this.el.parent();
if (e.getPageY() < parentElement.getTop() || parentElement.getBottom() < e.getPageY()) {
element = undefined;
}
}
return element;
},
/**
* Called when the DropZone determines that a {@link Ext.dd.DragSource} has entered a drop node
* that has either been registered or detected by a configured implementation of
* {@link #getTargetFromEvent}.
* This function will disable the {@link Ext.dd.DragZone#proxy} and will let the
* local {@link #proxy} handle the visualization.
* @param {Ext.Element} target The custom data associated with the drop node (as returned from {@link #getTargetFromEvent}.
* @param {Ext.dd.DragSource} The drag source that was dragged over this drop zone
* @param {Ext.EventObject} e The event
* @param {Object} data An object containing arbitrary data supplied by the drag source
* @protected
*/
onNodeEnter : function(target, dd, e, data)
{
var appointment = data.selections[0];
var DragStates = Zarafa.calendar.data.DragStates;
// If the appointment was being resized, but this dropzone is
// on a different calendar then the dragzone, then we fallback
// to dragging (as we can't resize from one calendar to another).
this.state = data.state;
if (this.state === DragStates.RESIZING_START || this.state === DragStates.RESIZING_DUE || this.state === DragStates.SELECTING) {
if (this.calendar !== dd.calendar) {
this.state = DragStates.DRAGGING;
}
}
if (appointment) {
// We are currently working with an appointment which is either being dragged or resized.
// Initialize the initDate based on the appointment. Note that in this case we don't need
// to be very accurate about the date.
if (data.state === DragStates.RESIZING_START || data.state === DragStates.DRAGGING) {
this.initDate = appointment.get('startdate');
} else {
this.initDate = appointment.get('duedate');
}
// The selected range will always be the entire appointment.
this.initDateRange = new Zarafa.core.DateRange({ startDate : appointment.get('startdate'), dueDate : appointment.get('duedate') });
} else {
// We are not working with an appointment, and thus are selecting a daterange.
// Determine what the basic selection should be based on the current zoomLevel.
var zoomLevel = this.calendar.getZoomLevel();
// The initial date is the exact date on which this events starts. Note that
// for the header events, this date will be moved to the start of the selected
// day.
this.initDate = this.calendar.screenLocationToDate(e.getPageX(), e.getPageY());
var dueDate, startDate;
// Now we start detecting the range which should be selected by default.
if (this.snapMode === Zarafa.calendar.data.SnapModes.DAY) {
// For the snapMode DAY, the initDate will have been rounded to
// the start of the day (but for safety we ensure that it will be anyway),
// the dueDate will always be exactly 1 day after the start. This way
// we select a single day by default.
startDate = this.initDate.clearTime(true);
dueDate = startDate.add(Date.DAY, 1);
} else {
// For the snapMode ZOOMLEVEL we must select a region the size of the zoomLevel.
// This is easiest down by using floor() for the initDate to obtain the start,
// and then add the zoomLevel to it to obtain the dueDate.
startDate = this.initDate.clone().floor(Date.MINUTE, zoomLevel);
dueDate = startDate.add(Date.MINUTE, zoomLevel);
}
this.initDateRange = new Zarafa.core.DateRange({ startDate : startDate, dueDate : dueDate });
}
// Activate the initial range
this.dateRange = this.initDateRange.clone();
// Change the function from the prototype to update the scope of the function.
// This is needed to ensure we can call removeEventListener again with the correct
// function reference later.
this.onDragKeyDown = Zarafa.calendar.ui.CalendarViewDropZone.prototype.onDragKeyDown.createDelegate({ dd : dd, dz : this });
this.onDragKeyUp = Zarafa.calendar.ui.CalendarViewDropZone.prototype.onDragKeyUp.createDelegate({ dd : dd, dz : this });
// During dragging (onNodeOver) we either apply dropAllowed or dropAllowedAdd based on the Ctrl-key,
// if the user didn't drag but just presses the button we must also update the icon. For that we
// have these 2 event handlers.
Ext.EventManager.on(Ext.getDoc(), 'keydown', this.onDragKeyDown, this);
Ext.EventManager.on(Ext.getDoc(), 'keyup', this.onDragKeyUp, this);
// Update the proxy
this.proxy.setShowTime(this.selectingSnapMode === Zarafa.calendar.data.SnapModes.ZOOMLEVEL);
this.proxy.setDateRange(this.dateRange);
this.updateProxy(e.getXY(), data.selections);
// DragZone is placed on the same calendar or different calendar DropZone,
// we will disable the default proxy of the DragZone and activate
// our own as replacement.
dd.proxy.hide();
this.proxy.setVisible(true);
},
/**
* Called while the DropZone determines that a {@link Ext.dd.DragSource} is over a drop node that
* has either been registered or detected by a configured implementation of {@link #getTargetFromEvent}.
* This function will call {@link #updateProxy} to update the selected range.
* @param {Ext.Element} target The custom data associated with the drop node (as returned from {@link #getTargetFromEvent}.
* @param {Ext.dd.DragSource} The drag source that was dragged over this drop zone
* @param {Ext.EventObject} e The event
* @param {Object} data An object containing arbitrary data supplied by the drag source
* @protected
*/
onNodeOver : function(target, dd, e, data)
{
this.updateProxy(e.getXY(), data.selections);
// If the Crl-key is pressed, return dropAllowedAdd to
// have the correct icon displayed which represents a copy
// rather then a move.
return e.ctrlKey ? this.dropAllowedAdd : this.dropAllowed;
},
/**
* Called when the DropZone determines that a {@link Ext.dd.DragSource} has been dragged
* out of the drop node without dropping.
* This function will disable the local {@link #proxy} and hand back the responsibility
* of the visualization back to the {@link Ext.dd.DragZone#proxy}.
* @param {Ext.Element} target The custom data associated with the drop node (as returned from {@link #getTargetFromEvent}.
* @param {Ext.dd.DragSource} The drag source that was dragged over this drop zone
* @param {Ext.EventObject} e The event
* @param {Object} data An object containing arbitrary data supplied by the drag source
* @protected
*/
onNodeOut : function(target, dd, e, data)
{
// Clear event handlers again
Ext.EventManager.un(Ext.getDoc(), 'keydown', this.onDragKeyDown, this);
Ext.EventManager.un(Ext.getDoc(), 'keyup', this.onDragKeyUp, this);
delete this.initDate;
delete this.initDateRange;
this.proxy.setVisible(false);
dd.proxy.show();
},
/**
* Event handler which is fired when the user presses a key, this is registered during {@link #onNodeEnter}
* and will be released in {@link #onNodeOut} and {@link #onNodeDrop}. When the user presses the Ctrl-key
* and the user is allowed to drop the item on this DropZone, we update the icon to {@link #dropAllowedAdd}
* to visualize the action will be a copy rather then a move.
*
* NOTE: This function is called using a special scope. 'this' is an object containing 2 fields,
* 'dd' which is the DragZone from where the item is dragged and 'dz' which is the DragZone over which
* the item is hovering.
*
* @param {Ext.EventObject} e The event
* @private
*/
onDragKeyDown : function(e)
{
if (e.ctrlKey || e.keyCode === Ext.EventObject.CONTROL) {
if (this.dd.proxy.dropStatus === this.dz.dropAllowed) {
this.dd.proxy.setStatus(this.dz.dropAllowedAdd);
}
}
},
/**
* Event handler which is fired when the user releases a key, this is registered during {@link #onNodeEnter}
* and will be released in {@link #onNodeOut} and {@link #onNodeDrop}. When the user releases the Ctrl-key
* and the current dropStatus is {@link #dropAllowedAdd} we change it back to {@link #dropAllowed} to visualize
* that the action will be a moved.
*
* NOTE: This function is called using a special scope. 'this' is an object containing 2 fields,
* 'dd' which is the DragZone from where the item is dragged and 'dz' which is the DragZone over which
* the item is hovering.
*
* @param {Ext.EventObject} e The event
* @private
*/
onDragKeyUp : function(e)
{
if (e.ctrlKey || e.keyCode === Ext.EventObject.CONTROL) {
if (this.dd.proxy.dropStatus === this.dz.dropAllowedAdd) {
this.dd.proxy.setStatus(this.dz.dropAllowed);
}
}
},
/**
* Called when the DropZone determines that a item has been dropped.
* This will determine what action has occured and call the appropriate
* callback function on the {@link #calendar}.
* @param {Ext.Element} target The custom data associated with the drop node (as returned from {@link #getTargetFromEvent}.
* @param {Ext.dd.DragSource} The drag source that was dragged over this drop zone
* @param {Ext.EventObject} e The event
* @param {Object} data An object containing arbitrary data supplied by the drag source
* @protected
*/
onNodeDrop : function(target, dd, e, data)
{
var DragStates = Zarafa.calendar.data.DragStates;
switch (data.state) {
case DragStates.SELECTING:
this.calendar.onSelect(e, this.dateRange);
break;
case DragStates.DRAGGING:
if (dd.calendar !== this.calendar) {
this.calendar.onDrop(e, dd.calendar, data.target, this.dateRange);
} else {
var appointment = data.selections[0];
// Check if we can modify the calendar or meeting request.
if(appointment.get('access') & Zarafa.core.mapi.Access.ACCESS_MODIFY) {
this.calendar.onMove(e, data.target, this.dateRange);
} else {
var text = appointment.isMeeting() ? _('a meeting request') : _('an appointment');
Ext.MessageBox.show({
title: _('Insufficient privileges'),
msg: _('You have insufficient privileges to move ' + text +
' in this calendar. The calendar owner can grant you these rights from: settings > delegates.'),
icon: 'zarafa-calendar-delegate-permission',
buttons: Ext.MessageBox.OK
});
return false;
}
}
break;
case DragStates.RESIZING_START:
case DragStates.RESIZING_DUE:
this.calendar.onResize(e, data.target, this.dateRange);
break;
}
},
/**
* Recalculate the {@link Zarafa.core.DateRange date values} for the {@link #proxy}. This
* will check at which position the mouse is currently hovering, and will update the
* start and the dueDate respectively.
*
* @param {Number|Array} xy The X and Y Coordinates of the cursor position
* @param {Zarafa.calendar.AppointmentRecord|Array} The array of currently selected appointments
* @private
*/
updateProxy : function(xy, selections)
{
var selection = !Ext.isEmpty(selections) ? selections[0] : undefined;
var overDate = this.calendar.screenLocationToDate(xy[0], xy[1]);
var zoomLevel = this.calendar.getZoomLevel();
var DragStates = Zarafa.calendar.data.DragStates;
var moveStart = (this.state === DragStates.SELECTING ||
this.state === DragStates.DRAGGING ||
this.state === DragStates.RESIZING_START);
var moveDue = (this.state === DragStates.SELECTING ||
this.state === DragStates.DRAGGING ||
this.state === DragStates.RESIZING_DUE);
var startDate = this.dateRange.getStartDate();
var dueDate = this.dateRange.getDueDate();
var duration = this.dateRange.getDuration(Date.MINUTE);
var snapMode;
// Determine what snapMode we should use to determine what changes
// need to be made to the start and due date.
if (this.state === DragStates.DRAGGING){
snapMode = this.draggingSnapMode;
} else {
snapMode = this.selectingSnapMode;
}
if(Ext.isDate(overDate)){
if (snapMode === Zarafa.calendar.data.SnapModes.DAY ) {
// We must snap the appointment to an entire day
if (moveStart) {
// Always floor the startDate to the start of the day
startDate = overDate.clearTime();
if (moveDue) {
// We are moving both the start as well as the dueDate
// of the selection. In other words we are dragging our selection
if (!selection) {
// If we don't have an appointment selected we ensure that the
// current duration is maintained (without an appointment,
// the duration is always a single day).
dueDate = startDate.add(Date.DAY, 1);
} else if (selection.get('alldayevent')) {
// We are dragging an allday appointment over the header,
// simply maintain the duration of the appointment by always
// updating the dueDate based on the appointment duration.
dueDate = startDate.add(Date.MINUTE, selection.get('duration'));
} else {
// If there is an appointment, but it is an non-allday appointment,
// we have to resize it to the entire day, as dropping this
// appointment on the body container will imply a resize of the
// appointment.
dueDate = startDate.add(Date.DAY, 1);
}
}
} else {
// We are only moving the dueDate of the selection. The overDate will always
// point to the start of the day over which we are hovering, while the dueDate
// should represent the end of that same day. Hence we always do Date.add().
dueDate = overDate.add(Date.DAY, 1);
if (dueDate === startDate) {
dueDate = dueDate.add(Date.DAY, 1);
}
}
} else if (snapMode === Zarafa.calendar.data.SnapModes.ZOOMLEVEL) {
// We must snap the appointment to the current zoomLevel
if (moveStart) {
// Always floor the startDate to the upper zoomLevel boundary.
startDate = overDate.floor(Date.MINUTE, zoomLevel);
if (moveDue) {
// We are moving both the start as well as the dueDate
// of the selection. In other words we are dragging our selection
if (!selection) {
// If we don't have an appointment selected we ensure that the
// current duration is maintained (without an appointment,
// the duration is always the zoomLevel).
dueDate = startDate.add(Date.MINUTE, zoomLevel);
} else if (selection.get('alldayevent')) {
// If there is an appointment, but it is an allday appointment,
// we have to resize it to the zoomLevel as well, as dropping
// this appointment on the body container will imply a resize
// of the appointment.
dueDate = startDate.add(Date.MINUTE, zoomLevel);
} else {
// We are dragging a normal appointment around, we maintain
// the duration of the appointment by always updating the dueDate
// based on the appointment duration.
dueDate = startDate.add(Date.MINUTE, selection.get('duration'));
}
}
} else {
// We are only moving the dueDate of the selection,
// round the selection up to the upper limit of the zoomLevel.
// Note that this will not change the dueDate when the date is
// an exact multiple of the zoomLevel. This doesn't matter as
// long as the startDate and dueDate are not equal, as the user
// will simply have to move an extra pixel to select the next
// block.
dueDate = overDate.ceil(Date.MINUTE, zoomLevel);
if (dueDate === startDate) {
dueDate = dueDate.add(Date.MINUTE, zoomLevel);
}
}
}else{
// Get number of minutes since start of day
var minSinceStartOfDay = startDate.getHours()*60 + startDate.getMinutes();
// We check whether the overDate is actually at 0:00. If not the DST change happens at
// midnight. The DST diff fix later on will not work then as it will search between the
// start of the day and the appointment time. Because 0:00 will become 01:00 it will not
// detect the DST change between 01:00 and for example 11:00. By taking the number of
// minutes that were supposed to be between 0:00 and 01:00 and subtract from
// minSinceStartOfDay, the Date object will calculate the Date correctly. An example of
// a DST change at 0:00 is the Brazilian DST change.
var minCorrectionForDSTChangeAtMidNight = overDate.getHours()*60 + overDate.getMinutes();
// Prevent negative numbers when the DST changes makes 0:15 go back to 23:15
minSinceStartOfDay = Math.max(0, minSinceStartOfDay - minCorrectionForDSTChangeAtMidNight);
// Set the startdate to the start of the day we are dragging it to
startDate = overDate.clone();
// Add the number of minutes since the start of the day
startDate = startDate.add(Date.MINUTE, minSinceStartOfDay);
// Check if there is an DST diff between the start of this new day and time of the
// appointment on that day. If so we need to add the DST diff, otherwise it will be an
// hour off on DST change days. If there is no DST diff it will just add 0.
startDate = startDate.add( Date.MILLI, Date.getDSTDiff(startDate, overDate) );
// Finally we set the due date based on the start date and duration
dueDate = startDate.add(Date.MINUTE, duration);
}
}
switch (this.state) {
// When selecting it is unclear if the user is changing the start or
// the dueDate of the selection. Here we check if the updated dates
// gives us a clear picture on the direction where the user is dragging
// towards. As soon as we have established that we will be using the
// resize dueDate and startDate handlers.
case DragStates.SELECTING:
if (startDate > this.initDate) {
this.dateRange.set(this.initDateRange.getStartDate(), dueDate);
this.state = DragStates.RESIZING_DUE;
} else if (dueDate < this.initDate || snapMode === Zarafa.calendar.data.SnapModes.DAY) {
this.state = DragStates.RESIZING_START;
this.dateRange.set(startDate, this.initDateRange.getDueDate());
}
break;
// While dragging we always update the start and dueDate to preserve
// the duration of the selected appointment.
case DragStates.DRAGGING:
this.dateRange.set(startDate, dueDate);
break;
// When resizing the startDate of the selection, beware that the user
// could also be moving his cursor to beyond the dueDate. If this
// happens, we swap the working state so during next round we will
// be moving the dueDate.
case DragStates.RESIZING_START:
if (startDate >= dueDate) {
this.state = DragStates.RESIZING_DUE;
this.dateRange.set(this.initDateRange.getStartDate(), startDate);
} else {
this.dateRange.setStartDate(startDate);
}
break;
// When resizing the dueDate of the selection, beware that the user
// could also be moving his cursor to before the startDate. If this
// happens, we swap the working state so during next round we will
// be moving the startDate.
case DragStates.RESIZING_DUE:
if (startDate >= dueDate) {
this.state = DragStates.RESIZING_START;
this.dateRange.set(dueDate, this.initDateRange.getDueDate());
} else {
this.dateRange.setDueDate(dueDate);
}
break;
}
}
});