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 = $(
'' +
''
);
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;
}
});
;;
/* 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 +
' ' +
' ' +
' ';
}
});
;;
/* Event-rendering methods for the TimeGrid class
----------------------------------------------------------------------------------------------------------------------*/
$.extend(TimeGrid.prototype, {
segs: null, // segment objects rendered in the component. null of events haven't been rendered yet
eventSkeletonEl: null, // has cells with event-containers, which contain absolutely positioned event elements
// Renders the events onto the grid and returns an array of segments that have been rendered
renderEvents: function(events) {
var res = this.renderEventTable(events);
this.eventSkeletonEl = $(' ').append(res.tableEl);
this.el.append(this.eventSkeletonEl);
this.segs = res.segs;
},
// Retrieves rendered segment objects
getSegs: function() {
return this.segs || [];
},
// Removes all event segment elements from the view
destroyEvents: function() {
Grid.prototype.destroyEvents.call(this); // call the super-method
if (this.eventSkeletonEl) {
this.eventSkeletonEl.remove();
this.eventSkeletonEl = null;
}
this.segs = null;
},
// Renders and returns the portion of the event-skeleton.
// Returns an object with properties 'tbodyEl' and 'segs'.
renderEventTable: function(events) {
var tableEl = $('');
var trEl = tableEl.find('tr');
var segs = this.eventsToSegs(events);
var segCols;
var i, seg;
var col, colSegs;
var containerEl;
segs = this.renderSegs(segs); // returns only the visible segs
segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg
this.computeSegVerticals(segs); // compute and assign top/bottom
for (col = 0; col < segCols.length; col++) { // iterate each column grouping
colSegs = segCols[col];
placeSlotSegs(colSegs); // compute horizontal coordinates, z-index's, and reorder the array
containerEl = $('');
// assign positioning CSS and insert into container
for (i = 0; i < colSegs.length; i++) {
seg = colSegs[i];
seg.el.css(this.generateSegPositionCss(seg));
// if the height is short, add a className for alternate styling
if (seg.bottom - seg.top < 30) {
seg.el.addClass('fc-short');
}
containerEl.append(seg.el);
}
trEl.append($(' | ').append(containerEl));
}
this.bookendCells(trEl, 'eventSkeleton');
return {
tableEl: tableEl,
segs: segs
};
},
// Refreshes the CSS top/bottom coordinates for each segment element. Probably after a window resize/zoom.
updateSegVerticals: function() {
var segs = this.segs;
var i;
if (segs) {
this.computeSegVerticals(segs);
for (i = 0; i < segs.length; i++) {
segs[i].el.css(
this.generateSegVerticalCss(segs[i])
);
}
}
},
// For each segment in an array, computes and assigns its top and bottom properties
computeSegVerticals: function(segs) {
var i, seg;
for (i = 0; i < segs.length; i++) {
seg = segs[i];
seg.top = this.computeDateTop(seg.start, seg.start);
seg.bottom = this.computeDateTop(seg.end, seg.start);
}
},
// Renders the HTML for a single event segment's default rendering
renderSegHtml: function(seg, disableResizing) {
var view = this.view;
var event = seg.event;
var isDraggable = view.isEventDraggable(event);
var isResizable = !disableResizing && seg.isEnd && view.isEventResizable(event);
var classes = this.getSegClasses(seg, isDraggable, isResizable);
var skinCss = this.getEventSkinCss(event);
var timeText;
var fullTimeText; // more verbose time text. for the print stylesheet
var startTimeText; // just the start time text
classes.unshift('fc-time-grid-event');
if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day...
// Don't display time text on segments that run entirely through a day.
// That would appear as midnight-midnight and would look dumb.
// Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am)
if (seg.isStart || seg.isEnd) {
timeText = view.getEventTimeText(seg.start, seg.end);
fullTimeText = view.getEventTimeText(seg.start, seg.end, 'LT');
startTimeText = view.getEventTimeText(seg.start, null);
}
} else {
// Display the normal time text for the *event's* times
timeText = view.getEventTimeText(event);
fullTimeText = view.getEventTimeText(event, 'LT');
startTimeText = view.getEventTimeText(event.start, null);
}
return '' +
'' +
(timeText ?
' ' +
'' + htmlEscape(timeText) + '' +
' ' :
''
) +
(event.title ?
' ' +
htmlEscape(event.title) +
' ' :
''
) +
' ' +
'' +
(isResizable ?
'' :
''
) +
'';
},
// Generates an object with CSS properties/values that should be applied to an event segment element.
// Contains important positioning-related properties that should be applied to any event element, customized or not.
generateSegPositionCss: function(seg) {
var view = this.view;
var isRTL = view.opt('isRTL');
var shouldOverlap = view.opt('slotEventOverlap');
var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point
var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point
var props = this.generateSegVerticalCss(seg); // get top/bottom first
var left; // amount of space from left edge, a fraction of the total width
var right; // amount of space from right edge, a fraction of the total width
if (shouldOverlap) {
// double the width, but don't go beyond the maximum forward coordinate (1.0)
forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2);
}
if (isRTL) {
left = 1 - forwardCoord;
right = backwardCoord;
}
else {
left = backwardCoord;
right = 1 - forwardCoord;
}
props.zIndex = seg.level + 1; // convert from 0-base to 1-based
props.left = left * 100 + '%';
props.right = right * 100 + '%';
if (shouldOverlap && seg.forwardPressure) {
// add padding to the edge so that forward stacked events don't cover the resizer's icon
props[isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width
}
return props;
},
// Generates an object with CSS properties for the top/bottom coordinates of a segment element
generateSegVerticalCss: function(seg) {
return {
top: seg.top,
bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container
};
},
// Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col
groupSegCols: function(segs) {
var view = this.view;
var segCols = [];
var i;
for (i = 0; i < view.colCnt; i++) {
segCols.push([]);
}
for (i = 0; i < segs.length; i++) {
segCols[segs[i].col].push(segs[i]);
}
return segCols;
}
});
// Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each.
// Also reorders the given array by date!
function placeSlotSegs(segs) {
var levels;
var level0;
var i;
segs.sort(compareSegs); // order by date
levels = buildSlotSegLevels(segs);
computeForwardSlotSegs(levels);
if ((level0 = levels[0])) {
for (i = 0; i < level0.length; i++) {
computeSlotSegPressures(level0[i]);
}
for (i = 0; i < level0.length; i++) {
computeSlotSegCoords(level0[i], 0, 0);
}
}
}
// Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is
// left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date.
function buildSlotSegLevels(segs) {
var levels = [];
var i, seg;
var j;
for (i=0; i seg2.top && seg1.top < seg2.bottom;
}
// A cmp function for determining which forward segment to rely on more when computing coordinates.
function compareForwardSlotSegs(seg1, seg2) {
// put higher-pressure first
return seg2.forwardPressure - seg1.forwardPressure ||
// put segments that are closer to initial edge first (and favor ones with no coords yet)
(seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) ||
// do normal sorting...
compareSegs(seg1, seg2);
}
;;
/* An abstract class from which other views inherit from
----------------------------------------------------------------------------------------------------------------------*/
// Newer methods should be written as prototype methods, not in the monster `View` function at the bottom.
View.prototype = {
calendar: null, // owner Calendar object
coordMap: null, // a CoordMap object for converting pixel regions to dates
el: null, // the view's containing element. set by Calendar
// important Moments
start: null, // the date of the very first cell
end: null, // the date after the very last cell
intervalStart: null, // the start of the interval of time the view represents (1st of month for month view)
intervalEnd: null, // the exclusive end of the interval of time the view represents
// used for cell-to-date and date-to-cell calculations
rowCnt: null, // # of weeks
colCnt: null, // # of days displayed in a week
isSelected: false, // boolean whether cells are user-selected or not
// subclasses can optionally use a scroll container
scrollerEl: null, // the element that will most likely scroll when content is too tall
scrollTop: null, // cached vertical scroll value
// classNames styled by jqui themes
widgetHeaderClass: null,
widgetContentClass: null,
highlightStateClass: null,
// document handlers, bound to `this` object
documentMousedownProxy: null,
documentDragStartProxy: null,
// Serves as a "constructor" to suppliment the monster `View` constructor below
init: function() {
var tm = this.opt('theme') ? 'ui' : 'fc';
this.widgetHeaderClass = tm + '-widget-header';
this.widgetContentClass = tm + '-widget-content';
this.highlightStateClass = tm + '-state-highlight';
// save references to `this`-bound handlers
this.documentMousedownProxy = $.proxy(this, 'documentMousedown');
this.documentDragStartProxy = $.proxy(this, 'documentDragStart');
},
// Renders the view inside an already-defined `this.el`.
// Subclasses should override this and then call the super method afterwards.
render: function() {
this.updateSize();
this.trigger('viewRender', this, this, this.el);
// attach handlers to document. do it here to allow for destroy/rerender
$(document)
.on('mousedown', this.documentMousedownProxy)
.on('dragstart', this.documentDragStartProxy); // jqui drag
},
// Clears all view rendering, event elements, and unregisters handlers
destroy: function() {
this.unselect();
this.trigger('viewDestroy', this, this, this.el);
this.destroyEvents();
this.el.empty(); // removes inner contents but leaves the element intact
$(document)
.off('mousedown', this.documentMousedownProxy)
.off('dragstart', this.documentDragStartProxy);
},
// Used to determine what happens when the users clicks next/prev. Given -1 for prev, 1 for next.
// Should apply the delta to `date` (a Moment) and return it.
incrementDate: function(date, delta) {
// subclasses should implement
},
/* Dimensions
------------------------------------------------------------------------------------------------------------------*/
// Refreshes anything dependant upon sizing of the container element of the grid
updateSize: function(isResize) {
if (isResize) {
this.recordScroll();
}
this.updateHeight();
this.updateWidth();
},
// Refreshes the horizontal dimensions of the calendar
updateWidth: function() {
// subclasses should implement
},
// Refreshes the vertical dimensions of the calendar
updateHeight: function() {
var calendar = this.calendar; // we poll the calendar for height information
this.setHeight(
calendar.getSuggestedViewHeight(),
calendar.isHeightAuto()
);
},
// Updates the vertical dimensions of the calendar to the specified height.
// if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height.
setHeight: function(height, isAuto) {
// subclasses should implement
},
// Given the total height of the view, return the number of pixels that should be used for the scroller.
// Utility for subclasses.
computeScrollerHeight: function(totalHeight) {
var both = this.el.add(this.scrollerEl);
var otherHeight; // cumulative height of everything that is not the scrollerEl in the view (header+borders)
// fuckin IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked
both.css({
position: 'relative', // cause a reflow, which will force fresh dimension recalculation
left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll
});
otherHeight = this.el.outerHeight() - this.scrollerEl.height(); // grab the dimensions
both.css({ position: '', left: '' }); // undo hack
return totalHeight - otherHeight;
},
// Called for remembering the current scroll value of the scroller.
// Should be called before there is a destructive operation (like removing DOM elements) that might inadvertently
// change the scroll of the container.
recordScroll: function() {
if (this.scrollerEl) {
this.scrollTop = this.scrollerEl.scrollTop();
}
},
// Set the scroll value of the scroller to the previously recorded value.
// Should be called after we know the view's dimensions have been restored following some type of destructive
// operation (like temporarily removing DOM elements).
restoreScroll: function() {
if (this.scrollTop !== null) {
this.scrollerEl.scrollTop(this.scrollTop);
}
},
/* Events
------------------------------------------------------------------------------------------------------------------*/
// Renders the events onto the view.
// Should be overriden by subclasses. Subclasses should call the super-method afterwards.
renderEvents: function(events) {
this.segEach(function(seg) {
this.trigger('eventAfterRender', seg.event, seg.event, seg.el);
});
this.trigger('eventAfterAllRender');
},
// Removes event elements from the view.
// Should be overridden by subclasses. Should call this super-method FIRST, then subclass DOM destruction.
destroyEvents: function() {
this.segEach(function(seg) {
this.trigger('eventDestroy', seg.event, seg.event, seg.el);
});
},
// Given an event and the default element used for rendering, returns the element that should actually be used.
// Basically runs events and elements through the eventRender hook.
resolveEventEl: function(event, el) {
var custom = this.trigger('eventRender', event, event, el);
if (custom === false) { // means don't render at all
el = null;
}
else if (custom && custom !== true) {
el = $(custom);
}
return el;
},
// Hides all rendered event segments linked to the given event
showEvent: function(event) {
this.segEach(function(seg) {
seg.el.css('visibility', '');
}, event);
},
// Shows all rendered event segments linked to the given event
hideEvent: function(event) {
this.segEach(function(seg) {
seg.el.css('visibility', 'hidden');
}, event);
},
// Iterates through event segments. Goes through all by default.
// If the optional `event` argument is specified, only iterates through segments linked to that event.
// The `this` value of the callback function will be the view.
segEach: function(func, event) {
var segs = this.getSegs();
var i;
for (i = 0; i < segs.length; i++) {
if (!event || segs[i].event._id === event._id) {
func.call(this, segs[i]);
}
}
},
// Retrieves all the rendered segment objects for the view
getSegs: function() {
// subclasses must implement
},
/* Event Drag Visualization
------------------------------------------------------------------------------------------------------------------*/
// Renders a visual indication of an event hovering over the specified date.
// `end` is a Moment and might be null.
// `seg` might be null. if specified, it is the segment object of the event being dragged.
// otherwise, an external event from outside the calendar is being dragged.
renderDrag: function(start, end, seg) {
// subclasses should implement
},
// Unrenders a visual indication of event hovering
destroyDrag: function() {
// subclasses should implement
},
// Handler for accepting externally dragged events being dropped in the view.
// Gets called when jqui's 'dragstart' is fired.
documentDragStart: function(ev, ui) {
var _this = this;
var dropDate = null;
var dragListener;
if (this.opt('droppable')) { // only listen if this setting is on
// listener that tracks mouse movement over date-associated pixel regions
dragListener = new DragListener(this.coordMap, {
cellOver: function(cell, date) {
dropDate = date;
_this.renderDrag(date);
},
cellOut: function() {
dropDate = null;
_this.destroyDrag();
}
});
// gets called, only once, when jqui drag is finished
$(document).one('dragstop', function(ev, ui) {
_this.destroyDrag();
if (dropDate) {
_this.trigger('drop', ev.target, dropDate, ev, ui);
}
});
dragListener.startDrag(ev); // start listening immediately
}
},
/* Selection
------------------------------------------------------------------------------------------------------------------*/
// Selects a date range on the view. `start` and `end` are both Moments.
// `ev` is the native mouse event that begin the interaction.
select: function(start, end, ev) {
this.unselect(ev);
this.renderSelection(start, end);
this.reportSelection(start, end, ev);
},
// Renders a visual indication of the selection
renderSelection: function(start, end) {
// subclasses should implement
},
// Called when a new selection is made. Updates internal state and triggers handlers.
reportSelection: function(start, end, ev) {
this.isSelected = true;
this.trigger('select', null, start, end, ev);
},
// Undoes a selection. updates in the internal state and triggers handlers.
// `ev` is the native mouse event that began the interaction.
unselect: function(ev) {
if (this.isSelected) {
this.isSelected = false;
this.destroySelection();
this.trigger('unselect', null, ev);
}
},
// Unrenders a visual indication of selection
destroySelection: function() {
// subclasses should implement
},
// Handler for unselecting when the user clicks something and the 'unselectAuto' setting is on
documentMousedown: function(ev) {
var ignore;
// is there a selection, and has the user made a proper left click?
if (this.isSelected && this.opt('unselectAuto') && isPrimaryMouseButton(ev)) {
// only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element
ignore = this.opt('unselectCancel');
if (!ignore || !$(ev.target).closest(ignore).length) {
this.unselect(ev);
}
}
}
};
// We are mixing JavaScript OOP design patterns here by putting methods and member variables in the closed scope of the
// constructor. Going forward, methods should be part of the prototype.
function View(calendar) {
var t = this;
// exports
t.calendar = calendar;
t.opt = opt;
t.trigger = trigger;
t.isEventDraggable = isEventDraggable;
t.isEventResizable = isEventResizable;
t.eventDrop = eventDrop;
t.eventResize = eventResize;
// imports
var reportEventChange = calendar.reportEventChange;
// locals
var options = calendar.options;
var nextDayThreshold = moment.duration(options.nextDayThreshold);
t.init(); // the "constructor" that concerns the prototype methods
function opt(name) {
var v = options[name];
if ($.isPlainObject(v) && !isForcedAtomicOption(name)) {
return smartProperty(v, t.name);
}
return v;
}
function trigger(name, thisObj) {
return calendar.trigger.apply(
calendar,
[name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t])
);
}
/* Event Editable Boolean Calculations
------------------------------------------------------------------------------*/
function isEventDraggable(event) {
var source = event.source || {};
return firstDefined(
event.startEditable,
source.startEditable,
opt('eventStartEditable'),
event.editable,
source.editable,
opt('editable')
);
}
function isEventResizable(event) {
var source = event.source || {};
return firstDefined(
event.durationEditable,
source.durationEditable,
opt('eventDurationEditable'),
event.editable,
source.editable,
opt('editable')
);
}
/* Event Elements
------------------------------------------------------------------------------*/
// Compute the text that should be displayed on an event's element.
// Based off the settings of the view. Possible signatures:
// .getEventTimeText(event, formatStr)
// .getEventTimeText(startMoment, endMoment, formatStr)
// .getEventTimeText(startMoment, null, formatStr)
// `timeFormat` is used but the `formatStr` argument can be used to override.
t.getEventTimeText = function(event, formatStr) {
var start;
var end;
if (typeof event === 'object' && typeof formatStr === 'object') {
// first two arguments are actually moments (or null). shift arguments.
start = event;
end = formatStr;
formatStr = arguments[2];
}
else {
// otherwise, an event object was the first argument
start = event.start;
end = event.end;
}
formatStr = formatStr || opt('timeFormat');
if (end && opt('displayEventEnd')) {
return calendar.formatRange(start, end, formatStr);
}
else {
return calendar.formatDate(start, formatStr);
}
};
/* Event Modification Reporting
---------------------------------------------------------------------------------*/
function eventDrop(el, event, newStart, ev) {
var mutateResult = calendar.mutateEvent(event, newStart, null);
trigger(
'eventDrop',
el,
event,
mutateResult.dateDelta,
function() {
mutateResult.undo();
reportEventChange();
},
ev,
{} // jqui dummy
);
reportEventChange();
}
function eventResize(el, event, newEnd, ev) {
var mutateResult = calendar.mutateEvent(event, null, newEnd);
trigger(
'eventResize',
el,
event,
mutateResult.durationDelta,
function() {
mutateResult.undo();
reportEventChange();
},
ev,
{} // jqui dummy
);
reportEventChange();
}
// ====================================================================================================
// Utilities for day "cells"
// ====================================================================================================
// The "basic" views are completely made up of day cells.
// The "agenda" views have day cells at the top "all day" slot.
// This was the obvious common place to put these utilities, but they should be abstracted out into
// a more meaningful class (like DayEventRenderer).
// ====================================================================================================
// For determining how a given "cell" translates into a "date":
//
// 1. Convert the "cell" (row and column) into a "cell offset" (the # of the cell, cronologically from the first).
// Keep in mind that column indices are inverted with isRTL. This is taken into account.
//
// 2. Convert the "cell offset" to a "day offset" (the # of days since the first visible day in the view).
//
// 3. Convert the "day offset" into a "date" (a Moment).
//
// The reverse transformation happens when transforming a date into a cell.
// exports
t.isHiddenDay = isHiddenDay;
t.skipHiddenDays = skipHiddenDays;
t.getCellsPerWeek = getCellsPerWeek;
t.dateToCell = dateToCell;
t.dateToDayOffset = dateToDayOffset;
t.dayOffsetToCellOffset = dayOffsetToCellOffset;
t.cellOffsetToCell = cellOffsetToCell;
t.cellToDate = cellToDate;
t.cellToCellOffset = cellToCellOffset;
t.cellOffsetToDayOffset = cellOffsetToDayOffset;
t.dayOffsetToDate = dayOffsetToDate;
t.rangeToSegments = rangeToSegments;
t.isMultiDayEvent = isMultiDayEvent;
// internals
var hiddenDays = opt('hiddenDays') || []; // array of day-of-week indices that are hidden
var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
var cellsPerWeek;
var dayToCellMap = []; // hash from dayIndex -> cellIndex, for one week
var cellToDayMap = []; // hash from cellIndex -> dayIndex, for one week
var isRTL = opt('isRTL');
// initialize important internal variables
(function() {
if (opt('weekends') === false) {
hiddenDays.push(0, 6); // 0=sunday, 6=saturday
}
// Loop through a hypothetical week and determine which
// days-of-week are hidden. Record in both hashes (one is the reverse of the other).
for (var dayIndex=0, cellIndex=0; dayIndex<7; dayIndex++) {
dayToCellMap[dayIndex] = cellIndex;
isHiddenDayHash[dayIndex] = $.inArray(dayIndex, hiddenDays) != -1;
if (!isHiddenDayHash[dayIndex]) {
cellToDayMap[cellIndex] = dayIndex;
cellIndex++;
}
}
cellsPerWeek = cellIndex;
if (!cellsPerWeek) {
throw 'invalid hiddenDays'; // all days were hidden? bad.
}
})();
// Is the current day hidden?
// `day` is a day-of-week index (0-6), or a Moment
function isHiddenDay(day) {
if (moment.isMoment(day)) {
day = day.day();
}
return isHiddenDayHash[day];
}
function getCellsPerWeek() {
return cellsPerWeek;
}
// Incrementing the current day until it is no longer a hidden day, returning a copy.
// If the initial value of `date` is not a hidden day, don't do anything.
// Pass `isExclusive` as `true` if you are dealing with an end date.
// `inc` defaults to `1` (increment one day forward each time)
function skipHiddenDays(date, inc, isExclusive) {
var out = date.clone();
inc = inc || 1;
while (
isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7]
) {
out.add(inc, 'days');
}
return out;
}
//
// TRANSFORMATIONS: cell -> cell offset -> day offset -> date
//
// cell -> date (combines all transformations)
// Possible arguments:
// - row, col
// - { row:#, col: # }
function cellToDate() {
var cellOffset = cellToCellOffset.apply(null, arguments);
var dayOffset = cellOffsetToDayOffset(cellOffset);
var date = dayOffsetToDate(dayOffset);
return date;
}
// cell -> cell offset
// Possible arguments:
// - row, col
// - { row:#, col:# }
function cellToCellOffset(row, col) {
var colCnt = t.colCnt;
// rtl variables. wish we could pre-populate these. but where?
var dis = isRTL ? -1 : 1;
var dit = isRTL ? colCnt - 1 : 0;
if (typeof row == 'object') {
col = row.col;
row = row.row;
}
var cellOffset = row * colCnt + (col * dis + dit); // column, adjusted for RTL (dis & dit)
return cellOffset;
}
// cell offset -> day offset
function cellOffsetToDayOffset(cellOffset) {
var day0 = t.start.day(); // first date's day of week
cellOffset += dayToCellMap[day0]; // normlize cellOffset to beginning-of-week
return Math.floor(cellOffset / cellsPerWeek) * 7 + // # of days from full weeks
cellToDayMap[ // # of days from partial last week
(cellOffset % cellsPerWeek + cellsPerWeek) % cellsPerWeek // crazy math to handle negative cellOffsets
] -
day0; // adjustment for beginning-of-week normalization
}
// day offset -> date
function dayOffsetToDate(dayOffset) {
return t.start.clone().add(dayOffset, 'days');
}
//
// TRANSFORMATIONS: date -> day offset -> cell offset -> cell
//
// date -> cell (combines all transformations)
function dateToCell(date) {
var dayOffset = dateToDayOffset(date);
var cellOffset = dayOffsetToCellOffset(dayOffset);
var cell = cellOffsetToCell(cellOffset);
return cell;
}
// date -> day offset
function dateToDayOffset(date) {
return date.clone().stripTime().diff(t.start, 'days');
}
// day offset -> cell offset
function dayOffsetToCellOffset(dayOffset) {
var day0 = t.start.day(); // first date's day of week
dayOffset += day0; // normalize dayOffset to beginning-of-week
return Math.floor(dayOffset / 7) * cellsPerWeek + // # of cells from full weeks
dayToCellMap[ // # of cells from partial last week
(dayOffset % 7 + 7) % 7 // crazy math to handle negative dayOffsets
] -
dayToCellMap[day0]; // adjustment for beginning-of-week normalization
}
// cell offset -> cell (object with row & col keys)
function cellOffsetToCell(cellOffset) {
var colCnt = t.colCnt;
// rtl variables. wish we could pre-populate these. but where?
var dis = isRTL ? -1 : 1;
var dit = isRTL ? colCnt - 1 : 0;
var row = Math.floor(cellOffset / colCnt);
var col = ((cellOffset % colCnt + colCnt) % colCnt) * dis + dit; // column, adjusted for RTL (dis & dit)
return {
row: row,
col: col
};
}
//
// Converts a date range into an array of segment objects.
// "Segments" are horizontal stretches of time, sliced up by row.
// A segment object has the following properties:
// - row
// - cols
// - isStart
// - isEnd
//
function rangeToSegments(start, end) {
var rowCnt = t.rowCnt;
var colCnt = t.colCnt;
var segments = []; // array of segments to return
// day offset for given date range
var dayRange = computeDayRange(start, end); // convert to a whole-day range
var rangeDayOffsetStart = dateToDayOffset(dayRange.start);
var rangeDayOffsetEnd = dateToDayOffset(dayRange.end); // an exclusive value
// first and last cell offset for the given date range
// "last" implies inclusivity
var rangeCellOffsetFirst = dayOffsetToCellOffset(rangeDayOffsetStart);
var rangeCellOffsetLast = dayOffsetToCellOffset(rangeDayOffsetEnd) - 1;
// loop through all the rows in the view
for (var row=0; row= nextDayThreshold) {
endDay.add(1, 'days');
}
}
// If no end was specified, or if it is within `startDay` but not past nextDayThreshold,
// assign the default duration of one day.
if (!end || endDay <= startDay) {
endDay = startDay.clone().add(1, 'days');
}
return { start: startDay, end: endDay };
}
// Does the given event visually appear to occupy more than one day?
function isMultiDayEvent(event) {
var range = computeDayRange(event.start, event.end);
return range.end.diff(range.start, 'days') > 1;
}
}
;;
/* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells.
----------------------------------------------------------------------------------------------------------------------*/
// It is a manager for a DayGrid subcomponent, which does most of the heavy lifting.
// It is responsible for managing width/height.
function BasicView(calendar) {
View.call(this, calendar); // call the super-constructor
this.dayGrid = new DayGrid(this);
this.coordMap = this.dayGrid.coordMap; // the view's date-to-cell mapping is identical to the subcomponent's
}
BasicView.prototype = createObject(View.prototype); // define the super-class
$.extend(BasicView.prototype, {
dayGrid: null, // the main subcomponent that does most of the heavy lifting
dayNumbersVisible: false, // display day numbers on each day cell?
weekNumbersVisible: false, // display week numbers along the side?
weekNumberWidth: null, // width of all the week-number cells running down the side
headRowEl: null, // the fake row element of the day-of-week header
// Renders the view into `this.el`, which should already be assigned.
// rowCnt, colCnt, and dayNumbersVisible have been calculated by a subclass and passed here.
render: function(rowCnt, colCnt, dayNumbersVisible) {
// needed for cell-to-date and date-to-cell calculations in View
this.rowCnt = rowCnt;
this.colCnt = colCnt;
this.dayNumbersVisible = dayNumbersVisible;
this.weekNumbersVisible = this.opt('weekNumbers');
this.dayGrid.numbersVisible = this.dayNumbersVisible || this.weekNumbersVisible;
this.el.addClass('fc-basic-view').html(this.renderHtml());
this.headRowEl = this.el.find('thead .fc-row');
this.scrollerEl = this.el.find('.fc-day-grid-container');
this.dayGrid.coordMap.containerEl = this.scrollerEl; // constrain clicks/etc to the dimensions of the scroller
this.dayGrid.el = this.el.find('.fc-day-grid');
this.dayGrid.render(this.hasRigidRows());
View.prototype.render.call(this); // call the super-method
},
// Make subcomponents ready for cleanup
destroy: function() {
this.dayGrid.destroy();
View.prototype.destroy.call(this); // call the super-method
},
// Builds the HTML skeleton for the view.
// The day-grid component will render inside of a container defined by this HTML.
renderHtml: function() {
return '' +
'' +
'' +
'' +
'' +
' ' +
'' +
'' +
'' +
'' +
'' +
' | ' +
' ' +
'' +
' ';
},
// Generates the HTML that will go before the day-of week header cells.
// Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL.
headIntroHtml: function() {
if (this.weekNumbersVisible) {
return '' +
'';
}
},
// Generates the HTML that will go before content-skeleton cells that display the day/week numbers.
// Queried by the DayGrid subcomponent. Ordering depends on isRTL.
numberIntroHtml: function(row) {
if (this.weekNumbersVisible) {
return '' +
'' +
'' + // needed for matchCellWidths
this.calendar.calculateWeekNumber(this.cellToDate(row, 0)) +
'' +
' | ';
}
},
// Generates the HTML that goes before the day bg cells for each day-row.
// Queried by the DayGrid subcomponent. Ordering depends on isRTL.
dayIntroHtml: function() {
if (this.weekNumbersVisible) {
return ' | ';
}
},
// Generates the HTML that goes before every other type of row generated by DayGrid. Ordering depends on isRTL.
// Affects helper-skeleton and highlight-skeleton rows.
introHtml: function() {
if (this.weekNumbersVisible) {
return ' | ';
}
},
// Generates the HTML for the s of the "number" row in the DayGrid's content skeleton.
// The number row will only exist if either day numbers or week numbers are turned on.
numberCellHtml: function(row, col, date) {
var classes;
if (!this.dayNumbersVisible) { // if there are week numbers but not day numbers
return ' | | '; // will create an empty space above events :(
}
classes = this.dayGrid.getDayClasses(date);
classes.unshift('fc-day-number');
return '' +
'' +
date.date() +
' | ';
},
// Generates an HTML attribute string for setting the width of the week number column, if it is known
weekNumberStyleAttr: function() {
if (this.weekNumberWidth !== null) {
return 'style="width:' + this.weekNumberWidth + 'px"';
}
return '';
},
// Determines whether each row should have a constant height
hasRigidRows: function() {
var eventLimit = this.opt('eventLimit');
return eventLimit && typeof eventLimit !== 'number';
},
/* Dimensions
------------------------------------------------------------------------------------------------------------------*/
// Refreshes the horizontal dimensions of the view
updateWidth: function() {
if (this.weekNumbersVisible) {
// Make sure all week number cells running down the side have the same width.
// Record the width for cells created later.
this.weekNumberWidth = matchCellWidths(
this.el.find('.fc-week-number')
);
}
},
// Adjusts the vertical dimensions of the view to the specified values
setHeight: function(totalHeight, isAuto) {
var eventLimit = this.opt('eventLimit');
var scrollerHeight;
// reset all heights to be natural
unsetScroller(this.scrollerEl);
uncompensateScroll(this.headRowEl);
this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed
// is the event limit a constant level number?
if (eventLimit && typeof eventLimit === 'number') {
this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after
}
scrollerHeight = this.computeScrollerHeight(totalHeight);
this.setGridHeight(scrollerHeight, isAuto);
// is the event limit dynamically calculated?
if (eventLimit && typeof eventLimit !== 'number') {
this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set
}
if (!isAuto && setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars?
compensateScroll(this.headRowEl, getScrollbarWidths(this.scrollerEl));
// doing the scrollbar compensation might have created text overflow which created more height. redo
scrollerHeight = this.computeScrollerHeight(totalHeight);
this.scrollerEl.height(scrollerHeight);
this.restoreScroll();
}
},
// Sets the height of just the DayGrid component in this view
setGridHeight: function(height, isAuto) {
if (isAuto) {
undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding
}
else {
distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows
}
},
/* Events
------------------------------------------------------------------------------------------------------------------*/
// Renders the given events onto the view and populates the segments array
renderEvents: function(events) {
this.dayGrid.renderEvents(events);
this.updateHeight(); // must compensate for events that overflow the row
View.prototype.renderEvents.call(this, events); // call the super-method
},
// Retrieves all segment objects that are rendered in the view
getSegs: function() {
return this.dayGrid.getSegs();
},
// Unrenders all event elements and clears internal segment data
destroyEvents: function() {
View.prototype.destroyEvents.call(this); // do this before dayGrid's segs have been cleared
this.recordScroll(); // removing events will reduce height and mess with the scroll, so record beforehand
this.dayGrid.destroyEvents();
// we DON'T need to call updateHeight() because:
// A) a renderEvents() call always happens after this, which will eventually call updateHeight()
// B) in IE8, this causes a flash whenever events are rerendered
},
/* Event Dragging
------------------------------------------------------------------------------------------------------------------*/
// Renders a visual indication of an event being dragged over the view.
// A returned value of `true` signals that a mock "helper" event has been rendered.
renderDrag: function(start, end, seg) {
return this.dayGrid.renderDrag(start, end, seg);
},
// Unrenders the visual indication of an event being dragged over the view
destroyDrag: function() {
this.dayGrid.destroyDrag();
},
/* Selection
------------------------------------------------------------------------------------------------------------------*/
// Renders a visual indication of a selection
renderSelection: function(start, end) {
this.dayGrid.renderSelection(start, end);
},
// Unrenders a visual indications of a selection
destroySelection: function() {
this.dayGrid.destroySelection();
}
});
;;
/* A month view with day cells running in rows (one-per-week) and columns
----------------------------------------------------------------------------------------------------------------------*/
setDefaults({
fixedWeekCount: true
});
fcViews.month = MonthView; // register the view
function MonthView(calendar) {
BasicView.call(this, calendar); // call the super-constructor
}
MonthView.prototype = createObject(BasicView.prototype); // define the super-class
$.extend(MonthView.prototype, {
name: 'month',
incrementDate: function(date, delta) {
return date.clone().stripTime().add(delta, 'months').startOf('month');
},
render: function(date) {
var rowCnt;
this.intervalStart = date.clone().stripTime().startOf('month');
this.intervalEnd = this.intervalStart.clone().add(1, 'months');
this.start = this.intervalStart.clone();
this.start = this.skipHiddenDays(this.start); // move past the first week if no visible days
this.start.startOf('week');
this.start = this.skipHiddenDays(this.start); // move past the first invisible days of the week
this.end = this.intervalEnd.clone();
this.end = this.skipHiddenDays(this.end, -1, true); // move in from the last week if no visible days
this.end.add((7 - this.end.weekday()) % 7, 'days'); // move to end of week if not already
this.end = this.skipHiddenDays(this.end, -1, true); // move in from the last invisible days of the week
rowCnt = Math.ceil( // need to ceil in case there are hidden days
this.end.diff(this.start, 'weeks', true) // returnfloat=true
);
if (this.isFixedWeeks()) {
this.end.add(6 - rowCnt, 'weeks');
rowCnt = 6;
}
this.title = this.calendar.formatDate(this.intervalStart, this.opt('titleFormat'));
BasicView.prototype.render.call(this, rowCnt, this.getCellsPerWeek(), true); // call the super-method
},
// Overrides the default BasicView behavior to have special multi-week auto-height logic
setGridHeight: function(height, isAuto) {
isAuto = isAuto || this.opt('weekMode') === 'variable'; // LEGACY: weekMode is deprecated
// if auto, make the height of each row the height that it would be if there were 6 weeks
if (isAuto) {
height *= this.rowCnt / 6;
}
distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows
},
isFixedWeeks: function() {
var weekMode = this.opt('weekMode'); // LEGACY: weekMode is deprecated
if (weekMode) {
return weekMode === 'fixed'; // if any other type of weekMode, assume NOT fixed
}
return this.opt('fixedWeekCount');
}
});
;;
/* A week view with simple day cells running horizontally
----------------------------------------------------------------------------------------------------------------------*/
// TODO: a WeekView mixin for calculating dates and titles
fcViews.basicWeek = BasicWeekView; // register this view
function BasicWeekView(calendar) {
BasicView.call(this, calendar); // call the super-constructor
}
BasicWeekView.prototype = createObject(BasicView.prototype); // define the super-class
$.extend(BasicWeekView.prototype, {
name: 'basicWeek',
incrementDate: function(date, delta) {
return date.clone().stripTime().add(delta, 'weeks').startOf('week');
},
render: function(date) {
this.intervalStart = date.clone().stripTime().startOf('week');
this.intervalEnd = this.intervalStart.clone().add(1, 'weeks');
this.start = this.skipHiddenDays(this.intervalStart);
this.end = this.skipHiddenDays(this.intervalEnd, -1, true);
this.title = this.calendar.formatRange(
this.start,
this.end.clone().subtract(1), // make inclusive by subtracting 1 ms
this.opt('titleFormat'),
' \u2014 ' // emphasized dash
);
BasicView.prototype.render.call(this, 1, this.getCellsPerWeek(), false); // call the super-method
}
});
;;
/* A view with a single simple day cell
----------------------------------------------------------------------------------------------------------------------*/
fcViews.basicDay = BasicDayView; // register this view
function BasicDayView(calendar) {
BasicView.call(this, calendar); // call the super-constructor
}
BasicDayView.prototype = createObject(BasicView.prototype); // define the super-class
$.extend(BasicDayView.prototype, {
name: 'basicDay',
incrementDate: function(date, delta) {
var out = date.clone().stripTime().add(delta, 'days');
out = this.skipHiddenDays(out, delta < 0 ? -1 : 1);
return out;
},
render: function(date) {
this.start = this.intervalStart = date.clone().stripTime();
this.end = this.intervalEnd = this.start.clone().add(1, 'days');
this.title = this.calendar.formatDate(this.start, this.opt('titleFormat'));
BasicView.prototype.render.call(this, 1, 1, false); // call the super-method
}
});
;;
/* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically.
----------------------------------------------------------------------------------------------------------------------*/
// Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on).
// Responsible for managing width/height.
setDefaults({
allDaySlot: true,
allDayText: 'all-day',
scrollTime: '06:00:00',
slotDuration: '00:30:00',
axisFormat: generateAgendaAxisFormat,
timeFormat: {
agenda: generateAgendaTimeFormat
},
minTime: '00:00:00',
maxTime: '24:00:00',
slotEventOverlap: true
});
var AGENDA_ALL_DAY_EVENT_LIMIT = 5;
function generateAgendaAxisFormat(options, langData) {
return langData.longDateFormat('LT')
.replace(':mm', '(:mm)')
.replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs
.replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
}
function generateAgendaTimeFormat(options, langData) {
return langData.longDateFormat('LT')
.replace(/\s*a$/i, ''); // remove trailing AM/PM
}
function AgendaView(calendar) {
View.call(this, calendar); // call the super-constructor
this.timeGrid = new TimeGrid(this);
if (this.opt('allDaySlot')) { // should we display the "all-day" area?
this.dayGrid = new DayGrid(this); // the all-day subcomponent of this view
// the coordinate grid will be a combination of both subcomponents' grids
this.coordMap = new ComboCoordMap([
this.dayGrid.coordMap,
this.timeGrid.coordMap
]);
}
else {
this.coordMap = this.timeGrid.coordMap;
}
}
AgendaView.prototype = createObject(View.prototype); // define the super-class
$.extend(AgendaView.prototype, {
timeGrid: null, // the main time-grid subcomponent of this view
dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null
axisWidth: null, // the width of the time axis running down the side
noScrollRowEls: null, // set of fake row elements that must compensate when scrollerEl has scrollbars
// when the time-grid isn't tall enough to occupy the given height, we render an underneath
bottomRuleEl: null,
bottomRuleHeight: null,
/* Rendering
------------------------------------------------------------------------------------------------------------------*/
// Renders the view into `this.el`, which has already been assigned.
// `colCnt` has been calculated by a subclass and passed here.
render: function(colCnt) {
// needed for cell-to-date and date-to-cell calculations in View
this.rowCnt = 1;
this.colCnt = colCnt;
this.el.addClass('fc-agenda-view').html(this.renderHtml());
// the element that wraps the time-grid that will probably scroll
this.scrollerEl = this.el.find('.fc-time-grid-container');
this.timeGrid.coordMap.containerEl = this.scrollerEl; // don't accept clicks/etc outside of this
this.timeGrid.el = this.el.find('.fc-time-grid');
this.timeGrid.render();
// the that sometimes displays under the time-grid
this.bottomRuleEl = $('')
.appendTo(this.timeGrid.el); // inject it into the time-grid
if (this.dayGrid) {
this.dayGrid.el = this.el.find('.fc-day-grid');
this.dayGrid.render();
// have the day-grid extend it's coordinate area over the dividing the two grids
this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight();
}
this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller
View.prototype.render.call(this); // call the super-method
this.resetScroll(); // do this after sizes have been set
},
// Make subcomponents ready for cleanup
destroy: function() {
this.timeGrid.destroy();
if (this.dayGrid) {
this.dayGrid.destroy();
}
View.prototype.destroy.call(this); // call the super-method
},
// Builds the HTML skeleton for the view.
// The day-grid and time-grid components will render inside containers defined by this HTML.
renderHtml: function() {
return '' +
'' +
'' +
'' +
'' +
' ' +
'' +
'' +
'' +
'' +
(this.dayGrid ?
'' +
'' :
''
) +
'' +
' | ' +
' ' +
'' +
' ';
},
// Generates the HTML that will go before the day-of week header cells.
// Queried by the TimeGrid subcomponent when generating rows. Ordering depends on isRTL.
headIntroHtml: function() {
var date;
var weekNumber;
var weekTitle;
var weekText;
if (this.opt('weekNumbers')) {
date = this.cellToDate(0, 0);
weekNumber = this.calendar.calculateWeekNumber(date);
weekTitle = this.opt('weekNumberTitle');
if (this.opt('isRTL')) {
weekText = weekNumber + weekTitle;
}
else {
weekText = weekTitle + weekNumber;
}
return '' +
'';
}
else {
return '';
}
},
// Generates the HTML that goes before the all-day cells.
// Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL.
dayIntroHtml: function() {
return '' +
'' +
'' + // needed for matchCellWidths
(this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'))) +
'' +
' | ';
},
// Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column.
slotBgIntroHtml: function() {
return ' | ';
},
// Generates the HTML that goes before all other types of cells.
// Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
// Queried by the TimeGrid and DayGrid subcomponents when generating rows. Ordering depends on isRTL.
introHtml: function() {
return ' | ';
},
// Generates an HTML attribute string for setting the width of the axis, if it is known
axisStyleAttr: function() {
if (this.axisWidth !== null) {
return 'style="width:' + this.axisWidth + 'px"';
}
return '';
},
/* Dimensions
------------------------------------------------------------------------------------------------------------------*/
updateSize: function(isResize) {
if (isResize) {
this.timeGrid.resize();
}
View.prototype.updateSize.call(this, isResize);
},
// Refreshes the horizontal dimensions of the view
updateWidth: function() {
// make all axis cells line up, and record the width so newly created axis cells will have it
this.axisWidth = matchCellWidths(this.el.find('.fc-axis'));
},
// Adjusts the vertical dimensions of the view to the specified values
setHeight: function(totalHeight, isAuto) {
var eventLimit;
var scrollerHeight;
if (this.bottomRuleHeight === null) {
// calculate the height of the rule the very first time
this.bottomRuleHeight = this.bottomRuleEl.outerHeight();
}
this.bottomRuleEl.hide(); // .show() will be called later if this is necessary
// reset all dimensions back to the original state
this.scrollerEl.css('overflow', '');
unsetScroller(this.scrollerEl);
uncompensateScroll(this.noScrollRowEls);
// limit number of events in the all-day area
if (this.dayGrid) {
this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed
eventLimit = this.opt('eventLimit');
if (eventLimit && typeof eventLimit !== 'number') {
eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number
}
if (eventLimit) {
this.dayGrid.limitRows(eventLimit);
}
}
if (!isAuto) { // should we force dimensions of the scroll container, or let the contents be natural height?
scrollerHeight = this.computeScrollerHeight(totalHeight);
if (setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars?
// make the all-day and header rows lines up
compensateScroll(this.noScrollRowEls, getScrollbarWidths(this.scrollerEl));
// the scrollbar compensation might have changed text flow, which might affect height, so recalculate
// and reapply the desired height to the scroller.
scrollerHeight = this.computeScrollerHeight(totalHeight);
this.scrollerEl.height(scrollerHeight);
this.restoreScroll();
}
else { // no scrollbars
// still, force a height and display the bottom rule (marks the end of day)
this.scrollerEl.height(scrollerHeight).css('overflow', 'hidden'); // in case goes outside
this.bottomRuleEl.show();
}
}
},
// Sets the scroll value of the scroller to the intial pre-configured state prior to allowing the user to change it.
resetScroll: function() {
var _this = this;
var scrollTime = moment.duration(this.opt('scrollTime'));
var top = this.timeGrid.computeTimeTop(scrollTime);
// zoom can give weird floating-point values. rather scroll a little bit further
top = Math.ceil(top);
if (top) {
top++; // to overcome top border that slots beyond the first have. looks better
}
function scroll() {
_this.scrollerEl.scrollTop(top);
}
scroll();
setTimeout(scroll, 0); // overrides any previous scroll state made by the browser
},
/* Events
------------------------------------------------------------------------------------------------------------------*/
// Renders events onto the view and populates the View's segment array
renderEvents: function(events) {
var dayEvents = [];
var timedEvents = [];
var daySegs = [];
var timedSegs;
var i;
// separate the events into all-day and timed
for (i = 0; i < events.length; i++) {
if (events[i].allDay) {
dayEvents.push(events[i]);
}
else {
timedEvents.push(events[i]);
}
}
// render the events in the subcomponents
timedSegs = this.timeGrid.renderEvents(timedEvents);
if (this.dayGrid) {
daySegs = this.dayGrid.renderEvents(dayEvents);
}
// the all-day area is flexible and might have a lot of events, so shift the height
this.updateHeight();
View.prototype.renderEvents.call(this, events); // call the super-method
},
// Retrieves all segment objects that are rendered in the view
getSegs: function() {
return this.timeGrid.getSegs().concat(
this.dayGrid ? this.dayGrid.getSegs() : []
);
},
// Unrenders all event elements and clears internal segment data
destroyEvents: function() {
View.prototype.destroyEvents.call(this); // do this before the grids' segs have been cleared
// if destroyEvents is being called as part of an event rerender, renderEvents will be called shortly
// after, so remember what the scroll value was so we can restore it.
this.recordScroll();
// destroy the events in the subcomponents
this.timeGrid.destroyEvents();
if (this.dayGrid) {
this.dayGrid.destroyEvents();
}
// we DON'T need to call updateHeight() because:
// A) a renderEvents() call always happens after this, which will eventually call updateHeight()
// B) in IE8, this causes a flash whenever events are rerendered
},
/* Event Dragging
------------------------------------------------------------------------------------------------------------------*/
// Renders a visual indication of an event being dragged over the view.
// A returned value of `true` signals that a mock "helper" event has been rendered.
renderDrag: function(start, end, seg) {
if (start.hasTime()) {
return this.timeGrid.renderDrag(start, end, seg);
}
else if (this.dayGrid) {
return this.dayGrid.renderDrag(start, end, seg);
}
},
// Unrenders a visual indications of an event being dragged over the view
destroyDrag: function() {
this.timeGrid.destroyDrag();
if (this.dayGrid) {
this.dayGrid.destroyDrag();
}
},
/* Selection
------------------------------------------------------------------------------------------------------------------*/
// Renders a visual indication of a selection
renderSelection: function(start, end) {
if (start.hasTime() || end.hasTime()) {
this.timeGrid.renderSelection(start, end);
}
else if (this.dayGrid) {
this.dayGrid.renderSelection(start, end);
}
},
// Unrenders a visual indications of a selection
destroySelection: function() {
this.timeGrid.destroySelection();
if (this.dayGrid) {
this.dayGrid.destroySelection();
}
}
});
;;
/* A week view with an all-day cell area at the top, and a time grid below
----------------------------------------------------------------------------------------------------------------------*/
// TODO: a WeekView mixin for calculating dates and titles
fcViews.agendaWeek = AgendaWeekView; // register the view
function AgendaWeekView(calendar) {
AgendaView.call(this, calendar); // call the super-constructor
}
AgendaWeekView.prototype = createObject(AgendaView.prototype); // define the super-class
$.extend(AgendaWeekView.prototype, {
name: 'agendaWeek',
incrementDate: function(date, delta) {
return date.clone().stripTime().add(delta, 'weeks').startOf('week');
},
render: function(date) {
this.intervalStart = date.clone().stripTime().startOf('week');
this.intervalEnd = this.intervalStart.clone().add(1, 'weeks');
this.start = this.skipHiddenDays(this.intervalStart);
this.end = this.skipHiddenDays(this.intervalEnd, -1, true);
this.title = this.calendar.formatRange(
this.start,
this.end.clone().subtract(1), // make inclusive by subtracting 1 ms
this.opt('titleFormat'),
' \u2014 ' // emphasized dash
);
AgendaView.prototype.render.call(this, this.getCellsPerWeek()); // call the super-method
}
});
;;
/* A day view with an all-day cell area at the top, and a time grid below
----------------------------------------------------------------------------------------------------------------------*/
fcViews.agendaDay = AgendaDayView; // register the view
function AgendaDayView(calendar) {
AgendaView.call(this, calendar); // call the super-constructor
}
AgendaDayView.prototype = createObject(AgendaView.prototype); // define the super-class
$.extend(AgendaDayView.prototype, {
name: 'agendaDay',
incrementDate: function(date, delta) {
var out = date.clone().stripTime().add(delta, 'days');
out = this.skipHiddenDays(out, delta < 0 ? -1 : 1);
return out;
},
render: function(date) {
this.start = this.intervalStart = date.clone().stripTime();
this.end = this.intervalEnd = this.start.clone().add(1, 'days');
this.title = this.calendar.formatDate(this.start, this.opt('titleFormat'));
AgendaView.prototype.render.call(this, 1); // call the super-method
}
});
;;
}); |