/* 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 (rowLevelLimit !== 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 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" links and 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 jQuery elements in the row var limitedNodes; // array of temporarily hidden level and segment 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" 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 = $('
').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 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 for each column the segment occupies. will be one for each colspan for (j = 0; j < colSegsBelow.length; j++) { moreTd = $('').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 = $('
').append(moreLink); moreTd.append(moreWrap); segMoreNodes.push(moreTd[0]); moreNodes.push(moreTd[0]); } td.addClass('fc-limited').after($(segMoreNodes)); // hide original 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 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 $('') .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
wrapper around the var topEl; // the element we want to match the top coordinate of var options; if (view.rowCnt == 1) { topEl = this.view.el; // will cause the popover to cover any sort of header } else { topEl = this.rowEls.eq(cell.row); // will align with top of row } options = { className: 'fc-more-popover', content: this.renderSegPopoverContent(date, segs), parentEl: this.el, top: topEl.offset().top, autoHide: true, // when the user clicks elsewhere, hide the popover viewportConstrain: view.opt('popoverViewportConstrain'), 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 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('dayPopoverFormat')); var content = $( '
' + '' + '' + htmlEscape(title) + '' + '
' + '
' + '
' + '
' + '
' ); 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++) { // because segments in the popover are not part of a grid coordinate system, provide a hint to any // grids that want to do drag-n-drop about which cell it came from segs[i].cellDate = date; 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(1, 'days'); 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; } });