/* 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) { 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') ); } }); }; views.agendaDay = function(element, options) { 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') ); } }); }; function Agenda(element, options, methods) { 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 = {}, daySelectionManager, slotSelectionManager, // ... 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.init(element, options); /* Time-slot rendering -----------------------------------------------------------------------------*/ element.addClass('fc-agenda'); if (element.disableSelection) { element.disableSelection(); } function renderAgenda(c, colFormat) { if (view.beforeRender) { view.beforeRender(); } 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); if (daySelectionManager) { tds.mousedown(daySelectionMousedown); } } function slotBind(tds) { tds.click(slotClick); if (slotSelectionManager) { tds.mousedown(slotSelectionMousedown); } } function slotClick(ev) { var col = Math.floor((ev.pageX - bg.offset().left) / colWidth), date = addDays(cloneDate(view.visStart), dit + dis*col), 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 origPosition, origWidth, resetElement, allDay=true, matrix; eventElement.draggable({ zIndex: 9, opacity: view.option('dragOpacity', 'month'), // use whatever the month view was using revertDuration: options.dragRevertDuration, start: function(ev, ui) { view.hideEvents(event, eventElement); view.trigger('eventDragStart', eventElement, event, ev, ui); origPosition = eventElement.position(); origWidth = eventElement.width(); resetElement = function() { if (!allDay) { eventElement .width(origWidth) .height('') .draggable('option', 'grid', null); allDay = true; } }; matrix = buildDayMatrix(function(cell) { eventElement.draggable('option', 'revert', !cell || !cell.rowDelta && !cell.colDelta); view.clearOverlays(); if (cell) { if (!cell.row) { // on full-days renderDayOverlay( matrix, dayDateCol(addDays(cloneDate(event.start), cell.colDelta)), dayDateCol(addDays(visEventEnd(event), cell.colDelta)) // visEventEnd returns a clone ); 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; } } } },true); matrix.mouse(ev); }, drag: function(ev, ui) { matrix.mouse(ev); }, stop: function(ev, ui) { view.trigger('eventDragStop', eventElement, event, ev, ui); view.clearOverlays(); var cell = matrix.cell; var dayDelta = dis * ( allDay ? // can't trust cell.colDelta when using slot grid (cell ? cell.colDelta : 0) : Math.floor((ui.position.left - origPosition.left) / colWidth) ); if (!cell || !dayDelta && !cell.rowDelta) { // over nothing (has reverted) resetElement(); if ($.browser.msie) { eventElement.css('filter', ''); // clear IE opacity side-effects } view.showEvents(event, eventElement); }else{ eventElement.find('a').removeAttr('href'); // prevents safari from visiting the link view.eventDrop( this, event, dayDelta, allDay ? 0 : // minute delta Math.round((eventElement.offset().top - bodyContent.offset().top) / slotHeight) * options.slotMinutes + minMinute - (event.start.getHours() * 60 + event.start.getMinutes()), allDay, ev, ui ); } } }); } } // when event starts out IN TIMESLOTS function draggableSlotEvent(event, eventElement, timeElement) { if (!options.disableDragging && eventElement.draggable) { var origPosition, resetElement, prevSlotDelta, slotDelta, allDay=false, matrix; 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.hideEvents(event, eventElement); view.trigger('eventDragStart', eventElement, event, ev, ui); if ($.browser.msie) { eventElement.find('span.fc-event-bg').hide(); // nested opacities mess up in IE, just hide } origPosition = eventElement.position(); resetElement = function() { // convert back to original slot-event if (allDay) { timeElement.css('display', ''); // show() was causing display=inline eventElement.draggable('option', 'grid', [colWidth, slotHeight]); allDay = false; } }; prevSlotDelta = 0; matrix = buildDayMatrix(function(cell) { eventElement.draggable('option', 'revert', !cell); view.clearOverlays(); if (cell) { if (!cell.row && options.allDaySlot) { // over full days if (!allDay) { // convert to temporary all-day event allDay = true; timeElement.hide(); eventElement.draggable('option', 'grid', null); } renderDayOverlay( matrix, dayDateCol(addDays(cloneDate(event.start), cell.colDelta)), dayDateCol(addDays(visEventEndAllDay(event), cell.colDelta)) // visEventEndAllDay returns a clone // TODO: test this stuff further!!! ); }else{ // on slots resetElement(); } } },true); matrix.mouse(ev); }, drag: function(ev, ui) { slotDelta = Math.round((ui.position.top - origPosition.top) / slotHeight); if (slotDelta != prevSlotDelta) { if (!allDay) { // update time header var minuteDelta = slotDelta*options.slotMinutes, newStart = addMinutes(cloneDate(event.start), minuteDelta), newEnd; if (event.end) { newEnd = addMinutes(cloneDate(event.end), minuteDelta); } timeElement.text(formatDates(newStart, newEnd, view.option('timeFormat'))); } prevSlotDelta = slotDelta; } matrix.mouse(ev); }, stop: function(ev, ui) { view.clearOverlays(); view.trigger('eventDragStop', eventElement, event, ev, ui); var cell = matrix.cell, dayDelta = dis * ( allDay ? // can't trust cell.colDelta when using slot grid (cell ? cell.colDelta : 0) : Math.floor((ui.position.left - origPosition.left) / colWidth) ); if (!cell || !slotDelta && !dayDelta) { resetElement(); if ($.browser.msie) { eventElement .css('filter', '') // clear IE opacity side-effects .find('span.fc-event-bg').css('display', ''); // .show() made display=inline } eventElement.css(origPosition); // sometimes fast drags make event revert to wrong position view.showEvents(event, eventElement); }else{ view.eventDrop( this, event, dayDelta, allDay ? 0 : slotDelta * options.slotMinutes, // minute delta allDay, ev, ui ); } } }); } } /* 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 } } }); } } /* Selecting -----------------------------------------------------------------------------*/ if (view.option('selectable')) { var selectionHelper; var selectionMatrix; if (options.allDaySlot) { // all-day selecting daySelectionManager = new SelectionManager( view, function(startDate, endDate) { renderDayOverlay( selectionMatrix, dayDateCol(startDate), dayDateCol(addDays(endDate, 1)) ); }, clearSelection ); function daySelectionMousedown(ev) { selectionMatrix = buildDayMatrix(function(cell) { if (cell) { var d = dayColDate(cell.col); daySelectionManager.drag(d, d, true); }else{ daySelectionManager.drag(); } }); documentDragHelp( function(ev) { selectionMatrix.mouse(ev); }, function(ev) { daySelectionManager.dragStop(ev); } ); daySelectionManager.dragStart(ev); selectionMatrix.mouse(ev); ev.stopPropagation(); // prevent auto-unselect } } // slot selecting slotSelectionManager = new SelectionManager( view, renderSlotSelection, clearSelection ); function slotSelectionMousedown(ev) { selectionMatrix = buildSlotMatrix(function(cell) { if (cell) { var d = slotCellDate(cell.row, cell.origCol); slotSelectionManager.drag(d, addMinutes(cloneDate(d), options.slotMinutes), false); }else{ slotSelectionManager.drag(); } }); documentDragHelp( function(ev) { selectionMatrix.mouse(ev); }, function(ev) { slotSelectionManager.dragStop(ev); } ); slotSelectionManager.dragStart(ev); selectionMatrix.mouse(ev); ev.stopPropagation(); // prevent auto-unselect } documentAutoUnselect(view, unselect); } function renderSlotSelection(startDate, endDate) { // startDate and endDate are assumed to be in same day var helperOption = view.option('selectHelper'); if (helperOption) { var startCell = slotDateCell(startDate); var endCell = slotDateCell(endDate); var rect = selectionMatrix.rect(startCell[0], startCell[1], endCell[0], startCell[1]+1, bodyContent); rect.left += 2; rect.width -= 5; if ($.isFunction(helperOption)) { selectionHelper = helperOption(startDate, endDate); if (selectionHelper) { selectionHelper.css(rect); } }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) { // IE makes the event completely clear!!? selectionHelper.css('opacity', view.option('dragOpacity')); } } if (selectionHelper) { slotBind(selectionHelper); bodyContent.append(selectionHelper); setOuterWidth(selectionHelper, rect.width, true); setOuterHeight(selectionHelper, rect.height, true); } }else{ renderSlotOverlay( selectionMatrix, slotDateCell(startDate), slotDateCell(endDate) ); } } function clearSelection() { clearOverlays(); if (selectionHelper) { selectionHelper.remove(); selectionHelper = null; } } this.select = function(start, end, allDay) { if (allDay) { if (daySelectionManager) { selectionMatrix = buildDayMatrix(); daySelectionManager.select(start, end, allDay); } }else{ if (slotSelectionManager) { selectionMatrix = buildSlotMatrix(); slotSelectionManager.select(start, end, allDay); } } }; function unselect() { if (slotSelectionManager) { slotSelectionManager.unselect(); if (daySelectionManager) { daySelectionManager.unselect(); } } } this.unselect = unselect; /* Semi-transparent Overlay Helpers -----------------------------------------------------*/ function renderDayOverlay(matrix, startCol, endCol) { renderDayOverlayRect( matrix.rect(0, startCol, 1, endCol, head) ); } function renderDayOverlayRect(rect) { dayBind( view.renderOverlay(rect, head) ); } function renderSlotOverlay(matrix, startCell, endCell) { renderSlotOverlayRect( matrix.rect(startCell[0], startCell[1], endCell[0], startCell[1]+1, bodyContent) ); // TODO: implement wrapping } function renderSlotOverlayRect(rect) { slotBind( view.renderOverlay(rect, bodyContent) ); } function clearOverlays() { view.clearOverlays(); } /* Coordinate Utilities -----------------------------------------------------------------------------*/ // 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) )); } function buildDayMatrix(changeCallback, includeSlotArea) { var matrix = new HoverMatrix(changeCallback); if (options.allDaySlot) { matrix.row(head.find('td')); } bg.find('td').each(function() { matrix.col(this); }); if (includeSlotArea) { matrix.row(body); } return matrix; } function buildSlotMatrix(changeCallback) { var matrix = new HoverMatrix(changeCallback); bodyTable.find('td').each(function() { matrix.row(this); }); bg.find('td').each(function() { matrix.col(this); }); return matrix; } /* Date Utilities ----------------------------------------------------*/ // generating event end dates function visEventEnd(event) { // returns exclusive 'visible' end, for rendering if (event.allDay) { return visEventEndAllDay(event); } if (event.end) { return cloneDate(event.end); }else{ return addMinutes(cloneDate(event.start), options.defaultEventMinutes); } } function visEventEndAllDay(event) { 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); } } // generating indexes from dates function dayDateCol(date) { var d = cloneDate(view.visStart); var c; for (c=0; c date) { break; } } return c*dis+dit; } function slotDateCell(date) { var col = dayDateCol(date); var row = Math.floor((date.getHours() * 60 + date.getMinutes()) / options.slotMinutes); return [row, col]; } function dayOfWeekCol(dayOfWeek) { return ((dayOfWeek - Math.max(firstDay,nwe)+colCnt) % colCnt)*dis+dit; } // generating dates from cell row & columns function dayColDate(col) { return addDays(cloneDate(view.visStart), col*dis+dit); } function slotCellDate(row, col) { var d = dayColDate(col); addMinutes(d, minMinute + row*options.slotMinutes); return d; } } // 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