596 lines
17 KiB
JavaScript
596 lines
17 KiB
JavaScript
|
|
function AgendaEventRenderer() {
|
|
var t = this;
|
|
|
|
|
|
// exports
|
|
t.renderEvents = renderEvents;
|
|
t.rerenderEvents = rerenderEvents;
|
|
t.clearEvents = clearEvents;
|
|
t.slotSegHtml = slotSegHtml;
|
|
t.bindDaySeg = bindDaySeg;
|
|
|
|
|
|
// imports
|
|
DayEventRenderer.call(t);
|
|
var opt = t.opt;
|
|
var trigger = t.trigger;
|
|
var eventEnd = t.eventEnd;
|
|
var reportEvents = t.reportEvents;
|
|
var clearEventData = t.clearEventData;
|
|
var eventElementHandlers = t.eventElementHandlers;
|
|
var setHeight = t.setHeight;
|
|
var getDaySegmentContainer = t.getDaySegmentContainer;
|
|
var getSlotSegmentContainer = t.getSlotSegmentContainer;
|
|
var getHoverListener = t.getHoverListener;
|
|
var getMaxMinute = t.getMaxMinute;
|
|
var getMinMinute = t.getMinMinute;
|
|
var timePosition = t.timePosition;
|
|
var colContentLeft = t.colContentLeft;
|
|
var colContentRight = t.colContentRight;
|
|
var renderDaySegs = t.renderDaySegs;
|
|
var resizableDayEvent = t.resizableDayEvent; // TODO: streamline binding architecture
|
|
var getColCnt = t.getColCnt;
|
|
var getColWidth = t.getColWidth;
|
|
var getSlotHeight = t.getSlotHeight;
|
|
var getBodyContent = t.getBodyContent;
|
|
var reportEventElement = t.reportEventElement;
|
|
var showEvents = t.showEvents;
|
|
var hideEvents = t.hideEvents;
|
|
var eventDrop = t.eventDrop;
|
|
var eventResize = t.eventResize;
|
|
var renderDayOverlay = t.renderDayOverlay;
|
|
var clearOverlays = t.clearOverlays;
|
|
var calendar = t.calendar;
|
|
var formatDate = calendar.formatDate;
|
|
var formatDates = calendar.formatDates;
|
|
|
|
|
|
|
|
/* Rendering
|
|
----------------------------------------------------------------------------*/
|
|
|
|
|
|
var cachedEvents = [];
|
|
|
|
|
|
function renderEvents(events, modifiedEventId) {
|
|
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]);
|
|
}
|
|
}
|
|
if (opt('allDaySlot')) {
|
|
renderDaySegs(compileDaySegs(dayEvents), modifiedEventId);
|
|
setHeight(); // no params means set to viewHeight
|
|
}
|
|
renderSlotSegs(compileSlotSegs(slotEvents), modifiedEventId);
|
|
}
|
|
|
|
|
|
function rerenderEvents(modifiedEventId) {
|
|
clearEvents();
|
|
renderEvents(cachedEvents, modifiedEventId);
|
|
}
|
|
|
|
|
|
function clearEvents() {
|
|
clearEventData();
|
|
getDaySegmentContainer().empty();
|
|
getSlotSegmentContainer().empty();
|
|
}
|
|
|
|
|
|
function compileDaySegs(events) {
|
|
var levels = stackSegs(sliceSegs(events, $.map(events, exclEndDay), t.visStart, t.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 colCnt = getColCnt(),
|
|
minMinute = getMinMinute(),
|
|
maxMinute = getMaxMinute(),
|
|
d = addMinutes(cloneDate(t.visStart), minMinute),
|
|
visEventEnds = $.map(events, slotEventEnd),
|
|
i, col,
|
|
j, level,
|
|
k, seg,
|
|
segs=[];
|
|
for (i=0; i<colCnt; i++) {
|
|
col = stackSegs(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;
|
|
}
|
|
|
|
|
|
function slotEventEnd(event) {
|
|
if (event.end) {
|
|
return cloneDate(event.end);
|
|
}else{
|
|
return addMinutes(cloneDate(event.start), opt('defaultEventMinutes'));
|
|
}
|
|
}
|
|
|
|
|
|
// 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,
|
|
slotSegmentContainer = getSlotSegmentContainer(),
|
|
rtl, dis, dit,
|
|
colCnt = getColCnt();
|
|
|
|
if (rtl = opt('isRTL')) {
|
|
dis = -1;
|
|
dit = colCnt - 1;
|
|
}else{
|
|
dis = 1;
|
|
dit = 0;
|
|
}
|
|
|
|
// 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 = colContentLeft(colI*dis + dit);
|
|
availWidth = colContentRight(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 = 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) {
|
|
bindSlotSeg(event, eventElement, seg);
|
|
}else{
|
|
eventElement[0]._fci = i; // for lazySegBind
|
|
}
|
|
reportEventElement(event, eventElement);
|
|
}
|
|
}
|
|
|
|
lazySegBind(slotSegmentContainer, segs, bindSlotSeg);
|
|
|
|
// 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, opt('timeFormat')) + ' - ' + event.title);
|
|
eventElement.find('span.fc-event-title')
|
|
.remove();
|
|
}
|
|
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, opt('timeFormat'))) + "</span>" +
|
|
"<span class='fc-event-title'>" + htmlEscape(event.title) + "</span>" +
|
|
"</a>" +
|
|
((event.editable || event.editable === undefined && opt('editable')) && !opt('disableResizing') && $.fn.resizable ?
|
|
"<div class='ui-resizable-handle ui-resizable-s'>=</div>"
|
|
: '') +
|
|
"</div>";
|
|
}
|
|
|
|
|
|
function bindDaySeg(event, eventElement, seg) {
|
|
eventElementHandlers(event, eventElement);
|
|
if (event.editable || event.editable === undefined && opt('editable')) {
|
|
draggableDayEvent(event, eventElement, seg.isStart);
|
|
if (seg.isEnd) {
|
|
resizableDayEvent(event, eventElement, getColWidth());
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function bindSlotSeg(event, eventElement, seg) {
|
|
eventElementHandlers(event, eventElement);
|
|
if (event.editable || event.editable === undefined && opt('editable')) {
|
|
var timeElement = eventElement.find('span.fc-event-time');
|
|
draggableSlotEvent(event, eventElement, timeElement);
|
|
if (seg.isEnd) {
|
|
resizableSlotEvent(event, eventElement, timeElement);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* Dragging
|
|
-----------------------------------------------------------------------------------*/
|
|
|
|
|
|
// when event starts out FULL-DAY
|
|
|
|
function draggableDayEvent(event, eventElement, isStart) {
|
|
if (!opt('disableDragging') && eventElement.draggable) {
|
|
var origWidth;
|
|
var allDay=true;
|
|
var dayDelta;
|
|
var dis = opt('isRTL') ? -1 : 1;
|
|
var hoverListener = getHoverListener();
|
|
var colWidth = getColWidth();
|
|
var slotHeight = getSlotHeight();
|
|
var minMinute = getMinMinute();
|
|
eventElement.draggable({
|
|
zIndex: 9,
|
|
opacity: opt('dragOpacity', 'month'), // use whatever the month view was using
|
|
revertDuration: opt('dragRevertDuration'),
|
|
start: function(ev, ui) {
|
|
trigger('eventDragStart', eventElement, event, ev, ui);
|
|
hideEvents(event, eventElement);
|
|
origWidth = eventElement.width();
|
|
hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
|
|
eventElement.draggable('option', 'revert', !cell || !rowDelta && !colDelta);
|
|
clearOverlays();
|
|
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) : opt('defaultEventMinutes'))
|
|
/ opt('slotMinutes')
|
|
)
|
|
);
|
|
eventElement.draggable('option', 'grid', [colWidth, 1]);
|
|
allDay = false;
|
|
}
|
|
}
|
|
}
|
|
}, ev, 'drag');
|
|
},
|
|
stop: function(ev, ui) {
|
|
var cell = hoverListener.stop();
|
|
clearOverlays();
|
|
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 - getBodyContent().offset().top) / slotHeight)
|
|
* opt('slotMinutes')
|
|
+ minMinute
|
|
- (event.start.getHours() * 60 + event.start.getMinutes());
|
|
}
|
|
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
|
|
}
|
|
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 (!opt('disableDragging') && eventElement.draggable) {
|
|
var origPosition;
|
|
var allDay=false;
|
|
var dayDelta;
|
|
var minuteDelta;
|
|
var prevMinuteDelta;
|
|
var dis = opt('isRTL') ? -1 : 1;
|
|
var hoverListener = getHoverListener();
|
|
var colCnt = getColCnt();
|
|
var colWidth = getColWidth();
|
|
var slotHeight = getSlotHeight();
|
|
eventElement.draggable({
|
|
zIndex: 9,
|
|
scroll: false,
|
|
grid: [colWidth, slotHeight],
|
|
axis: colCnt==1 ? 'y' : false,
|
|
opacity: opt('dragOpacity'),
|
|
revertDuration: opt('dragRevertDuration'),
|
|
start: function(ev, ui) {
|
|
trigger('eventDragStart', eventElement, event, ev, ui);
|
|
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);
|
|
clearOverlays();
|
|
if (cell) {
|
|
dayDelta = colDelta * dis;
|
|
if (opt('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) * opt('slotMinutes');
|
|
if (minuteDelta != prevMinuteDelta) {
|
|
if (!allDay) {
|
|
updateTimeText(minuteDelta);
|
|
}
|
|
prevMinuteDelta = minuteDelta;
|
|
}
|
|
},
|
|
stop: function(ev, ui) {
|
|
var cell = hoverListener.stop();
|
|
clearOverlays();
|
|
trigger('eventDragStop', eventElement, event, ev, ui);
|
|
if (cell && (dayDelta || minuteDelta || allDay)) {
|
|
// changed!
|
|
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
|
|
}
|
|
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, opt('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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* Resizing
|
|
--------------------------------------------------------------------------------------*/
|
|
|
|
|
|
function resizableSlotEvent(event, eventElement, timeElement) {
|
|
if (!opt('disableResizing') && eventElement.resizable) {
|
|
var slotDelta, prevSlotDelta;
|
|
var slotHeight = getSlotHeight();
|
|
eventElement.resizable({
|
|
handles: {
|
|
s: 'div.ui-resizable-s'
|
|
},
|
|
grid: slotHeight,
|
|
start: function(ev, ui) {
|
|
slotDelta = prevSlotDelta = 0;
|
|
hideEvents(event, eventElement);
|
|
if ($.browser.msie && $.browser.version == '6.0') {
|
|
eventElement.css('overflow', 'hidden');
|
|
}
|
|
eventElement.css('z-index', 9);
|
|
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(eventEnd(event), opt('slotMinutes')*slotDelta),
|
|
opt('timeFormat')
|
|
)
|
|
);
|
|
prevSlotDelta = slotDelta;
|
|
}
|
|
},
|
|
stop: function(ev, ui) {
|
|
trigger('eventResizeStop', this, event, ev, ui);
|
|
if (slotDelta) {
|
|
eventResize(this, event, 0, opt('slotMinutes')*slotDelta, ev, ui);
|
|
}else{
|
|
eventElement.css('z-index', 8);
|
|
showEvents(event, eventElement);
|
|
// BUG: if event was really short, need to put title back in span
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|