1250 lines
34 KiB
JavaScript
1250 lines
34 KiB
JavaScript
|
|
/* Agenda Views: agendaWeek/agendaDay
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
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
|
|
});
|
|
|
|
views.agendaWeek = function(element, options, viewName) {
|
|
return new Agenda(element, options, {
|
|
render: function(date, delta) {
|
|
if (delta) {
|
|
addDays(date, delta * 7);
|
|
}
|
|
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);
|
|
}
|
|
this.title = formatDates(
|
|
visStart,
|
|
addDays(cloneDate(visEnd), -1),
|
|
this.option('titleFormat'),
|
|
options
|
|
);
|
|
this.renderAgenda(
|
|
options.weekends ? 7 : 5,
|
|
this.option('columnFormat')
|
|
);
|
|
}
|
|
}, viewName);
|
|
};
|
|
|
|
views.agendaDay = function(element, options, viewName) {
|
|
return new Agenda(element, options, {
|
|
render: function(date, delta) {
|
|
if (delta) {
|
|
addDays(date, delta);
|
|
if (!options.weekends) {
|
|
skipWeekend(date, delta < 0 ? -1 : 1);
|
|
}
|
|
}
|
|
this.title = formatDate(date, this.option('titleFormat'), options);
|
|
this.start = this.visStart = cloneDate(date, true);
|
|
this.end = this.visEnd = addDays(cloneDate(this.start), 1);
|
|
this.renderAgenda(
|
|
1,
|
|
this.option('columnFormat')
|
|
);
|
|
}
|
|
}, viewName);
|
|
};
|
|
|
|
function Agenda(element, options, methods, viewName) {
|
|
|
|
var head, body, bodyContent, bodyTable, bg,
|
|
colCnt,
|
|
slotCnt=0, // spanning all the way across
|
|
axisWidth, colWidth, slotHeight,
|
|
viewWidth, viewHeight,
|
|
savedScrollTop,
|
|
cachedEvents=[],
|
|
daySegmentContainer,
|
|
slotSegmentContainer,
|
|
tm, firstDay,
|
|
nwe, // no weekends (int)
|
|
rtl, dis, dit, // day index sign / translate
|
|
minMinute, maxMinute,
|
|
colContentPositions = new HorizontalPositionCache(function(col) {
|
|
return bg.find('td:eq(' + col + ') div div');
|
|
}),
|
|
slotTopCache = {},
|
|
// ...
|
|
|
|
view = $.extend(this, viewMethods, methods, {
|
|
renderAgenda: renderAgenda,
|
|
renderEvents: renderEvents,
|
|
rerenderEvents: rerenderEvents,
|
|
clearEvents: clearEvents,
|
|
setHeight: setHeight,
|
|
setWidth: setWidth,
|
|
beforeHide: function() {
|
|
savedScrollTop = body.scrollTop();
|
|
},
|
|
afterShow: function() {
|
|
body.scrollTop(savedScrollTop);
|
|
},
|
|
defaultEventEnd: function(event) {
|
|
var start = cloneDate(event.start);
|
|
if (event.allDay) {
|
|
return start;
|
|
}
|
|
return addMinutes(start, options.defaultEventMinutes);
|
|
}
|
|
});
|
|
view.name = viewName;
|
|
view.init(element, options);
|
|
|
|
|
|
|
|
/* Time-slot rendering
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
|
|
disableTextSelection(element.addClass('fc-agenda'));
|
|
|
|
|
|
function renderAgenda(c, colFormat) {
|
|
|
|
colCnt = c;
|
|
|
|
// update option-derived variables
|
|
tm = options.theme ? 'ui' : 'fc';
|
|
nwe = options.weekends ? 0 : 1;
|
|
firstDay = options.firstDay;
|
|
if (rtl = options.isRTL) {
|
|
dis = -1;
|
|
dit = colCnt - 1;
|
|
}else{
|
|
dis = 1;
|
|
dit = 0;
|
|
}
|
|
minMinute = parseTime(options.minTime);
|
|
maxMinute = parseTime(options.maxTime);
|
|
|
|
var d0 = rtl ? addDays(cloneDate(view.visEnd), -1) : cloneDate(view.visStart),
|
|
d = cloneDate(d0),
|
|
today = clearTime(new Date());
|
|
|
|
if (!head) { // first time rendering, build from scratch
|
|
|
|
var i,
|
|
minutes,
|
|
slotNormal = options.slotMinutes % 15 == 0, //...
|
|
|
|
// head
|
|
s = "<div class='fc-agenda-head' style='position:relative;z-index:4'>" +
|
|
"<table style='width:100%'>" +
|
|
"<tr class='fc-first" + (options.allDaySlot ? '' : ' fc-last') + "'>" +
|
|
"<th class='fc-leftmost " +
|
|
tm + "-state-default'> </th>";
|
|
for (i=0; i<colCnt; i++) {
|
|
s += "<th class='fc-" +
|
|
dayIDs[d.getDay()] + ' ' + // needs to be first
|
|
tm + '-state-default' +
|
|
"'>" + formatDate(d, colFormat, options) + "</th>";
|
|
addDays(d, dis);
|
|
if (nwe) {
|
|
skipWeekend(d, dis);
|
|
}
|
|
}
|
|
s += "<th class='" + tm + "-state-default'> </th></tr>";
|
|
if (options.allDaySlot) {
|
|
s += "<tr class='fc-all-day'>" +
|
|
"<th class='fc-axis fc-leftmost " + tm + "-state-default'>" + options.allDayText + "</th>" +
|
|
"<td colspan='" + colCnt + "' class='" + tm + "-state-default'>" +
|
|
"<div class='fc-day-content'><div style='position:relative'> </div></div></td>" +
|
|
"<th class='" + tm + "-state-default'> </th>" +
|
|
"</tr><tr class='fc-divider fc-last'><th colspan='" + (colCnt+2) + "' class='" +
|
|
tm + "-state-default fc-leftmost'><div/></th></tr>";
|
|
}
|
|
s+= "</table></div>";
|
|
head = $(s).appendTo(element);
|
|
dayBind(head.find('td'));
|
|
|
|
// all-day event container
|
|
daySegmentContainer = $("<div style='position:absolute;z-index:8;top:0;left:0'/>").appendTo(head);
|
|
|
|
// body
|
|
d = zeroDate();
|
|
var maxd = addMinutes(cloneDate(d), maxMinute);
|
|
addMinutes(d, minMinute);
|
|
s = "<table>";
|
|
for (i=0; d < maxd; i++) {
|
|
minutes = d.getMinutes();
|
|
s += "<tr class='" +
|
|
(!i ? 'fc-first' : (!minutes ? '' : 'fc-minor')) +
|
|
"'><th class='fc-axis fc-leftmost " + tm + "-state-default'>" +
|
|
((!slotNormal || !minutes) ? formatDate(d, options.axisFormat) : ' ') +
|
|
"</th><td class='fc-slot" + i + ' ' +
|
|
tm + "-state-default'><div style='position:relative'> </div></td></tr>";
|
|
addMinutes(d, options.slotMinutes);
|
|
slotCnt++;
|
|
}
|
|
s += "</table>";
|
|
body = $("<div class='fc-agenda-body' style='position:relative;z-index:2;overflow:auto'/>")
|
|
.append(bodyContent = $("<div style='position:relative;overflow:hidden'>")
|
|
.append(bodyTable = $(s)))
|
|
.appendTo(element);
|
|
slotBind(body.find('td'));
|
|
|
|
// slot event container
|
|
slotSegmentContainer = $("<div style='position:absolute;z-index:8;top:0;left:0'/>").appendTo(bodyContent);
|
|
|
|
// background stripes
|
|
d = cloneDate(d0);
|
|
s = "<div class='fc-agenda-bg' style='position:absolute;z-index:1'>" +
|
|
"<table style='width:100%;height:100%'><tr class='fc-first'>";
|
|
for (i=0; i<colCnt; i++) {
|
|
s += "<td class='fc-" +
|
|
dayIDs[d.getDay()] + ' ' + // needs to be first
|
|
tm + '-state-default ' +
|
|
(!i ? 'fc-leftmost ' : '') +
|
|
(+d == +today ? tm + '-state-highlight fc-today' : 'fc-not-today') +
|
|
"'><div class='fc-day-content'><div> </div></div></td>";
|
|
addDays(d, dis);
|
|
if (nwe) {
|
|
skipWeekend(d, dis);
|
|
}
|
|
}
|
|
s += "</tr></table></div>";
|
|
bg = $(s).appendTo(element);
|
|
|
|
}else{ // skeleton already built, just modify it
|
|
|
|
clearEvents();
|
|
|
|
// redo column header text and class
|
|
head.find('tr:first th').slice(1, -1).each(function() {
|
|
$(this).text(formatDate(d, colFormat, options));
|
|
this.className = this.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]);
|
|
addDays(d, dis);
|
|
if (nwe) {
|
|
skipWeekend(d, dis);
|
|
}
|
|
});
|
|
|
|
// change classes of background stripes
|
|
d = cloneDate(d0);
|
|
bg.find('td').each(function() {
|
|
this.className = this.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]);
|
|
if (+d == +today) {
|
|
$(this)
|
|
.removeClass('fc-not-today')
|
|
.addClass('fc-today')
|
|
.addClass(tm + '-state-highlight');
|
|
}else{
|
|
$(this)
|
|
.addClass('fc-not-today')
|
|
.removeClass('fc-today')
|
|
.removeClass(tm + '-state-highlight');
|
|
}
|
|
addDays(d, dis);
|
|
if (nwe) {
|
|
skipWeekend(d, dis);
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
function resetScroll() {
|
|
var d0 = zeroDate(),
|
|
scrollDate = cloneDate(d0);
|
|
scrollDate.setHours(options.firstHour);
|
|
var top = timePosition(d0, scrollDate) + 1, // +1 for the border
|
|
scroll = function() {
|
|
body.scrollTop(top);
|
|
};
|
|
scroll();
|
|
setTimeout(scroll, 0); // overrides any previous scroll state made by the browser
|
|
}
|
|
|
|
|
|
function setHeight(height, dateChanged) {
|
|
viewHeight = height;
|
|
slotTopCache = {};
|
|
|
|
body.height(height - head.height());
|
|
|
|
slotHeight = body.find('tr:first div').height() + 1;
|
|
|
|
bg.css({
|
|
top: head.find('tr').height(),
|
|
height: height
|
|
});
|
|
|
|
if (dateChanged) {
|
|
resetScroll();
|
|
}
|
|
}
|
|
|
|
|
|
function setWidth(width) {
|
|
viewWidth = width;
|
|
colContentPositions.clear();
|
|
|
|
body.width(width);
|
|
bodyTable.width('');
|
|
|
|
var topTDs = head.find('tr:first th'),
|
|
stripeTDs = bg.find('td'),
|
|
clientWidth = body[0].clientWidth;
|
|
|
|
bodyTable.width(clientWidth);
|
|
|
|
// time-axis width
|
|
axisWidth = 0;
|
|
setOuterWidth(
|
|
head.find('tr:lt(2) th:first').add(body.find('tr:first th'))
|
|
.width('')
|
|
.each(function() {
|
|
axisWidth = Math.max(axisWidth, $(this).outerWidth());
|
|
}),
|
|
axisWidth
|
|
);
|
|
|
|
// column width
|
|
colWidth = Math.floor((clientWidth - axisWidth) / colCnt);
|
|
setOuterWidth(stripeTDs.slice(0, -1), colWidth);
|
|
setOuterWidth(topTDs.slice(1, -2), colWidth);
|
|
setOuterWidth(topTDs.slice(-2, -1), clientWidth - axisWidth - colWidth*(colCnt-1));
|
|
|
|
bg.css({
|
|
left: axisWidth,
|
|
width: clientWidth - axisWidth
|
|
});
|
|
}
|
|
|
|
|
|
|
|
/* Slot/Day clicking and binding
|
|
-----------------------------------------------------------------------*/
|
|
|
|
|
|
function dayBind(tds) {
|
|
tds.click(slotClick)
|
|
.mousedown(daySelectionMousedown);
|
|
}
|
|
|
|
|
|
function slotBind(tds) {
|
|
tds.click(slotClick)
|
|
.mousedown(slotSelectionMousedown);
|
|
}
|
|
|
|
|
|
function slotClick(ev) {
|
|
if (!view.option('selectable')) { // SelectionManager will worry about dayClick
|
|
var col = Math.min(colCnt-1, Math.floor((ev.pageX - bg.offset().left) / colWidth)),
|
|
date = addDays(cloneDate(view.visStart), col*dis+dit),
|
|
rowMatch = this.className.match(/fc-slot(\d+)/);
|
|
if (rowMatch) {
|
|
var mins = parseInt(rowMatch[1]) * options.slotMinutes,
|
|
hours = Math.floor(mins/60);
|
|
date.setHours(hours);
|
|
date.setMinutes(mins%60 + minMinute);
|
|
view.trigger('dayClick', this, date, false, ev);
|
|
}else{
|
|
view.trigger('dayClick', this, date, true, ev);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* Event Rendering
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
function renderEvents(events, modifiedEventId) {
|
|
view.reportEvents(cachedEvents = events);
|
|
var i, len=events.length,
|
|
dayEvents=[],
|
|
slotEvents=[];
|
|
for (i=0; i<len; i++) {
|
|
if (events[i].allDay) {
|
|
dayEvents.push(events[i]);
|
|
}else{
|
|
slotEvents.push(events[i]);
|
|
}
|
|
}
|
|
renderDaySegs(compileDaySegs(dayEvents), modifiedEventId);
|
|
renderSlotSegs(compileSlotSegs(slotEvents), modifiedEventId);
|
|
}
|
|
|
|
|
|
function rerenderEvents(modifiedEventId) {
|
|
clearEvents();
|
|
renderEvents(cachedEvents, modifiedEventId);
|
|
}
|
|
|
|
|
|
function clearEvents() {
|
|
view._clearEvents(); // only clears the hashes
|
|
daySegmentContainer.empty();
|
|
slotSegmentContainer.empty();
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function compileDaySegs(events) {
|
|
var levels = stackSegs(view.sliceSegs(events, $.map(events, exclEndDay), view.visStart, view.visEnd)),
|
|
i, levelCnt=levels.length, level,
|
|
j, seg,
|
|
segs=[];
|
|
for (i=0; i<levelCnt; i++) {
|
|
level = levels[i];
|
|
for (j=0; j<level.length; j++) {
|
|
seg = level[j];
|
|
seg.row = 0;
|
|
seg.level = i;
|
|
segs.push(seg);
|
|
}
|
|
}
|
|
return segs;
|
|
}
|
|
|
|
|
|
function compileSlotSegs(events) {
|
|
var d = addMinutes(cloneDate(view.visStart), minMinute),
|
|
visEventEnds = $.map(events, slotEventEnd),
|
|
i, col,
|
|
j, level,
|
|
k, seg,
|
|
segs=[];
|
|
for (i=0; i<colCnt; i++) {
|
|
col = stackSegs(view.sliceSegs(events, visEventEnds, d, addMinutes(cloneDate(d), maxMinute-minMinute)));
|
|
countForwardSegs(col);
|
|
for (j=0; j<col.length; j++) {
|
|
level = col[j];
|
|
for (k=0; k<level.length; k++) {
|
|
seg = level[k];
|
|
seg.col = i;
|
|
seg.level = j;
|
|
segs.push(seg);
|
|
}
|
|
}
|
|
addDays(d, 1, true);
|
|
}
|
|
return segs;
|
|
}
|
|
|
|
|
|
|
|
|
|
// renders 'all-day' events at the top
|
|
|
|
function renderDaySegs(segs, modifiedEventId) {
|
|
if (options.allDaySlot) {
|
|
_renderDaySegs(
|
|
segs,
|
|
1,
|
|
view,
|
|
axisWidth,
|
|
viewWidth,
|
|
function() {
|
|
return head.find('tr.fc-all-day');
|
|
},
|
|
function(dayOfWeek) {
|
|
return axisWidth + colContentPositions.left(dayOfWeekCol(dayOfWeek));
|
|
},
|
|
function(dayOfWeek) {
|
|
return axisWidth + colContentPositions.right(dayOfWeekCol(dayOfWeek));
|
|
},
|
|
daySegmentContainer,
|
|
daySegBind,
|
|
modifiedEventId
|
|
);
|
|
setHeight(viewHeight); // might have pushed the body down, so resize
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// renders events in the 'time slots' at the bottom
|
|
|
|
function renderSlotSegs(segs, modifiedEventId) {
|
|
|
|
var i, segCnt=segs.length, seg,
|
|
event,
|
|
className,
|
|
top, bottom,
|
|
colI, levelI, forward,
|
|
leftmost,
|
|
availWidth,
|
|
outerWidth,
|
|
left,
|
|
html='',
|
|
eventElements,
|
|
eventElement,
|
|
triggerRes,
|
|
vsideCache={},
|
|
hsideCache={},
|
|
key, val,
|
|
titleSpan,
|
|
height;
|
|
|
|
// calculate position/dimensions, create html
|
|
for (i=0; i<segCnt; i++) {
|
|
seg = segs[i];
|
|
event = seg.event;
|
|
className = 'fc-event fc-event-vert ';
|
|
if (seg.isStart) {
|
|
className += 'fc-corner-top ';
|
|
}
|
|
if (seg.isEnd) {
|
|
className += 'fc-corner-bottom ';
|
|
}
|
|
top = timePosition(seg.start, seg.start);
|
|
bottom = timePosition(seg.start, seg.end);
|
|
colI = seg.col;
|
|
levelI = seg.level;
|
|
forward = seg.forward || 0;
|
|
leftmost = axisWidth + colContentPositions.left(colI*dis + dit);
|
|
availWidth = axisWidth + colContentPositions.right(colI*dis + dit) - leftmost;
|
|
availWidth = Math.min(availWidth-6, availWidth*.95); // TODO: move this to CSS
|
|
if (levelI) {
|
|
// indented and thin
|
|
outerWidth = availWidth / (levelI + forward + 1);
|
|
}else{
|
|
if (forward) {
|
|
// moderately wide, aligned left still
|
|
outerWidth = ((availWidth / (forward + 1)) - (12/2)) * 2; // 12 is the predicted width of resizer =
|
|
}else{
|
|
// can be entire width, aligned left
|
|
outerWidth = availWidth;
|
|
}
|
|
}
|
|
left = leftmost + // leftmost possible
|
|
(availWidth / (levelI + forward + 1) * levelI) // indentation
|
|
* dis + (rtl ? availWidth - outerWidth : 0); // rtl
|
|
seg.top = top;
|
|
seg.left = left;
|
|
seg.outerWidth = outerWidth;
|
|
seg.outerHeight = bottom - top;
|
|
html += slotSegHtml(event, seg, className);
|
|
}
|
|
slotSegmentContainer[0].innerHTML = html; // faster than html()
|
|
eventElements = slotSegmentContainer.children();
|
|
|
|
// retrieve elements, run through eventRender callback, bind event handlers
|
|
for (i=0; i<segCnt; i++) {
|
|
seg = segs[i];
|
|
event = seg.event;
|
|
eventElement = $(eventElements[i]); // faster than eq()
|
|
triggerRes = view.trigger('eventRender', event, event, eventElement);
|
|
if (triggerRes === false) {
|
|
eventElement.remove();
|
|
}else{
|
|
if (triggerRes && triggerRes !== true) {
|
|
eventElement.remove();
|
|
eventElement = $(triggerRes)
|
|
.css({
|
|
position: 'absolute',
|
|
top: seg.top,
|
|
left: seg.left
|
|
})
|
|
.appendTo(slotSegmentContainer);
|
|
}
|
|
seg.element = eventElement;
|
|
if (event._id === modifiedEventId) {
|
|
slotSegBind(event, eventElement, seg);
|
|
}else{
|
|
eventElement[0]._fci = i; // for lazySegBind
|
|
}
|
|
view.reportEventElement(event, eventElement);
|
|
}
|
|
}
|
|
|
|
lazySegBind(slotSegmentContainer, segs, slotSegBind);
|
|
|
|
// record event sides and title positions
|
|
for (i=0; i<segCnt; i++) {
|
|
seg = segs[i];
|
|
if (eventElement = seg.element) {
|
|
val = vsideCache[key = seg.key = cssKey(eventElement[0])];
|
|
seg.vsides = val === undefined ? (vsideCache[key] = vsides(eventElement[0], true)) : val;
|
|
val = hsideCache[key];
|
|
seg.hsides = val === undefined ? (hsideCache[key] = hsides(eventElement[0], true)) : val;
|
|
titleSpan = eventElement.find('span.fc-event-title');
|
|
if (titleSpan.length) {
|
|
seg.titleTop = titleSpan[0].offsetTop;
|
|
}
|
|
}
|
|
}
|
|
|
|
// set all positions/dimensions at once
|
|
for (i=0; i<segCnt; i++) {
|
|
seg = segs[i];
|
|
if (eventElement = seg.element) {
|
|
eventElement[0].style.width = seg.outerWidth - seg.hsides + 'px';
|
|
eventElement[0].style.height = (height = seg.outerHeight - seg.vsides) + 'px';
|
|
event = seg.event;
|
|
if (seg.titleTop !== undefined && height - seg.titleTop < 10) {
|
|
// not enough room for title, put it in the time header
|
|
eventElement.find('span.fc-event-time')
|
|
.text(formatDate(event.start, view.option('timeFormat')) + ' - ' + event.title);
|
|
eventElement.find('span.fc-event-title')
|
|
.remove();
|
|
}
|
|
view.trigger('eventAfterRender', event, event, eventElement);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
function slotSegHtml(event, seg, className) {
|
|
return "<div class='" + className + event.className.join(' ') + "' style='position:absolute;z-index:8;top:" + seg.top + "px;left:" + seg.left + "px'>" +
|
|
"<a" + (event.url ? " href='" + htmlEscape(event.url) + "'" : '') + ">" +
|
|
"<span class='fc-event-bg'></span>" +
|
|
"<span class='fc-event-time'>" + htmlEscape(formatDates(event.start, event.end, view.option('timeFormat'))) + "</span>" +
|
|
"<span class='fc-event-title'>" + htmlEscape(event.title) + "</span>" +
|
|
"</a>" +
|
|
((event.editable || event.editable === undefined && options.editable) && !options.disableResizing && $.fn.resizable ?
|
|
"<div class='ui-resizable-handle ui-resizable-s'>=</div>"
|
|
: '') +
|
|
"</div>";
|
|
}
|
|
|
|
|
|
|
|
function daySegBind(event, eventElement, seg) {
|
|
view.eventElementHandlers(event, eventElement);
|
|
if (event.editable || event.editable === undefined && options.editable) {
|
|
draggableDayEvent(event, eventElement, seg.isStart);
|
|
if (seg.isEnd) {
|
|
view.resizableDayEvent(event, eventElement, colWidth);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function slotSegBind(event, eventElement, seg) {
|
|
view.eventElementHandlers(event, eventElement);
|
|
if (event.editable || event.editable === undefined && options.editable) {
|
|
var timeElement = eventElement.find('span.fc-event-time');
|
|
draggableSlotEvent(event, eventElement, timeElement);
|
|
if (seg.isEnd) {
|
|
resizableSlotEvent(event, eventElement, timeElement);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Event Dragging
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
|
|
|
|
// when event starts out FULL-DAY
|
|
|
|
function draggableDayEvent(event, eventElement, isStart) {
|
|
if (!options.disableDragging && eventElement.draggable) {
|
|
var origWidth;
|
|
var allDay=true;
|
|
var dayDelta;
|
|
eventElement.draggable({
|
|
zIndex: 9,
|
|
opacity: view.option('dragOpacity', 'month'), // use whatever the month view was using
|
|
revertDuration: options.dragRevertDuration,
|
|
start: function(ev, ui) {
|
|
view.trigger('eventDragStart', eventElement, event, ev, ui);
|
|
view.hideEvents(event, eventElement);
|
|
origWidth = eventElement.width();
|
|
hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
|
|
eventElement.draggable('option', 'revert', !cell || !rowDelta && !colDelta);
|
|
clearOverlay();
|
|
if (cell) {
|
|
dayDelta = colDelta * dis;
|
|
if (!cell.row) {
|
|
// on full-days
|
|
renderDayOverlay(
|
|
addDays(cloneDate(event.start), dayDelta),
|
|
addDays(exclEndDay(event), dayDelta)
|
|
);
|
|
resetElement();
|
|
}else{
|
|
// mouse is over bottom slots
|
|
if (isStart && allDay) {
|
|
// convert event to temporary slot-event
|
|
setOuterHeight(
|
|
eventElement.width(colWidth - 10), // don't use entire width
|
|
slotHeight * Math.round(
|
|
(event.end ? ((event.end - event.start) / MINUTE_MS) : options.defaultEventMinutes)
|
|
/ options.slotMinutes
|
|
)
|
|
);
|
|
eventElement.draggable('option', 'grid', [colWidth, 1]);
|
|
allDay = false;
|
|
}
|
|
}
|
|
}
|
|
}, ev, 'drag');
|
|
},
|
|
stop: function(ev, ui) {
|
|
var cell = hoverListener.stop();
|
|
clearOverlay();
|
|
view.trigger('eventDragStop', eventElement, event, ev, ui);
|
|
if (cell && (!allDay || dayDelta)) {
|
|
// changed!
|
|
eventElement.find('a').removeAttr('href'); // prevents safari from visiting the link
|
|
var minuteDelta = 0;
|
|
if (!allDay) {
|
|
minuteDelta = Math.round((eventElement.offset().top - bodyContent.offset().top) / slotHeight)
|
|
* options.slotMinutes
|
|
+ minMinute
|
|
- (event.start.getHours() * 60 + event.start.getMinutes());
|
|
}
|
|
view.eventDrop(this, event, dayDelta, minuteDelta, allDay, ev, ui);
|
|
}else{
|
|
// hasn't moved or is out of bounds (draggable has already reverted)
|
|
resetElement();
|
|
if ($.browser.msie) {
|
|
eventElement.css('filter', ''); // clear IE opacity side-effects
|
|
}
|
|
view.showEvents(event, eventElement);
|
|
}
|
|
}
|
|
});
|
|
function resetElement() {
|
|
if (!allDay) {
|
|
eventElement
|
|
.width(origWidth)
|
|
.height('')
|
|
.draggable('option', 'grid', null);
|
|
allDay = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// when event starts out IN TIMESLOTS
|
|
|
|
function draggableSlotEvent(event, eventElement, timeElement) {
|
|
if (!options.disableDragging && eventElement.draggable) {
|
|
var origPosition;
|
|
var allDay=false;
|
|
var dayDelta;
|
|
var minuteDelta;
|
|
var prevMinuteDelta;
|
|
eventElement.draggable({
|
|
zIndex: 9,
|
|
scroll: false,
|
|
grid: [colWidth, slotHeight],
|
|
axis: colCnt==1 ? 'y' : false,
|
|
opacity: view.option('dragOpacity'),
|
|
revertDuration: options.dragRevertDuration,
|
|
start: function(ev, ui) {
|
|
view.trigger('eventDragStart', eventElement, event, ev, ui);
|
|
view.hideEvents(event, eventElement);
|
|
if ($.browser.msie) {
|
|
eventElement.find('span.fc-event-bg').hide(); // nested opacities mess up in IE, just hide
|
|
}
|
|
origPosition = eventElement.position();
|
|
minuteDelta = prevMinuteDelta = 0;
|
|
hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
|
|
eventElement.draggable('option', 'revert', !cell);
|
|
clearOverlay();
|
|
if (cell) {
|
|
dayDelta = colDelta * dis;
|
|
if (options.allDaySlot && !cell.row) {
|
|
// over full days
|
|
if (!allDay) {
|
|
// convert to temporary all-day event
|
|
allDay = true;
|
|
timeElement.hide();
|
|
eventElement.draggable('option', 'grid', null);
|
|
}
|
|
renderDayOverlay(
|
|
addDays(cloneDate(event.start), dayDelta),
|
|
addDays(exclEndDay(event), dayDelta)
|
|
);
|
|
}else{
|
|
// on slots
|
|
resetElement();
|
|
}
|
|
}
|
|
}, ev, 'drag');
|
|
},
|
|
drag: function(ev, ui) {
|
|
minuteDelta = Math.round((ui.position.top - origPosition.top) / slotHeight) * options.slotMinutes;
|
|
if (minuteDelta != prevMinuteDelta) {
|
|
if (!allDay) {
|
|
updateTimeText(minuteDelta);
|
|
}
|
|
prevMinuteDelta = minuteDelta;
|
|
}
|
|
},
|
|
stop: function(ev, ui) {
|
|
var cell = hoverListener.stop();
|
|
clearOverlay();
|
|
view.trigger('eventDragStop', eventElement, event, ev, ui);
|
|
if (cell && (dayDelta || minuteDelta || allDay)) {
|
|
// changed!
|
|
view.eventDrop(this, event, dayDelta, allDay ? 0 : minuteDelta, allDay, ev, ui);
|
|
}else{
|
|
// either no change or out-of-bounds (draggable has already reverted)
|
|
resetElement();
|
|
eventElement.css(origPosition); // sometimes fast drags make event revert to wrong position
|
|
updateTimeText(0);
|
|
if ($.browser.msie) {
|
|
eventElement
|
|
.css('filter', '') // clear IE opacity side-effects
|
|
.find('span.fc-event-bg')
|
|
.css('display', ''); // .show() made display=inline
|
|
}
|
|
view.showEvents(event, eventElement);
|
|
}
|
|
}
|
|
});
|
|
function updateTimeText(minuteDelta) {
|
|
var newStart = addMinutes(cloneDate(event.start), minuteDelta);
|
|
var newEnd;
|
|
if (event.end) {
|
|
newEnd = addMinutes(cloneDate(event.end), minuteDelta);
|
|
}
|
|
timeElement.text(formatDates(newStart, newEnd, view.option('timeFormat')));
|
|
}
|
|
function resetElement() {
|
|
// convert back to original slot-event
|
|
if (allDay) {
|
|
timeElement.css('display', ''); // show() was causing display=inline
|
|
eventElement.draggable('option', 'grid', [colWidth, slotHeight]);
|
|
allDay = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Event Resizing
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
// for TIMESLOT events
|
|
|
|
function resizableSlotEvent(event, eventElement, timeElement) {
|
|
if (!options.disableResizing && eventElement.resizable) {
|
|
var slotDelta, prevSlotDelta;
|
|
eventElement.resizable({
|
|
handles: {
|
|
s: 'div.ui-resizable-s'
|
|
},
|
|
grid: slotHeight,
|
|
start: function(ev, ui) {
|
|
slotDelta = prevSlotDelta = 0;
|
|
view.hideEvents(event, eventElement);
|
|
if ($.browser.msie && $.browser.version == '6.0') {
|
|
eventElement.css('overflow', 'hidden');
|
|
}
|
|
eventElement.css('z-index', 9);
|
|
view.trigger('eventResizeStart', this, event, ev, ui);
|
|
},
|
|
resize: function(ev, ui) {
|
|
// don't rely on ui.size.height, doesn't take grid into account
|
|
slotDelta = Math.round((Math.max(slotHeight, eventElement.height()) - ui.originalSize.height) / slotHeight);
|
|
if (slotDelta != prevSlotDelta) {
|
|
timeElement.text(
|
|
formatDates(
|
|
event.start,
|
|
(!slotDelta && !event.end) ? null : // no change, so don't display time range
|
|
addMinutes(view.eventEnd(event), options.slotMinutes*slotDelta),
|
|
view.option('timeFormat')
|
|
)
|
|
);
|
|
prevSlotDelta = slotDelta;
|
|
}
|
|
},
|
|
stop: function(ev, ui) {
|
|
view.trigger('eventResizeStop', this, event, ev, ui);
|
|
if (slotDelta) {
|
|
view.eventResize(this, event, 0, options.slotMinutes*slotDelta, ev, ui);
|
|
}else{
|
|
eventElement.css('z-index', 8);
|
|
view.showEvents(event, eventElement);
|
|
// BUG: if event was really short, need to put title back in span
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Coordinate Utilities
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
var coordinateGrid = new CoordinateGrid(function(rows, cols) {
|
|
var e, n, p;
|
|
bg.find('td').each(function(i, _e) {
|
|
e = $(_e);
|
|
n = e.offset().left;
|
|
if (i) {
|
|
p[1] = n;
|
|
}
|
|
p = [n];
|
|
cols[i] = p;
|
|
});
|
|
p[1] = n + e.outerWidth();
|
|
if (options.allDaySlot) {
|
|
e = head.find('td');
|
|
n = e.offset().top;
|
|
rows[0] = [n, n+e.outerHeight()];
|
|
}
|
|
var bodyContentTop = bodyContent.offset().top;
|
|
var bodyTop = body.offset().top;
|
|
var bodyBottom = bodyTop + body.outerHeight();
|
|
function constrain(n) {
|
|
return Math.max(bodyTop, Math.min(bodyBottom, n));
|
|
}
|
|
for (var i=0; i<slotCnt; i++) {
|
|
rows.push([
|
|
constrain(bodyContentTop + slotHeight*i),
|
|
constrain(bodyContentTop + slotHeight*(i+1))
|
|
]);
|
|
}
|
|
});
|
|
|
|
var hoverListener = new HoverListener(coordinateGrid);
|
|
|
|
// 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 bodyContent.height();
|
|
}
|
|
var slotMinutes = options.slotMinutes,
|
|
minutes = time.getHours()*60 + time.getMinutes() - minMinute,
|
|
slotI = Math.floor(minutes / slotMinutes),
|
|
slotTop = slotTopCache[slotI];
|
|
if (slotTop === undefined) {
|
|
slotTop = slotTopCache[slotI] = body.find('tr:eq(' + slotI + ') td div')[0].offsetTop;
|
|
}
|
|
return Math.max(0, Math.round(
|
|
slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes)
|
|
));
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Selecting
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
var selected = false;
|
|
var daySelectionMousedown = selection_dayMousedown(
|
|
view, hoverListener, cellDate, cellIsAllDay, renderDayOverlay, clearOverlay, reportSelection, unselect
|
|
);
|
|
|
|
function slotSelectionMousedown(ev) {
|
|
if (view.option('selectable')) {
|
|
unselect(ev);
|
|
var _mousedownElement = this;
|
|
var dates;
|
|
hoverListener.start(function(cell, origCell) {
|
|
clearSelection();
|
|
if (cell && cell.col == origCell.col && !cellIsAllDay(cell)) {
|
|
var d1 = cellDate(origCell);
|
|
var d2 = cellDate(cell);
|
|
dates = [
|
|
d1,
|
|
addMinutes(cloneDate(d1), options.slotMinutes),
|
|
d2,
|
|
addMinutes(cloneDate(d2), options.slotMinutes)
|
|
].sort(cmp);
|
|
renderSlotSelection(dates[0], dates[3]);
|
|
}else{
|
|
dates = null;
|
|
}
|
|
}, ev);
|
|
$(document).one('mouseup', function(ev) {
|
|
hoverListener.stop();
|
|
if (dates) {
|
|
if (+dates[0] == +dates[1]) {
|
|
view.trigger('dayClick', _mousedownElement, dates[0], false, ev);
|
|
// BUG: _mousedownElement will sometimes be the overlay
|
|
}
|
|
reportSelection(dates[0], dates[3], false, ev);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
view.select = function(startDate, endDate, allDay) {
|
|
coordinateGrid.build();
|
|
unselect();
|
|
if (allDay) {
|
|
if (options.allDaySlot) {
|
|
if (!endDate) {
|
|
endDate = cloneDate(startDate);
|
|
}
|
|
renderDayOverlay(startDate, addDays(cloneDate(endDate), 1));
|
|
}
|
|
}else{
|
|
if (!endDate) {
|
|
endDate = addMinutes(cloneDate(startDate), options.slotMinutes);
|
|
}
|
|
renderSlotSelection(startDate, endDate);
|
|
}
|
|
reportSelection(startDate, endDate, allDay);
|
|
};
|
|
|
|
function reportSelection(startDate, endDate, allDay, ev) {
|
|
selected = true;
|
|
view.trigger('select', view, startDate, endDate, allDay, ev);
|
|
}
|
|
|
|
function unselect(ev) {
|
|
if (selected) {
|
|
clearSelection();
|
|
selected = false;
|
|
view.trigger('unselect', view, ev);
|
|
}
|
|
}
|
|
view.unselect = unselect;
|
|
|
|
selection_unselectAuto(view, unselect);
|
|
|
|
|
|
|
|
|
|
/* Selecting drawing utils
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
var selectionHelper;
|
|
|
|
function renderSlotSelection(startDate, endDate) {
|
|
var helperOption = view.option('selectHelper');
|
|
if (helperOption) {
|
|
var col = dayDiff(startDate, view.visStart) * dis + dit;
|
|
if (col >= 0 && col < colCnt) { // only works when times are on same day
|
|
var rect = coordinateGrid.rect(0, col, 0, col, bodyContent); // 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';
|
|
rect.zIndex = 8;
|
|
selectionHelper = $(helperRes)
|
|
.css(rect)
|
|
.appendTo(bodyContent);
|
|
}
|
|
}else{
|
|
selectionHelper = $(slotSegHtml(
|
|
{
|
|
title: '',
|
|
start: startDate,
|
|
end: endDate,
|
|
className: [],
|
|
editable: false
|
|
},
|
|
rect,
|
|
'fc-event fc-event-vert fc-corner-top fc-corner-bottom '
|
|
));
|
|
if ($.browser.msie) {
|
|
selectionHelper.find('span.fc-event-bg').hide(); // nested opacities mess up in IE, just hide
|
|
}
|
|
selectionHelper.css('opacity', view.option('dragOpacity'));
|
|
}
|
|
if (selectionHelper) {
|
|
slotBind(selectionHelper);
|
|
bodyContent.append(selectionHelper);
|
|
setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended
|
|
setOuterHeight(selectionHelper, rect.height, true);
|
|
}
|
|
}
|
|
}
|
|
}else{
|
|
renderSlotOverlay(startDate, endDate);
|
|
}
|
|
}
|
|
|
|
function clearSelection() {
|
|
clearOverlay();
|
|
if (selectionHelper) {
|
|
selectionHelper.remove();
|
|
selectionHelper = null;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Semi-transparent Overlay Helpers
|
|
-----------------------------------------------------*/
|
|
|
|
function renderDayOverlay(startDate, endDate) {
|
|
var startCol, endCol;
|
|
if (rtl) {
|
|
startCol = dayDiff(endDate, view.visStart)*dis+dit+1;
|
|
endCol = dayDiff(startDate, view.visStart)*dis+dit+1;
|
|
}else{
|
|
startCol = dayDiff(startDate, view.visStart);
|
|
endCol = dayDiff(endDate, view.visStart);
|
|
}
|
|
startCol = Math.max(0, startCol);
|
|
endCol = Math.min(colCnt, endCol);
|
|
if (startCol < endCol) {
|
|
dayBind(
|
|
_renderDayOverlay(0, startCol, 0, endCol-1)
|
|
);
|
|
}
|
|
}
|
|
|
|
function _renderDayOverlay(col0, row0, col1, row1) {
|
|
var rect = coordinateGrid.rect(col0, row0, col1, row1, head);
|
|
return view.renderOverlay(rect, head);
|
|
}
|
|
|
|
function renderSlotOverlay(overlayStart, overlayEnd) {
|
|
var dayStart = cloneDate(view.visStart);
|
|
var dayEnd = addDays(cloneDate(dayStart), 1);
|
|
for (var i=0; i<colCnt; i++) {
|
|
var stretchStart = new Date(Math.max(dayStart, overlayStart));
|
|
var stretchEnd = new Date(Math.min(dayEnd, overlayEnd));
|
|
if (stretchStart < stretchEnd) {
|
|
var col = i*dis+dit;
|
|
var rect = coordinateGrid.rect(0, col, 0, col, bodyContent); // only use it for horizontal coords
|
|
var top = timePosition(dayStart, stretchStart);
|
|
var bottom = timePosition(dayStart, stretchEnd);
|
|
rect.top = top;
|
|
rect.height = bottom - top;
|
|
slotBind(
|
|
view.renderOverlay(rect, bodyContent)
|
|
);
|
|
}
|
|
addDays(dayStart, 1);
|
|
addDays(dayEnd, 1);
|
|
}
|
|
}
|
|
|
|
function clearOverlay() {
|
|
view.clearOverlays();
|
|
}
|
|
|
|
|
|
|
|
|
|
/* External dragging
|
|
-----------------------------------------------------*/
|
|
|
|
view.dragStart = function(_dragElement, ev, ui) {
|
|
hoverListener.start(function(cell) {
|
|
clearOverlay();
|
|
if (cell) {
|
|
if (cellIsAllDay(cell)) {
|
|
_renderDayOverlay(cell.row, cell.col, cell.row, cell.col);
|
|
}else{
|
|
var d1 = cellDate(cell);
|
|
var d2 = addMinutes(cloneDate(d1), options.defaultEventMinutes);
|
|
renderSlotOverlay(d1, d2);
|
|
}
|
|
}
|
|
}, ev);
|
|
};
|
|
|
|
view.dragStop = function(_dragElement, ev, ui) {
|
|
var cell = hoverListener.stop();
|
|
clearOverlay();
|
|
if (cell) {
|
|
view.trigger('drop', _dragElement, cellDate(cell), cellIsAllDay(cell), ev, ui);
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
/* Date Utilities
|
|
----------------------------------------------------*/
|
|
|
|
function slotEventEnd(event) {
|
|
if (event.end) {
|
|
return cloneDate(event.end);
|
|
}else{
|
|
return addMinutes(cloneDate(event.start), options.defaultEventMinutes);
|
|
}
|
|
}
|
|
|
|
function dayOfWeekCol(dayOfWeek) {
|
|
return ((dayOfWeek - Math.max(firstDay,nwe)+colCnt) % colCnt)*dis+dit;
|
|
}
|
|
|
|
function cellDate(cell) {
|
|
var d = addDays(cloneDate(view.visStart), cell.col*dis+dit);
|
|
var slotIndex = cell.row;
|
|
if (options.allDaySlot) {
|
|
slotIndex--;
|
|
}
|
|
if (slotIndex >= 0) {
|
|
addMinutes(d, minMinute + slotIndex*options.slotMinutes);
|
|
}
|
|
return d;
|
|
}
|
|
|
|
function cellIsAllDay(cell) {
|
|
return options.allDaySlot && !cell.row;
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
// count the number of colliding, higher-level segments (for event squishing)
|
|
|
|
function countForwardSegs(levels) {
|
|
var i, j, k, level, segForward, segBack;
|
|
for (i=levels.length-1; i>0; i--) {
|
|
level = levels[i];
|
|
for (j=0; j<level.length; j++) {
|
|
segForward = level[j];
|
|
for (k=0; k<levels[i-1].length; k++) {
|
|
segBack = levels[i-1][k];
|
|
if (segsCollide(segForward, segBack)) {
|
|
segBack.forward = Math.max(segBack.forward||0, (segForward.forward||0)+1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|