setDefaults({
allDaySlot: true,
allDayText: 'all-day',
firstHour: 6,
slotMinutes: 30,
defaultEventMinutes: 120,
axisFormat: 'h(:mm)tt',
timeFormat: {
agenda: 'h:mm{ - h:mm}'
},
dragOpacity: {
agenda: .5
},
minTime: 0,
maxTime: 24
});
// 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.defaultEventEnd = defaultEventEnd;
t.timePosition = timePosition;
t.getIsCellAllDay = getIsCellAllDay;
t.allDayRow = getAllDayRow;
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.getMinMinute = function() { return minMinute };
t.getMaxMinute = function() { return maxMinute };
t.getSlotContainer = function() { return slotContainer };
t.getRowCnt = function() { return 1 };
t.getColCnt = function() { return colCnt };
t.getColWidth = function() { return colWidth };
t.getSnapHeight = function() { return snapHeight };
t.getSnapMinutes = function() { return snapMinutes };
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;
// 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 slotTableFirstInner;
var selectionHelper;
var viewWidth;
var viewHeight;
var axisWidth;
var colWidth;
var gutterWidth;
var slotHeight; // TODO: what if slotHeight changes? (see issue 650)
var snapMinutes;
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 minMinute, maxMinute;
var colFormat;
var showWeekNumbers;
var weekNumberTitle;
var weekNumberFormat;
var resources = t.getResources;
/* Rendering
-----------------------------------------------------------------------------*/
disableTextSelection(element.addClass('fc-agenda'));
function renderResource(c) {
colCnt = c;
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')
minMinute = parseTime(opt('minTime'));
maxMinute = parseTime(opt('maxTime'));
colFormat = opt('columnFormat');
// week # options. (TODO: bad, logic also in other views)
showWeekNumbers = opt('weekNumbers');
weekNumberTitle = opt('weekNumberTitle');
if (opt('weekNumberCalculation') != 'iso') {
weekNumberFormat = "w";
}
else {
weekNumberFormat = "W";
}
snapMinutes = opt('snapMinutes') || opt('slotMinutes');
}
/* Build DOM
-----------------------------------------------------------------------*/
function buildSkeleton() {
var headerClass = tm + "-widget-header";
var contentClass = tm + "-widget-content";
var s;
var d;
var i;
var maxd;
var minutes;
var slotNormal = opt('slotMinutes') % 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 =
"" +
"";
d = zeroDate();
maxd = addMinutes(cloneDate(d), maxMinute);
addMinutes(d, minMinute);
slotCnt = 0;
for (i=0; d < maxd; i++) {
minutes = d.getMinutes();
s +=
"" +
"" +
"| " +
" " +
" | " +
"
";
addMinutes(d, opt('slotMinutes'));
slotCnt++;
}
s +=
"" +
"
";
slotTable = $(s).appendTo(slotContainer);
slotTableFirstInner = slotTable.find('div:first');
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 (showWeekNumbers) {
weekText = formatDate(date, weekNumberFormat);
if (rtl) {
weekText += weekNumberTitle;
}
else {
weekText = weekNumberTitle + weekText;
}
html +=
"";
}
else {
html += "";
}
for (col=0; col" +
htmlEscape(resources[col].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 = clearTime(new Date());
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);
slotHeight = slotTableFirstInner.height() + 1; // +1 for border
snapRatio = opt('slotMinutes') / snapMinutes;
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 d0 = zeroDate();
var scrollDate = cloneDate(d0);
scrollDate.setHours(opt('firstHour'));
var top = timePosition(d0, scrollDate) + 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 rowMatch = this.parentNode.className.match(/fc-slot(\d+)/); // TODO: maybe use data
if (rowMatch) {
var mins = parseInt(rowMatch[1]) * opt('slotMinutes');
var hours = Math.floor(mins/60);
date.setHours(hours);
date.setMinutes(mins%60 + minMinute);
trigger('dayClick', dayBodyCells[col], date, false, ev);
}else{
trigger('dayClick', dayBodyCells[col], date, true, ev);
}
}
}
/* Semi-transparent Overlay Helpers
-----------------------------------------------------*/
// TODO: should be consolidated with BasicView's methods
function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive
if (refreshCoordinateGrid) {
coordinateGrid.build();
}
var segments = rangeToSegments(overlayStart, overlayEnd);
for (var i=0; i= 0) {
addMinutes(d, minMinute + slotIndex * snapMinutes);
}
return d;
}
// get the Y coordinate of the given time on the given day (both Date objects)
function timePosition(day, time) { // both date objects. day holds 00:00 of current day
day = cloneDate(day, true);
if (time < addMinutes(cloneDate(day), minMinute)) {
return 0;
}
if (time >= addMinutes(cloneDate(day), maxMinute)) {
return slotTable.height();
}
var slotMinutes = opt('slotMinutes'),
minutes = time.getHours()*60 + time.getMinutes() - minMinute,
slotI = Math.floor(minutes / slotMinutes),
slotTop = slotTopCache[slotI];
if (slotTop === undefined) {
slotTop = slotTopCache[slotI] =
slotTable.find('tr').eq(slotI).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
}
return Math.max(0, Math.round(
slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes)
));
}
function getAllDayRow(index) {
return allDayRow;
}
function defaultEventEnd(event) {
var start = cloneDate(event.start);
if (event.allDay) {
return start;
}
return addMinutes(start, opt('defaultEventMinutes'));
}
/* Selection
---------------------------------------------------------------------------------*/
function defaultSelectionEnd(startDate, allDay) {
if (allDay) {
return cloneDate(startDate);
}
return addMinutes(cloneDate(startDate), opt('slotMinutes'));
}
function renderSelection(startDate, endDate, allDay) { // only for all-day
if (allDay) {
if (opt('allDaySlot')) {
renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true);
}
}else{
renderSlotSelection(startDate, endDate);
}
}
function renderSlotSelection(startDate, endDate) {
var helperOption = opt('selectHelper');
coordinateGrid.build();
if (helperOption) {
var 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 = timePosition(startDate, startDate);
var bottom = timePosition(startDate, endDate);
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);
}
}
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;
hoverListener.start(function(cell, origCell) {
clearSelection();
if (cell && cell.col == origCell.col && !getIsCellAllDay(cell)) {
var d1 = realCellToDate(origCell);
var d2 = realCellToDate(cell);
dates = [
d1,
addMinutes(cloneDate(d1), snapMinutes), // calculate minutes depending on selection slot minutes
d2,
addMinutes(cloneDate(d2), snapMinutes)
].sort(dateCompare);
renderSlotSelection(dates[0], dates[3]);
}else{
dates = null;
}
}, ev);
$(document).one('mouseup', function(ev) {
hoverListener.stop();
if (dates) {
if (+dates[0] == +dates[1]) {
reportDayClick(dates[0], false, ev);
}
reportSelection(dates[0], dates[3], false, ev);
}
});
}
}
function reportDayClick(date, allDay, ev) {
trigger('dayClick', dayBodyCells[dateToCell(date).col], date, allDay, ev);
}
/* External Dragging
--------------------------------------------------------------------------------*/
function dragStart(_dragElement, ev, ui) {
hoverListener.start(function(cell) {
clearOverlays();
if (cell) {
if (getIsCellAllDay(cell)) {
renderCellOverlay(cell.row, cell.col, cell.row, cell.col);
}else{
var d1 = realCellToDate(cell);
var d2 = addMinutes(cloneDate(d1), opt('defaultEventMinutes'));
renderSlotOverlay(d1, d2);
}
}
}, ev);
}
function dragStop(_dragElement, ev, ui) {
var cell = hoverListener.stop();
clearOverlays();
if (cell) {
trigger('drop', _dragElement, realCellToDate(cell), getIsCellAllDay(cell), ev, ui);
}
}
}