/* 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 = "
" + "" + "" + ""; for (i=0; i" + formatDate(d, colFormat, options) + ""; addDays(d, dis); if (nwe) { skipWeekend(d, dis); } } s += ""; if (options.allDaySlot) { s += "" + "" + "" + "" + ""; } s+= "
  
" + options.allDayText + "" + "
 
 
"; head = $(s).appendTo(element); dayBind(head.find('td')); // all-day event container daySegmentContainer = $("
").appendTo(head); // body d = zeroDate(); var maxd = addMinutes(cloneDate(d), maxMinute); addMinutes(d, minMinute); s = ""; for (i=0; d < maxd; i++) { minutes = d.getMinutes(); s += ""; addMinutes(d, options.slotMinutes); slotCnt++; } s += "
" + ((!slotNormal || !minutes) ? formatDate(d, options.axisFormat) : ' ') + "
 
"; body = $("
") .append(bodyContent = $("
") .append(bodyTable = $(s))) .appendTo(element); slotBind(body.find('td')); // slot event container slotSegmentContainer = $("
").appendTo(bodyContent); // background stripes d = cloneDate(d0); s = "
" + ""; for (i=0; i
 
"; addDays(d, dis); if (nwe) { skipWeekend(d, dis); } } s += "
"; 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" + "" + "" + "" + htmlEscape(formatDates(event.start, event.end, view.option('timeFormat'))) + "" + "" + htmlEscape(event.title) + "" + "" + ((event.editable || event.editable === undefined && options.editable) && !options.disableResizing && $.fn.resizable ? "
=
" : '') + "
"; } 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= 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= 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