Ext.namespace('Zarafa.calendar.ui.canvas');
/**
* @class Zarafa.calendar.ui.canvas.AppointmentBoxView
* @extends Zarafa.calendar.ui.canvas.AppointmentView
*
* Appointment view used in {@link Zarafa.calendar.ui.AbstractCalendarBoxView CalendarBoxView}. It represents each appointment as one
* or more horizontal rectangles on one or more week rows.
* <p>
* The view tries to minimise the number of required HTML elements by using background images to represent the busy status
* strips on the left side of appointments. The background of each view is implemented using a scaled IMG tag.
*/
Zarafa.calendar.ui.canvas.AppointmentBoxView = Ext.extend(Zarafa.calendar.ui.canvas.AppointmentView, {
/**
* Array of objects containing the {@link Zarafa.calendar.data.AppointmentBounds bounds} for the
* {@link #body} elements which are part of the appointment. This field is initialized in
* {@link #layoutInBody} using the function
* {@link Zarafa.calendar.ui.AbstractCalendarBoxView#dateRangeToBodyBounds dateRangeToBodyBounds}.
* @property
* @type Array
*/
bounds : undefined,
/**
* The main text which will be rendered into the body of the appointment. This field
* is initialized using the {@link #mainTextRenderer}.
* @property
* @type String
*/
mainRenderedText : '',
/**
* The subtext which will be rendered alongside the {@link #mainRenderedText}. This field
* is initialized using the {@link #subTextRenderer}.
* @property
* @type String
*/
subRenderedText : '',
/**
* This will mark the appointment as selected, and will draw the
* Selection Outline on the 3rd layer of the canvas {@link Zarafa.calendar.ui.canvas.CalendarBoxView calendar}.
* @param {Boolean} selected True iff the appointment should be marked as selected.
* @override
*/
setSelected : function(selected)
{
Zarafa.calendar.ui.canvas.AppointmentBoxView.superclass.setSelected.call(this, selected);
if (selected && !Ext.isEmpty(this.bounds)) {
this.drawBodySelectionOutline(this.bounds);
// when selecting appointment set focus also so key shortcuts work properly
this.focus();
}
},
/**
* Tests whether a mouse event is over the body start (left) resize handle.
* @param {Ext.EventObject} event event object.
* @return {Boolean} true iff the event is over the resize handle.
* @override
*/
eventOverBodyStartHandle : function(event)
{
if (this.isHeaderRange() || !this.bounds || this.bounds.length === 0) {
return false;
}
var position = this.getEventBodyPosition(event);
var bounds = this.bounds[0];
var element = {
left : bounds.left,
right : (bounds.left + this.dragHandleWidth),
top : bounds.top,
bottom : bounds.bottom
};
return this.isEventOverElement(position, element);
},
/**
* Tests whether a mouse event is over the body due (right) resize handle.
* @param {Ext.EventObject} event event object.
* @return {Boolean} true iff the event is over the resize handle.
* @override
*/
eventOverBodyDueHandle : function(event)
{
if (this.isHeaderRange() || !this.bounds || this.bounds.length === 0) {
return false;
}
var position = this.getEventBodyPosition(event);
var bounds = this.bounds[this.bounds.length - 1];
var element = {
left : (bounds.right - this.dragHandleWidth),
right : bounds.right,
top : bounds.top,
bottom : bounds.bottom
};
return this.isEventOverElement(position, element);
},
/**
* Tests whether a mouse event is over the appointment when laid out in the calendar body.
* @param {Ext.EventObject} event event object.
* @return {Boolean} true iff the event is over the appointment.
* @override
*/
eventOverBody : function(event)
{
if (this.isHeaderRange() || !this.bounds || this.bounds.length === 0) {
return false;
}
var position = this.getEventBodyPosition(event);
for (var i = 0, len = this.bounds.length; i < len; i++) {
if (this.isEventOverElement(position, this.bounds[i])) {
return true;
}
}
return false;
},
* Lays out the header elements of the view.
* NOTE: this function does nothing as the boxView does not render
* any headers for an appointment.
* @override
* @private
*/
layoutInHeader : Ext.emptyFn,
* Creates elements to represent the range when shown in the header.
* NOTE: this function does nothing as the boxView does not render
* any headers for an appointment.
* @private
* @override
*/
createHeader : Ext.emptyFn,
* Destroys the header elements.
* NOTE: this function does nothing as the boxView does not render
* any headers for an appointment.
* @private
* @override
*/
destroyHeader : Ext.emptyFn,
/**
* Draws the selection outline for the appointment inside the Calendar header.
* This is shown when the appointment is {@link #selected}.
* The draghandles are rendered using {@link #drawDragHandle}.
* @param {Zarafa.calendar.data.AppointmentBounds} bounds The bounds object containing
* the location of this body.
* @private
*/
drawBodySelectionOutline : function(bounds)
{
var context = this.parentView.getBodySelectionCanvas().dom.getContext('2d');
var borderWidth = 1;
// Draw a border on each of the bounds which indicate that the
// appointment is selected.
context.save();
context.lineWidth = borderWidth;
context.strokeStyle = 'black';
for (var i = 0, len = bounds.length; i < len; i++) {
var bound = bounds[i];
// Get the left-top position of the appointment.
// When drawing the border, our position will be the center
// of the border. Thus update the coordinates, to move them
// to the correct center.
var x = bound.left + (borderWidth / 2);
var y = bound.top + (borderWidth / 2);
// Determine the exact dimensions of the appointment
// When drawing the border, our position will be the center
// of the border. The coordinates will have been moved to reflect
// this, thus we must update our dimensions as well.
var width = bound.right - bound.left - borderWidth;
var height = bound.bottom - bound.top - borderWidth;
context.strokeRect(x, y, width, height);
if (bound.firstBox) {
// Draghandles must be positioned in the center of the appointment.
y += Math.ceil(height / 2);
// Draw the top dragHandle
this.drawDragHandle(context, x, y);
}
if (bound.lastBox) {
// Draghandles must be positioned in the center of the appointment.
// If this is also the firstBox, then we have already repositioned
// the x coordinate to the center.
if (!bound.firstBox) {
y += Math.ceil(height / 2);
}
this.drawDragHandle(context, x + width, y);
}
}
context.restore();
},
/**
* Draws a single body element for the appointment body on the Canvas context.
* @param {CanvasRenderingContext2D} context The canvas object on which we are drawing.
* @param {Zarafa.calendar.data.AppointmentBounds} bound The bounds of this element.
* @param {Number} index the index of bound.
* @private
*/
drawBodyElement : function(context, bound, index)
{
var width = bound.right - bound.left;
var height = bound.bottom - bound.top;
context.save();
context.translate(bound.left, bound.top);
context.lineWidth = 1;
// Draw a white background for transparent appointment
context.fillStyle = 'white';
context.fillRect(3, 3, width-6, height-6);
var color = this.getAppointmentColor();
var appointmentOpacity = 1;
if ( !this.isActive() ) {
appointmentOpacity = this.opacityNonActiveAppointment;
}
var stripWidth = this.getStripWidth();
context.globalAlpha = appointmentOpacity;
// Draw the appointment box
context.fillStyle = color;
context.fillRect(3 + stripWidth, 3, width-6-stripWidth, height-6);
var busyStatus = this.getBusyStatus();
switch (busyStatus)
{
case Zarafa.core.mapi.BusyStatus.FREE:
context.fillStyle = 'white';
context.fillRect(3, 3, stripWidth, height - 6);
context.strokeStyle = color;
context.strokeRect(3.5, 3.5, stripWidth, height -7);
break;
case Zarafa.core.mapi.BusyStatus.TENTATIVE:
context.fillStyle = color;
context.fillRect(3, 3, stripWidth, height - 6);
// For tentative we use an image to only show parts of the background. This
// image should not be transparent otherwise the color behind that will show
// in the places where it should not be shown. So we reset the alpha for this.
context.globalAlpha = 1;
context.fillStyle = context.createPattern(Zarafa.calendar.ui.IconCache.getDashedImage(), 'repeat');
context.fillRect(3, 3, stripWidth, height - 6);
break;
case Zarafa.core.mapi.BusyStatus.OUTOFOFFICE:
context.fillStyle = '#912787';
context.fillRect(3, 3, stripWidth, height - 6);
break;
default :
// Draw the busystatus box in the same color as the appointment
context.fillStyle = '#0f70bd';
context.fillRect(3, 3, stripWidth, height - 6);
break;
}
var x = stripWidth + this.parentView.headerBackgroundCanvasStylingElement.getPadding('l');
context.globalAlpha = this.isActive() ? 1.0 : this.opacityNonActiveAppointment;
// Get the font
var font = this.parentView.headerBackgroundCanvasStylingElement.getStyle('font');
// Check if we have a light or dark color
var isDarkColor = Zarafa.core.ColorSchemes.getLuma(color) < 155;
var fontColor = this.isActive() && isDarkColor ? 'white' : 'black';
// First start drawing all icons
var icons = this.iconRenderer();
for (var i = 0, len = icons.length; i < len; i++) {
var img = Zarafa.calendar.ui.IconCache['get' + Ext.util.Format.capitalize(icons[i]) + 'Icon' + (fontColor==='white'?'Active':'')]();
context.drawImage(img, x, Math.ceil((height - img.height) / 2));
x += img.width + 5;
}
// Set a clip so we won't draw text outside the box
context.beginPath();
context.rect(3, 3, width-6, height-6);
context.clip();
var stop = Math.min(1, Math.max(0.1, (width - x) / width));
var gradient = context.createLinearGradient(0, 0, width, 0);
gradient.addColorStop(0, this.isActive() ? 'black' : '#666666');
gradient.addColorStop(stop, this.isActive() ? 'black' : '#666666');
gradient.addColorStop(1, 'rgba(0,0,0,0)');
context.fillStyle = gradient;
context.lineWidth = 1;
context.font = font;
context.fillStyle = fontColor;
// create an object which is used to show text on appointment.
var drawTextObject = {
xPosition : x,
yPosition : height - Math.ceil((height-parseInt(this.parentView.headerBackgroundCanvasStylingElement.getStyle('font-size')))/2) - 2,
width : width,
showStartTime : false,
showEndTime : false
};
// it will check that appointment was all day event then don't show
// start and end time.
if(!this.isAllDay()) {
drawTextObject.startTimeText = Ext.util.Format.htmlDecode(this.startTimeTextRenderer());
drawTextObject.endTimeText = Ext.util.Format.htmlDecode(this.endTimeTextRenderer());
// it will check that bounds length more then one it means
// appointment is lies in to multiple weeks.
if(this.bounds.length > 1) {
// we have to show the start time and appointment title to first bounds.
if(index === 0) {
drawTextObject.showStartTime = true;
} else if ((this.bounds.length - 1) === index) {
// we have to show the end time and appointment title but
// we don't have to show start time because it is last bounds for the appointment.
drawTextObject.showEndTime = true;
}
} else if(this.bounds.length === 1) {
// if bounds length was one it means appointment does not
// lies in to multiple weeks. but it may possible that appointment
// lies in to multiple days
// it will check that appointment is more then
// one day event. then show the start/end time and appointment title with
// its sub text.
if(this.getDateRange().getNumDays() > 0) {
drawTextObject.showStartTime = true;
drawTextObject.showEndTime = true;
} else {
// appointment was less then one day event
// so we have to just show the start time and appointment title.
drawTextObject.showStartTime = true;
}
}
}
this.drawTextOnAppointment(context, drawTextObject);
context.restore();
},
/**
* Function is responsible to draw start time , end time and
* appointment title. it will draw the text based on the given object.
*
* @param {CanvasRenderingContext2D} context The canvas object on which we are drawing.
* @param {Object} Obj the Obj contains the configuration option which used to draw the text
* on appointment
* @private
*/
drawTextOnAppointment : function(context, obj)
{
var titleText = this.mainRenderedText + ' '+ this.subRenderedText;
var endTimeWidth = context.textWidth(obj.endTimeText);
var startTimeWidth = context.textWidth(obj.startTimeText + ' ');
var rightFlot = obj.width - (endTimeWidth + this.getStripWidth());
var perTextSize, size;
// draw the end time and appointment title.
if(obj.showEndTime && !obj.showStartTime) {
// draw end time at extreme right position of appointment.
context.drawText(obj.endTimeText, rightFlot, obj.yPosition);
// find the character size and based on that character size find the
// approximated characters are draw in remaining width.
perTextSize = endTimeWidth / obj.endTimeText.length;
size = Math.floor(rightFlot/perTextSize);
titleText = Ext.util.Format.ellipsis(titleText, size, false);
} else if(obj.showStartTime && !obj.showEndTime) {
// draw the start time and update the x position on which appointment title was draw.
context.drawText(obj.startTimeText, obj.xPosition, obj.yPosition);
obj.xPosition += startTimeWidth;
} else if(obj.showStartTime && obj.showEndTime) {
// draw the start and end time on respective appointment.
// draw end time at extreme right position of appointment.
context.drawText(obj.endTimeText, rightFlot, obj.yPosition);
// draw the start time at left most position of appointment.
context.drawText(obj.startTimeText, obj.xPosition, obj.yPosition);
obj.xPosition += startTimeWidth;
// find remaining width to draw the appointment title.
var remainingWidth = rightFlot - obj.xPosition;
// find the character size and based on that character size find the
// approximated characters are draw in remaining width.
perTextSize = startTimeWidth / obj.startTimeText.length;
size = Math.floor(remainingWidth/perTextSize);
// apply ellipsis if text is bigger then remaining width.
titleText = Ext.util.Format.ellipsis(titleText, size, false);
}
context.drawText(titleText, obj.xPosition, obj.yPosition);
},
/**
* Draws the elements for the appointment body on the Canvas context.
* @param {Array} bounds array of {@link Zarafa.calendar.data.AppointmentBounds bounds} objects.
* @private
*/
drawBodyElements : function(bounds)
{
// Obtain the context object on which we
// will be drawing our appointment.
var context = this.parentView.getBodyAppointmentCanvas().dom.getContext('2d');
// Draw all bounds onto the context
for (var i = 0, len = bounds.length; i < len; i++) {
this.drawBodyElement(context, bounds[i], i);
}
if (this.isSelected()) {
this.drawBodySelectionOutline(bounds);
}
},
/**
* Lays out the body of the view. This will generate the Body bounts using the
* function {@link Zarafa.calendar.ui.AbstractCalendarBoxView.dateRangeToBodyBounds dateRangeToBodyBounds},
* the bounds will be used for laying out the body elements using {@link #layoutBodyElements}.
* @private
* @override
*/
layoutInBody : function()
{
// get an array of bounds (left, right, top, bottom) objects to represent the range on the calendar body
this.bounds = this.parentView.dateRangeToBodyBounds(this.getDateRange(), this.slot, undefined, false);
// Draw the body elements to match the bounds
if (!Ext.isEmpty(this.bounds)) {
this.drawBodyElements(this.bounds);
}
},
/**
* Body start time text renderer. This will return the start time in string which
* must be displayed most prominently in the appointment when it has been rendered in the main contents
* section of the calendar. This applies to non-all-day appointments and while calender was
* {@link Zarafa.calendar.data.DataModes#MONTH month view} mode.
* @return {String} The start time text of appointment.
* @private
*/
startTimeTextRenderer : function()
{
return Ext.util.Format.htmlEncode(this.record.get('startdate').format(this.timeFormat));
},
/**
* Body end time text renderer. This will return the start time in string which
* must be displayed most prominently in the appointment when it has been rendered in the main contents
* section of the calendar. This applies to non-all-day appointments and while calender was
* {@link Zarafa.calendar.data.DataModes#MONTH month view} mode.
* @return {String} The end time text of appointment.
* @private
*/
endTimeTextRenderer : function()
{
return Ext.util.Format.htmlEncode(this.record.get('duedate').format(this.timeFormat));
},
/**
* Lays out the view. This function is called after {@link #render} and is used
* to update the view to the latest situation. When an appointment, or setting
* has been changed, the {@link #layout} function must change the look to reflect
* the new changes.
* @protected
*/
onLayout : function()
{
// The text Renderers deliver everything in HTML encoded strings. We must
// decode it here, as Canvas doesn't need encoded text
this.mainRenderedText = Ext.util.Format.htmlDecode(this.mainTextRenderer());
this.subRenderedText = Ext.util.Format.htmlDecode(this.subTextRenderer());
Zarafa.calendar.ui.canvas.AppointmentBoxView.superclass.onLayout.call(this);
}
});