Bläddra i källkod

first round of commits for 1.4, agenda view

Adam Shaw 16 år sedan
förälder
incheckning
8e0312a750
7 ändrade filer med 921 tillägg och 931 borttagningar
  1. 798 892
      src/agenda.js
  2. 71 15
      src/css/agenda.css
  3. 5 1
      src/css/main.css
  4. 6 6
      src/grid.js
  5. 28 14
      src/main.js
  6. 11 3
      tests/basic.html
  7. 2 0
      tests/loader.js

+ 798 - 892
src/agenda.js

@@ -1,892 +1,798 @@
-
-function segAfters(levels) { // TODO: put in agenda.js
-	var i, j, k, level, seg, seg2;
-	for (i=levels.length-1; i>0; i--) {
-		level = levels[i];
-		for (j=0; j<level.length; j++) {
-			seg = level[j];
-			for (k=0; k<segLevels[i-1].length; k++) {
-				seg2 = segLevels[i-1][k];
-				if (segsCollide(seg, seg2)) {
-					seg2.after = Math.max(seg2.after, seg.after+1);
-				}
-			}
-		}
-	}
-}
-
-/********************************* week view ***********************************/
-
-$.fullCalendar.views.week = function(element, options) {
-
-	var agenda = new Agenda(element, options);
-	
-	safeExtend(options, {
-		weekTitleFormat: 'M j Y{ - M j Y}' // TODO: shift around
-	});
-	
-	agenda.render = function(date, delta, fetchEvents) {
-	
-		if (delta) {
-			addDays(date, delta * 7);
-		}
-		
-		this.start = addDays(cloneDate(date), -date.getDay());
-		this.end = addDays(cloneDate(this.start), 7);
-		this.title = formatDates(this.start, this.end, options.weekTitleFormat);
-		
-		this.renderAgenda(fetchEvents);
-	
-	};
-	
-	return agenda;
-};
-
-/******************************* day view *************************************/
-
-$.fullCalendar.views.day = function(element, options) {
-
-	var agenda = new Agenda(element, options);
-	
-	safeExtend(options, {
-		dayTitleFormat: 'l F j Y' // TODO: shift around
-	});
-	
-	agenda.render = function(date, delta, fetchEvents) {
-	
-		if (delta) {
-			addDays(date, delta);
-		}
-		
-		this.start = cloneDate(date, true);
-		this.end = addDays(cloneDate(date), 1);
-		this.title = formatDate(date, options.dayTitleFormat);
-		
-		this.renderAgenda(fetchEvents);
-	
-	};
-	
-	return agenda;
-};
-
-/*********************** shared by month and day views *************************/
-
-function Agenda(element, options) {
-
-	safeExtend(options, {
-		slotMinutes: 30,
-		defaultEventMinutes: 120,
-		agendaEventTimeFormat: 'g:i{ - g:i}',
-		agendaSideTimeFormat: 'ga',
-		agendaEventDragOpacity: .5
-	});
-
-	var view = this,
-		head, body, panel, bg,
-		dayCnt,
-		dayWidth, slotHeight,
-		timeWidth,
-		cachedEvents,
-		cachedSlotSegs, cachedDaySegs,
-		eventElements = [],
-		eventElementsByID = {},
-		eventsByID = {};
-	
-	element.addClass('fc-agenda').css('position', 'relative');
-	
-	
-	
-	/******************************** cell rendering ********************************/
-	
-	
-	this.renderAgenda = function(fetchEvents) { // TODO: get z-indexes sorted out
-		
-		var start = view.start,
-			end = view.end,
-			today = getToday(),
-			todayI = -1,
-			tm = options.theme ? 'ui' : 'fc',
-			slotNormal = options.slotMinutes % 15 == 0,
-			dayAbbrevs = $.fullCalendar.dayAbbrevs;
-		
-		if (!head) { // first time rendering, build from scratch TODO: need all the nbsp's?
-			
-			// head
-			var i, d, dDay, dMinutes,
-				s = "<div class='fc-agenda-head' style='position:relative;z-index:3'>" +
-					"<table style='width:100%' cellpadding='0' cellspacing='0'>" +
-						"<tr class='fc-first'>" +
-							"<th class='fc-first " + tm + "-state-default'>&nbsp;</th>";
-			dayCnt = 0;
-			for (d=cloneDate(start); d<end; addDays(d, 1)) {
-				s += "<th class='fc-" +
-					dayIDs[d.getDay()] + ' ' + // needs to be first
-					tm + '-state-default' +
-					"'>" + dayAbbrevs[d.getDay()] + "</th>";
-				if (+d == +today) {
-					todayI = dayCnt;
-				}
-				dayCnt++;
-			}
-			s += "<th class='fc-last " + tm + "-state-default'>&nbsp;</th></tr>" +
-				"<tr class='fc-last'>" +
-					"<th class='fc-first " + tm + "-state-default' style='font-weight:normal;text-align:right;padding:4px 2px'>all day</th>" +
-					"<td colspan='" + dayCnt + "' class='" + tm + "-state-default'>" +
-						"<div class='fc-day-content'><div/></div></td>" +
-					"<th class='fc-last " + tm + "-state-default'>&nbsp;</th>" +
-				"</tr></table></div>";
-			head = $(s).appendTo(element);
-			
-			// body & event panel
-			s = "<div style='position:relative;overflow:hidden'>" +
-				"<table cellpadding='0' cellspacing='0'>";
-			d = getToday();
-			dDay = d.getDay();
-			for (i=0; d.getDay()==dDay; i++, addMinutes(d, options.slotMinutes)) {
-				dMinutes = d.getMinutes();
-				s += "<tr class='" +
-					(i==0 ? 'fc-first' : (dMinutes==0 ? '' : 'fc-minor')) +
-					"'><th class='" + tm + "-state-default'>" +
-						(!slotNormal || dMinutes==0 ? formatDate(d, options.agendaSideTimeFormat) : '&nbsp;') + 
-						"</th><td class='fc-slot " + tm + "-state-default'>&nbsp;</td></tr>";
-			}
-			s += "</table></div>";
-			body = $("<div class='fc-agenda-body' style='position:relative;z-index:2'/>")
-				.append(panel = $(s))
-				.appendTo(element);
-			
-			// background stripes
-			s = "<div class='fc-agenda-bg' style='position:absolute;top:0;z-index:1'>" +
-				"<table style='width:100%;height:100%' cellpadding='0' cellspacing='0'><tr>";
-			for (i=0; i<dayCnt; i++) {
-				s += "<td class='fc-" +
-					dayIDs[i] + ' ' + // needs to be first
-					tm + '-state-default ' + 
-					(i==todayI ? tm + '-state-highlight fc-today' : 'fc-not-today') +
-					"'><div class='fc-day-content'><div>&nbsp;</div></div></td>";
-			}
-			s += "</tr></table></div>";
-			bg = $(s).appendTo(element);
-			
-		}else{ // skeleton already built, just modify it
-		
-			clearEvents();
-			
-			// change classes of background stripes
-			todayI = Math.round((today - start) / msInDay);
-			bg.find('td').each(function(i) {
-				if (i == todayI) {
-					$(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');
-				}
-			});
-			
-			// if 1-day view, change day-of-week class and header text
-			if (dayCnt == 1) {
-				var th = head.find('th:eq(1)').html(dayAbbrevs[start.getDay()])[0],
-					td = bg.find('td')[0];
-				th.className = th.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[start.getDay()]);
-				td.className = td.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[start.getDay()]);
-			}
-		
-		}
-		
-		updateSize();
-		fetchEvents(renderEvents);
-		
-	};
-	
-	
-	function updateSize() {
-		
-		// align first 'time' column
-		timeWidth = body.find('th:first').outerWidth();
-		head.find('th:first').width(timeWidth);
-		
-		// set table width (100% in css wasn't working in IE)
-		var panelWidth = body[0].clientWidth || body.width(); // first time, there are no scrollbars!? for IE6?
-		body.find('table').width(panelWidth);
-		
-		// align spacer column to scrollbar width
-		setOuterWidth(head.find('th:last'), body.width() - panelWidth);
-		
-		// position background stripe container
-		bg.css({
-			left: timeWidth,
-			width: panelWidth - timeWidth,
-			height: element.height()
-		});
-		
-		// align other columns
-		dayWidth = Math.floor((panelWidth - timeWidth) / dayCnt);
-		var topCells = head.find('tr:first th:gt(0)'),
-			bgCells = bg.find('td');
-		for (var i=0, len=bgCells.length-1; i<len; i++) { // TODO: use slice
-			setOuterWidth(topCells.eq(i), dayWidth);
-			setOuterWidth(bgCells.eq(i), dayWidth);
-		}
-		
-		slotHeight = body.find('tr:eq(1)').height(); // use second, first prob doesn't have a border
-		
-		// body height
-		body.height(Math.round(body.width() / contentAspectRatio) - head.height());
-		// but this will add scrollbars...
-		// TODO: bug, iE6 view heights dont match up
-		// also, no scrollbars
-		
-	}
-	
-	
-	
-	/********************************** event rendering *********************************/
-	
-	
-	function renderEvents(events) {
-		
-		var i, len=events.length, event,
-			fakeID=0, nextDay,
-			slotEvents=[], dayEvents=[];
-			
-		for (i=0; i<len; i++) {
-			event = events[i];
-			event._id = typeof event.id == 'undefined' ? '_fc' + fakeID++ : event.id + '';
-			if (eventsByID[event._id]) {
-				eventsByID[event._id].push(event);
-			}else{
-				eventsByID[event._id] = [event];
-			}
-			if (event.hasTime) {
-				event._end = event.end || addMinutes(cloneDate(event.start), options.defaultEventMinutes);
-			}else{
-				event._end = addDays(cloneDate(event.end || event.start), 1);
-			}
-			if (event.start < view.end && event._end > view.start) {
-				if (event.hasTime) {
-					event._end = event.end || addMinutes(cloneDate(event.start), options.defaultEventMinutes);
-					slotEvents.push(event);
-				}else{
-					event._end = addDays(cloneDate(event.end || event.start), 1);
-					dayEvents.push(event);
-				}
-			}
-		}
-		
-		cachedEvents = events;
-		cachedSlotSegs = compileSlotSegs(slotEvents, view.start, view.end);
-		cachedDaySegs = levelizeSegs(sliceSegs(dayEvents, view.start, view.end));
-		
-		renderSlotSegs(cachedSlotSegs);
-		renderDaySegs(cachedDaySegs);
-		
-	}
-	
-	
-	function rerenderEvents(skipCompile) {
-		clearEvents();
-		if (skipCompile) {
-			renderSlotSegs(cachedSlotSegs);
-			renderDaySegs(cachedDaySegs);
-		}else{
-			renderEvents(cachedEvents);
-		}
-	}
-	
-	
-	function clearEvents() {
-		for (var i=0; i<eventElements.length; i++) {
-			eventElements[i].remove();
-		}
-		eventElements = [];
-		eventElementsByID = {};
-		eventsByID = {};
-	}
-	
-	
-	// renders events in the 'time slots' at the bottom
-	
-	function renderSlotSegs(segCols) {
-		var colI, colLen=segCols.length, col,
-			levelI, level,
-			segI, seg,
-			event, start, end,
-			top, bottom,
-			tdInner, left, width,
-			eventElement, anchorElement, timeElement, titleElement;
-		for (colI=0; colI<colLen; colI++) {
-			col = segCols[colI];
-			for (levelI=0; levelI<col.length; levelI++) {
-				level = col[levelI];
-				for (segI=0; segI<level.length; segI++) {
-					seg = level[segI];
-					event = seg.event;
-					top = timeCoord(seg.start, seg.start);
-					bottom = timeCoord(seg.start, seg.end);
-					tdInner = bg.find('td:eq('+colI+') div div');
-					availWidth = tdInner.width();
-					left = timeWidth + tdInner.position().left + // leftmost possible
-						(availWidth / (levelI + seg.right + 1) * levelI); // indentation
-					if (levelI == 0) {
-						if (seg.right == 0) {
-							// can be entire width, aligned left
-							width = availWidth * .96;
-						}else{
-							// moderately wide, aligned left still
-							width = ((availWidth / (seg.right + 1)) - (12/2)) * 2; // 12 is the predicted width of resizer =
-						}
-					}else{
-						// indented and thinner
-						width = availWidth / (levelI + seg.right + 1);
-					}
-					eventElement = $("<div class='fc-event fc-event-vert' />")
-						.append(anchorElement = $("<a><span class='fc-event-bg'/></a>")
-							.append(titleElement = $("<span class='fc-event-title'/>")
-								.text(event.title)))
-						.css({
-							position: 'absolute',
-							zIndex: 1000,
-							top: top,
-							left: left
-						});
-					if (event.url) {
-						anchorElement.attr('href', event.url);
-					}
-					if (seg.isStart) {
-						eventElement.addClass('fc-corner-top');
-						// add the time header
-						anchorElement
-							.prepend(timeElement = $("<span class='fc-event-time'/>")
-								.text(formatDates(event.start, event.end, options.agendaEventTimeFormat)))
-					}else{
-						timeElement = null;
-					}
-					if (seg.isEnd) {
-						eventElement.addClass('fc-corner-bottom');
-						resizableSlotEvent(event, eventElement, timeElement);
-					}
-					eventElement.appendTo(panel);
-					setOuterWidth(eventElement, width, true);
-					setOuterHeight(eventElement, bottom-top, true);
-					if (timeElement && eventElement.height() - titleElement.position().top < 10) {
-						// event title doesn't have enough room, but next to the time
-						timeElement.text(formatDate(event.start, options.agendaEventTimeFormat) + ' - ' + event.title);
-						titleElement.remove();
-					}
-					draggableSlotEvent(event, eventElement, timeElement);
-					reportEventElement(event, eventElement);
-				}
-			}
-		}
-	}
-	
-	
-	// renders 'all-day' events at the top
-	
-	function renderDaySegs(segRow) {
-		var td = head.find('td');
-		var tdInner = td.find('div div');
-		var top = tdInner.position().top,
-			rowHeight = 0,
-			i, len=segRow.length, level,
-			levelHeight,
-			j, seg,
-			event, left, right,
-			eventElement, anchorElement;
-		for (i=0; i<len; i++) {
-			level = segRow[i];
-			levelHeight = 0;
-			for (j=0; j<level.length; j++) {
-				seg = level[j];
-				event = seg.event;
-				left = seg.isStart ?
-					bg.find('td:eq('+((seg.start.getDay()+dayCnt)%dayCnt)+') div div') :
-					bg.find('td:eq('+((seg.start.getDay()+dayCnt)%dayCnt)+')');
-				left = left.position().left;
-				right = seg.isEnd ?
-					bg.find('td:eq('+((seg.end.getDay()-1+dayCnt)%dayCnt)+') div div') :
-					bg.find('td:eq('+((seg.end.getDay()-1+dayCnt)%dayCnt)+')');
-				right = right.position().left + right.outerWidth();
-				eventElement = $("<div class='fc-event fc-event-hori' />")
-					.append(anchorElement = $("<a/>")
-						.append($("<span class='fc-event-title' />")
-							.text(event.title)))
-					.css({
-						position: 'absolute',
-						top: top,
-						left: timeWidth + left
-					});
-				if (seg.isStart) {
-					eventElement.addClass('fc-corner-left');
-				}
-				if (seg.isEnd) {
-					eventElement.addClass('fc-corner-right');
-				}
-				if (event.url) {
-					anchorElement.attr('href', event.url);
-				}
-				eventElement.appendTo(head);
-				setOuterWidth(eventElement, right-left, true);
-				draggableDayEvent(event, eventElement);
-				//resizableDayEvent(event, eventElement);
-				reportEventElement(event, eventElement);
-				levelHeight = Math.max(levelHeight, eventElement.outerHeight(true));
-			}
-			top += levelHeight;
-			rowHeight += levelHeight;
-		}
-		tdInner.height(rowHeight);
-		//bg.height(element.height()); // tdInner might have pushed the body down, so resize
-		//updateSize();
-	}
-	
-	
-	
-	/******************************************* draggable *****************************************/
-	
-	
-	// when event starts out IN TIMESLOTS
-	
-	function draggableSlotEvent(event, eventElement, timeElement) {
-		var origPosition, origMarginTop,
-			prevSlotDelta, slotDelta,
-			matrix;
-		eventElement.draggable({
-			zIndex: 1001,
-			scroll: false,
-			grid: [dayWidth, slotHeight],
-			axis: dayCnt==1 ? 'y' : false,
-			cancel: '.ui-resizable-handle',
-			opacity: .5,
-			start: function(ev, ui) {
-				if ($.browser.msie) {
-					eventElement.find('span.fc-event-bg').hide();
-				}
-				origPosition = eventElement.position();
-				origMarginTop = parseInt(eventElement.css('margin-top')) || 0;
-				prevSlotDelta = 0;
-				matrix = new HoverMatrix(function(cell) {
-					if (event.hasTime) {
-						// event is an original slot-event
-						if (cell && cell.row == 0) {
-							// but needs to convert to temporary full-day-event
-							var topDiff = panel.offset().top - head.offset().top;
-							eventElement.css('margin-top', origMarginTop + topDiff)
-								.appendTo(head);
-							// TODO: bug in IE8 w/ above technique, draggable ends immediately
-							event.hasTime = false;
-							if (timeElement) {
-								timeElement.hide();
-							}
-							eventElement.draggable('option', 'grid', null);
-						}
-					}else{
-						// event is a temporary full-day-event
-						if (cell && cell.row == 1) {
-							// but needs to convert to original slot-event
-							eventElement.css('margin-top', origMarginTop)
-								.appendTo(panel);
-							event.hasTime = true;
-							if (timeElement) {
-								timeElement.css('display', ''); // show() was causing display=inline
-							}
-							eventElement.draggable('option', 'grid', [dayWidth, slotHeight]);
-						}
-					}
-					if (cell && cell.row == 0) {
-						showDayOverlay(cell);
-					}else{
-						hideDayOverlay();
-					}
-				});
-				matrix.row(head.find('td'));
-				bg.find('td').each(function() {
-					matrix.col(this);
-				});
-				matrix.row(body);
-				matrix.start();
-				hideSimilarEvents(event, eventElement);
-			},
-			drag: function(ev, ui) {
-				slotDelta = Math.round((ui.position.top - origPosition.top) / slotHeight);
-				if (slotDelta != prevSlotDelta) {
-					if (timeElement && event.hasTime) {
-						// update time header
-						var newStart = addMinutes(cloneDate(event.start), slotDelta * options.slotMinutes),
-							newEnd;
-						if (event.end) {
-							newEnd = addMinutes(cloneDate(event.end), slotDelta * options.slotMinutes);
-						}
-						timeElement.text(formatDates(newStart, newEnd, options.agendaEventTimeFormat));
-					}
-					prevSlotDelta = slotDelta;
-				}
-				matrix.mouse(ev.pageX, ev.pageY);
-			},
-			stop: function(ev, ui) {
-				if (event.hasTime) {
-					if (matrix.cell) {
-						// over slots
-						var dayDelta = Math.round((ui.position.left - origPosition.left) / dayWidth);
-						reportEventMove(event, dayDelta, true, slotDelta * options.slotMinutes);
-					}
-				}else{
-					// over full-days
-					if (!matrix.cell) {
-						// was being dragged over full-days, but finished over nothing, reset
-						event.hasTime = true;
-					}else{
-						event.end = null;
-						reportEventMove(event, matrix.cell.colDelta);
-					}
-				}
-				hideDayOverlay();
-				rerenderEvents();
-			}
-		});
-		
-	}
-	
-	
-	// when event starts out FULL-DAY
-	
-	function draggableDayEvent(event, eventElement) {
-		var origWidth, matrix;
-		eventElement.draggable({
-			zIndex: 1001,
-			start: function() {
-				origWidth = eventElement.width();
-				matrix = new HoverMatrix(function(cell) {
-					if (!cell) {
-						// mouse is outside of everything
-						hideDayOverlay();
-					}else{
-						if (cell.row == 0) {
-							// on full-days
-							if (event.hasTime) {
-								// and needs to be original full-day event
-								eventElement
-									.width(origWidth)
-									.height('')
-									.draggable('option', 'grid', null);
-								event.hasTime = false;
-							}
-							showDayOverlay(cell);
-						}else{
-							// mouse is over bottom slots
-							if (!event.hasTime) {
-								// convert event to temporary slot-event
-								//if (+cloneDate(event.start, true) == +cloneDate(event._end, true)) {
-									// only change styles if a 1-day event
-									eventElement
-										.width(dayWidth - 10) // don't use entire width
-										.height(slotHeight * Math.round(options.defaultEventMinutes/options.slotMinutes) - 2);
-								//}
-								eventElement.draggable('option', 'grid', [dayWidth, 1]);
-								event.hasTime = true;
-							}
-							hideDayOverlay();
-						}
-					}
-				});
-				matrix.row(head.find('td'));
-				bg.find('td').each(function() {
-					matrix.col(this);
-				});
-				matrix.row(body);
-				matrix.start();
-				hideSimilarEvents(event, eventElement);
-			},
-			drag: function(ev, ui) {
-				matrix.mouse(ev.pageX, ev.pageY);
-			},
-			stop: function() {
-				var cell = matrix.cell;
-				if (!cell) {
-					// over nothing
-					if (event.hasTime) {
-						// event was on the slots before going out, convert back
-						event.hasTime = false;
-					}
-				}else{
-					if (!event.hasTime) {
-						// event has been dropped on a full-day
-						reportEventMove(event, cell.colDelta);
-					}else{
-						// event has been dropped on the slots
-						var slots = Math.floor((eventElement.offset().top - panel.offset().top) / slotHeight);
-						event.end = null;
-						reportEventMove(event, cell.colDelta, false, slots * options.slotMinutes);
-					}
-				}
-				hideDayOverlay();
-				rerenderEvents();
-			}
-		});
-	}
-	
-	
-	// hover effect when dragging events over top days
-	
-	var dayOverlay;
-	
-	function showDayOverlay(props) {
-		if (!dayOverlay) {
-			dayOverlay = $("<div class='fc-day-overlay' style='position:absolute;display:none'/>")
-				.appendTo(element);
-		}
-		var o = element.offset();
-		dayOverlay
-			.css({
-				top: props.top - o.top,
-				left: props.left - o.left,
-				width: props.width,
-				height: props.height
-			})
-			.show();
-	}
-	
-	function hideDayOverlay() {
-		if (dayOverlay) {
-			dayOverlay.hide();
-		}
-	}
-	
-	
-	
-	/************************************* resizable **************************************/
-	
-
-	function resizableSlotEvent(event, eventElement, timeElement) {
-		var prevSlotDelta, slotDelta, newEnd;
-		eventElement
-			.resizable({
-				handles: 's',
-				grid: [0, slotHeight],
-				start: function() {
-					prevSlotDelta = 0;
-					hideSimilarEvents(event, eventElement);
-					if ($.browser.msie && $.browser.version == '6.0') {
-						eventElement.css('overflow', 'hidden');
-					}
-				},
-				resize: function(ev, ui) {
-					slotDelta = Math.round((Math.max(slotHeight, ui.size.height) - ui.originalSize.height) / slotHeight);
-					if (slotDelta != prevSlotDelta) {
-						newEnd = addMinutes(cloneDate(event._end), options.slotMinutes * slotDelta);
-						if (timeElement) {
-							timeElement.text(formatDates(event.start, newEnd, options.agendaEventTimeFormat));
-						}
-						prevSlotDelta = slotDelta;
-					}
-				},
-				stop: function(ev, ui) {
-					reportEventResize(event, 0, true, options.slotMinutes * slotDelta);
-					rerenderEvents();
-				}
-			})
-			.find('div.ui-resizable-s').text('=');
-	}
-	
-	
-	function resizableDayEvent(event, eventElement) {
-		eventElement.resizable({
-			handles: 'e',
-			grid: [dayWidth, 0],
-			start: function() {
-				hideSimilarEvents(event, eventElement);
-			},
-			stop: function(ev, ui) {
-				var dayDelta = Math.round((Math.max(dayWidth, ui.size.width) - ui.originalSize.width) / dayWidth);
-				reportEventResize(event, dayDelta);
-				rerenderEvents();
-			}
-		});
-	}
-	
-	
-	
-	/**************************************** misc **************************************/
-	
-	
-	function reportEventElement(event, eventElement) {
-		eventElements.push(eventElement);
-		if (eventElementsByID[event._id]) {
-			eventElementsByID[event._id].push(eventElement);
-		}else{
-			eventElementsByID[event._id] = [eventElement];
-		}
-	}
-	
-	
-	function hideSimilarEvents(event, eventElement) {
-		var elements = eventElementsByID[event._id];
-		for (var i=0; i<elements.length; i++) {
-			if (elements[i] != eventElement) {
-				elements[i].hide();
-			}
-		}
-	}
-	
-	
-	function reportEventMove(event, days, keepTime, minutes) {
-		minutes = minutes || 0;
-		var events = eventsByID[event._id];
-		for (var i=0, event2; i<events.length; i++) {
-			event2 = events[i];
-			event2.hasTime = event.hasTime;
-			addMinutes(addDays(event2.start, days, keepTime), minutes);
-			if (event.end) {
-				event2.end = addMinutes(addDays(event2.end || event2._end, days, keepTime), minutes);
-			}else{
-				event2.end = event2._end = null;
-					// hopefully renderEvents() will always be called after this
-					// to reset _end.... TODO?
-			}
-		}
-	}
-	
-	
-	function reportEventResize(event, days, keepTime, minutes) {
-		minutes = minutes || 0;
-		var events = eventsByID[event._id];
-		for (var i=0, event2; i<events.length; i++) {
-			event2 = events[i];
-			event2.end = addMinutes(addDays(event2.end || event2._end, days, keepTime), minutes);
-		}
-	}
-	
-	
-	// get the Y coordinate of the given time on the given day
-	
-	function timeCoord(day, time) {
-		var nextDay = addDays(cloneDate(day), 1);
-		if (time < nextDay) {
-			var slotMinutes = options.slotMinutes;
-			var minutes = time.getHours()*60 + time.getMinutes();
-			var slotI = Math.floor(minutes / slotMinutes);
-			var td = body.find('tr:eq(' + slotI + ') td');
-			return Math.round(td.position().top + slotHeight * ((minutes % slotMinutes) / slotMinutes));
-		}else{
-			return panel.height();
-		}
-	}
-
-}
-
-
-function compileSlotSegs(events, start, end) {
-
-	// slice by day
-	var segCols = [],
-		d1 = cloneDate(start),
-		d2 = addDays(cloneDate(start), 1);
-	for (; d1<end; addDays(d1, 1), addDays(d2, 1)) {
-		segCols.push(sliceSegs(events, d1, d2));
-	}
-	
-	var segLevelCols = [],
-		segLevels,
-		segs,
-		segI, seg,
-		levelI, level,
-		collide,
-		segI2, seg2;
-		
-	for (var i=0; i<segCols.length; i++) {
-	
-		// divide segments into levels
-		segLevels = segLevelCols[i] = [];
-		segs = segCols[i];
-		for (segI=0; segI<segs.length; segI++) {
-			seg = segs[segI];
-			for (levelI=0; true; levelI++) {
-				level = segLevels[levelI];
-				if (!level) {
-					segLevels[levelI] = [seg];
-					break;
-				}else{
-					collide = false;
-					for (segI2=0; segI2<level.length; segI2++) {
-						if (segsCollide(level[segI2], seg)) {
-							collide = true;
-							break;
-						}
-					}
-					if (!collide) {
-						level.push(seg);
-						break;
-					}
-				}
-			}
-			seg.right = 0;
-		}
-		
-		// determine # of segments to the 'right' of each segment
-		for (levelI=segLevels.length-1; levelI>0; levelI--) {
-			level = segLevels[levelI];
-			for (segI=0; segI<level.length; segI++) {
-				seg = level[segI];
-				for (segI2=0; segI2<segLevels[levelI-1].length; segI2++) {
-					seg2 = segLevels[levelI-1][segI2];
-					if (segsCollide(seg, seg2)) {
-						seg2.right = Math.max(seg2.right, seg.right+1);
-					}
-				}
-			}
-		}
-		
-	}
-	
-	return segLevelCols;
-	
-}
-
-
-// TODO: move to month.js
-
-function sliceSegs(events, start, end) {
-	var segs = [],
-		i, len=events.length, event,
-		eventStart, eventEnd,
-		segStart, segEnd,
-		isStart, isEnd;
-	for (i=0; i<len; i++) {
-		event = events[i];
-		eventStart = event.start;
-		eventEnd = event._end;
-		if (eventEnd > start && eventStart < end) {
-			if (eventStart < start) {
-				segStart = cloneDate(start);
-				isStart = false;
-			}else{
-				segStart = eventStart;
-				isStart = true;
-			}
-			if (eventEnd > end) {
-				segEnd = cloneDate(end);
-				isEnd = false;
-			}else{
-				segEnd = eventEnd;
-				isEnd = true;
-			}
-			segs.push({
-				event: event,
-				start: segStart,
-				end: segEnd,
-				isStart: isStart,
-				isEnd: isEnd,
-				msLength: segEnd - segStart
-			});
-		}
-	}
-	return segs.sort(segCmp);
-}
-
-
-function segCmp(a, b) {
-	return  (b.msLength - a.msLength) * 100 + (a.event.start - b.event.start);
-}
+
+/* Agenda Views: agendaWeek/agendaDay
+-----------------------------------------------------------------------------*/
+
+setDefaults({
+	slotMinutes: 30,
+	defaultEventMinutes: 120,
+	agendaTimeFormat: 'g:i{ - g:i}', // todo: merge into object w/ timeFormat
+	axisFormat: 'htt',
+	agendaDragOpacity: .5 // maybe merge into object
+});
+
+views.agendaWeek = function(element, options) {
+	return new Agenda(element, options, {
+		render: function(date, delta, fetchEvents) {
+			if (delta) {
+				addDays(date, delta * 7);
+			}
+			this.title = formatDates(
+				this.start = this.visStart = addDays(cloneDate(date), -((date.getDay() - options.firstDay + 7) % 7)),
+				addDays(cloneDate(this.end = this.visEnd = addDays(cloneDate(this.start), 7)), -1),
+				strProp(options.titleFormat, 'week'),
+				options
+			);
+			this.renderAgenda(7, strProp(options.columnFormat, 'week'), fetchEvents);
+		}
+	});
+};
+
+views.agendaDay = function(element, options) {
+	return new Agenda(element, options, {
+		render: function(date, delta, fetchEvents) {
+			if (delta) {
+				addDays(date, delta);
+			}
+			this.title = formatDate(date, strProp(options.titleFormat, 'day'), options);
+			this.start = this.visStart = cloneDate(date, true);
+			this.end = this.visEnd = addDays(cloneDate(this.start), 1);
+			this.renderAgenda(1, strProp(options.columnFormat, 'day'), fetchEvents);
+		}
+	});
+};
+
+function Agenda(element, options, methods) {
+
+	var head, body, bodyContent, bodyTable, bg,
+		colCnt,
+		timeWidth, colWidth, rowHeight,
+		cachedSlotSegs, cachedDaySegs,
+		tm, firstDay,
+		rtl, dis, dit,  // day index sign / translate
+		// ...
+		
+	view = $.extend(this, viewMethods, methods, {
+		renderAgenda: renderAgenda,
+		renderEvents: renderEvents,
+		rerenderEvents: rerenderEvents,
+		updateSize: updateSize,
+		defaultEventEnd: function(event) {
+			return addMinutes(cloneDate(event.start), options.defaultEventMinutes);
+		},
+		visEventEnd: function(event) {
+			return addMinutes(cloneDate(event.start), options.defaultEventMinutes);
+		}
+	});
+	view.init(element, options);
+	
+	
+	
+	/* Time-slot rendering
+	-----------------------------------------------------------------------------*/
+	
+	
+	element.addClass('fc-agenda').css('position', 'relative');
+	if (element.disableSelection) {
+		element.disableSelection();
+	}
+	
+	function renderAgenda(c, colFormat, fetchEvents) { // TODO: get z-indexes sorted out
+		colCnt = c;
+		
+		// update option-derived variables
+		tm = options.theme ? 'ui' : 'fc'; 
+		firstDay = options.firstDay;
+		if (rtl = options.isRTL) {
+			dis = -1;
+			dit = colCnt - 1;
+		}else{
+			dis = 1;
+			dit = 0;
+		}
+		
+		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:3'>" +
+				"<table style='width:100%'>" +
+				"<tr class='fc-first'>" +
+				"<th class='fc-leftmost " +
+					tm + "-state-default'>&nbsp;</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);
+			}
+			s+= "<th class='" + tm + "-state-default'>&nbsp;</th></tr>" +
+				"<tr class='fc-all-day'>" +
+					"<th class='fc-axis fc-leftmost " + tm + "-state-default'>all day</th>" +
+					"<td colspan='" + colCnt + "' class='" + tm + "-state-default'>" +
+						"<div class='fc-day-content'><div>&nbsp;</div></div></td>" +
+					"<th class='" + tm + "-state-default'>&nbsp;</th>" +
+				"</tr><tr class='fc-divider'><th colspan='" + (colCnt+2) + "' class='" +
+					tm + "-state-default fc-leftmost'></th></tr></table></div>";
+			head = $(s).appendTo(element);
+			head.find('td').click(slotClick);
+			
+			// body
+			d = new Date(1970, 0, 1);
+			s = "<table>";
+			for (i=0; d.getDate() != 2; i++) {
+				minutes = d.getMinutes();
+				s += "<tr class='" +
+					(i==0 ? 'fc-first' : (minutes==0 ? '' : 'fc-minor')) +
+					"'><th class='fc-axis fc-leftmost " + tm + "-state-default'>" +
+					((!slotNormal || minutes==0) ? formatDate(d, options.axisFormat) : '&nbsp;') + 
+					"</th><td class='fc-slot" + i + ' ' +
+						tm + "-state-default'><div class='fc-day-content'><div>&nbsp;</div></div></td></tr>";
+				addMinutes(d, options.slotMinutes);
+			}
+			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);
+			body.find('td').click(slotClick);
+			
+			// 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[i] + ' ' + // needs to be first
+					tm + '-state-default ' +
+					(i==0 ? 'fc-leftmost ' : '') +
+					(+d == +today ? tm + '-state-highlight fc-today' : 'fc-not-today') +
+					"'><div class='fc-day-content'><div>&nbsp;</div></div></td>";
+				addDays(d, dis);
+			}
+			s += "</tr></table></div>";
+			bg = $(s).appendTo(element);
+			
+		}else{ // skeleton already built, just modify it
+		
+			view.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);
+			});
+			
+			// 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);
+			});
+		
+		}
+		
+		updateSize();
+		fetchEvents(renderEvents);
+		
+	};
+	
+	
+	function updateSize() {
+		
+		bodyTable.width('');
+		body.height(Math.round(body.width() / options.aspectRatio) - head.height());
+		
+		// need this for IE6/7. triggers clientWidth to be calculated for 
+		// later user in this function. this is ridiculous
+		body[0].clientWidth;
+		
+		var topTDs = head.find('tr:first th'),
+			stripeTDs = bg.find('td'),
+			contentWidth = body[0].clientWidth;
+		bodyTable.width(contentWidth);
+		
+		// time-axis width
+		timeWidth = 0;
+		setOuterWidth(
+			head.find('th.fc-axis').add(body.find('th.fc-axis:first'))
+				.width('')
+				.each(function() {
+					timeWidth = Math.max(timeWidth, $(this).outerWidth());
+				})
+				.add(stripeTDs.eq(0)),
+			timeWidth
+		);
+		
+		// column width
+		colWidth = Math.floor((contentWidth - timeWidth) / colCnt);
+		setOuterWidth(stripeTDs.slice(0, -1), colWidth);
+		setOuterWidth(topTDs.slice(1, -2), colWidth);
+		setOuterWidth(topTDs.slice(-2, -1), contentWidth - timeWidth - colWidth*(colCnt-1));
+		
+		bg.css({
+			top: head.find('tr').height(),
+			left: timeWidth,
+			width: contentWidth - timeWidth,
+			height: element.height()
+		});
+		
+		rowHeight = body.find('tr:eq(1)').height(); // use second, first prob doesn't have a border
+	}
+	
+	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);
+			view.trigger('dayClick', this, date, false, ev);
+		}else{
+			view.trigger('dayClick', this, date, true, ev);
+		}
+	}
+	
+	
+	
+	
+	
+	
+	
+	
+	
+	/********************************** event rendering *********************************/
+	
+	
+	function renderEvents(events) {
+		return;
+		
+		var i, len=events.length, event,
+			fakeID=0, nextDay,
+			slotEvents=[], dayEvents=[];
+			
+		for (i=0; i<len; i++) {
+			event = events[i];
+			event._id = typeof event.id == 'undefined' ? '_fc' + fakeID++ : event.id + '';
+			if (eventsByID[event._id]) {
+				eventsByID[event._id].push(event);
+			}else{
+				eventsByID[event._id] = [event];
+			}
+			if (event.hasTime) {
+				event._end = event.end || addMinutes(cloneDate(event.start), options.defaultEventMinutes);
+			}else{
+				event._end = addDays(cloneDate(event.end || event.start), 1);
+			}
+			if (event.start < view.end && event._end > view.start) {
+				if (event.hasTime) {
+					event._end = event.end || addMinutes(cloneDate(event.start), options.defaultEventMinutes);
+					slotEvents.push(event);
+				}else{
+					event._end = addDays(cloneDate(event.end || event.start), 1);
+					dayEvents.push(event);
+				}
+			}
+		}
+		
+		cachedEvents = events;
+		cachedSlotSegs = compileSlotSegs(slotEvents, view.start, view.end);
+		cachedDaySegs = levelizeSegs(sliceSegs(dayEvents, view.start, view.end));
+		
+		renderSlotSegs(cachedSlotSegs);
+		renderDaySegs(cachedDaySegs);
+		
+	}
+	
+	
+	function rerenderEvents(skipCompile) {
+		return;
+		clearEvents();
+		if (skipCompile) {
+			renderSlotSegs(cachedSlotSegs);
+			renderDaySegs(cachedDaySegs);
+		}else{
+			renderEvents(cachedEvents);
+		}
+	}
+	
+	
+	// renders events in the 'time slots' at the bottom
+	
+	function renderSlotSegs(segCols) {
+		var colI, colLen=segCols.length, col,
+			levelI, level,
+			segI, seg,
+			event, start, end,
+			top, bottom,
+			tdInner, left, width,
+			eventElement, anchorElement, timeElement, titleElement;
+		for (colI=0; colI<colLen; colI++) {
+			col = segCols[colI];
+			for (levelI=0; levelI<col.length; levelI++) {
+				level = col[levelI];
+				for (segI=0; segI<level.length; segI++) {
+					seg = level[segI];
+					event = seg.event;
+					top = timeCoord(seg.start, seg.start);
+					bottom = timeCoord(seg.start, seg.end);
+					tdInner = bg.find('td:eq('+colI+') div div');
+					availWidth = tdInner.width();
+					left = timeWidth + tdInner.position().left + // leftmost possible
+						(availWidth / (levelI + seg.right + 1) * levelI); // indentation
+					if (levelI == 0) {
+						if (seg.right == 0) {
+							// can be entire width, aligned left
+							width = availWidth * .96;
+						}else{
+							// moderately wide, aligned left still
+							width = ((availWidth / (seg.right + 1)) - (12/2)) * 2; // 12 is the predicted width of resizer =
+						}
+					}else{
+						// indented and thinner
+						width = availWidth / (levelI + seg.right + 1);
+					}
+					eventElement = $("<div class='fc-event fc-event-vert' />")
+						.append(anchorElement = $("<a><span class='fc-event-bg'/></a>")
+							.append(titleElement = $("<span class='fc-event-title'/>")
+								.text(event.title)))
+						.css({
+							position: 'absolute',
+							zIndex: 1000,
+							top: top,
+							left: left
+						});
+					if (event.url) {
+						anchorElement.attr('href', event.url);
+					}
+					if (seg.isStart) {
+						eventElement.addClass('fc-corner-top');
+						// add the time header
+						anchorElement
+							.prepend(timeElement = $("<span class='fc-event-time'/>")
+								.text(formatDates(event.start, event.end, options.agendaEventTimeFormat)))
+					}else{
+						timeElement = null;
+					}
+					if (seg.isEnd) {
+						eventElement.addClass('fc-corner-bottom');
+						resizableSlotEvent(event, eventElement, timeElement);
+					}
+					eventElement.appendTo(panel);
+					setOuterWidth(eventElement, width, true);
+					setOuterHeight(eventElement, bottom-top, true);
+					if (timeElement && eventElement.height() - titleElement.position().top < 10) {
+						// event title doesn't have enough room, but next to the time
+						timeElement.text(formatDate(event.start, options.agendaEventTimeFormat) + ' - ' + event.title);
+						titleElement.remove();
+					}
+					draggableSlotEvent(event, eventElement, timeElement);
+					reportEventElement(event, eventElement);
+				}
+			}
+		}
+	}
+	
+	
+	// renders 'all-day' events at the top
+	
+	function renderDaySegs(segRow) {
+		var td = head.find('td');
+		var tdInner = td.find('div div');
+		var top = tdInner.position().top,
+			rowHeight = 0,
+			i, len=segRow.length, level,
+			levelHeight,
+			j, seg,
+			event, left, right,
+			eventElement, anchorElement;
+		for (i=0; i<len; i++) {
+			level = segRow[i];
+			levelHeight = 0;
+			for (j=0; j<level.length; j++) {
+				seg = level[j];
+				event = seg.event;
+				left = seg.isStart ?
+					bg.find('td:eq('+((seg.start.getDay()+dayCnt)%dayCnt)+') div div') :
+					bg.find('td:eq('+((seg.start.getDay()+dayCnt)%dayCnt)+')');
+				left = left.position().left;
+				right = seg.isEnd ?
+					bg.find('td:eq('+((seg.end.getDay()-1+dayCnt)%dayCnt)+') div div') :
+					bg.find('td:eq('+((seg.end.getDay()-1+dayCnt)%dayCnt)+')');
+				right = right.position().left + right.outerWidth();
+				eventElement = $("<div class='fc-event fc-event-hori' />")
+					.append(anchorElement = $("<a/>")
+						.append($("<span class='fc-event-title' />")
+							.text(event.title)))
+					.css({
+						position: 'absolute',
+						top: top,
+						left: timeWidth + left
+					});
+				if (seg.isStart) {
+					eventElement.addClass('fc-corner-left');
+				}
+				if (seg.isEnd) {
+					eventElement.addClass('fc-corner-right');
+				}
+				if (event.url) {
+					anchorElement.attr('href', event.url);
+				}
+				eventElement.appendTo(head);
+				setOuterWidth(eventElement, right-left, true);
+				draggableDayEvent(event, eventElement);
+				//resizableDayEvent(event, eventElement);
+				reportEventElement(event, eventElement);
+				levelHeight = Math.max(levelHeight, eventElement.outerHeight(true));
+			}
+			top += levelHeight;
+			rowHeight += levelHeight;
+		}
+		tdInner.height(rowHeight);
+		//bg.height(element.height()); // tdInner might have pushed the body down, so resize
+		//updateSize();
+	}
+	
+	
+	
+	/******************************************* draggable *****************************************/
+	
+	
+	// when event starts out IN TIMESLOTS
+	
+	function draggableSlotEvent(event, eventElement, timeElement) {
+		var origPosition, origMarginTop,
+			prevSlotDelta, slotDelta,
+			matrix;
+		eventElement.draggable({
+			zIndex: 1001,
+			scroll: false,
+			grid: [dayWidth, slotHeight],
+			axis: dayCnt==1 ? 'y' : false,
+			cancel: '.ui-resizable-handle',
+			opacity: .5,
+			start: function(ev, ui) {
+				if ($.browser.msie) {
+					eventElement.find('span.fc-event-bg').hide();
+				}
+				origPosition = eventElement.position();
+				origMarginTop = parseInt(eventElement.css('margin-top')) || 0;
+				prevSlotDelta = 0;
+				matrix = new HoverMatrix(function(cell) {
+					if (event.hasTime) {
+						// event is an original slot-event
+						if (cell && cell.row == 0) {
+							// but needs to convert to temporary full-day-event
+							var topDiff = panel.offset().top - head.offset().top;
+							eventElement.css('margin-top', origMarginTop + topDiff)
+								.appendTo(head);
+							// TODO: bug in IE8 w/ above technique, draggable ends immediately
+							event.hasTime = false;
+							if (timeElement) {
+								timeElement.hide();
+							}
+							eventElement.draggable('option', 'grid', null);
+						}
+					}else{
+						// event is a temporary full-day-event
+						if (cell && cell.row == 1) {
+							// but needs to convert to original slot-event
+							eventElement.css('margin-top', origMarginTop)
+								.appendTo(panel);
+							event.hasTime = true;
+							if (timeElement) {
+								timeElement.css('display', ''); // show() was causing display=inline
+							}
+							eventElement.draggable('option', 'grid', [dayWidth, slotHeight]);
+						}
+					}
+					if (cell && cell.row == 0) {
+						showDayOverlay(cell);
+					}else{
+						hideDayOverlay();
+					}
+				});
+				matrix.row(head.find('td'));
+				bg.find('td').each(function() {
+					matrix.col(this);
+				});
+				matrix.row(body);
+				matrix.start();
+				hideSimilarEvents(event, eventElement);
+			},
+			drag: function(ev, ui) {
+				slotDelta = Math.round((ui.position.top - origPosition.top) / slotHeight);
+				if (slotDelta != prevSlotDelta) {
+					if (timeElement && event.hasTime) {
+						// update time header
+						var newStart = addMinutes(cloneDate(event.start), slotDelta * options.slotMinutes),
+							newEnd;
+						if (event.end) {
+							newEnd = addMinutes(cloneDate(event.end), slotDelta * options.slotMinutes);
+						}
+						timeElement.text(formatDates(newStart, newEnd, options.agendaEventTimeFormat));
+					}
+					prevSlotDelta = slotDelta;
+				}
+				matrix.mouse(ev.pageX, ev.pageY);
+			},
+			stop: function(ev, ui) {
+				if (event.hasTime) {
+					if (matrix.cell) {
+						// over slots
+						var dayDelta = Math.round((ui.position.left - origPosition.left) / dayWidth);
+						reportEventMove(event, dayDelta, true, slotDelta * options.slotMinutes);
+					}
+				}else{
+					// over full-days
+					if (!matrix.cell) {
+						// was being dragged over full-days, but finished over nothing, reset
+						event.hasTime = true;
+					}else{
+						event.end = null;
+						reportEventMove(event, matrix.cell.colDelta);
+					}
+				}
+				hideDayOverlay();
+				rerenderEvents();
+			}
+		});
+		
+	}
+	
+	
+	// when event starts out FULL-DAY
+	
+	function draggableDayEvent(event, eventElement) {
+		var origWidth, matrix;
+		eventElement.draggable({
+			zIndex: 1001,
+			start: function() {
+				origWidth = eventElement.width();
+				matrix = new HoverMatrix(function(cell) {
+					if (!cell) {
+						// mouse is outside of everything
+						hideDayOverlay();
+					}else{
+						if (cell.row == 0) {
+							// on full-days
+							if (event.hasTime) {
+								// and needs to be original full-day event
+								eventElement
+									.width(origWidth)
+									.height('')
+									.draggable('option', 'grid', null);
+								event.hasTime = false;
+							}
+							showDayOverlay(cell);
+						}else{
+							// mouse is over bottom slots
+							if (!event.hasTime) {
+								// convert event to temporary slot-event
+								//if (+cloneDate(event.start, true) == +cloneDate(event._end, true)) {
+									// only change styles if a 1-day event
+									eventElement
+										.width(dayWidth - 10) // don't use entire width
+										.height(slotHeight * Math.round(options.defaultEventMinutes/options.slotMinutes) - 2);
+								//}
+								eventElement.draggable('option', 'grid', [dayWidth, 1]);
+								event.hasTime = true;
+							}
+							hideDayOverlay();
+						}
+					}
+				});
+				matrix.row(head.find('td'));
+				bg.find('td').each(function() {
+					matrix.col(this);
+				});
+				matrix.row(body);
+				matrix.start();
+				hideSimilarEvents(event, eventElement);
+			},
+			drag: function(ev, ui) {
+				matrix.mouse(ev.pageX, ev.pageY);
+			},
+			stop: function() {
+				var cell = matrix.cell;
+				if (!cell) {
+					// over nothing
+					if (event.hasTime) {
+						// event was on the slots before going out, convert back
+						event.hasTime = false;
+					}
+				}else{
+					if (!event.hasTime) {
+						// event has been dropped on a full-day
+						reportEventMove(event, cell.colDelta);
+					}else{
+						// event has been dropped on the slots
+						var slots = Math.floor((eventElement.offset().top - panel.offset().top) / slotHeight);
+						event.end = null;
+						reportEventMove(event, cell.colDelta, false, slots * options.slotMinutes);
+					}
+				}
+				hideDayOverlay();
+				rerenderEvents();
+			}
+		});
+	}
+	
+	
+	
+	/************************************* resizable **************************************/
+	
+
+	function resizableSlotEvent(event, eventElement, timeElement) {
+		var prevSlotDelta, slotDelta, newEnd;
+		eventElement
+			.resizable({
+				handles: 's',
+				grid: [0, slotHeight],
+				start: function() {
+					prevSlotDelta = 0;
+					hideSimilarEvents(event, eventElement);
+					if ($.browser.msie && $.browser.version == '6.0') {
+						eventElement.css('overflow', 'hidden');
+					}
+				},
+				resize: function(ev, ui) {
+					slotDelta = Math.round((Math.max(slotHeight, ui.size.height) - ui.originalSize.height) / slotHeight);
+					if (slotDelta != prevSlotDelta) {
+						newEnd = addMinutes(cloneDate(event._end), options.slotMinutes * slotDelta);
+						if (timeElement) {
+							timeElement.text(formatDates(event.start, newEnd, options.agendaEventTimeFormat));
+						}
+						prevSlotDelta = slotDelta;
+					}
+				},
+				stop: function(ev, ui) {
+					reportEventResize(event, 0, true, options.slotMinutes * slotDelta);
+					rerenderEvents();
+				}
+			})
+			.find('div.ui-resizable-s').text('=');
+	}
+	
+	
+	function resizableDayEvent(event, eventElement) {
+		eventElement.resizable({
+			handles: 'e',
+			grid: [dayWidth, 0],
+			start: function() {
+				hideSimilarEvents(event, eventElement);
+			},
+			stop: function(ev, ui) {
+				var dayDelta = Math.round((Math.max(dayWidth, ui.size.width) - ui.originalSize.width) / dayWidth);
+				reportEventResize(event, dayDelta);
+				rerenderEvents();
+			}
+		});
+	}
+	
+	
+	
+	/**************************************** misc **************************************/
+	
+	// get the Y coordinate of the given time on the given day
+	
+	function timeCoord(day, time) {
+		var nextDay = addDays(cloneDate(day), 1);
+		if (time < nextDay) {
+			var slotMinutes = options.slotMinutes;
+			var minutes = time.getHours()*60 + time.getMinutes();
+			var slotI = Math.floor(minutes / slotMinutes);
+			var td = body.find('tr:eq(' + slotI + ') td');
+			return Math.round(td.position().top + slotHeight * ((minutes % slotMinutes) / slotMinutes));
+		}else{
+			return panel.height();
+		}
+	}
+
+}
+
+
+function compileSlotSegs(events, start, end) {
+
+	// slice by day
+	var segCols = [],
+		d1 = cloneDate(start),
+		d2 = addDays(cloneDate(start), 1);
+	for (; d1<end; addDays(d1, 1), addDays(d2, 1)) {
+		segCols.push(sliceSegs(events, d1, d2));
+	}
+	
+	var segLevelCols = [],
+		segLevels,
+		segs,
+		segI, seg,
+		levelI, level,
+		collide,
+		segI2, seg2;
+		
+	for (var i=0; i<segCols.length; i++) {
+	
+		// divide segments into levels
+		segLevels = segLevelCols[i] = [];
+		segs = segCols[i];
+		for (segI=0; segI<segs.length; segI++) {
+			seg = segs[segI];
+			for (levelI=0; true; levelI++) {
+				level = segLevels[levelI];
+				if (!level) {
+					segLevels[levelI] = [seg];
+					break;
+				}else{
+					collide = false;
+					for (segI2=0; segI2<level.length; segI2++) {
+						if (segsCollide(level[segI2], seg)) {
+							collide = true;
+							break;
+						}
+					}
+					if (!collide) {
+						level.push(seg);
+						break;
+					}
+				}
+			}
+			seg.right = 0;
+		}
+		
+		// determine # of segments to the 'right' of each segment
+		for (levelI=segLevels.length-1; levelI>0; levelI--) {
+			level = segLevels[levelI];
+			for (segI=0; segI<level.length; segI++) {
+				seg = level[segI];
+				for (segI2=0; segI2<segLevels[levelI-1].length; segI2++) {
+					seg2 = segLevels[levelI-1][segI2];
+					if (segsCollide(seg, seg2)) {
+						seg2.right = Math.max(seg2.right, seg.right+1);
+					}
+				}
+			}
+		}
+		
+	}
+	
+	return segLevelCols;
+	
+}
+
+
+function segAfters(levels) { // TODO: put in agenda.js
+	var i, j, k, level, seg, seg2;
+	for (i=levels.length-1; i>0; i--) {
+		level = levels[i];
+		for (j=0; j<level.length; j++) {
+			seg = level[j];
+			for (k=0; k<segLevels[i-1].length; k++) {
+				seg2 = segLevels[i-1][k];
+				if (segsCollide(seg, seg2)) {
+					seg2.after = Math.max(seg2.after, seg.after+1);
+				}
+			}
+		}
+	}
+}

+ 71 - 15
src/css/agenda.css

@@ -1,18 +1,78 @@
 
-.fc-event,
+/*.fc-event,
 .fc-event a,
 .fc-agenda .fc-event-time {
 	color: #fff;
 	border-style: solid;
 	border-color: blue;
 	background-color: blue;
+	}*/
+
+
+.fc .fc-axis {
+	width: 50px;
+	height: 1.6em;
+	padding: 0 4px 0 0;
+	vertical-align: middle;
+	white-space: nowrap;
+	text-align: right;
+	font-weight: normal;
+	}
+	
+	
+	
+	
+.fc .fc-agenda th,
+.fc .fc-agenda td {
+	border-width: 1px 0 0 1px;
+	}
+	
+.fc-agenda-head tr.fc-all-day th {
+	height: 2em;
+	}
+	
+.fc-agenda tr.fc-first th,
+.fc-agenda tr.fc-first td {
+	border-top: 0;
+	}
+	
+.fc .fc-agenda .fc-leftmost {
+	border-left: 0;
+	}
+	
+	
+	
+
+.fc .fc-agenda-head td,
+.fc .fc-agenda-body td {
+	background: none;
 	}
 
 
 
 	
-/* header styles */
+.fc .fc-agenda-body tr.fc-minor th,
+.fc .fc-agenda-body tr.fc-minor td {
+	border-top-style: dotted;
+	}
+	
+	
+	
+.fc .fc-divider th {
+	height: 3px;
+	border-bottom-width: 1px;
+	}
+	
+.fc .fc-divider .fc-state-default {
+	background: #eee;
+	}
+
 	
+	
+	
+	
+/* header styles */
+/*	
 .fc .fc-agenda-head th.fc-first {
 	border-left: 0;
 	}
@@ -31,39 +91,33 @@
 	border-bottom-width: 2px;
 	}
 	
-
-	
 .fc-agenda-head tr.fc-last th {
-	/*border-width: 1px 0 1px 1px;*/
+	#border-width: 1px 0 1px 1px;
 	background-image: none;
 	}
 	
 .fc-agenda-head tr.fc-last th.fc-first {
-	/*border-width: 0 2px 1px 0;*/
+	#border-width: 0 2px 1px 0;
 	}
 	
 .fc-agenda-head tr.fc-last th.fc-last {
-	/*border-width: 0 0 0 3px;*/
+	#border-width: 0 0 0 3px;
 	}
 	
 .fc .fc-agenda-head td {
-	/*border-width: 3px 0 3px 1px;*/
+	#border-width: 3px 0 3px 1px;
 	background: none;
 	}
 	
-	
-	
-	
 .fc-agenda-body {
-	/*width: 100%;*/
+	#width: 100%;
 	overflow: auto;
 	}
-	
-	
+*/
 	
 	
 
-	
+/*
 .fc .fc-agenda-body th {
 	border-width: 1px 0 0 0;
 	background-image: none;
@@ -106,6 +160,8 @@
 .fc-agenda .fc-day-content {
 	padding: 2px 1px 14px;
 	}
+*/
+	
 	
 	
 	

+ 5 - 1
src/css/main.css

@@ -14,6 +14,10 @@
 	font-size: 1em;
 	}
 	
+.fc {
+	direction: ltr;
+	}
+	
 .fc table {
 	border-collapse: collapse;
 	border-spacing: 0;
@@ -145,7 +149,7 @@ table.fc-header {
 	}
 
 .fc-header .fc-state-default span {
-	border-color: #fff #fff #cecece; /* inner border */
+	border-color: #fff #fff #d1d1d1; /* inner border */
 	background: #e8e8e8;
 	}
 	

+ 6 - 6
src/grid.js

@@ -116,10 +116,6 @@ function Grid(element, options, methods) {
 		rowCnt = r;
 		colCnt = c;
 		
-		var month = view.start.getMonth(),
-			today = clearTime(new Date()),
-			s, i, j, d = cloneDate(view.visStart);
-		
 		// update option-derived variables
 		tm = options.theme ? 'ui' : 'fc'; 
 		firstDay = options.firstDay;
@@ -131,6 +127,10 @@ function Grid(element, options, methods) {
 			dit = 0;
 		}
 		
+		var month = view.start.getMonth(),
+			today = clearTime(new Date()),
+			s, i, j, d = cloneDate(view.visStart);
+		
 		if (!tbody) { // first time, build all cells from scratch
 		
 			var table = $("<table/>").appendTo(element);
@@ -247,12 +247,12 @@ function Grid(element, options, methods) {
 	};
 	
 	
-	function dayClick() {
+	function dayClick(ev) {
 		var date = addDays(
 			cloneDate(view.visStart),
 			parseInt(this.className.match(/fc\-day(\d+)/)[1])
 		);
-		view.trigger('dayClick', this, date);
+		view.trigger('dayClick', this, date, true, ev);
 	}
 	
 	

+ 28 - 14
src/main.js

@@ -113,7 +113,7 @@ $.fn.fullCalendar = function(options) {
 	delete options.eventSources;
 	if (options.events) {
 		eventSources.push(options.events);
-		delete options.event;
+		delete options.events;
 	}
 	
 	// first event source reserved for 'sticky' events
@@ -137,7 +137,7 @@ $.fn.fullCalendar = function(options) {
 		// element
 		var _element = this,
 			element = $(this).addClass('fc'),
-			content = $("<div class='fc-content " + tm + "-widget-content'/>").appendTo(this);
+			content = $("<div class='fc-content " + tm + "-widget-content' style='position:relative'/>").appendTo(this); // relative for ie6
 		if (options.isRTL) {
 			element.addClass('fc-rtl');
 		}
@@ -148,7 +148,6 @@ $.fn.fullCalendar = function(options) {
 		// view managing
 		var date = new Date(),
 			viewName, view, // the current view
-			prevView,
 			viewInstances = {};
 		if (options.year != undefined) {
 			date.setYear(options.year);
@@ -167,7 +166,14 @@ $.fn.fullCalendar = function(options) {
 		
 		function changeView(v) {
 			if (v != viewName) {
-				prevView = view;
+				lockContentSize();
+				if (view) {
+					if (view.eventsChanged) {
+						eventsDirtyExcept(view);
+						view.eventsChanged = false;
+					}
+					view.element.hide();
+				}
 				if (viewInstances[v]) {
 					(view = viewInstances[v]).element.show();
 				}else{
@@ -175,11 +181,6 @@ $.fn.fullCalendar = function(options) {
 						$("<div class='fc-view fc-view-" + v + "'/>").appendTo(content),
 						options);
 				}
-				if (prevView && prevView.eventsChanged) {
-					// if previous view's events have been changed, mark future views' events as dirty
-					eventsDirtyExcept(prevView);
-					prevView.eventsChanged = false;
-				}
 				if (header) {
 					// update 'active' view button
 					header.find('div.fc-button-' + viewName).removeClass(tm + '-state-active');
@@ -187,10 +188,7 @@ $.fn.fullCalendar = function(options) {
 				}
 				view.name = viewName = v;
 				render();
-				if (prevView) {
-					// hide the old element AFTER the new has been rendered, preserves scrollbars
-					prevView.element.hide();
-				}
+				unlockContentSize();
 			}
 		}
 		
@@ -622,6 +620,20 @@ $.fn.fullCalendar = function(options) {
 		/* Resizing
 		-----------------------------------------------------------------------------*/
 		
+		function lockContentSize() {
+			content.css({
+				overflow: 'hidden',
+				height: Math.round(content.width() / options.aspectRatio)
+			});
+		}
+		
+		function unlockContentSize() {
+			content.css({
+				overflow: '',
+				height: ($.browser.msie && $.browser.version == '6.0') ? 1 : ''
+			});
+		}
+		
 		var elementWidth,
 			ignoreWindowResizes = false,
 			resizeCnt = 0;
@@ -630,11 +642,13 @@ $.fn.fullCalendar = function(options) {
 			if (!ignoreWindowResizes && view.date) { // view.date means the view has been rendered
 				var rcnt = ++resizeCnt; // add a delay
 				setTimeout(function() {
-					if (rcnt == resizeCnt) {
+					if (rcnt == resizeCnt && !ignoreWindowResizes) {
 						var newWidth = element.width();
 						if (newWidth != elementWidth) {
 							elementWidth = newWidth;
+							lockContentSize();
 							view.updateSize();
+							unlockContentSize();
 							view.rerenderEvents(true);
 							sizesDirtyExcept(view);
 							view.trigger('windowResize', _element);

+ 11 - 3
tests/basic.html

@@ -1,6 +1,7 @@
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 <html>
 <head>
+<link rel='stylesheet' type='text/css' href='../examples/redmond/theme.css' />
 <script type='text/javascript' src='loader.js'></script>
 <script type='text/javascript'>
 
@@ -11,11 +12,15 @@
 		var m = d.getMonth();
 		
 		$('#calendar').fullCalendar({
+			//weekMode: 'variable',
+			theme: true,
+			//isRTL: true,
 			editable: true,
+			defaultView: 'agendaWeek',
 			header: {
 				left: 'prev,next today',
 				center: 'title',
-				right: 'month,basicWeek,basicDay'
+				right: 'month,agendaWeek,agendaDay'
 			},
 			events: [
 				{
@@ -51,7 +56,10 @@
 					url: "http://facebook.com/",
 					allDay: false
 				}
-			]
+			],
+			dayClick: function(date, allDay) {
+				alert(date + ' allDay:' + allDay);
+			}
 		});
 		
 	});
@@ -67,7 +75,7 @@
 		}
 
 	#calendar {
-		width: 900px;
+		width: 80%;
 		margin: 0 auto;
 		}
 

+ 2 - 0
tests/loader.js

@@ -47,6 +47,7 @@ if (_build) {
 }else{
 	includeCSS('../src/css/main.css');
 	includeCSS('../src/css/grid.css');
+	includeCSS('../src/css/agenda.css');
 }
 
 if (_legacy) {
@@ -78,6 +79,7 @@ if (_build) {
 }else{
 	includeJS('../src/main.js');
 	includeJS('../src/grid.js');
+	includeJS('../src/agenda.js');
 	includeJS('../src/view.js');
 	includeJS('../src/util.js');
 	includeJS('../src/gcal.js');