/* A component that renders one or more columns of vertical time slots
----------------------------------------------------------------------------------------------------------------------*/
function TimeGrid(view) {
Grid.call(this, view); // call the super-constructor
}
TimeGrid.prototype = createObject(Grid.prototype); // define the super-class
$.extend(TimeGrid.prototype, {
slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines
snapDuration: null, // granularity of time for dragging and selecting
minTime: null, // Duration object that denotes the first visible time of any given day
maxTime: null, // Duration object that denotes the exclusive visible end time of any given day
dayEls: null, // cells elements in the day-row background
slatEls: null, // elements running horizontally across all columns
slatTops: null, // an array of top positions, relative to the container. last item holds bottom of last slot
highlightEl: null, // cell skeleton element for rendering the highlight
helperEl: null, // cell skeleton element for rendering the mock event "helper"
// Renders the time grid into `this.el`, which should already be assigned.
// Relies on the view's colCnt. In the future, this component should probably be self-sufficient.
render: function() {
this.processOptions();
this.el.html(this.renderHtml());
this.dayEls = this.el.find('.fc-day');
this.slatEls = this.el.find('.fc-slats tr');
this.computeSlatTops();
Grid.prototype.render.call(this); // call the super-method
},
// Renders the basic HTML skeleton for the grid
renderHtml: function() {
return '' +
'
' +
'
' +
this.rowHtml('slotBg') + // leverages RowRenderer, which will call slotBgCellHtml
'
' +
'
' +
'' +
'
' +
this.slatRowHtml() +
'
' +
'
';
},
// Renders the HTML for a vertical background cell behind the slots.
// This method is distinct from 'bg' because we wanted a new `rowType` so the View could customize the rendering.
slotBgCellHtml: function(row, col, date) {
return this.bgCellHtml(row, col, date);
},
// Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL.
slatRowHtml: function() {
var view = this.view;
var calendar = view.calendar;
var isRTL = view.opt('isRTL');
var html = '';
var slotNormal = this.slotDuration.asMinutes() % 15 === 0;
var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations
var slotDate; // will be on the view's first day, but we only care about its time
var minutes;
var axisHtml;
// Calculate the time for each slot
while (slotTime < this.maxTime) {
slotDate = view.start.clone().time(slotTime); // will be in UTC but that's good. to avoid DST issues
minutes = slotDate.minutes();
axisHtml =
'' +
((!slotNormal || !minutes) ? // if irregular slot duration, or on the hour, then display the time
'' + // for matchCellWidths
htmlEscape(calendar.formatDate(slotDate, view.opt('axisFormat'))) +
'' :
''
) +
' | ';
html +=
'' +
(!isRTL ? axisHtml : '') +
' | ' +
(isRTL ? axisHtml : '') +
"
";
slotTime.add(this.slotDuration);
}
return html;
},
// Parses various options into properties of this object
processOptions: function() {
var view = this.view;
var slotDuration = view.opt('slotDuration');
var snapDuration = view.opt('snapDuration');
slotDuration = moment.duration(slotDuration);
snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration;
this.slotDuration = slotDuration;
this.snapDuration = snapDuration;
this.cellDuration = snapDuration; // important to assign this for Grid.events.js
this.minTime = moment.duration(view.opt('minTime'));
this.maxTime = moment.duration(view.opt('maxTime'));
},
// Slices up a date range into a segment for each column
rangeToSegs: function(rangeStart, rangeEnd) {
var view = this.view;
var segs = [];
var seg;
var col;
var cellDate;
var colStart, colEnd;
// normalize
rangeStart = rangeStart.clone().stripZone();
rangeEnd = rangeEnd.clone().stripZone();
for (col = 0; col < view.colCnt; col++) {
cellDate = view.cellToDate(0, col); // use the View's cell system for this
colStart = cellDate.clone().time(this.minTime);
colEnd = cellDate.clone().time(this.maxTime);
seg = intersectionToSeg(rangeStart, rangeEnd, colStart, colEnd);
if (seg) {
seg.col = col;
segs.push(seg);
}
}
return segs;
},
/* Coordinates
------------------------------------------------------------------------------------------------------------------*/
// Called when there is a window resize/zoom and we need to recalculate coordinates for the grid
resize: function() {
this.computeSlatTops();
this.updateSegVerticals();
},
// Populates the given empty `rows` and `cols` arrays with offset positions of the "snap" cells.
// "Snap" cells are different the slots because they might have finer granularity.
buildCoords: function(rows, cols) {
var colCnt = this.view.colCnt;
var originTop = this.el.offset().top;
var snapTime = moment.duration(+this.minTime);
var p = null;
var e, n;
this.dayEls.slice(0, colCnt).each(function(i, _e) {
e = $(_e);
n = e.offset().left;
if (p) {
p[1] = n;
}
p = [ n ];
cols[i] = p;
});
p[1] = n + e.outerWidth();
p = null;
while (snapTime < this.maxTime) {
n = originTop + this.computeTimeTop(snapTime);
if (p) {
p[1] = n;
}
p = [ n ];
rows.push(p);
snapTime.add(this.snapDuration);
}
p[1] = originTop + this.computeTimeTop(snapTime); // the position of the exclusive end
},
// Gets the datetime for the given slot cell
getCellDate: function(cell) {
var view = this.view;
var calendar = view.calendar;
return calendar.rezoneDate( // since we are adding a time, it needs to be in the calendar's timezone
view.cellToDate(0, cell.col) // View's coord system only accounts for start-of-day for column
.time(this.minTime + this.snapDuration * cell.row)
);
},
// Gets the element that represents the whole-day the cell resides on
getCellDayEl: function(cell) {
return this.dayEls.eq(cell.col);
},
// Computes the top coordinate, relative to the bounds of the grid, of the given date.
// A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
computeDateTop: function(date, startOfDayDate) {
return this.computeTimeTop(
moment.duration(
date.clone().stripZone() - startOfDayDate.clone().stripTime()
)
);
},
// Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
computeTimeTop: function(time) {
var slatCoverage = (time - this.minTime) / this.slotDuration; // floating-point value of # of slots covered
var slatIndex;
var slatRemainder;
var slatTop;
var slatBottom;
// constrain. because minTime/maxTime might be customized
slatCoverage = Math.max(0, slatCoverage);
slatCoverage = Math.min(this.slatEls.length, slatCoverage);
slatIndex = Math.floor(slatCoverage); // an integer index of the furthest whole slot
slatRemainder = slatCoverage - slatIndex;
slatTop = this.slatTops[slatIndex]; // the top position of the furthest whole slot
if (slatRemainder) { // time spans part-way into the slot
slatBottom = this.slatTops[slatIndex + 1];
return slatTop + (slatBottom - slatTop) * slatRemainder; // part-way between slots
}
else {
return slatTop;
}
},
// Queries each `slatEl` for its position relative to the grid's container and stores it in `slatTops`.
// Includes the the bottom of the last slat as the last item in the array.
computeSlatTops: function() {
var tops = [];
var top;
this.slatEls.each(function(i, node) {
top = $(node).position().top;
tops.push(top);
});
tops.push(top + this.slatEls.last().outerHeight()); // bottom of the last slat
this.slatTops = tops;
},
/* Event Drag Visualization
------------------------------------------------------------------------------------------------------------------*/
// Renders a visual indication of an event being dragged over the specified date(s).
// `end` and `seg` can be null. See View's documentation on renderDrag for more info.
renderDrag: function(start, end, seg) {
var opacity;
if (seg) { // if there is event information for this drag, render a helper event
this.renderRangeHelper(start, end, seg);
opacity = this.view.opt('dragOpacity');
if (opacity !== undefined) {
this.helperEl.css('opacity', opacity);
}
return true; // signal that a helper has been rendered
}
else {
// otherwise, just render a highlight
this.renderHighlight(
start,
end || this.view.calendar.getDefaultEventEnd(false, start)
);
}
},
// Unrenders any visual indication of an event being dragged
destroyDrag: function() {
this.destroyHelper();
this.destroyHighlight();
},
/* Event Resize Visualization
------------------------------------------------------------------------------------------------------------------*/
// Renders a visual indication of an event being resized
renderResize: function(start, end, seg) {
this.renderRangeHelper(start, end, seg);
},
// Unrenders any visual indication of an event being resized
destroyResize: function() {
this.destroyHelper();
},
/* Event Helper
------------------------------------------------------------------------------------------------------------------*/
// Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag)
renderHelper: function(event, sourceSeg) {
var res = this.renderEventTable([ event ]);
var tableEl = res.tableEl;
var segs = res.segs;
var i, seg;
var sourceEl;
// Try to make the segment that is in the same row as sourceSeg look the same
for (i = 0; i < segs.length; i++) {
seg = segs[i];
if (sourceSeg && sourceSeg.col === seg.col) {
sourceEl = sourceSeg.el;
seg.el.css({
left: sourceEl.css('left'),
right: sourceEl.css('right'),
'margin-left': sourceEl.css('margin-left'),
'margin-right': sourceEl.css('margin-right')
});
}
}
this.helperEl = $('')
.append(tableEl)
.appendTo(this.el);
},
// Unrenders any mock helper event
destroyHelper: function() {
if (this.helperEl) {
this.helperEl.remove();
this.helperEl = null;
}
},
/* Selection
------------------------------------------------------------------------------------------------------------------*/
// Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight.
renderSelection: function(start, end) {
if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered
this.renderRangeHelper(start, end);
}
else {
this.renderHighlight(start, end);
}
},
// Unrenders any visual indication of a selection
destroySelection: function() {
this.destroyHelper();
this.destroyHighlight();
},
/* Highlight
------------------------------------------------------------------------------------------------------------------*/
// Renders an emphasis on the given date range. `start` is inclusive. `end` is exclusive.
renderHighlight: function(start, end) {
this.highlightEl = $(
this.highlightSkeletonHtml(start, end)
).appendTo(this.el);
},
// Unrenders the emphasis on a date range
destroyHighlight: function() {
if (this.highlightEl) {
this.highlightEl.remove();
this.highlightEl = null;
}
},
// Generates HTML for a table element with containers in each column, responsible for absolutely positioning the
// highlight elements to cover the highlighted slots.
highlightSkeletonHtml: function(start, end) {
var view = this.view;
var segs = this.rangeToSegs(start, end);
var cellHtml = '';
var col = 0;
var i, seg;
var dayDate;
var top, bottom;
for (i = 0; i < segs.length; i++) { // loop through the segments. one per column
seg = segs[i];
// need empty cells beforehand?
if (col < seg.col) {
cellHtml += ' | ';
col = seg.col;
}
// compute vertical position
dayDate = view.cellToDate(0, col);
top = this.computeDateTop(seg.start, dayDate);
bottom = this.computeDateTop(seg.end, dayDate); // the y position of the bottom edge
// generate the cell HTML. bottom becomes negative because it needs to be a CSS value relative to the
// bottom edge of the zero-height container.
cellHtml +=
'' +
'' +
' | ';
col++;
}
// need empty cells after the last segment?
if (col < view.colCnt) {
cellHtml += ' | ';
}
cellHtml = this.bookendCells(cellHtml, 'highlight');
return '' +
'' +
'
' +
'' +
cellHtml +
'
' +
'
' +
'
';
}
});