mirror of
https://github.com/wassname/fullcalendar.git
synced 2026-07-09 00:20:27 +08:00
330 lines
11 KiB
JavaScript
330 lines
11 KiB
JavaScript
|
|
/* Methods relate to limiting the number events for a given day on a DayGrid
|
|
----------------------------------------------------------------------------------------------------------------------*/
|
|
|
|
$.extend(DayGrid.prototype, {
|
|
|
|
|
|
segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible
|
|
popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible
|
|
|
|
|
|
destroySegPopover: function() {
|
|
if (this.segPopover) {
|
|
this.segPopover.hide(); // will trigger destruction of `segPopover` and `popoverSegs`
|
|
}
|
|
},
|
|
|
|
|
|
// Limits the number of "levels" (vertically stacking layers of events) for each row of the grid.
|
|
// `levelLimit` can be false (don't limit), a number, or true (should be computed).
|
|
limitRows: function(levelLimit) {
|
|
var rowStructs = this.rowStructs || [];
|
|
var row; // row #
|
|
var rowLevelLimit;
|
|
|
|
for (row = 0; row < rowStructs.length; row++) {
|
|
this.unlimitRow(row);
|
|
|
|
if (!levelLimit) {
|
|
rowLevelLimit = false;
|
|
}
|
|
else if (typeof levelLimit === 'number') {
|
|
rowLevelLimit = levelLimit;
|
|
}
|
|
else {
|
|
rowLevelLimit = this.computeRowLevelLimit(row);
|
|
}
|
|
|
|
if (levelLimit !== false) {
|
|
this.limitRow(row, rowLevelLimit);
|
|
}
|
|
}
|
|
},
|
|
|
|
|
|
// Computes the number of levels a row will accomodate without going outside its bounds.
|
|
// Assumes the row is "rigid" (maintains a constant height regardless of what is inside).
|
|
// `row` is the row number.
|
|
computeRowLevelLimit: function(row) {
|
|
var rowEl = this.rowEls.eq(row); // the containing "fake" row div
|
|
var rowHeight = rowEl.height(); // TODO: cache somehow?
|
|
var trEls = this.rowStructs[row].tbodyEl.children();
|
|
var i, trEl;
|
|
|
|
// Reveal one level <tr> at a time and stop when we find one out of bounds
|
|
for (i = 0; i < trEls.length; i++) {
|
|
trEl = trEls.eq(i).removeClass('fc-limited'); // get and reveal
|
|
if (trEl.position().top + trEl.outerHeight() > rowHeight) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return false; // should not limit at all
|
|
},
|
|
|
|
|
|
// Limits the given grid row to the maximum number of levels and injects "more" links if necessary.
|
|
// `row` is the row number.
|
|
// `levelLimit` is a number for the maximum (inclusive) number of levels allowed.
|
|
limitRow: function(row, levelLimit) {
|
|
var _this = this;
|
|
var view = this.view;
|
|
var rowStruct = this.rowStructs[row];
|
|
var moreNodes = []; // array of "more" <a> links and <td> DOM nodes
|
|
var col = 0; // col #
|
|
var cell;
|
|
var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right
|
|
var cellMatrix; // a matrix (by level, then column) of all <td> jQuery elements in the row
|
|
var limitedNodes; // array of temporarily hidden level <tr> and segment <td> DOM nodes
|
|
var i, seg;
|
|
var segsBelow; // array of segment objects below `seg` in the current `col`
|
|
var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies
|
|
var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column)
|
|
var td, rowspan;
|
|
var segMoreNodes; // array of "more" <td> cells that will stand-in for the current seg's cell
|
|
var j;
|
|
var moreTd, moreWrap, moreLink;
|
|
|
|
// Iterates through empty level cells and places "more" links inside if need be
|
|
function emptyCellsUntil(endCol) { // goes from current `col` to `endCol`
|
|
while (col < endCol) {
|
|
cell = { row: row, col: col };
|
|
segsBelow = _this.getCellSegs(cell, levelLimit);
|
|
if (segsBelow.length) {
|
|
td = cellMatrix[levelLimit - 1][col];
|
|
moreLink = _this.renderMoreLink(cell, segsBelow);
|
|
moreWrap = $('<div/>').append(moreLink);
|
|
td.append(moreWrap);
|
|
moreNodes.push(moreWrap[0]);
|
|
}
|
|
col++;
|
|
}
|
|
}
|
|
|
|
if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit?
|
|
levelSegs = rowStruct.segLevels[levelLimit - 1];
|
|
cellMatrix = rowStruct.cellMatrix;
|
|
|
|
limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level <tr> elements past the limit
|
|
.addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array
|
|
|
|
// iterate though segments in the last allowable level
|
|
for (i = 0; i < levelSegs.length; i++) {
|
|
seg = levelSegs[i];
|
|
emptyCellsUntil(seg.leftCol); // process empty cells before the segment
|
|
|
|
// determine *all* segments below `seg` that occupy the same columns
|
|
colSegsBelow = [];
|
|
totalSegsBelow = 0;
|
|
while (col <= seg.rightCol) {
|
|
cell = { row: row, col: col };
|
|
segsBelow = this.getCellSegs(cell, levelLimit);
|
|
colSegsBelow.push(segsBelow);
|
|
totalSegsBelow += segsBelow.length;
|
|
col++;
|
|
}
|
|
|
|
if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links?
|
|
td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell
|
|
rowspan = td.attr('rowspan') || 1;
|
|
segMoreNodes = [];
|
|
|
|
// make a replacement <td> for each column the segment occupies. will be one for each colspan
|
|
for (j = 0; j < colSegsBelow.length; j++) {
|
|
moreTd = $('<td class="fc-more-cell"/>').attr('rowspan', rowspan);
|
|
segsBelow = colSegsBelow[j];
|
|
cell = { row: row, col: seg.leftCol + j };
|
|
moreLink = this.renderMoreLink(cell, [ seg ].concat(segsBelow)); // count seg as hidden too
|
|
moreWrap = $('<div/>').append(moreLink);
|
|
moreTd.append(moreWrap);
|
|
segMoreNodes.push(moreTd[0]);
|
|
moreNodes.push(moreTd[0]);
|
|
}
|
|
|
|
td.addClass('fc-limited').after($(segMoreNodes)); // hide original <td> and inject replacements
|
|
limitedNodes.push(td[0]);
|
|
}
|
|
}
|
|
|
|
emptyCellsUntil(view.colCnt); // finish off the level
|
|
rowStruct.moreEls = $(moreNodes); // for easy undoing later
|
|
rowStruct.limitedEls = $(limitedNodes); // for easy undoing later
|
|
}
|
|
},
|
|
|
|
|
|
// Reveals all levels and removes all "more"-related elements for a grid's row.
|
|
// `row` is a row number.
|
|
unlimitRow: function(row) {
|
|
var rowStruct = this.rowStructs[row];
|
|
|
|
if (rowStruct.moreEls) {
|
|
rowStruct.moreEls.remove();
|
|
rowStruct.moreEls = null;
|
|
}
|
|
|
|
if (rowStruct.limitedEls) {
|
|
rowStruct.limitedEls.removeClass('fc-limited');
|
|
rowStruct.limitedEls = null;
|
|
}
|
|
},
|
|
|
|
|
|
// Renders an <a> element that represents hidden event element for a cell.
|
|
// Responsible for attaching click handler as well.
|
|
renderMoreLink: function(cell, hiddenSegs) {
|
|
var _this = this;
|
|
var view = this.view;
|
|
|
|
return $('<a class="fc-more"/>')
|
|
.text(
|
|
this.getMoreLinkText(hiddenSegs.length)
|
|
)
|
|
.on('click', function(ev) {
|
|
var clickOption = view.opt('eventLimitClick');
|
|
var date = view.cellToDate(cell);
|
|
var moreEl = $(this);
|
|
var dayEl = _this.getCellDayEl(cell);
|
|
var allSegs = _this.getCellSegs(cell);
|
|
|
|
// rescope the segments to be within the cell's date
|
|
var reslicedAllSegs = _this.resliceDaySegs(allSegs, date);
|
|
var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date);
|
|
|
|
if (typeof clickOption === 'function') {
|
|
// the returned value can be an atomic option
|
|
clickOption = view.trigger('eventLimitClick', null, {
|
|
date: date,
|
|
dayEl: dayEl,
|
|
moreEl: moreEl,
|
|
segs: reslicedAllSegs,
|
|
hiddenSegs: reslicedHiddenSegs
|
|
}, ev);
|
|
}
|
|
|
|
if (clickOption === 'popover') {
|
|
_this.showSegPopover(date, cell, moreEl, reslicedAllSegs);
|
|
}
|
|
else if (typeof clickOption === 'string') { // a view name
|
|
view.calendar.zoomTo(date, clickOption);
|
|
}
|
|
});
|
|
},
|
|
|
|
|
|
// Reveals the popover that displays all events within a cell
|
|
showSegPopover: function(date, cell, moreLink, segs) {
|
|
var _this = this;
|
|
var view = this.view;
|
|
var moreWrap = moreLink.parent(); // the <div> wrapper around the <a>
|
|
var options = {
|
|
className: 'fc-more-popover',
|
|
content: this.renderSegPopoverContent(date, segs),
|
|
parentEl: this.el,
|
|
top: this.rowEls.eq(cell.row).offset().top, // better than the <td>. no border confusion
|
|
autoHide: true, // when the user clicks elsewhere, hide the popover
|
|
hide: function() {
|
|
// destroy everything when the popover is hidden
|
|
_this.segPopover.destroy();
|
|
_this.segPopover = null;
|
|
_this.popoverSegs = null;
|
|
}
|
|
};
|
|
|
|
// Determine horizontal coordinate.
|
|
// We use the moreWrap instead of the <td> to avoid border confusion.
|
|
if (view.opt('isRTL')) {
|
|
options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border
|
|
}
|
|
else {
|
|
options.left = moreWrap.offset().left - 1; // -1 to be over cell border
|
|
}
|
|
|
|
this.segPopover = new Popover(options);
|
|
this.segPopover.show();
|
|
},
|
|
|
|
|
|
// Builds the inner DOM contents of the segment popover
|
|
renderSegPopoverContent: function(date, segs) {
|
|
var view = this.view;
|
|
var isTheme = view.opt('theme');
|
|
var title = date.format(view.opt('eventLimitPopoverFormat'));
|
|
var content = $(
|
|
'<div class="fc-header ' + view.widgetHeaderClass + '">' +
|
|
'<span class="fc-close ' +
|
|
(isTheme ? 'ui-icon ui-icon-closethick' : 'fc-icon fc-icon-x') +
|
|
'"></span>' +
|
|
'<span class="fc-title">' +
|
|
htmlEscape(title) +
|
|
'</span>' +
|
|
'<div class="fc-clear"/>' +
|
|
'</div>' +
|
|
'<div class="fc-body ' + view.widgetContentClass + '">' +
|
|
'<div class="fc-event-container"></div>' +
|
|
'</div>'
|
|
);
|
|
var segContainer = content.find('.fc-event-container');
|
|
var i;
|
|
|
|
// render each seg's `el` and only return the visible segs
|
|
segs = this.renderSegs(segs, true); // disableResizing=true
|
|
this.popoverSegs = segs;
|
|
|
|
for (i = 0; i < segs.length; i++) {
|
|
segs[i].isDetached = true; // signals the segment doesn't live in a cell. needed for event DnD
|
|
segContainer.append(segs[i].el);
|
|
}
|
|
|
|
return content;
|
|
},
|
|
|
|
|
|
// Given the events within an array of segment objects, reslice them to be in a single day
|
|
resliceDaySegs: function(segs, dayDate) {
|
|
var events = $.map(segs, function(seg) {
|
|
return seg.event;
|
|
});
|
|
var dayStart = dayDate.clone().stripTime();
|
|
var dayEnd = dayStart.clone().add('days', 1);
|
|
|
|
return this.eventsToSegs(events, dayStart, dayEnd);
|
|
},
|
|
|
|
|
|
// Generates the text that should be inside a "more" link, given the number of events it represents
|
|
getMoreLinkText: function(num) {
|
|
var view = this.view;
|
|
var opt = view.opt('eventLimitText');
|
|
|
|
if (typeof opt === 'function') {
|
|
return opt(num);
|
|
}
|
|
else {
|
|
return '+' + num + ' ' + opt;
|
|
}
|
|
},
|
|
|
|
|
|
// Returns segments within a given cell.
|
|
// If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs.
|
|
getCellSegs: function(cell, startLevel) {
|
|
var segMatrix = this.rowStructs[cell.row].segMatrix;
|
|
var level = startLevel || 0;
|
|
var segs = [];
|
|
var seg;
|
|
|
|
while (level < segMatrix.length) {
|
|
seg = segMatrix[level][cell.col];
|
|
if (seg) {
|
|
segs.push(seg);
|
|
}
|
|
level++;
|
|
}
|
|
|
|
return segs;
|
|
}
|
|
|
|
});
|