Ext.namespace('Zarafa.calendar.ui.canvas');
/**
* @class Zarafa.calendar.ui.canvas.AppointmentDaysView
* @extends Zarafa.calendar.ui.canvas.AppointmentView
*
* Appointment view used in {@link Zarafa.calendar.ui.AbstractCalendarDaysView CalendarDaysView}.
* Each instance of {@link Zarafa.calendar.ui.canvas.AppointmentDaysView} represents a single
* appointment which consists of one or horizontal rectangles on one or more day columns.
* <p>
* This class should be used when displaying appointments in a more detailed view (compared
* to the {@link Zarafa.calendar.ui.canvas.AppointmentBoxView AppointmentBoxView} using a
* header and a body.
*/
Zarafa.calendar.ui.canvas.AppointmentDaysView = Ext.extend(Zarafa.calendar.ui.canvas.AppointmentView, {
/**
* Array of objects containing the {@link Zarafa.calendar.data.AppointmentBounds bounds} for the
* {@link #body} or {@link #header} elements which are part of the appointment.
* This field is initialized in {@link #layoutInBody} or {@link #layoutInHeader} using the function
* {@link Zarafa.calendar.ui.AbstractCalendarDaysView#dateRangeToBodyBounds dateRangeToBodyBounds}.
* @property
* @type Array
*/
bounds : undefined,
/**
* @cfg {Number} lineHeight The textheight for the text which will be rendered
*/
lineHeight : 13,
/**
* @cfg {Number} bodyTextTopOffset The offset from the top of the appointment used to render the text in the body
*/
bodyTextTopOffset: 4,
/**
* @cfg {Number} bodyTextBottomOffset The offset from the bottom of the appointment until where the text in the body can be rendered.
*/
bodyTextBottomOffset : 2,
/**
* @cfg {Number} leftPadding Left padding in pixels of the appointment text within the appointment body
*/
leftPadding : 4,
/**
* @cfg {Number} iconSpacing The x distance between consecutive icons
*/
iconSpacing : 5,
/**
* 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.CalendarDaysView calendar}.
* @param {Boolean} selected True iff the appointment should be marked as selected.
* @override
*/
setSelected : function(selected)
{
Zarafa.calendar.ui.canvas.AppointmentDaysView.superclass.setSelected.call(this, selected);
if (selected && !Ext.isEmpty(this.bounds)) {
if (this.isHeaderRange()) {
this.drawHeaderSelectionOutline(this.bounds);
} else {
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 header start (left) resize handle.
* @param {Ext.EventObject} event event object.
* @return {Boolean} true iff the event is over the resize handle.
* @override
*/
eventOverHeaderStartHandle: function(event)
{
if (!this.isHeaderRange() || !this.bounds || this.bounds.length === 0) {
return false;
}
var position = this.getEventHeaderPosition(event);
var element = {
left : this.bounds.left,
right : (this.bounds.left + this.dragHandleWidth),
top : this.bounds.top,
bottom : this.bounds.bottom
};
return this.isEventOverElement(position, element);
},
/**
* Tests whether a mouse event is over the header due (right) resize handle.
* @param {Ext.EventObject} event event object.
* @return {Boolean} true iff the event is over the resize handle.
* @override
*/
eventOverHeaderDueHandle : function(event)
{
if (!this.isHeaderRange() || !this.bounds || this.bounds.length === 0) {
return false;
}
var position = this.getEventHeaderPosition(event);
var element = {
left : (this.bounds.right - this.dragHandleWidth),
right : this.bounds.right,
top : this.bounds.top,
bottom : this.bounds.bottom
};
return this.isEventOverElement(position, element);
},
* Tests whether a mouse event is over the appointment when laid out in the calendar header.
* @param {Ext.EventObject} event event object.
* @return {Boolean} true iff the event is over the appointment.
* @override
*/
eventOverHeader : function(event)
{
if (!this.isHeaderRange() || !this.bounds || this.bounds.length === 0) {
return false;
}
var position = this.getEventHeaderPosition(event);
return this.isEventOverElement(position, this.bounds);
},
/**
* Tests whether a mouse event is over the body start (top) 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.right,
top : bounds.top,
bottom : (bounds.top + this.dragHandleHeight)
};
return this.isEventOverElement(position, element);
},
/**
* Tests whether a mouse event is over the body due (bottom) 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.left,
right : bounds.right,
top : (bounds.bottom - this.dragHandleHeight),
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;
},
* 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} bound The bounds object containing
* the position for the header.
* @private
*/
drawHeaderSelectionOutline : function(bound)
{
var context = this.parentView.getHeaderSelectionCanvas().dom.getContext('2d');
var borderWidth = 1;
// 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;
// Draw the border as rounder rectangular.
context.save();
context.beginPath();
context.lineWidth = borderWidth;
context.strokeStyle = 'black';
context.rect(x, y, width, height);
context.stroke();
// Draghandles must be positioned in the middle of the appointment.
y += Math.floor(height / 2);
// Draw the left and right draghandles.
if (bound.firstBox) {
this.drawDragHandle(context, x, y);
}
if (bound.lastBox) {
this.drawDragHandle(context, x + width, y);
}
context.restore();
},
/**
* Draws the selection outline for the appointment inside the Calendar body.
* This is shown when the appointment is {@link #selected}.
* The draghandles are rendered using {@link #drawDragHandle}.
* @param {Array} bounds The {@link Zarafa.calendar.data.AppointmentBounds #bounds} array containing
* the position for the 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.beginPath();
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;
// Draw the border as rounded rectangular.
context.rect(x, y, width, height);
context.stroke();
if (bound.firstBox) {
// Draghandles must be positioned in the center of the appointment.
x += Math.ceil(width / 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) {
x += Math.ceil(width / 2);
}
this.drawDragHandle(context, x, y + height);
}
}
context.restore();
},
/**
* Draws text on the appointment when laid out on the calendar body. A gradient with an alpha component is used
* to fade out text that is close to the right border of the appointment which looks prettier.
* @param {CanvasRenderingContext2D} context canvas drawing context.
* @param {Number} x horizontal position.
* @param {Number} y vertical position.
* @param {Number} width maximum text width.
* @param {Number} maxHeight maximum available height for the text
* @private
*/
drawBodyText : function(context, x, y, width, maxHeight)
{
// Avoid division by zero.
if (width <= 0) {
return;
}
var color = this.getAppointmentColor();
// 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();
var clipX = 0;
var clipY = y;
var img;
for (var i = 0, len = icons.length; i < len; i++) {
img = Zarafa.calendar.ui.IconCache['get' + Ext.util.Format.capitalize(icons[i]) + 'Icon' + (this.isActive() && isDarkColor?'Active':'')]();
context.drawImage(img, x+clipX, clipY + this.bodyTextTopOffset);
clipX += img.width + this.iconSpacing;
}
//add icon height to the clipping rectangle height
if(icons.length>0) {
img = Zarafa.calendar.ui.IconCache['get' + Ext.util.Format.capitalize(icons[0]) + 'Icon']();
clipY += img.height;
}
// Create a gradient that fades out gradually near the right border.
var gradient = context.createLinearGradient(x, y, x + width, y);
// Start fading out to transparent from 12 pixels from the right border.
var stop = Math.min(1, Math.max(0.1, (width - 12) / width));
gradient.addColorStop(0, fontColor);
gradient.addColorStop(stop, fontColor);
gradient.addColorStop(1, color);
context.fillStyle = gradient;
context.lineWidth = 1;
context.font = this.parentView.headerBackgroundCanvasStylingElement.getStyle('font');
// Draw text using simple wrapping.
var textHeight = context.drawWrappedText(this.mainRenderedText, x, y + this.lineHeight, width, this.lineHeight, maxHeight, clipX, clipY);
// Check if we have sufficient room for at least 1 extra line which contains the
// subText.
if ((textHeight + this.lineHeight) < maxHeight) {
context.lineWidth = 1;
context.font = this.parentView.headerBackgroundCanvasStylingElement.getStyle('font');
// Draw text using simple wrapping.
context.drawWrappedText(this.subRenderedText, x, y + this.lineHeight + textHeight, width, this.lineHeight, maxHeight - textHeight);
}
},
* Lays out the header elements of the view.
* @private
*/
layoutInHeader : function()
{
var color = this.getAppointmentColor();
// Check if we have a light or dark color
var isDarkColor = Zarafa.core.ColorSchemes.getLuma(color) < 155;
var fontColor = this.isActive() && isDarkColor ? 'white' : 'black';
// Get the bounds of the header from the parent calendar.
this.bounds = this.parentView.dateRangeToHeaderBounds(this.getDateRange(), this.slot, 1, true);
if (Ext.isEmpty(this.bounds)) {
return;
}
var width = this.bounds.right - this.bounds.left;
var height = this.bounds.bottom - this.bounds.top;
var context = this.parentView.getHeaderAppointmentCanvas().dom.getContext('2d');
context.save();
context.translate(this.bounds.left, this.bounds.top);
context.lineWidth = 1;
var appointmentOpacity = 0.8;
if(!this.isActive()){
appointmentOpacity = this.opacityNonActiveAppointment;
}
context.fillStyle = 'white';
context.fillRect(0,0, width, height-1);
var stripWidth = this.getStripWidth();
context.fillStyle = color;
context.globalAlpha = appointmentOpacity;
context.fillRect(stripWidth+1, 1, width-2-stripWidth, height-3);
var busyStatus = this.getBusyStatus();
// Dimimensions of the busybox
var busyBoxLeft = 1;
var busyBoxTop = 1;
var busyBoxWidth = stripWidth;
var busyBoxHeight = height -3;
switch (busyStatus) {
case Zarafa.core.mapi.BusyStatus.FREE:
// The busybox is already white, so do nothing here
break;
case Zarafa.core.mapi.BusyStatus.TENTATIVE:
// First
context.fillStyle = color;
context.fillRect(busyBoxLeft, busyBoxTop, busyBoxWidth, busyBoxHeight);
// Then use a striped image to cover part of the background with white.
context.globalAlpha = 1;
context.fillStyle = context.createPattern(Zarafa.calendar.ui.IconCache.getDashedImage(), 'repeat');
context.fillRect(busyBoxLeft, busyBoxTop, busyBoxWidth, busyBoxHeight);
break;
case Zarafa.core.mapi.BusyStatus.OUTOFOFFICE:
// Draw the busy status box in purple
context.fillStyle = '#912787';
context.fillRect(busyBoxLeft, busyBoxTop, busyBoxWidth, busyBoxHeight);
break;
default :
// Draw the busy status box in Kopano blue
context.fillStyle = '#0f70bd';
context.fillRect(busyBoxLeft, busyBoxTop, busyBoxWidth, busyBoxHeight);
break;
}
var x = this.leftPadding + stripWidth;
context.globalAlpha = this.isActive() ? 1.0 : this.opacityNonActiveAppointment;
// 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' + (this.isActive() && isDarkColor?'Active':'')]();
context.drawImage(img, x, 6);
x += img.width + 5;
}
// Start fading out to transparent from 12 pixels from the right border.
var stop = Math.min(1, Math.max(0.1, (width - 12) / width));
var gradient = context.createLinearGradient(0, 0, width, 0);
gradient.addColorStop(0, fontColor);
gradient.addColorStop(stop, fontColor);
gradient.addColorStop(1, this.isActive() ? 'rgba(255, 255, 255, 0)' : 'rgba(0, 0, 0, 0)');
context.fillStyle = gradient;
context.save();
// Create a clip on the appointment, so the text will never be drawn outside of it
context.beginPath();
context.rect(x, 0, width - x, height);
context.clip();
context.lineWidth = 1;
context.font = this.parentView.headerBackgroundCanvasStylingElement.getStyle('font');
context.drawText(this.mainRenderedText, x, height - 8);
// Update the X position with the text we just drawn
x += context.textWidth(this.mainRenderedText + ' ');
context.drawText(this.subRenderedText, x, height - 8);
context.restore();
// Drag drag handles if selected
if (this.isSelected()) {
this.drawHeaderSelectionOutline(this.bounds);
}
context.restore();
},
/**
* Draws an element of the appointment when laid out in the calendar body.
* @param {CanvasRenderingContext2D} context canvas drawing context.
* @param {Zarafa.calendar.data.AppointmentBounds} bound The bounds of this element.
* @private
*/
drawBodyElement : function(context, bound)
{
var busyStatus = this.getBusyStatus();
var width = bound.right - bound.left - 2; // -2 to have it within the vertical day lines
var height = bound.bottom - bound.top;
var realHeight = this.parentView.getRangeVerticalHeight(this.getDateRange());
context.save();
context.translate(bound.left, bound.top);
context.lineWidth = 1;
var color = this.getAppointmentColor();
var appointmentOpacity = 0.8;
if(!this.isActive()){
appointmentOpacity = this.opacityNonActiveAppointment;
}
context.globalAlpha = appointmentOpacity;
context.fillStyle = color;
var stripWidth = this.getStripWidth();
// The outer strip is the attached bar that shows the duration of the appointment when it is
// shorter than the resolution of the calendar view.
var showOuterStrip = (realHeight + 1) < height;
// The inner strip is the bar that shows duration of the appointment when the resolution is
// small enough to show it inside the body of the appointment.
var showInnerStrip = !showOuterStrip && stripWidth > 0;
var textLeft = stripWidth;
context.fillRect(stripWidth + 1, 1, width - stripWidth, height);
// Dimimensions of the busybox
var busyBoxLeft = 1;
var busyBoxTop = 1;
var busyBoxWidth = stripWidth;
var busyBoxHeight = height -1;
if ( showInnerStrip ) {
// Draw a white line around the appointment for inner strips
context.strokeStyle = 'white';
// If you are wondering about the additions of 0.5, please read http://stackoverflow.com/a/8696641
context.globalAlpha = 1;
context.strokeRect(0.5, 0.5, width, height);
context.globalAlpha = appointmentOpacity;
} else if ( showOuterStrip ) {
busyBoxHeight = realHeight - 1;
}
switch (busyStatus) {
case Zarafa.core.mapi.BusyStatus.FREE:
// First draw the outline of the busystatus box in the same color as the appointment
context.strokeStyle = context.convertHexRgbToDecRgba(color);
context.strokeRect(busyBoxLeft + 0.5, busyBoxTop + 0.5, busyBoxWidth-1, busyBoxHeight-1);
// Then fill the box with white in the same opacity as the appointment
context.fillStyle = 'white';
busyBoxLeft += 1;
busyBoxTop += 1;
busyBoxWidth -= 1;
busyBoxHeight -= 2;
context.fillRect(busyBoxLeft, busyBoxTop, busyBoxWidth, busyBoxHeight);
break;
case Zarafa.core.mapi.BusyStatus.TENTATIVE:
// First draw the busystatus box background in the same color as the appointment
context.fillRect(busyBoxLeft, busyBoxTop, busyBoxWidth, busyBoxHeight);
// Then use a striped image to cover part of the background with white.
context.globalAlpha = 1;
context.fillStyle = context.createPattern(Zarafa.calendar.ui.IconCache.getDashedImage(), 'repeat');
context.fillRect(busyBoxLeft, busyBoxTop, busyBoxWidth, busyBoxHeight);
break;
case Zarafa.core.mapi.BusyStatus.OUTOFOFFICE:
// Draw the busystatus box in purple
context.fillStyle = '#912787';
context.fillRect(busyBoxLeft, busyBoxTop, busyBoxWidth, busyBoxHeight);
break;
default :
// Draw the busystatus box in the same color as the appointment
context.fillRect(busyBoxLeft, busyBoxTop, busyBoxWidth, busyBoxHeight);
break;
}
// Create a clip on the appointment, so the text will never be drawn outside of it
context.beginPath();
context.rect(stripWidth + 1, 1, width - stripWidth - 2, height-1);
context.clip();
context.globalAlpha = this.isActive() ? 1.0 : this.opacityNonActiveAppointment;
this.drawBodyText(
context,
textLeft + this.leftPadding,
this.bodyTextTopOffset,
width - textLeft - this.leftPadding,
height - this.bodyTextTopOffset - this.bodyTextBottomOffset
);
context.restore();
},
/**
* 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 the individual body elements.
for (var i = 0, len = bounds.length; i < len; i++) {
this.drawBodyElement(context, bounds[i]);
}
// Optionally draw drag handles
if (this.isSelected()) {
this.drawBodySelectionOutline(bounds);
}
},
/**
* Lays out the body of the view. This will generate the Body bounds using the
* function {@link Zarafa.calendar.ui.AbstractCalendarDaysView#dateRangeToBodyBounds dateRangeToBodyBounds}.
* Finally the bounds will be used for laying out the body elements using {@link #drawBodyElement}.
* @private
* @override
*/
layoutInBody : function()
{
this.bounds = this.parentView.dateRangeToBodyBounds(
this.getAdjustedDateRange(),
this.slot,
this.slotCount,
false
);
// Draw the body elements to match the bounds
if (!Ext.isEmpty(this.bounds)) {
this.drawBodyElements(this.bounds);
}
},
/**
* 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.
* If the range represented by this view spans over 24 hours,
* the body is made invisible and the header element is shown instead.
* @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.AppointmentView.superclass.onLayout.call(this);
}
});