mirror of
https://github.com/wassname/fullcalendar.git
synced 2026-07-28 11:20:16 +08:00
462 lines
13 KiB
JavaScript
462 lines
13 KiB
JavaScript
|
|
/* 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 '' +
|
|
'<div class="fc-bg">' +
|
|
'<table>' +
|
|
this.rowHtml('slotBg') + // leverages RowRenderer, which will call slotBgCellHtml
|
|
'</table>' +
|
|
'</div>' +
|
|
'<div class="fc-slats">' +
|
|
'<table>' +
|
|
this.slatRowHtml() +
|
|
'</table>' +
|
|
'</div>';
|
|
},
|
|
|
|
|
|
// 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 =
|
|
'<td class="fc-axis fc-time ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' +
|
|
((!slotNormal || !minutes) ? // if irregular slot duration, or on the hour, then display the time
|
|
'<span>' + // for matchCellWidths
|
|
htmlEscape(calendar.formatDate(slotDate, view.opt('axisFormat'))) +
|
|
'</span>' :
|
|
''
|
|
) +
|
|
'</td>';
|
|
|
|
html +=
|
|
'<tr ' + (!minutes ? '' : 'class="fc-minor"') + '>' +
|
|
(!isRTL ? axisHtml : '') +
|
|
'<td class="' + view.widgetContentClass + '"/>' +
|
|
(isRTL ? axisHtml : '') +
|
|
"</tr>";
|
|
|
|
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 = $('<div class="fc-helper-skeleton"/>')
|
|
.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 += '<td colspan="' + (seg.col - col) + '"/>';
|
|
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 +=
|
|
'<td>' +
|
|
'<div class="fc-highlight-container">' +
|
|
'<div class="fc-highlight" style="top:' + top + 'px;bottom:-' + bottom + 'px"/>' +
|
|
'</div>' +
|
|
'</td>';
|
|
|
|
col++;
|
|
}
|
|
|
|
// need empty cells after the last segment?
|
|
if (col < view.colCnt) {
|
|
cellHtml += '<td colspan="' + (view.colCnt - col) + '"/>';
|
|
}
|
|
|
|
cellHtml = this.bookendCells(cellHtml, 'highlight');
|
|
|
|
return '' +
|
|
'<div class="fc-highlight-skeleton">' +
|
|
'<table>' +
|
|
'<tr>' +
|
|
cellHtml +
|
|
'</tr>' +
|
|
'</table>' +
|
|
'</div>';
|
|
}
|
|
|
|
});
|