mirror of
https://github.com/wassname/fullcalendar.git
synced 2026-08-15 12:25:20 +08:00
large code refactor. new HTML/CSS skeleton, no more JQUI dependency, among other things
This commit is contained in:
@@ -0,0 +1,474 @@
|
||||
|
||||
/* 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.widgetHeaderClass + '">' +
|
||||
((!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(start, end) {
|
||||
var view = this.view;
|
||||
var segs = [];
|
||||
var col;
|
||||
var cellDate;
|
||||
var colStart, colEnd;
|
||||
var segStart, segEnd;
|
||||
var isStart, isEnd;
|
||||
|
||||
// normalize
|
||||
start = start.clone().stripZone();
|
||||
end = end.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().stripZone().time(this.minTime); // normalize and calculate
|
||||
colEnd = cellDate.clone().stripZone().time(this.maxTime); // normalize and calculate
|
||||
|
||||
if (end > colStart && start < colEnd) { // in bounds at all?
|
||||
|
||||
if (start >= colStart) {
|
||||
segStart = start.clone();
|
||||
isStart = true;
|
||||
}
|
||||
else {
|
||||
segStart = colStart; // don't need to clone
|
||||
isStart = false;
|
||||
}
|
||||
|
||||
if (end <= colEnd) {
|
||||
segEnd = end.clone();
|
||||
isEnd = true;
|
||||
}
|
||||
else {
|
||||
segEnd = colEnd; // don't need to clone
|
||||
isEnd = false;
|
||||
}
|
||||
|
||||
segs.push({
|
||||
col: col,
|
||||
start: segStart,
|
||||
end: segEnd,
|
||||
isStart: isStart,
|
||||
isEnd: isEnd
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return segs;
|
||||
},
|
||||
|
||||
|
||||
/* Coordinates
|
||||
------------------------------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
// 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) {
|
||||
// the View's cellToDate system only accounts for the beginning of whole days
|
||||
return this.view.cellToDate(0, cell.col).time(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>';
|
||||
}
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user