fullcalendar/src/grid.js

640 lines
17 KiB
JavaScript
Raw Normal View History

2009-09-21 06:57:20 +02:00
/* Grid-based Views: month, basicWeek, basicDay
-----------------------------------------------------------------------------*/
setDefaults({
weekMode: 'fixed'
});
views.month = function(element, options) {
return new Grid(element, options, {
render: function(date, delta, width, height, fetchEvents) {
2009-09-21 06:57:20 +02:00
if (delta) {
addMonths(date, delta);
2009-10-01 07:39:02 +02:00
date.setDate(1);
2009-09-21 06:57:20 +02:00
}
2009-11-01 00:51:30 +01:00
// start/end
2009-09-21 06:57:20 +02:00
var start = this.start = cloneDate(date, true);
start.setDate(1);
2009-11-01 00:51:30 +01:00
this.end = addMonths(cloneDate(start), 1);
// visStart/visEnd
var visStart = this.visStart = cloneDate(start),
visEnd = this.visEnd = cloneDate(this.end),
nwe = options.weekends ? 0 : 1;
if (nwe) {
skipWeekend(visStart);
skipWeekend(visEnd, -1, true);
}
addDays(visStart, -((visStart.getDay() - Math.max(options.firstDay, nwe) + 7) % 7));
addDays(visEnd, (7 - visEnd.getDay() + Math.max(options.firstDay, nwe)) % 7);
// row count
var rowCnt = Math.round((visEnd - visStart) / (DAY_MS * 7));
if (options.weekMode == 'fixed') {
addDays(visEnd, (6 - rowCnt) * 7);
rowCnt = 6;
}
// title
this.title = formatDate(
2009-09-21 06:57:20 +02:00
start,
2009-10-10 10:12:40 +02:00
this.option('titleFormat'),
2009-09-21 06:57:20 +02:00
options
);
2009-11-01 00:51:30 +01:00
// render
this.renderGrid(
rowCnt, options.weekends ? 7 : 5,
this.option('columnFormat'),
true,
width, height,
2009-11-01 00:51:30 +01:00
fetchEvents
);
2009-09-21 06:57:20 +02:00
}
});
}
views.basicWeek = function(element, options) {
return new Grid(element, options, {
render: function(date, delta, width, height, fetchEvents) {
2009-09-21 06:57:20 +02:00
if (delta) {
addDays(date, delta * 7);
}
2009-11-01 00:51:30 +01:00
var visStart = this.visStart = cloneDate(
this.start = addDays(cloneDate(date), -((date.getDay() - options.firstDay + 7) % 7))
),
visEnd = this.visEnd = cloneDate(
this.end = addDays(cloneDate(visStart), 7)
);
if (!options.weekends) {
skipWeekend(visStart);
skipWeekend(visEnd, -1, true);
}
2009-09-21 06:57:20 +02:00
this.title = formatDates(
2009-11-01 00:51:30 +01:00
visStart,
addDays(cloneDate(visEnd), -1),
2009-10-10 10:12:40 +02:00
this.option('titleFormat'),
2009-09-21 06:57:20 +02:00
options
);
2009-11-01 00:51:30 +01:00
this.renderGrid(
1, options.weekends ? 7 : 5,
this.option('columnFormat'),
false,
width, height,
2009-11-01 00:51:30 +01:00
fetchEvents
);
2009-09-21 06:57:20 +02:00
}
});
};
views.basicDay = function(element, options) {
return new Grid(element, options, {
render: function(date, delta, width, height, fetchEvents) {
2009-09-21 06:57:20 +02:00
if (delta) {
addDays(date, delta);
2009-11-01 00:51:30 +01:00
if (!options.weekends) {
skipWeekend(date, delta < 0 ? -1 : 1);
}
2009-09-21 06:57:20 +02:00
}
2009-10-10 10:12:40 +02:00
this.title = formatDate(date, this.option('titleFormat'), options);
2009-09-21 06:57:20 +02:00
this.start = this.visStart = cloneDate(date, true);
this.end = this.visEnd = addDays(cloneDate(this.start), 1);
this.renderGrid(
1, 1,
this.option('columnFormat'),
false,
width, height,
fetchEvents
);
2009-09-21 06:57:20 +02:00
}
});
}
// rendering bugs
var tdHeightBug;
2009-09-21 06:57:20 +02:00
function Grid(element, options, methods) {
var tm, firstDay,
2009-11-01 00:51:30 +01:00
nwe, // no weekends (int)
2009-09-21 06:57:20 +02:00
rtl, dis, dit, // day index sign / translate
viewWidth, viewHeight,
2009-09-21 06:57:20 +02:00
rowCnt, colCnt,
colWidth,
thead, tbody,
cachedEvents=[],
segmentContainer,
dayContentPositions = new HorizontalPositionCache(function(dayOfWeek) {
return tbody.find('td:eq(' + ((dayOfWeek - Math.max(firstDay,nwe)+colCnt) % colCnt) + ') div div')
}),
// ...
2009-09-21 06:57:20 +02:00
// initialize superclass
view = $.extend(this, viewMethods, methods, {
renderGrid: renderGrid,
renderEvents: renderEvents,
rerenderEvents: rerenderEvents,
clearEvents: clearEvents,
2009-09-21 06:57:20 +02:00
updateSize: updateSize,
defaultEventEnd: function(event) { // calculates an end if event doesnt have one, mostly for resizing
return cloneDate(event.start);
}
});
view.init(element, options);
/* Grid Rendering
-----------------------------------------------------------------------------*/
element.addClass('fc-grid').css('position', 'relative');
if (element.disableSelection) {
element.disableSelection();
}
function renderGrid(r, c, colFormat, showNumbers, width, height, fetchEvents) {
2009-09-21 06:57:20 +02:00
rowCnt = r;
colCnt = c;
// update option-derived variables
2009-11-01 00:51:30 +01:00
tm = options.theme ? 'ui' : 'fc';
nwe = options.weekends ? 0 : 1;
2009-09-21 06:57:20 +02:00
firstDay = options.firstDay;
if (rtl = options.isRTL) {
dis = -1;
dit = colCnt - 1;
}else{
dis = 1;
dit = 0;
}
var month = view.start.getMonth(),
today = clearTime(new Date()),
s, i, j, d = cloneDate(view.visStart);
2009-09-21 06:57:20 +02:00
if (!tbody) { // first time, build all cells from scratch
var table = $("<table/>").appendTo(element);
s = "<thead><tr>";
for (i=0; i<colCnt; i++) {
s += "<th class='fc-" +
dayIDs[d.getDay()] + ' ' + // needs to be first
tm + '-state-default' +
(i==dit ? ' fc-leftmost' : '') +
"'>" + formatDate(d, colFormat, options) + "</th>";
addDays(d, 1);
2009-11-01 00:51:30 +01:00
if (nwe) {
skipWeekend(d);
}
2009-09-21 06:57:20 +02:00
}
thead = $(s + "</tr></thead>").appendTo(table);
s = "<tbody>";
d = cloneDate(view.visStart);
for (i=0; i<rowCnt; i++) {
s += "<tr class='fc-week" + i + "'>";
for (j=0; j<colCnt; j++) {
s += "<td class='fc-" +
dayIDs[d.getDay()] + ' ' + // needs to be first
tm + '-state-default fc-day' + (i*colCnt+j) +
(j==dit ? ' fc-leftmost' : '') +
(rowCnt>1 && d.getMonth() != month ? ' fc-other-month' : '') +
(+d == +today ?
' fc-today '+tm+'-state-highlight' :
' fc-not-today') + "'>" +
(showNumbers ? "<div class='fc-day-number'>" + d.getDate() + "</div>" : '') +
"<div class='fc-day-content'><div>&nbsp;</div></div></td>";
addDays(d, 1);
2009-11-01 00:51:30 +01:00
if (nwe) {
skipWeekend(d);
}
2009-09-21 06:57:20 +02:00
}
s += "</tr>";
}
tbody = $(s + "</tbody>").appendTo(table);
tbody.find('td').click(dayClick);
segmentContainer = $("<div/>").appendTo(element);
2009-09-21 06:57:20 +02:00
}else{ // NOT first time, reuse as many cells as possible
clearEvents();
2009-09-21 06:57:20 +02:00
var prevRowCnt = tbody.find('tr').length;
if (rowCnt < prevRowCnt) {
tbody.find('tr:gt(' + (rowCnt-1) + ')').remove(); // remove extra rows
}
else if (rowCnt > prevRowCnt) { // needs to create new rows...
s = '';
for (i=prevRowCnt; i<rowCnt; i++) {
s += "<tr class='fc-week" + i + "'>";
for (j=0; j<colCnt; j++) {
s += "<td class='fc-" +
dayIDs[d.getDay()] + ' ' + // needs to be first
tm + '-state-default fc-new fc-day' + (i*colCnt+j) +
(j==dit ? ' fc-leftmost' : '') + "'>" +
(showNumbers ? "<div class='fc-day-number'></div>" : '') +
"<div class='fc-day-content'><div>&nbsp;</div></div>" +
"</td>";
addDays(d, 1);
2009-11-01 00:51:30 +01:00
if (nwe) {
skipWeekend(d);
}
2009-09-21 06:57:20 +02:00
}
s += "</tr>";
}
tbody.append(s);
}
tbody.find('td.fc-new').removeClass('fc-new').click(dayClick);
// re-label and re-class existing cells
d = cloneDate(view.visStart);
tbody.find('td').each(function() {
var td = $(this);
if (rowCnt > 1) {
if (d.getMonth() == month) {
td.removeClass('fc-other-month');
}else{
td.addClass('fc-other-month');
}
}
if (+d == +today) {
td.removeClass('fc-not-today')
.addClass('fc-today')
.addClass(tm + '-state-highlight');
}else{
td.addClass('fc-not-today')
.removeClass('fc-today')
.removeClass(tm + '-state-highlight');
}
td.find('div.fc-day-number').text(d.getDate());
addDays(d, 1);
2009-11-01 00:51:30 +01:00
if (nwe) {
skipWeekend(d);
}
2009-09-21 06:57:20 +02:00
});
if (rowCnt == 1) { // more changes likely (week or day view)
// redo column header text and class
d = cloneDate(view.visStart);
thead.find('th').each(function() {
$(this).text(formatDate(d, colFormat, options));
this.className = this.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]);
addDays(d, 1);
2009-11-01 00:51:30 +01:00
if (nwe) {
skipWeekend(d);
}
2009-09-21 06:57:20 +02:00
});
// redo cell day-of-weeks
d = cloneDate(view.visStart);
tbody.find('td').each(function() {
this.className = this.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]);
addDays(d, 1);
2009-11-01 00:51:30 +01:00
if (nwe) {
skipWeekend(d);
}
2009-09-21 06:57:20 +02:00
});
}
}
updateSize(width, height);
2009-09-21 06:57:20 +02:00
fetchEvents(renderEvents);
};
function dayClick(ev) {
2009-11-01 00:51:30 +01:00
var n = parseInt(this.className.match(/fc\-day(\d+)/)[1]),
date = addDays(
cloneDate(view.visStart),
Math.floor(n/colCnt) * 7 + n % colCnt
);
view.trigger('dayClick', this, date, true, ev);
2009-09-21 06:57:20 +02:00
}
function updateSize(width, height) { // does not render/position the events
viewWidth = width;
viewHeight = height;
dayContentPositions.clear();
var leftTDs = tbody.find('tr td:first-child'),
tbodyHeight = viewHeight - thead.height(),
2009-09-21 06:57:20 +02:00
rowHeight1, rowHeight2;
2009-09-21 12:11:08 +02:00
2009-09-21 06:57:20 +02:00
if (options.weekMode == 'variable') {
rowHeight1 = rowHeight2 = Math.floor(tbodyHeight / (rowCnt==1 ? 2 : 6));
}else{
rowHeight1 = Math.floor(tbodyHeight / rowCnt);
rowHeight2 = tbodyHeight - rowHeight1*(rowCnt-1);
}
2009-10-13 06:22:40 +02:00
2009-09-21 06:57:20 +02:00
if (tdHeightBug == undefined) {
// bug in firefox where cell height includes padding
2009-10-13 06:22:40 +02:00
var tr = tbody.find('tr:first'),
td = tr.find('td:first');
2009-09-21 06:57:20 +02:00
td.height(rowHeight1);
tdHeightBug = rowHeight1 != td.height();
}
if (tdHeightBug) {
leftTDs.slice(0, -1).height(rowHeight1);
leftTDs.slice(-1).height(rowHeight2);
}else{
setOuterHeight(leftTDs.slice(0, -1), rowHeight1);
setOuterHeight(leftTDs.slice(-1), rowHeight2);
}
2009-09-21 12:11:08 +02:00
setOuterWidth(
thead.find('th').slice(0, -1),
colWidth = Math.floor(viewWidth / colCnt)
2009-09-21 12:11:08 +02:00
);
2009-09-21 06:57:20 +02:00
}
2009-09-21 06:57:20 +02:00
/* Event Rendering
-----------------------------------------------------------------------------*/
function renderEvents(events) {
view.reportEvents(cachedEvents = events);
renderSegs(compileSegs(events));
2009-09-21 06:57:20 +02:00
}
function rerenderEvents() {
clearEvents();
renderSegs(compileSegs(cachedEvents));
}
function clearEvents() {
view._clearEvents(); // only clears the hashes
segmentContainer.empty();
2009-09-21 06:57:20 +02:00
}
function compileSegs(events) {
2009-10-10 10:12:40 +02:00
var d1 = cloneDate(view.visStart),
d2 = addDays(cloneDate(d1), colCnt),
rows = [],
i=0;
for (; i<rowCnt; i++) {
rows.push(stackSegs(view.sliceSegs(events, $.map(events, visEventEnd), d1, d2)));
2009-09-21 06:57:20 +02:00
addDays(d1, 7);
addDays(d2, 7);
}
return rows;
}
function renderSegs(segCols) {
_renderDaySegs(
segCols,
view,
0,
viewWidth,
function(i) {
return tbody.find('tr:eq('+i+')');
},
dayContentPositions.left,
dayContentPositions.right,
segmentContainer,
bootstrapEventHandlers
);
2009-09-21 06:57:20 +02:00
}
function visEventEnd(event) { // returns exclusive 'visible' end, for rendering
if (event.end) {
var end = cloneDate(event.end);
return (event.allDay || end.getHours() || end.getMinutes()) ? addDays(end, 1) : end;
}else{
return addDays(cloneDate(event.start), 1);
}
}
function bootstrapEventHandlers(event, seg, eventElement) {
function mouseover(ev) {
view.trigger('eventMouseover', this, event, ev);
eventElement.unbind('mouseover', mouseover);
setTimeout(function() { // because IE will immediately trigger eventElementHandlers's mouseover
view.eventElementHandlers(event, eventElement);
if (event.editable || event.editable == undefined && options.editable) {
draggableEvent(event, eventElement);
if (seg.isEnd) {
view.resizableDayEvent(event, eventElement, colWidth);
}
}
},0);
}
eventElement.mouseover(mouseover);
}
2009-09-21 06:57:20 +02:00
2009-10-10 10:12:40 +02:00
/* Event Dragging
2009-09-21 06:57:20 +02:00
-----------------------------------------------------------------------------*/
function draggableEvent(event, eventElement) {
if (!options.disableDragging && eventElement.draggable) {
var matrix;
eventElement.draggable({
2009-10-10 10:12:40 +02:00
zIndex: 9,
2009-09-21 12:11:08 +02:00
delay: 50,
2009-10-10 10:12:40 +02:00
opacity: view.option('dragOpacity'),
2009-09-21 06:57:20 +02:00
revertDuration: options.dragRevertDuration,
start: function(ev, ui) {
view.hideEvents(event, eventElement);
view.trigger('eventDragStart', eventElement, event, ev, ui);
2009-09-21 06:57:20 +02:00
matrix = new HoverMatrix(function(cell) {
eventElement.draggable('option', 'revert', !cell || !cell.rowDelta && !cell.colDelta);
if (cell) {
view.showOverlay(cell);
}else{
view.hideOverlay();
}
});
tbody.find('tr').each(function() {
2009-10-13 06:22:40 +02:00
matrix.row(this);
2009-09-21 06:57:20 +02:00
});
var tds = tbody.find('tr:first td');
if (rtl) {
tds = $(tds.get().reverse());
}
tds.each(function() {
matrix.col(this);
});
matrix.mouse(ev.pageX, ev.pageY);
},
drag: function(ev) {
matrix.mouse(ev.pageX, ev.pageY);
},
stop: function(ev, ui) {
view.hideOverlay();
view.trigger('eventDragStop', eventElement, event, ev, ui);
var cell = matrix.cell;
if (!cell || !cell.rowDelta && !cell.colDelta) {
2009-10-13 06:22:40 +02:00
if ($.browser.msie) {
eventElement.css('filter', ''); // clear IE opacity side-effects
}
2009-09-21 06:57:20 +02:00
view.showEvents(event, eventElement);
}else{
eventElement.find('a').removeAttr('href'); // prevents safari from visiting the link
2009-10-10 10:12:40 +02:00
view.eventDrop(this, event, cell.rowDelta*7+cell.colDelta*dis, 0, event.allDay, ev, ui);
2009-09-21 06:57:20 +02:00
}
}
});
}
}
2009-10-10 10:12:40 +02:00
// event resizing w/ 'view' methods...
2009-09-21 06:57:20 +02:00
};
function _renderDaySegs(segRows, view, minLeft, maxLeft, getTr, dayContentLeft, dayContentRight, segmentContainer, bootstrapEventHandlers) {
var options=view.options,
rtl=options.isRTL,
event,
className,
left,
right,
eventLefts=[],
eventRights=[],
html='',
eventElements,
eventElement,
triggerRes,
eventOuterHeights=[],
eventHSides=[],
l=0,
i=0, len=segRows.length, levels,
td,
innerDiv,
top,
rowContentHeight,
j, segs,
levelHeight,
k, seg;
// calculate desired position/dimensions, create html
eachLeaf(segRows, function(l, seg) {
event = seg.event;
className = 'fc-event fc-event-hori ';
if (rtl) {
if (seg.isStart) {
className += 'fc-corner-right ';
}
if (seg.isEnd) {
className += 'fc-corner-left ';
}
left = seg.isEnd ? dayContentLeft(seg.end.getDay()-1) : minLeft;
right = seg.isStart ? dayContentRight(seg.start.getDay()) : maxLeft;
}else{
if (seg.isStart) {
className += 'fc-corner-left ';
}
if (seg.isEnd) {
className += 'fc-corner-right ';
}
left = seg.isStart ? dayContentLeft(seg.start.getDay()) : minLeft;
right = seg.isEnd ? dayContentRight(seg.end.getDay()-1) : maxLeft;
}
eventLefts[l] = left;
eventRights[l] = right;
html +=
"<div class='" + className + event.className.join(' ') + "' style='position:absolute;z-index:8;left:"+left+"px'>" +
"<a" + (event.url ? " href='" + htmlEscape(event.url) + "'" : '') + ">" +
(!event.allDay && seg.isStart ?
"<span class='fc-event-time'>" +
htmlEscape(formatDates(event.start, event.end, view.option('timeFormat'), options)) +
"</span>"
:'') +
"<span class='fc-event-title'>" + htmlEscape(event.title) + "</span>" +
"</a>" +
"</div>";
});
segmentContainer.html(html);
eventElements = segmentContainer.children();
// retrieve elements, run through eventRender callback, record outer-edge dimensions
eachLeaf(segRows, function(l, seg) {
event = seg.event;
eventElement = eventElements.eq(l);
triggerRes = view.trigger('eventRender', event, event, eventElement);
if (triggerRes === false) {
eventElement.remove();
}else{
if (triggerRes && triggerRes !== true) {
eventElement.remove();
eventElement = $(triggerRes)
.css({
position: 'absolute',
left: eventLefts[l]
})
.appendTo(segmentContainer);
}
seg.element = eventElement; // will be useful for future rerender optimizations
eventOuterHeights[l] = eventElement.outerHeight(true);
eventHSides[l] = hsides(eventElement, true);
bootstrapEventHandlers(event, seg, eventElement);
view.reportEventElement(event, eventElement);
}
});
// set all positions/dimensions at once
for (; i<len; i++) {
levels = segRows[i];
td = getTr(i).find('td:first');
innerDiv = td.find('div.fc-day-content div')
.css('position', 'relative')
.height(''); // this is needed for IE7 to get an accurate position
top = innerDiv.position().top + topCorrect(td);
rowContentHeight = 0;
for (j=0; j<levels.length; j++) {
segs = levels[j];
levelHeight = 0;
for (k=0; k<segs.length; k++) {
seg = segs[k];
if (eventElement = seg.element) {
eventElement.css('top', top);
//IE6 right-to-left sort-of-off-by-one bug
//if (rtl && rtlLeftDiff == undefined) {
// // bug in IE6 where offsets are miscalculated with direction:rtl
// rtlLeftDiff = eventLefts[l] - eventElement.position().left;
// if (rtlLeftDiff) {
// eventElement.css('left', eventLefts[l] + rtlLeftDiff);
// }
//}
eventElement.width(eventRights[l] - eventLefts[l] - eventHSides[l]);
event = seg.event;
view.trigger('eventAfterRender', event, event, eventElement);
levelHeight = Math.max(levelHeight, eventOuterHeights[l]);
}
l++;
}
rowContentHeight += levelHeight;
top += levelHeight;
}
innerDiv.height(rowContentHeight);
}
}