Files
fullcalendar/src/agenda/AgendaView.js
T

395 lines
12 KiB
JavaScript

/* 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
});
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 <hr> 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 <hr> that sometimes displays under the time-grid
this.bottomRuleEl = $('<hr class="' + this.widgetHeaderClass + '"/>')
.appendTo(this.timeGrid.el); // inject it into the time-grid
if (this.dayGrid) {
this.dayRowThemeClass = this.widgetHeaderClass; // forces this class on each day-row
this.dayGrid.el = this.el.find('.fc-day-grid');
this.dayGrid.render();
// have the day-grid extend it's coordinate area over the <hr> 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
},
// 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 '' +
'<table>' +
'<thead>' +
'<tr>' +
'<td class="' + this.widgetHeaderClass + '">' +
this.timeGrid.headHtml() + // render the day-of-week headers
'</td>' +
'</tr>' +
'</thead>' +
'<tbody>' +
'<tr>' +
'<td class="' + this.widgetHeaderClass + '">' +
(this.dayGrid ?
'<div class="fc-day-grid"/>' +
'<hr class="' + this.widgetHeaderClass + '"/>' :
''
) +
'<div class="fc-time-grid-container">' +
'<div class="fc-time-grid"/>' +
'</div>' +
'</td>' +
'</tr>' +
'</tbody>' +
'</table>';
},
// 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 '' +
'<th class="fc-axis fc-week-number ' + this.widgetHeaderClass + '">' +
'<span>' + // needed for matchCellWidths
htmlEscape(weekText) +
'</span>' +
'</th>';
}
else {
return '<th class="fc-axis ' + this.widgetHeaderClass + '"' +
(this.axisWidth !== null ?
' style="width:' + this.axisWidth + 'px"' :
''
) +
'></th>';
}
},
// 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 '' +
'<td class="' + this.widgetHeaderClass + ' fc-axis">' +
'<span>' + // needed for matchCellWidths
(this.opt('allDayHTML') || htmlEscape(this.opt('allDayText'))) +
'</span>' +
'</td>';
},
// 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 '<td class="fc-axis"' +
(this.axisWidth !== null ?
' style="width:' + this.axisWidth + 'px"' :
''
) +
'></td>';
},
/* Dimensions
------------------------------------------------------------------------------------------------------------------*/
// 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 scrollerHeight;
var timeGridHeight;
var extraHeight; // # of pixels the time-grid element needs to expand to fill the scroller
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 <hr> is necessary
// reset all dimensions back to the original state
this.scrollerEl.height('').removeClass('fc-scroller');
uncompensateScroll(this.noScrollRowEls);
if (!isAuto) { // should we force dimensions of the scroll container, or let the contents be natural height?
scrollerHeight = this.computeScrollerHeight(totalHeight);
timeGridHeight = this.timeGrid.el.height();
this.scrollerEl.height(scrollerHeight);
if (timeGridHeight > scrollerHeight) { // do we need scrollbars?
// force scrollbars and make the all-day and header rows lines up
this.scrollerEl.addClass('fc-scroller');
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 {
// display the <hr> if there is enough extra space
extraHeight = scrollerHeight - timeGridHeight;
if (extraHeight > this.bottomRuleHeight + 5) {
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();
this.segs = daySegs.concat(timedSegs); // needed by the View super-class
View.prototype.renderEvents.call(this, events); // call the super-method
},
// Unrenders all event elements and clears internal segment data
destroyEvents: function() {
// 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();
}
this.updateHeight();
View.prototype.destroyEvents.call(this); // call the super-method. will kill `this.segs`
},
/* 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();
}
}
});