setDefaults({
allDaySlot: true,
allDayText: 'all-day',
scrollTime: '06:00:00',
slotDuration: '00:30:00',
axisFormat: generateAgendaAxisFormat,
timeFormat: {
resource: generateAgendaTimeFormat
},
dragOpacity: {
resource: .5
},
minTime: '00:00:00',
maxTime: '24:00:00',
slotEventOverlap: true
});
// TODO: make it work in quirks mode (event corners, all-day height)
// TODO: test liquid width, especially in IE6
function ResourceView(element, calendar, viewName) {
var t = this;
// exports
t.renderResource = renderResource;
t.setWidth = setWidth;
t.setHeight = setHeight;
t.afterRender = afterRender;
t.computeDateTop = computeDateTop;
t.getIsCellAllDay = getIsCellAllDay;
t.allDayRow = function() { return allDayRow; }; // badly named
t.getCoordinateGrid = function() { return coordinateGrid; }; // specifically for AgendaEventRenderer
t.getHoverListener = function() { return hoverListener; };
t.colLeft = colLeft;
t.colRight = colRight;
t.colContentLeft = colContentLeft;
t.colContentRight = colContentRight;
t.getDaySegmentContainer = function() { return daySegmentContainer; };
t.getSlotSegmentContainer = function() { return slotSegmentContainer; };
t.getSlotContainer = function() { return slotContainer; };
t.getRowCnt = function() { return 1; };
t.getColCnt = function() { return 1; };
t.getColWidth = function() { return colWidth; };
t.getSnapHeight = function() { return snapHeight; };
t.getSnapDuration = function() { return snapDuration; };
t.getSlotHeight = function() { return slotHeight; };
t.getSlotDuration = function() { return slotDuration; };
t.getMinTime = function() { return minTime; };
t.getMaxTime = function() { return maxTime; };
t.defaultSelectionEnd = defaultSelectionEnd;
t.renderDayOverlay = renderDayOverlay;
t.renderSelection = renderSelection;
t.clearSelection = clearSelection;
t.reportDayClick = reportDayClick; // selection mousedown hack
t.dragStart = dragStart;
t.dragStop = dragStop;
t.getResources = calendar.fetchResources;
// imports
View.call(t, element, calendar, viewName);
OverlayManager.call(t);
SelectionManager.call(t);
ResourceEventRenderer.call(t);
var opt = t.opt;
var trigger = t.trigger;
var renderOverlay = t.renderOverlay;
var clearOverlays = t.clearOverlays;
var reportSelection = t.reportSelection;
var unselect = t.unselect;
//var daySelectionMousedown = t.daySelectionMousedown;
var slotSegHtml = t.slotSegHtml;
var cellToDate = t.cellToDate;
var dateToCell = t.dateToCell;
var rangeToSegments = t.rangeToSegments;
var formatDate = calendar.formatDate;
var calculateWeekNumber = calendar.calculateWeekNumber;
// locals
var dayTable;
var dayHead;
var dayHeadCells;
var dayBody;
var dayBodyCells;
var dayBodyCellInners;
var dayBodyCellContentInners;
var dayBodyFirstCell;
var dayBodyFirstCellStretcher;
var slotLayer;
var daySegmentContainer;
var allDayTable;
var allDayRow;
var slotScroller;
var slotContainer;
var slotSegmentContainer;
var slotTable;
var selectionHelper;
var viewWidth;
var viewHeight;
var axisWidth;
var colWidth;
var gutterWidth;
var slotDuration;
var slotHeight; // TODO: what if slotHeight changes? (see issue 650)
var snapDuration;
var snapRatio; // ratio of number of "selection" slots to normal slots. (ex: 1, 2, 4)
var snapHeight; // holds the pixel hight of a "selection" slot
var colCnt;
var slotCnt;
var coordinateGrid;
var hoverListener;
var colPositions;
var colContentPositions;
var slotTopCache = {};
var tm;
var rtl;
var minTime;
var maxTime;
var colFormat;
var resources = t.getResources;
/* Rendering
-----------------------------------------------------------------------------*/
disableTextSelection(element.addClass('fc-agenda'));
function renderResource(resourceColumnsCnt) {
colCnt = resourceColumnsCnt;
updateOptions();
if (!dayTable) { // first time rendering?
buildSkeleton(); // builds day table, slot area, events containers
}
else {
buildDayTable(); // rebuilds day table
}
}
function updateOptions() {
tm = opt('theme') ? 'ui' : 'fc';
rtl = opt('isRTL');
colFormat = opt('columnFormat');
minTime = moment.duration(opt('minTime'));
maxTime = moment.duration(opt('maxTime'));
slotDuration = moment.duration(opt('slotDuration'));
snapDuration = opt('snapDuration');
snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration;
}
/* Build DOM
-----------------------------------------------------------------------*/
function buildSkeleton() {
var s;
var headerClass = tm + "-widget-header";
var contentClass = tm + "-widget-content";
var slotTime;
var slotDate;
var minutes;
var slotNormal = slotDuration.asMinutes() % 15 === 0;
buildDayTable();
slotLayer =
$("
")
.appendTo(element);
if (opt('allDaySlot')) {
daySegmentContainer =
$("")
.appendTo(slotLayer);
s =
"" +
"" +
"" +
"| " +
"" +
" | " +
"" +
"
" +
"
";
allDayTable = $(s).appendTo(slotLayer);
allDayRow = allDayTable.find('tr');
dayBind(allDayRow.find('td'));
slotLayer.append(
""
);
}else{
daySegmentContainer = $([]); // in jQuery 1.4, we can just do $()
}
slotScroller =
$("")
.appendTo(slotLayer);
slotContainer =
$("")
.appendTo(slotScroller);
slotSegmentContainer =
$("")
.appendTo(slotContainer);
s =
"" +
"";
slotTime = moment.duration(+minTime); // i wish there was .clone() for durations
slotCnt = 0;
while (slotTime < maxTime) {
slotDate = t.start.clone().time(slotTime); // will be in UTC but that's good. to avoid DST issues
minutes = slotDate.minutes();
s +=
"" +
"" +
"| " +
" " +
" | " +
"
";
slotTime.add(slotDuration);
slotCnt++;
}
s +=
"" +
"
";
slotTable = $(s).appendTo(slotContainer);
slotBind(slotTable.find('td'));
}
/* Build Day Table
-----------------------------------------------------------------------*/
function buildDayTable() {
var html = buildDayTableHTML();
if (dayTable) {
dayTable.remove();
}
dayTable = $(html).appendTo(element);
dayHead = dayTable.find('thead');
dayHeadCells = dayHead.find('th').slice(1, -1); // exclude gutter
dayBody = dayTable.find('tbody');
dayBodyCells = dayBody.find('td').slice(0, -1); // exclude gutter
dayBodyCellInners = dayBodyCells.find('> div');
dayBodyCellContentInners = dayBodyCells.find('.fc-day-content > div');
dayBodyFirstCell = dayBodyCells.eq(0);
dayBodyFirstCellStretcher = dayBodyCellInners.eq(0);
markFirstLast(dayHead.add(dayHead.find('tr')));
markFirstLast(dayBody.add(dayBody.find('tr')));
// TODO: now that we rebuild the cells every time, we should call dayRender
}
function buildDayTableHTML() {
var html =
"" +
buildDayTableHeadHTML() +
buildDayTableBodyHTML() +
"
";
return html;
}
function buildDayTableHeadHTML() {
var headerClass = tm + "-widget-header";
var date;
var html = '';
var weekText;
var col;
html +=
"" +
"";
if (opt('weekNumbers')) {
date = cellToDate(0, 0);
weekText = calculateWeekNumber(date);
if (rtl) {
weekText += opt('weekNumberTitle');
}
else {
weekText = opt('weekNumberTitle') + weekText;
}
html +=
"";
}
else {
html += "";
}
for (col=0; col" +
htmlEscape(resource.name) +
"";
}
html +=
"" +
"
" +
"";
return html;
}
function buildDayTableBodyHTML() {
var headerClass = tm + "-widget-header"; // TODO: make these when updateOptions() called
var contentClass = tm + "-widget-content";
var date;
var today = calendar.getNow().stripTime();
var col;
var cellsHTML;
var cellHTML;
var classNames;
var html = '';
html +=
"" +
"" +
"";
cellsHTML = '';
for (col=0; col" +
"" +
"";
cellsHTML += cellHTML;
}
html += cellsHTML;
html +=
"| | " +
"
" +
"";
return html;
}
// TODO: data-date on the cells
/* Dimensions
-----------------------------------------------------------------------*/
function setHeight(height) {
if (height === undefined) {
height = viewHeight;
}
viewHeight = height;
slotTopCache = {};
var headHeight = dayBody.position().top;
var allDayHeight = slotScroller.position().top; // including divider
var bodyHeight = Math.min( // total body height, including borders
height - headHeight, // when scrollbars
slotTable.height() + allDayHeight + 1 // when no scrollbars. +1 for bottom border
);
dayBodyFirstCellStretcher
.height(bodyHeight - vsides(dayBodyFirstCell));
slotLayer.css('top', headHeight);
slotScroller.height(bodyHeight - allDayHeight - 1);
// the stylesheet guarantees that the first row has no border.
// this allows .height() to work well cross-browser.
var slotHeight0 = slotTable.find('tr:first').height() + 1; // +1 for bottom border
var slotHeight1 = slotTable.find('tr:eq(1)').height();
// HACK: i forget why we do this, but i think a cross-browser issue
slotHeight = (slotHeight0 + slotHeight1) / 2;
snapRatio = slotDuration / snapDuration;
snapHeight = slotHeight / snapRatio;
}
function setWidth(width) {
viewWidth = width;
colPositions.clear();
colContentPositions.clear();
var axisFirstCells = dayHead.find('th:first');
if (allDayTable) {
axisFirstCells = axisFirstCells.add(allDayTable.find('th:first'));
}
axisFirstCells = axisFirstCells.add(slotTable.find('th:first'));
axisWidth = 0;
setOuterWidth(
axisFirstCells
.width('')
.each(function(i, _cell) {
axisWidth = Math.max(axisWidth, $(_cell).outerWidth());
}),
axisWidth
);
var gutterCells = dayTable.find('.fc-agenda-gutter');
if (allDayTable) {
gutterCells = gutterCells.add(allDayTable.find('th.fc-agenda-gutter'));
}
var slotTableWidth = slotScroller[0].clientWidth; // needs to be done after axisWidth (for IE7)
gutterWidth = slotScroller.width() - slotTableWidth;
if (gutterWidth) {
setOuterWidth(gutterCells, gutterWidth);
gutterCells
.show()
.prev()
.removeClass('fc-last');
}else{
gutterCells
.hide()
.prev()
.addClass('fc-last');
}
colWidth = Math.floor((slotTableWidth - axisWidth) / colCnt);
setOuterWidth(dayHeadCells.slice(0, -1), colWidth);
}
/* Scrolling
-----------------------------------------------------------------------*/
function resetScroll() {
var top = computeTimeTop(
moment.duration(opt('scrollTime'))
) + 1; // +1 for the border
function scroll() {
slotScroller.scrollTop(top);
}
scroll();
setTimeout(scroll, 0); // overrides any previous scroll state made by the browser
}
function afterRender() { // after the view has been freshly rendered and sized
resetScroll();
}
/* Slot/Day clicking and binding
-----------------------------------------------------------------------*/
function dayBind(cells) {
cells.click(slotClick)
.mousedown(daySelectionMousedown);
}
function slotBind(cells) {
cells.click(slotClick)
.mousedown(slotSelectionMousedown);
}
function slotClick(ev) {
if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick
var col = Math.min(colCnt-1, Math.floor((ev.pageX - dayTable.offset().left - axisWidth) / colWidth));
var date = cellToDate(0, col);
var match = this.parentNode.className.match(/fc-slot(\d+)/); // TODO: maybe use data
ev.data = resources()[col]; // added
if (match) {
var slotIndex = parseInt(match[1], 10);
date.add(minTime + slotIndex * slotDuration);
date = calendar.rezoneDate(date);
trigger(
'dayClick',
dayBodyCells[col],
date,
ev
);
}else{
trigger(
'dayClick',
dayBodyCells[col],
date,
ev
);
}
}
}
/* Semi-transparent Overlay Helpers
-----------------------------------------------------*/
// TODO: should be consolidated with BasicView's methods
function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid, col) { // overlayEnd is exclusive
if (refreshCoordinateGrid) {
coordinateGrid.build();
}
var segments = rangeToSegments(overlayStart, overlayEnd);
for (var i=0; i= 0) {
date.time(moment.duration(minTime + snapIndex * snapDuration));
date = calendar.rezoneDate(date);
}
return date;
}
function computeDateTop(date, startOfDayDate) {
return computeTimeTop(
moment.duration(
date.clone().stripZone() - startOfDayDate.clone().stripTime()
)
);
}
function computeTimeTop(time) { // time is a duration
if (time < minTime) {
return 0;
}
if (time >= maxTime) {
return slotTable.height();
}
var slots = (time - minTime) / slotDuration;
var slotIndex = Math.floor(slots);
var slotPartial = slots - slotIndex;
var slotTop = slotTopCache[slotIndex];
// find the position of the corresponding
// need to use this tecnhique because not all rows are rendered at same height sometimes.
if (slotTop === undefined) {
slotTop = slotTopCache[slotIndex] =
slotTable.find('tr').eq(slotIndex).find('td div')[0].offsetTop;
// .eq() is faster than ":eq()" selector
// [0].offsetTop is faster than .position().top (do we really need this optimization?)
// a better optimization would be to cache all these divs
}
var top =
slotTop - 1 + // because first row doesn't have a top border
slotPartial * slotHeight; // part-way through the row
top = Math.max(top, 0);
return top;
}
/* Selection
---------------------------------------------------------------------------------*/
function defaultSelectionEnd(start) {
if (start.hasTime()) {
return start.clone().add(slotDuration);
}
else {
return start.clone().add(1, 'days');
}
}
function renderSelection(start, end, col) {
if (start.hasTime() || end.hasTime()) {
renderSlotSelection(start, end); //, col);
}
else if (opt('allDaySlot')) {
renderDayOverlay(start, end, true, col); // true for refreshing coordinate grid
}
}
function renderSlotSelection(startDate, endDate, col) {
var helperOption = opt('selectHelper');
coordinateGrid.build();
if (helperOption) {
col = col || dateToCell(startDate).col;
if (col >= 0 && col < colCnt) { // only works when times are on same day
var rect = coordinateGrid.rect(0, col, 0, col, slotContainer); // only for horizontal coords
var top = computeDateTop(startDate, startDate);
var bottom = computeDateTop(endDate, startDate);
if (bottom > top) { // protect against selections that are entirely before or after visible range
rect.top = top;
rect.height = bottom - top;
rect.left += 2;
rect.width -= 5;
if ($.isFunction(helperOption)) {
var helperRes = helperOption(startDate, endDate);
if (helperRes) {
rect.position = 'absolute';
selectionHelper = $(helperRes)
.css(rect)
.appendTo(slotContainer);
}
}else{
rect.isStart = true; // conside rect a "seg" now
rect.isEnd = true; //
selectionHelper = $(slotSegHtml(
{
title: '',
start: startDate,
end: endDate,
className: ['fc-select-helper'],
editable: false
},
rect
));
selectionHelper.css('opacity', opt('dragOpacity'));
}
if (selectionHelper) {
slotBind(selectionHelper);
slotContainer.append(selectionHelper);
setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended
setOuterHeight(selectionHelper, rect.height, true);
}
}
}
}else{
renderSlotOverlay(startDate, endDate, col);
}
}
function clearSelection() {
clearOverlays();
if (selectionHelper) {
selectionHelper.remove();
selectionHelper = null;
}
}
function slotSelectionMousedown(ev) {
if (ev.which == 1 && opt('selectable')) { // ev.which==1 means left mouse button
unselect(ev);
var dates;
var col;
hoverListener.start(function(cell, origCell) {
clearSelection();
if (cell && cell.col == origCell.col && !getIsCellAllDay(cell)) {
col = cell.col;
var d1 = realCellToDate(origCell);
var d2 = realCellToDate(cell);
dates = [
d1,
d1.clone().add(snapDuration), // calculate minutes depending on selection slot minutes
d2,
d2.clone().add(snapDuration)
].sort(dateCompare);
renderSlotSelection(dates[0], dates[3], cell.col); // updated
}else{
dates = null;
}
}, ev);
$(document).one('mouseup', function(ev) {
hoverListener.stop();
if (dates) {
if (+dates[0] == +dates[1]) {
reportDayClick(dates[0], ev);
}
ev.data = resources()[col]; // added
reportSelection(dates[0], dates[3], ev);
}
});
}
}
function reportDayClick(date, ev) {
trigger('dayClick', dayBodyCells[dateToCell(date).col], date, ev);
}
/* External Dragging
--------------------------------------------------------------------------------*/
function dragStart(_dragElement, ev, ui) {
hoverListener.start(function(cell) {
clearOverlays();
if (cell) {
var d1 = realCellToDate(cell);
var d2 = d1.clone();
if (d1.hasTime()) {
d2.add(calendar.defaultTimedEventDuration);
renderSlotOverlay(d1, d2, cell.col);
}
else {
d2.add(calendar.defaultAllDayEventDuration);
renderDayOverlay(d1, d2, true, cell.col);
}
}
}, ev);
}
function dragStop(_dragElement, ev, ui) {
var cell = hoverListener.stop();
clearOverlays();
if (cell) {
ev.data = resources()[cell.col];
trigger(
'drop',
_dragElement,
realCellToDate(cell),
ev,
ui
);
}
}
/* OVERRIDES */
function daySelectionMousedown(ev) {
var getIsCellAllDay = t.getIsCellAllDay;
var hoverListener = t.getHoverListener();
var reportDayClick = t.reportDayClick; // this is hacky and sort of weird
var col;
if (ev.which == 1 && opt('selectable')) { // which==1 means left mouse button
unselect(ev);
var dates;
hoverListener.start(function(cell, origCell) { // TODO: maybe put cellToDate/getIsCellAllDay info in cell
clearSelection();
if (cell && getIsCellAllDay(cell)) {
col = cell.col;
dates = [ realCellToDate(origCell), realCellToDate(cell) ].sort(dateCompare);
renderSelection(dates[0], dates[1], col);
}else{
dates = null;
}
}, ev);
$(document).one('mouseup', function(ev) {
hoverListener.stop();
if (dates) {
if (+dates[0] == +dates[1]) {
reportDayClick(dates[0], true, ev);
}
ev.data = resources()[col];
reportSelection(dates[0], dates[1], ev);
}
});
}
}
}