فهرست منبع

getting closer to 1.4

Adam Shaw 16 سال پیش
والد
کامیت
20208deb66
8فایلهای تغییر یافته به همراه850 افزوده شده و 766 حذف شده
  1. 383 389
      src/agenda.js
  2. 29 6
      src/css/agenda.css
  3. 1 0
      src/css/main.css
  4. 33 70
      src/grid.js
  5. 38 24
      src/main.js
  6. 2 5
      src/util.js
  7. 346 264
      src/view.js
  8. 18 8
      tests/basic.html

+ 383 - 389
src/agenda.js

@@ -1,13 +1,24 @@
 
 
+// todo: scrolling
+// todo: check all other options
+// cleanup CSS
+// optimize moveEvent/resizeEvent, to return revert function
+
+
 /* Agenda Views: agendaWeek/agendaDay
 /* Agenda Views: agendaWeek/agendaDay
 -----------------------------------------------------------------------------*/
 -----------------------------------------------------------------------------*/
 
 
 setDefaults({
 setDefaults({
+	allDayHeader: true,
 	slotMinutes: 30,
 	slotMinutes: 30,
 	defaultEventMinutes: 120,
 	defaultEventMinutes: 120,
-	agendaTimeFormat: 'g:i{ - g:i}', // todo: merge into object w/ timeFormat
 	axisFormat: 'htt',
 	axisFormat: 'htt',
-	agendaDragOpacity: .5 // maybe merge into object
+	timeFormat: {
+		agenda: 'h:mm{ - h:mm}'
+	},
+	dragOpacity: {
+		agenda: .5
+	}
 });
 });
 
 
 views.agendaWeek = function(element, options) {
 views.agendaWeek = function(element, options) {
@@ -19,10 +30,10 @@ views.agendaWeek = function(element, options) {
 			this.title = formatDates(
 			this.title = formatDates(
 				this.start = this.visStart = addDays(cloneDate(date), -((date.getDay() - options.firstDay + 7) % 7)),
 				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),
 				addDays(cloneDate(this.end = this.visEnd = addDays(cloneDate(this.start), 7)), -1),
-				strProp(options.titleFormat, 'week'),
+				this.option('titleFormat'),
 				options
 				options
 			);
 			);
-			this.renderAgenda(7, strProp(options.columnFormat, 'week'), fetchEvents);
+			this.renderAgenda(7, this.option('columnFormat'), fetchEvents);
 		}
 		}
 	});
 	});
 };
 };
@@ -33,10 +44,10 @@ views.agendaDay = function(element, options) {
 			if (delta) {
 			if (delta) {
 				addDays(date, delta);
 				addDays(date, delta);
 			}
 			}
-			this.title = formatDate(date, strProp(options.titleFormat, 'day'), options);
+			this.title = formatDate(date, this.option('titleFormat'), options);
 			this.start = this.visStart = cloneDate(date, true);
 			this.start = this.visStart = cloneDate(date, true);
 			this.end = this.visEnd = addDays(cloneDate(this.start), 1);
 			this.end = this.visEnd = addDays(cloneDate(this.start), 1);
-			this.renderAgenda(1, strProp(options.columnFormat, 'day'), fetchEvents);
+			this.renderAgenda(1, this.option('columnFormat'), fetchEvents);
 		}
 		}
 	});
 	});
 };
 };
@@ -45,8 +56,8 @@ function Agenda(element, options, methods) {
 
 
 	var head, body, bodyContent, bodyTable, bg,
 	var head, body, bodyContent, bodyTable, bg,
 		colCnt,
 		colCnt,
-		timeWidth, colWidth, rowHeight,
-		cachedSlotSegs, cachedDaySegs,
+		timeWidth, colWidth, rowHeight, // todo: timeWidth -> axisWidth, rowHeight->slotHeight ?
+		cachedDaySegs, cachedSlotSegs,
 		tm, firstDay,
 		tm, firstDay,
 		rtl, dis, dit,  // day index sign / translate
 		rtl, dis, dit,  // day index sign / translate
 		// ...
 		// ...
@@ -57,10 +68,26 @@ function Agenda(element, options, methods) {
 		rerenderEvents: rerenderEvents,
 		rerenderEvents: rerenderEvents,
 		updateSize: updateSize,
 		updateSize: updateSize,
 		defaultEventEnd: function(event) {
 		defaultEventEnd: function(event) {
-			return addMinutes(cloneDate(event.start), options.defaultEventMinutes);
+			var start = cloneDate(event.start);
+			if (event.allDay) {
+				return start;
+			}
+			return addMinutes(start, options.defaultEventMinutes);
 		},
 		},
 		visEventEnd: function(event) {
 		visEventEnd: function(event) {
-			return addMinutes(cloneDate(event.start), options.defaultEventMinutes);
+			if (event.allDay) {
+				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);
+				}
+			}
+			if (event.end) {
+				return cloneDate(event.end);
+			}else{
+				return addMinutes(cloneDate(event.start), options.defaultEventMinutes);
+			}
 		}
 		}
 	});
 	});
 	view.init(element, options);
 	view.init(element, options);
@@ -76,7 +103,7 @@ function Agenda(element, options, methods) {
 		element.disableSelection();
 		element.disableSelection();
 	}
 	}
 	
 	
-	function renderAgenda(c, colFormat, fetchEvents) { // TODO: get z-indexes sorted out
+	function renderAgenda(c, colFormat, fetchEvents) {
 		colCnt = c;
 		colCnt = c;
 		
 		
 		// update option-derived variables
 		// update option-derived variables
@@ -101,9 +128,9 @@ function Agenda(element, options, methods) {
 				slotNormal = options.slotMinutes % 15 == 0, //...
 				slotNormal = options.slotMinutes % 15 == 0, //...
 			
 			
 			// head
 			// head
-			s = "<div class='fc-agenda-head' style='position:relative;z-index:3'>" +
+			s = "<div class='fc-agenda-head' style='position:relative;z-index:4'>" +
 				"<table style='width:100%'>" +
 				"<table style='width:100%'>" +
-				"<tr class='fc-first'>" +
+				"<tr class='fc-first" + (options.allDayHeader ? '' : ' fc-last') + "'>" +
 				"<th class='fc-leftmost " +
 				"<th class='fc-leftmost " +
 					tm + "-state-default'>&nbsp;</th>";
 					tm + "-state-default'>&nbsp;</th>";
 			for (i=0; i<colCnt; i++) {
 			for (i=0; i<colCnt; i++) {
@@ -113,14 +140,17 @@ function Agenda(element, options, methods) {
 					"'>" + formatDate(d, colFormat, options) + "</th>";
 					"'>" + formatDate(d, colFormat, options) + "</th>";
 				addDays(d, dis);
 				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>";
+			s+= "<th class='" + tm + "-state-default'>&nbsp;</th></tr>";
+			if (options.allDayHeader) {
+				s+= "<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 fc-last'><th colspan='" + (colCnt+2) + "' class='" +
+						tm + "-state-default fc-leftmost'><div/></th></tr>";
+			}
+			s+= "</table></div>";
 			head = $(s).appendTo(element);
 			head = $(s).appendTo(element);
 			head.find('td').click(slotClick);
 			head.find('td').click(slotClick);
 			
 			
@@ -134,7 +164,7 @@ function Agenda(element, options, methods) {
 					"'><th class='fc-axis fc-leftmost " + tm + "-state-default'>" +
 					"'><th class='fc-axis fc-leftmost " + tm + "-state-default'>" +
 					((!slotNormal || minutes==0) ? formatDate(d, options.axisFormat) : '&nbsp;') + 
 					((!slotNormal || minutes==0) ? formatDate(d, options.axisFormat) : '&nbsp;') + 
 					"</th><td class='fc-slot" + i + ' ' +
 					"</th><td class='fc-slot" + i + ' ' +
-						tm + "-state-default'><div class='fc-day-content'><div>&nbsp;</div></div></td></tr>";
+						tm + "-state-default'><div>&nbsp;</div></td></tr>";
 				addMinutes(d, options.slotMinutes);
 				addMinutes(d, options.slotMinutes);
 			}
 			}
 			s += "</table>";
 			s += "</table>";
@@ -214,7 +244,7 @@ function Agenda(element, options, methods) {
 		// time-axis width
 		// time-axis width
 		timeWidth = 0;
 		timeWidth = 0;
 		setOuterWidth(
 		setOuterWidth(
-			head.find('th.fc-axis').add(body.find('th.fc-axis:first'))
+			head.find('tr:lt(2) th:first').add(body.find('tr:first th'))
 				.width('')
 				.width('')
 				.each(function() {
 				.each(function() {
 					timeWidth = Math.max(timeWidth, $(this).outerWidth());
 					timeWidth = Math.max(timeWidth, $(this).outerWidth());
@@ -256,77 +286,146 @@ function Agenda(element, options, methods) {
 	
 	
 	
 	
 	
 	
-	
-	
-	
-	
-	
-	
-	/********************************** event rendering *********************************/
+	/* Event Rendering
+	-----------------------------------------------------------------------------*/
 	
 	
 	
 	
 	function renderEvents(events) {
 	function renderEvents(events) {
-		return;
+		view.reportEvents(events);
 		
 		
-		var i, len=events.length, event,
-			fakeID=0, nextDay,
-			slotEvents=[], dayEvents=[];
-			
+		var i, len=events.length,
+			dayEvents=[],
+			slotEvents=[];
 		for (i=0; i<len; i++) {
 		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);
+			if (events[i].allDay) {
+				dayEvents.push(events[i]);
 			}else{
 			}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);
-				}
+				slotEvents.push(events[i]);
 			}
 			}
 		}
 		}
 		
 		
-		cachedEvents = events;
-		cachedSlotSegs = compileSlotSegs(slotEvents, view.start, view.end);
-		cachedDaySegs = levelizeSegs(sliceSegs(dayEvents, view.start, view.end));
-		
-		renderSlotSegs(cachedSlotSegs);
-		renderDaySegs(cachedDaySegs);
-		
+		renderDaySegs(cachedDaySegs = stackSegs(view.sliceSegs(dayEvents, view.visStart, view.visEnd)));
+		renderSlotSegs(cachedSlotSegs = compileSlotSegs(slotEvents));
 	}
 	}
 	
 	
 	
 	
 	function rerenderEvents(skipCompile) {
 	function rerenderEvents(skipCompile) {
-		return;
-		clearEvents();
+		view.clearEvents();
 		if (skipCompile) {
 		if (skipCompile) {
-			renderSlotSegs(cachedSlotSegs);
 			renderDaySegs(cachedDaySegs);
 			renderDaySegs(cachedDaySegs);
+			renderSlotSegs(cachedSlotSegs);
 		}else{
 		}else{
-			renderEvents(cachedEvents);
+			renderEvents(view.cachedEvents);
+		}
+	}
+	
+	
+	function compileSlotSegs(events) {
+		var d1 = cloneDate(view.visStart),
+			d2 = addDays(cloneDate(d1), 1),
+			levels,
+			segCols = [],
+			i=0;
+		for (; i<colCnt; i++) {
+			levels = stackSegs(view.sliceSegs(events, d1, d2));
+			countForwardSegs(levels);
+			segCols.push(levels);
+			addDays(d1, 1);
+			addDays(d2, 1);
+		}
+		return segCols;
+	}
+	
+	
+	
+	// renders 'all-day' events at the top
+	
+	function renderDaySegs(segRow) {
+		if (options.allDayHeader) {
+			var td = head.find('td'),
+				tdInner = td.find('div div'),
+				top = tdInner.position().top,
+				rowHeight = 0,
+				i, len=segRow.length, level,
+				levelHeight,
+				j, seg,
+				event,
+				eventClasses,
+				leftDay, leftRounded,
+				rightDay, rightRounded,
+				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;
+					eventClasses = ['fc-event', 'fc-event-hori'];
+					if (rtl) {
+						leftDay = seg.end.getDay() - 1;
+						leftRounded = seg.isEnd;
+						rightDay = seg.start.getDay();
+						rightRounded = seg.isStart;
+					}else{
+						leftDay = seg.start.getDay();
+						leftRounded = seg.isStart;
+						rightDay = seg.end.getDay() - 1;
+						rightRounded = seg.isEnd;
+					}
+					if (leftRounded) {
+						eventClasses.push('fc-corner-left');
+						left = bg.find('td:eq('+(((leftDay-firstDay+colCnt)%colCnt)*dis+dit)+') div div').position().left + timeWidth;
+					}else{
+						left = timeWidth;
+					}
+					if (rightRounded) {
+						eventClasses.push('fc-corner-right');
+						right = bg.find('td:eq('+(((rightDay-firstDay+colCnt)%colCnt)*dis+dit)+') div div');
+						right = right.position().left + right.width() + timeWidth;
+					}else{
+						right = timeWidth + bg.width();
+					}
+					eventElement = $("<div class='" + eventClasses.join(' ') + "'/>")
+						.append(anchorElement = $("<a/>")
+							.append($("<span class='fc-event-title' />")
+								.text(event.title)))
+						.css({
+							position: 'absolute',
+							top: top,
+							left: left,
+							zIndex: 8
+						})
+						.appendTo(head);
+					setOuterWidth(eventElement, right-left, true);
+					if (seg.isEnd) {
+						view.resizableDayEvent(event, eventElement, colWidth);
+					}
+					draggableDayEvent(event, eventElement, seg.isStart);
+					view.reportEventElement(event, eventElement);
+					levelHeight = Math.max(levelHeight, eventElement.outerHeight(true));
+				}
+				top += levelHeight;
+				rowHeight += levelHeight;
+			}
+			tdInner.height(rowHeight);
+			updateSize(); // tdInner might have pushed the body down, so resize
 		}
 		}
 	}
 	}
 	
 	
 	
 	
+	
 	// renders events in the 'time slots' at the bottom
 	// renders events in the 'time slots' at the bottom
 	
 	
 	function renderSlotSegs(segCols) {
 	function renderSlotSegs(segCols) {
 		var colI, colLen=segCols.length, col,
 		var colI, colLen=segCols.length, col,
 			levelI, level,
 			levelI, level,
 			segI, seg,
 			segI, seg,
-			event, start, end,
+			forward,
+			event,
 			top, bottom,
 			top, bottom,
-			tdInner, left, width,
+			tdInner,
+			width, left,
 			eventElement, anchorElement, timeElement, titleElement;
 			eventElement, anchorElement, timeElement, titleElement;
 		for (colI=0; colI<colLen; colI++) {
 		for (colI=0; colI<colLen; colI++) {
 			col = segCols[colI];
 			col = segCols[colI];
@@ -334,35 +433,38 @@ function Agenda(element, options, methods) {
 				level = col[levelI];
 				level = col[levelI];
 				for (segI=0; segI<level.length; segI++) {
 				for (segI=0; segI<level.length; segI++) {
 					seg = level[segI];
 					seg = level[segI];
+					forward = seg.forward || 0;
 					event = seg.event;
 					event = seg.event;
-					top = timeCoord(seg.start, seg.start);
-					bottom = timeCoord(seg.start, seg.end);
-					tdInner = bg.find('td:eq('+colI+') div div');
+					top = timePosition(seg.start, seg.start);
+					bottom = timePosition(seg.start, seg.end);
+					tdInner = bg.find('td:eq(' + (colI*dis + dit) + ') div div');
 					availWidth = tdInner.width();
 					availWidth = tdInner.width();
-					left = timeWidth + tdInner.position().left + // leftmost possible
-						(availWidth / (levelI + seg.right + 1) * levelI); // indentation
-					if (levelI == 0) {
-						if (seg.right == 0) {
+					if (levelI) {
+						// indented and thin
+						width = availWidth / (levelI + forward + 1);
+					}else{
+						if (forward) {
+							// moderately wide, aligned left still
+							width = ((availWidth / (forward + 1)) - (12/2)) * 2; // 12 is the predicted width of resizer =
+						}else{
 							// can be entire width, aligned left
 							// can be entire width, aligned left
 							width = availWidth * .96;
 							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);
 					}
 					}
+					left = timeWidth + tdInner.position().left +       // leftmost possible
+						(availWidth / (levelI + forward + 1) * levelI) // indentation
+						* dis + (rtl ? availWidth - width : 0);        // rtl
 					eventElement = $("<div class='fc-event fc-event-vert' />")
 					eventElement = $("<div class='fc-event fc-event-vert' />")
 						.append(anchorElement = $("<a><span class='fc-event-bg'/></a>")
 						.append(anchorElement = $("<a><span class='fc-event-bg'/></a>")
 							.append(titleElement = $("<span class='fc-event-title'/>")
 							.append(titleElement = $("<span class='fc-event-title'/>")
 								.text(event.title)))
 								.text(event.title)))
 						.css({
 						.css({
 							position: 'absolute',
 							position: 'absolute',
-							zIndex: 1000,
+							zIndex: 8,
 							top: top,
 							top: top,
 							left: left
 							left: left
-						});
+						})
+						.appendTo(bodyContent);
 					if (event.url) {
 					if (event.url) {
 						anchorElement.attr('href', event.url);
 						anchorElement.attr('href', event.url);
 					}
 					}
@@ -371,7 +473,7 @@ function Agenda(element, options, methods) {
 						// add the time header
 						// add the time header
 						anchorElement
 						anchorElement
 							.prepend(timeElement = $("<span class='fc-event-time'/>")
 							.prepend(timeElement = $("<span class='fc-event-time'/>")
-								.text(formatDates(event.start, event.end, options.agendaEventTimeFormat)))
+								.text(formatDates(event.start, event.end, view.option('timeFormat'))))
 					}else{
 					}else{
 						timeElement = null;
 						timeElement = null;
 					}
 					}
@@ -379,420 +481,312 @@ function Agenda(element, options, methods) {
 						eventElement.addClass('fc-corner-bottom');
 						eventElement.addClass('fc-corner-bottom');
 						resizableSlotEvent(event, eventElement, timeElement);
 						resizableSlotEvent(event, eventElement, timeElement);
 					}
 					}
-					eventElement.appendTo(panel);
 					setOuterWidth(eventElement, width, true);
 					setOuterWidth(eventElement, width, true);
 					setOuterHeight(eventElement, bottom-top, true);
 					setOuterHeight(eventElement, bottom-top, true);
 					if (timeElement && eventElement.height() - titleElement.position().top < 10) {
 					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);
+						// event title doesn't have enough room, put next to the time
+						timeElement.text(formatDate(event.start, view.option('timeFormat')) + ' - ' + event.title);
 						titleElement.remove();
 						titleElement.remove();
 					}
 					}
 					draggableSlotEvent(event, eventElement, timeElement);
 					draggableSlotEvent(event, eventElement, timeElement);
-					reportEventElement(event, eventElement);
+					view.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();
-	}
-	
 	
 	
+	/* Event Dragging
+	-----------------------------------------------------------------------------*/
 	
 	
-	/******************************************* draggable *****************************************/
 	
 	
 	
 	
-	// when event starts out IN TIMESLOTS
+	// when event starts out FULL-DAY
 	
 	
-	function draggableSlotEvent(event, eventElement, timeElement) {
-		var origPosition, origMarginTop,
-			prevSlotDelta, slotDelta,
+	function draggableDayEvent(event, eventElement, isStart) {
+		var origPosition, origWidth,
+			resetElement,
+			allDay=true,
 			matrix;
 			matrix;
 		eventElement.draggable({
 		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();
-				}
+			zIndex: 9,
+			opacity: view.option('month'), // use whatever the month view was using
+			start: function(ev) {
 				origPosition = eventElement.position();
 				origPosition = eventElement.position();
-				origMarginTop = parseInt(eventElement.css('margin-top')) || 0;
-				prevSlotDelta = 0;
+				origWidth = eventElement.width();
+				resetElement = function() {
+					if (!allDay) {
+						eventElement
+							.width(origWidth)
+							.height('')
+							.draggable('option', 'grid', null);
+						allDay = true;
+					}
+				};
 				matrix = new HoverMatrix(function(cell) {
 				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', 'revert', !cell || !cell.rowDelta && !cell.colDelta);
+					if (cell) {
+						if (!cell.row) { // on full-days
+							resetElement();
+							view.showOverlay(cell);
+						}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
+									rowHeight * Math.round(
+										(event.end ? ((event.end - event.start)/MINUTE_MS) : options.defaultEventMinutes)
+										/options.slotMinutes)
+								);
+								eventElement.draggable('option', 'grid', [colWidth, 1]);
+								allDay = false;
 							}
 							}
-							eventElement.draggable('option', 'grid', [dayWidth, slotHeight]);
+							view.hideOverlay();
 						}
 						}
-					}
-					if (cell && cell.row == 0) {
-						showDayOverlay(cell);
-					}else{
-						hideDayOverlay();
+					}else{ // mouse is outside of everything
+						view.hideOverlay();
 					}
 					}
 				});
 				});
+				view.hideEvents(event, eventElement);
 				matrix.row(head.find('td'));
 				matrix.row(head.find('td'));
 				bg.find('td').each(function() {
 				bg.find('td').each(function() {
 					matrix.col(this);
 					matrix.col(this);
 				});
 				});
 				matrix.row(body);
 				matrix.row(body);
-				matrix.start();
-				hideSimilarEvents(event, eventElement);
+				matrix.mouse(ev.pageX, ev.pageY);
 			},
 			},
 			drag: function(ev, ui) {
 			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);
 				matrix.mouse(ev.pageX, ev.pageY);
 			},
 			},
 			stop: function(ev, ui) {
 			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);
-					}
+				view.hideOverlay();
+				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 || !dayDelta && !cell.rowDelta) {
+					// over nothing (has reverted)
+					resetElement();
+					view.showEvents(event, eventElement);
 				}else{
 				}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);
-					}
+					view.eventDrop(
+						this, event, dayDelta,
+						allDay ? 0 : // minute delta
+							Math.round((eventElement.offset().top - bodyContent.offset().top) / rowHeight)
+							* options.slotMinutes
+							- (event.start.getHours() * 60 + event.start.getMinutes()),
+						allDay, ev, ui
+					);
 				}
 				}
-				hideDayOverlay();
-				rerenderEvents();
 			}
 			}
 		});
 		});
-		
 	}
 	}
 	
 	
 	
 	
-	// when event starts out FULL-DAY
 	
 	
-	function draggableDayEvent(event, eventElement) {
-		var origWidth, matrix;
+	// when event starts out IN TIMESLOTS
+	
+	function draggableSlotEvent(event, eventElement, timeElement) {
+		var origPosition,
+			resetElement,
+			prevSlotDelta, slotDelta,
+			allDay=false,
+			matrix;
 		eventElement.draggable({
 		eventElement.draggable({
-			zIndex: 1001,
-			start: function() {
-				origWidth = eventElement.width();
+			zIndex: 9,
+			scroll: false,
+			grid: [colWidth, rowHeight],
+			axis: colCnt==1 ? 'y' : false,
+			opacity: view.option('dragOpacity'),
+			start: function(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) {
+						if (timeElement) {
+							timeElement.css('display', ''); // show() was causing display=inline
+						}
+						eventElement.draggable('option', 'grid', [colWidth, rowHeight]);
+						allDay = false;
+					}
+				};
+				prevSlotDelta = 0;
 				matrix = new HoverMatrix(function(cell) {
 				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;
+					eventElement.draggable('option', 'revert', !cell);
+					if (cell) {
+						if (!cell.row && options.allDayHeader) { // over full days
+							if (!allDay) {
+								// convert to temporary all-day event
+								allDay = true;
+								if (timeElement) {
+									timeElement.hide();
+								}
+								eventElement.draggable('option', 'grid', null);
 							}
 							}
-							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();
+							view.showOverlay(cell);
+						}else{ // on slots
+							resetElement();
+							view.hideOverlay();
 						}
 						}
+					}else{
+						view.hideOverlay();
 					}
 					}
 				});
 				});
-				matrix.row(head.find('td'));
+				if (options.allDayHeader) {
+					matrix.row(head.find('td'));
+				}
 				bg.find('td').each(function() {
 				bg.find('td').each(function() {
 					matrix.col(this);
 					matrix.col(this);
 				});
 				});
 				matrix.row(body);
 				matrix.row(body);
-				matrix.start();
-				hideSimilarEvents(event, eventElement);
+				matrix.mouse(ev.pageX, ev.pageY);
+				view.hideEvents(event, eventElement);
 			},
 			},
 			drag: function(ev, ui) {
 			drag: function(ev, ui) {
+				slotDelta = Math.round((ui.position.top - origPosition.top) / rowHeight);
+				if (slotDelta != prevSlotDelta) {
+					if (timeElement && !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.pageX, ev.pageY);
 				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;
+			stop: function(ev, ui) {
+				view.hideOverlay();
+				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{
 				}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);
-					}
+					view.eventDrop(
+						this, event, dayDelta,
+						allDay ? 0 : slotDelta * options.slotMinutes, // minute delta
+						allDay, ev, ui
+					);
 				}
 				}
-				hideDayOverlay();
-				rerenderEvents();
 			}
 			}
 		});
 		});
+		
 	}
 	}
 	
 	
 	
 	
 	
 	
-	/************************************* resizable **************************************/
 	
 	
+	/* Event Resizing
+	-----------------------------------------------------------------------------*/
+	
+	// for TIMESLOT events
 
 
 	function resizableSlotEvent(event, eventElement, timeElement) {
 	function resizableSlotEvent(event, eventElement, timeElement) {
-		var prevSlotDelta, slotDelta, newEnd;
+		var slotDelta, prevSlotDelta;
 		eventElement
 		eventElement
 			.resizable({
 			.resizable({
 				handles: 's',
 				handles: 's',
-				grid: [0, slotHeight],
+				grid: rowHeight,
 				start: function() {
 				start: function() {
-					prevSlotDelta = 0;
-					hideSimilarEvents(event, eventElement);
+					slotDelta = prevSlotDelta = 0;
+					view.hideEvents(event, eventElement);
 					if ($.browser.msie && $.browser.version == '6.0') {
 					if ($.browser.msie && $.browser.version == '6.0') {
 						eventElement.css('overflow', 'hidden');
 						eventElement.css('overflow', 'hidden');
 					}
 					}
+					eventElement.css('z-index', 9);
 				},
 				},
 				resize: function(ev, ui) {
 				resize: function(ev, ui) {
-					slotDelta = Math.round((Math.max(slotHeight, ui.size.height) - ui.originalSize.height) / slotHeight);
+					// don't rely on ui.size.height, doesn't take grid into account
+					slotDelta = Math.round((Math.max(rowHeight, eventElement.height()) - ui.originalSize.height) / rowHeight);
 					if (slotDelta != prevSlotDelta) {
 					if (slotDelta != prevSlotDelta) {
-						newEnd = addMinutes(cloneDate(event._end), options.slotMinutes * slotDelta);
 						if (timeElement) {
 						if (timeElement) {
-							timeElement.text(formatDates(event.start, newEnd, options.agendaEventTimeFormat));
+							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;
 						prevSlotDelta = slotDelta;
 					}
 					}
 				},
 				},
 				stop: function(ev, ui) {
 				stop: function(ev, ui) {
-					reportEventResize(event, 0, true, options.slotMinutes * slotDelta);
-					rerenderEvents();
+					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
+					}
 				}
 				}
 			})
 			})
 			.find('div.ui-resizable-s').text('=');
 			.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();
-			}
-		});
-	}
+	// ALL-DAY event resizing w/ 'view' methods...
 	
 	
 	
 	
 	
 	
-	/**************************************** misc **************************************/
 	
 	
-	// get the Y coordinate of the given time on the given day
+	/* Misc
+	-----------------------------------------------------------------------------*/
 	
 	
-	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();
+	// get the Y coordinate of the given time on the given day (both Date objects)
+	
+	function timePosition(day, time) {
+		if (time > day && time.getDay() != day.getDay()) {
+			return bodyContent.height();
 		}
 		}
+		var slotMinutes = options.slotMinutes,
+			minutes = time.getHours()*60 + time.getMinutes(),
+			slotI = Math.floor(minutes / slotMinutes),
+			innerDiv = body.find('tr:eq(' + slotI + ') td div');
+		return Math.max(0, Math.round(innerDiv.position().top - 1 + rowHeight * ((minutes % slotMinutes) / slotMinutes)));
 	}
 	}
 
 
 }
 }
 
 
 
 
-function compileSlotSegs(events, start, end) {
+// count the number of colliding, higher-level segments (for event squishing)
 
 
-	// 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;
+function countForwardSegs(levels) {
+	var i, j, k, level, segForward, segBack;
 	for (i=levels.length-1; i>0; i--) {
 	for (i=levels.length-1; i>0; i--) {
 		level = levels[i];
 		level = levels[i];
 		for (j=0; j<level.length; j++) {
 		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);
+			segForward = level[j];
+			for (k=0; k<levels[i-1].length; k++) {
+				segBack = levels[i-1][k];
+				if (segsCollide(segForward, segBack)) {
+					segBack.forward = Math.max(segBack.forward||0, (segForward.forward||0)+1);
 				}
 				}
 			}
 			}
 		}
 		}
 	}
 	}
 }
 }
+

+ 29 - 6
src/css/agenda.css

@@ -11,7 +11,6 @@
 
 
 .fc .fc-axis {
 .fc .fc-axis {
 	width: 50px;
 	width: 50px;
-	height: 1.6em;
 	padding: 0 4px 0 0;
 	padding: 0 4px 0 0;
 	vertical-align: middle;
 	vertical-align: middle;
 	white-space: nowrap;
 	white-space: nowrap;
@@ -28,7 +27,7 @@
 	}
 	}
 	
 	
 .fc-agenda-head tr.fc-all-day th {
 .fc-agenda-head tr.fc-all-day th {
-	height: 2em;
+	height: 35px;
 	}
 	}
 	
 	
 .fc-agenda tr.fc-first th,
 .fc-agenda tr.fc-first th,
@@ -47,6 +46,10 @@
 .fc .fc-agenda-body td {
 .fc .fc-agenda-body td {
 	background: none;
 	background: none;
 	}
 	}
+	
+.fc .fc-agenda-body td div {
+	height: 20px;
+	}
 
 
 
 
 
 
@@ -57,15 +60,34 @@
 	}
 	}
 	
 	
 	
 	
-	
-.fc .fc-divider th {
-	height: 3px;
-	border-bottom-width: 1px;
+
+.fc .fc-divider div {
+	font-size: 1px; /* for IE6/7 */
+	height: 2px;
 	}
 	}
 	
 	
+	
+	
 .fc .fc-divider .fc-state-default {
 .fc .fc-divider .fc-state-default {
 	background: #eee;
 	background: #eee;
 	}
 	}
+	
+	
+	
+.fc-agenda-head tr.fc-last th {
+	border-bottom-width: 1px;
+	}
+	
+	
+	
+	
+.fc-agenda .fc-day-content {
+	padding: 2px 2px 0;
+	}
+	
+.fc-agenda-head .fc-day-content {
+	padding-bottom: 10px;
+	}
 
 
 	
 	
 	
 	
@@ -205,6 +227,7 @@
 	_white-space: normal;
 	_white-space: normal;
 	overflow: hidden;
 	overflow: hidden;
 	font-size: 10px;
 	font-size: 10px;
+	border: 0;
 	}
 	}
 	
 	
 .fc-event-vert span.fc-event-title {
 .fc-event-vert span.fc-event-title {

+ 1 - 0
src/css/main.css

@@ -224,6 +224,7 @@ table.fc-header {
 ------------------------------------------------------------------------*/
 ------------------------------------------------------------------------*/
 
 
 .fc-event,
 .fc-event,
+.fc-agenda .fc-event-time,
 .fc-event a {
 .fc-event a {
 	border-style: solid; 
 	border-style: solid; 
 	border-color: #36c;     /* default BORDER color (probably the same as background-color) */
 	border-color: #36c;     /* default BORDER color (probably the same as background-color) */

+ 33 - 70
src/grid.js

@@ -18,7 +18,7 @@ views.month = function(element, options) {
 			this.title = formatDates(
 			this.title = formatDates(
 				start,
 				start,
 				addDays(cloneDate(this.end = addMonths(cloneDate(start), 1)), -1),
 				addDays(cloneDate(this.end = addMonths(cloneDate(start), 1)), -1),
-				strProp(options.titleFormat, 'month'),
+				this.option('titleFormat'),
 				options
 				options
 			);
 			);
 			addDays(this.visStart = cloneDate(start), -((start.getDay() - options.firstDay + 7) % 7));
 			addDays(this.visStart = cloneDate(start), -((start.getDay() - options.firstDay + 7) % 7));
@@ -28,7 +28,7 @@ views.month = function(element, options) {
 				addDays(this.visEnd, (6 - rowCnt) * 7);
 				addDays(this.visEnd, (6 - rowCnt) * 7);
 				rowCnt = 6;
 				rowCnt = 6;
 			}
 			}
-			this.renderGrid(rowCnt, 7, strProp(options.columnFormat, 'month'), true, fetchEvents);
+			this.renderGrid(rowCnt, 7, this.option('columnFormat'), true, fetchEvents);
 		}
 		}
 	});
 	});
 }
 }
@@ -42,10 +42,10 @@ views.basicWeek = function(element, options) {
 			this.title = formatDates(
 			this.title = formatDates(
 				this.start = this.visStart = addDays(cloneDate(date), -((date.getDay() - options.firstDay + 7) % 7)),
 				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),
 				addDays(cloneDate(this.end = this.visEnd = addDays(cloneDate(this.start), 7)), -1),
-				strProp(options.titleFormat, 'week'),
+				this.option('titleFormat'),
 				options
 				options
 			);
 			);
-			this.renderGrid(1, 7, strProp(options.columnFormat, 'week'), false, fetchEvents);
+			this.renderGrid(1, 7, this.option('columnFormat'), false, fetchEvents);
 		}
 		}
 	});
 	});
 };
 };
@@ -56,10 +56,10 @@ views.basicDay = function(element, options) {
 			if (delta) {
 			if (delta) {
 				addDays(date, delta);
 				addDays(date, delta);
 			}
 			}
-			this.title = formatDate(date, strProp(options.titleFormat, 'day'), options);
+			this.title = formatDate(date, this.option('titleFormat'), options);
 			this.start = this.visStart = cloneDate(date, true);
 			this.start = this.visStart = cloneDate(date, true);
 			this.end = this.visEnd = addDays(cloneDate(this.start), 1);
 			this.end = this.visEnd = addDays(cloneDate(this.start), 1);
-			this.renderGrid(1, 1, strProp(options.columnFormat, 'day'), false, fetchEvents);
+			this.renderGrid(1, 1, this.option('columnFormat'), false, fetchEvents);
 		}
 		}
 	});
 	});
 }
 }
@@ -326,10 +326,11 @@ function Grid(element, options, methods) {
 	
 	
 	
 	
 	function compileSegs(events) {
 	function compileSegs(events) {
-		var d1 = cloneDate(view.visStart);
-		var d2 = addDays(cloneDate(d1), colCnt);
-		var rows = [];
-		for (var i=0; i<rowCnt; i++) {
+		var d1 = cloneDate(view.visStart),
+			d2 = addDays(cloneDate(d1), colCnt),
+			rows = [],
+			i=0;
+		for (; i<rowCnt; i++) {
 			rows.push(stackSegs(view.sliceSegs(events, d1, d2)));
 			rows.push(stackSegs(view.sliceSegs(events, d1, d2)));
 			addDays(d1, 7);
 			addDays(d1, 7);
 			addDays(d2, 7);
 			addDays(d2, 7);
@@ -350,7 +351,7 @@ function Grid(element, options, methods) {
 			event,
 			event,
 			eventClasses,
 			eventClasses,
 			startElm, endElm,
 			startElm, endElm,
-			left1, left2,
+			left, right,
 			eventElement, eventAnchor,
 			eventElement, eventAnchor,
 			triggerRes;
 			triggerRes;
 		for (i=0; i<len; i++) {
 		for (i=0; i<len; i++) {
@@ -387,14 +388,14 @@ function Grid(element, options, methods) {
 					}
 					}
 					eventClasses.push('fc-event', 'fc-event-hori');
 					eventClasses.push('fc-event', 'fc-event-hori');
 					startElm = seg.isStart ?
 					startElm = seg.isStart ?
-						tr.find('td:eq('+((seg.start.getDay()-firstDay+colCnt)%colCnt)+') div.fc-day-content div') :
+						tr.find('td:eq('+((seg.start.getDay()-firstDay+colCnt)%colCnt)+') div div') :
 						tbody;
 						tbody;
 					endElm = seg.isEnd ?
 					endElm = seg.isEnd ?
-						tr.find('td:eq('+((seg.end.getDay()-firstDay+colCnt-1)%colCnt)+') div.fc-day-content div') :
+						tr.find('td:eq('+((seg.end.getDay()-firstDay+colCnt-1)%colCnt)+') div div') :
 						tbody;
 						tbody;
 					if (rtl) {
 					if (rtl) {
-						left1 = endElm.position().left;
-						left2 = startElm.position().left + startElm.width();
+						left = endElm.position().left;
+						right = startElm.position().left + startElm.width();
 						if (seg.isStart) {
 						if (seg.isStart) {
 							eventClasses.push('fc-corner-right');
 							eventClasses.push('fc-corner-right');
 						}
 						}
@@ -402,8 +403,8 @@ function Grid(element, options, methods) {
 							eventClasses.push('fc-corner-left');
 							eventClasses.push('fc-corner-left');
 						}
 						}
 					}else{
 					}else{
-						left1 = startElm.position().left;
-						left2 = endElm.position().left + endElm.width();
+						left = startElm.position().left;
+						right = endElm.position().left + endElm.width();
 						if (seg.isStart) {
 						if (seg.isStart) {
 							eventClasses.push('fc-corner-left');
 							eventClasses.push('fc-corner-left');
 						}
 						}
@@ -415,7 +416,7 @@ function Grid(element, options, methods) {
 						.append(eventAnchor = $("<a/>")
 						.append(eventAnchor = $("<a/>")
 							.append(event.allDay || !seg.isStart ? null :
 							.append(event.allDay || !seg.isStart ? null :
 								$("<span class='fc-event-time'/>")
 								$("<span class='fc-event-time'/>")
-									.html(formatDates(event.start, event.end, options.timeFormat, options)))
+									.html(formatDates(event.start, event.end, view.option('timeFormat'), options)))
 							.append($("<span class='fc-event-title'/>")
 							.append($("<span class='fc-event-title'/>")
 								.text(event.title)));
 								.text(event.title)));
 					if (event.url) {
 					if (event.url) {
@@ -430,23 +431,23 @@ function Grid(element, options, methods) {
 							.css({
 							.css({
 								position: 'absolute',
 								position: 'absolute',
 								top: top,
 								top: top,
-								left: left1 + (rtlLeftDiff||0),
-								zIndex: 2
+								left: left + (rtlLeftDiff||0),
+								zIndex: 8
 							})
 							})
 							.appendTo(element);
 							.appendTo(element);
-						setOuterWidth(eventElement, left2-left1, true);
+						setOuterWidth(eventElement, right-left, true);
 						if (rtl && rtlLeftDiff == undefined) {
 						if (rtl && rtlLeftDiff == undefined) {
 							// bug in IE6 where offsets are miscalculated with direction:rtl
 							// bug in IE6 where offsets are miscalculated with direction:rtl
-							rtlLeftDiff = left1 - eventElement.position().left;
+							rtlLeftDiff = left - eventElement.position().left;
 							if (rtlLeftDiff) {
 							if (rtlLeftDiff) {
-								eventElement.css('left', left1 + rtlLeftDiff);
+								eventElement.css('left', left + rtlLeftDiff);
 							}
 							}
 						}
 						}
 						eventElementHandlers(event, eventElement);
 						eventElementHandlers(event, eventElement);
 						if (event.editable || event.editable == undefined && options.editable) {
 						if (event.editable || event.editable == undefined && options.editable) {
 							draggableEvent(event, eventElement);
 							draggableEvent(event, eventElement);
 							if (seg.isEnd) {
 							if (seg.isEnd) {
-								resizableEvent(event, eventElement);
+								view.resizableDayEvent(event, eventElement, colWidth);
 							}
 							}
 						}
 						}
 						view.reportEventElement(event, eventElement);
 						view.reportEventElement(event, eventElement);
@@ -479,7 +480,7 @@ function Grid(element, options, methods) {
 	
 	
 	
 	
 	
 	
-	/* Draggable
+	/* Event Dragging
 	-----------------------------------------------------------------------------*/
 	-----------------------------------------------------------------------------*/
 	
 	
 	
 	
@@ -487,9 +488,9 @@ function Grid(element, options, methods) {
 		if (!options.disableDragging && eventElement.draggable) {
 		if (!options.disableDragging && eventElement.draggable) {
 			var matrix;
 			var matrix;
 			eventElement.draggable({
 			eventElement.draggable({
-				zIndex: 3,
+				zIndex: 9,
 				delay: 50,
 				delay: 50,
-				opacity: options.dragOpacity,
+				opacity: view.option('dragOpacity'),
 				revertDuration: options.dragRevertDuration,
 				revertDuration: options.dragRevertDuration,
 				start: function(ev, ui) {
 				start: function(ev, ui) {
 					matrix = new HoverMatrix(function(cell) {
 					matrix = new HoverMatrix(function(cell) {
@@ -518,20 +519,17 @@ function Grid(element, options, methods) {
 					matrix.mouse(ev.pageX, ev.pageY);
 					matrix.mouse(ev.pageX, ev.pageY);
 				},
 				},
 				stop: function(ev, ui) {
 				stop: function(ev, ui) {
+					if ($.browser.msie) {
+						eventElement.css('filter', ''); // clear IE opacity side-effects
+					}
 					view.hideOverlay();
 					view.hideOverlay();
 					view.trigger('eventDragStop', eventElement, event, ev, ui);
 					view.trigger('eventDragStop', eventElement, event, ev, ui);
 					var cell = matrix.cell;
 					var cell = matrix.cell;
 					if (!cell || !cell.rowDelta && !cell.colDelta) {
 					if (!cell || !cell.rowDelta && !cell.colDelta) {
 						view.showEvents(event, eventElement);
 						view.showEvents(event, eventElement);
 					}else{
 					}else{
-						var dayDelta = cell.rowDelta*7 + cell.colDelta*dis;
-						view.moveEvent(event, dayDelta);
-						view.trigger('eventDrop', this, event, dayDelta, 0, function() {
-							view.moveEvent(event, -dayDelta);
-							rerenderEvents();
-						}, ev, ui);
 						eventElement.find('a').removeAttr('href'); // prevents safari from visiting the link
 						eventElement.find('a').removeAttr('href'); // prevents safari from visiting the link
-						rerenderEvents();
+						view.eventDrop(this, event, cell.rowDelta*7+cell.colDelta*dis, 0, event.allDay, ev, ui);
 					}
 					}
 				}
 				}
 			});
 			});
@@ -539,42 +537,7 @@ function Grid(element, options, methods) {
 	}
 	}
 	
 	
 	
 	
-	
-	/* Resizable
-	-----------------------------------------------------------------------------*/
-	
-	
-	function resizableEvent(event, eventElement) {
-		if (!options.disableResizing && eventElement.resizable) {
-			eventElement.resizable({
-				handles: rtl ? 'w' : 'e',
-				grid: colWidth,
-				minWidth: colWidth/2, // need this or else IE throws errors when too small
-				containment: element,
-				start: function(ev, ui) {
-					eventElement.css('z-index', 3);
-					view.hideEvents(event, eventElement);
-					view.trigger('eventResizeStart', this, event, ev, ui);
-				},
-				stop: function(ev, ui) {
-					view.trigger('eventResizeStop', this, event, ev, ui);
-					// ui.size.width wasn't working with grid correctly, use .width()
-					var dayDelta = Math.round((eventElement.width() - ui.originalSize.width) / colWidth);
-					if (dayDelta) {
-						view.resizeEvent(event, dayDelta);
-						view.trigger('eventResize', this, event, dayDelta, 0, function() {
-							view.resizeEvent(event, -dayDelta);
-							rerenderEvents();
-						}, ev, ui);
-						rerenderEvents();
-					}else{
-						view.showEvents(event, eventElement);
-					}
-					eventElement.css('z-index', 2);
-				}
-			});
-		}
-	}
+	// event resizing w/ 'view' methods...
 
 
 };
 };
 
 

+ 38 - 24
src/main.js

@@ -30,7 +30,6 @@ var defaults = {
 	cacheParam: '_',
 	cacheParam: '_',
 	
 	
 	// time formats
 	// time formats
-	timeFormat: 'h(:mm)t', // for events
 	titleFormat: {
 	titleFormat: {
 		month: 'MMMM yyyy',
 		month: 'MMMM yyyy',
 		week: "MMM d[ yyyy]{ '&#8212;'[ MMM] d yyyy}",
 		week: "MMM d[ yyyy]{ '&#8212;'[ MMM] d yyyy}",
@@ -41,6 +40,9 @@ var defaults = {
 		week: 'ddd M/d',
 		week: 'ddd M/d',
 		day: 'dddd M/d'
 		day: 'dddd M/d'
 	},
 	},
+	timeFormat: { // for event elements
+		'': 'h(:mm)t' // default
+	},
 	
 	
 	// locale
 	// locale
 	isRTL: false,
 	isRTL: false,
@@ -166,7 +168,7 @@ $.fn.fullCalendar = function(options) {
 		
 		
 		function changeView(v) {
 		function changeView(v) {
 			if (v != viewName) {
 			if (v != viewName) {
-				lockContentSize();
+				fixContentSize();
 				if (view) {
 				if (view) {
 					if (view.eventsChanged) {
 					if (view.eventsChanged) {
 						eventsDirtyExcept(view);
 						eventsDirtyExcept(view);
@@ -188,14 +190,14 @@ $.fn.fullCalendar = function(options) {
 				}
 				}
 				view.name = viewName = v;
 				view.name = viewName = v;
 				render();
 				render();
-				unlockContentSize();
+				unfixContentSize();
 			}
 			}
 		}
 		}
 		
 		
 		function render(inc) {
 		function render(inc) {
 			if (_element.offsetWidth !== 0) { // visible on the screen
 			if (_element.offsetWidth !== 0) { // visible on the screen
 				if (inc || !view.date || +view.date != +date) { // !view.date means it hasn't been rendered yet
 				if (inc || !view.date || +view.date != +date) { // !view.date means it hasn't been rendered yet
-					ignoreWindowResizes = true;
+					fixContentSize();
 					view.render(date, inc || 0, function(callback) {
 					view.render(date, inc || 0, function(callback) {
 						// dont refetch if new view contains the same events (or a subset)
 						// dont refetch if new view contains the same events (or a subset)
 						if (!eventStart || view.visStart < eventStart || view.visEnd > eventEnd) {
 						if (!eventStart || view.visStart < eventStart || view.visEnd > eventEnd) {
@@ -204,7 +206,7 @@ $.fn.fullCalendar = function(options) {
 							callback(events); // no refetching
 							callback(events); // no refetching
 						}
 						}
 					});
 					});
-					ignoreWindowResizes = false;
+					unfixContentSize();
 					view.date = cloneDate(date);
 					view.date = cloneDate(date);
 					if (header) {
 					if (header) {
 						// enable/disable 'today' button
 						// enable/disable 'today' button
@@ -620,35 +622,47 @@ $.fn.fullCalendar = function(options) {
 		/* Resizing
 		/* Resizing
 		-----------------------------------------------------------------------------*/
 		-----------------------------------------------------------------------------*/
 		
 		
-		function lockContentSize() {
-			content.css({
-				overflow: 'hidden',
-				height: Math.round(content.width() / options.aspectRatio)
-			});
-		}
+		var elementWidth,
+			contentSizeFixed = false,
+			resizeCnt = 0;
 		
 		
-		function unlockContentSize() {
-			content.css({
-				overflow: '',
-				height: ($.browser.msie && $.browser.version == '6.0') ? 1 : ''
-			});
+		function fixContentSize() {
+			if (!contentSizeFixed) {
+				contentSizeFixed = true;
+				content.css({
+					overflow: 'hidden',
+					height: Math.round(content.width() / options.aspectRatio)
+				});
+			}
 		}
 		}
 		
 		
-		var elementWidth,
-			ignoreWindowResizes = false,
-			resizeCnt = 0;
+		function unfixContentSize() {
+			if (contentSizeFixed) {
+				content.css({
+					overflow: 'visible',
+					height: ''
+				});
+				if ($.browser.msie && ($.browser.version=='6.0' || $.browser.version=='7.0')) {
+					// in IE6/7 the inside of the content div was invisible
+					// bizarre hack to get this work... need both lines
+					content[0].clientHeight;
+					content.hide().show();
+				}
+				contentSizeFixed = false;
+			}
+		}
 		
 		
 		$(window).resize(function() {
 		$(window).resize(function() {
-			if (!ignoreWindowResizes && view.date) { // view.date means the view has been rendered
+			if (!contentSizeFixed && view.date) { // view.date means the view has been rendered
 				var rcnt = ++resizeCnt; // add a delay
 				var rcnt = ++resizeCnt; // add a delay
 				setTimeout(function() {
 				setTimeout(function() {
-					if (rcnt == resizeCnt && !ignoreWindowResizes) {
+					if (rcnt == resizeCnt && !contentSizeFixed) {
 						var newWidth = element.width();
 						var newWidth = element.width();
 						if (newWidth != elementWidth) {
 						if (newWidth != elementWidth) {
 							elementWidth = newWidth;
 							elementWidth = newWidth;
-							lockContentSize();
+							fixContentSize();
 							view.updateSize();
 							view.updateSize();
-							unlockContentSize();
+							unfixContentSize();
 							view.rerenderEvents(true);
 							view.rerenderEvents(true);
 							sizesDirtyExcept(view);
 							sizesDirtyExcept(view);
 							view.trigger('windowResize', _element);
 							view.trigger('windowResize', _element);
@@ -686,7 +700,7 @@ function normalizeEvent(event, options) {
 	}
 	}
 	event._start = cloneDate(event.start = parseDate(event.start));
 	event._start = cloneDate(event.start = parseDate(event.start));
 	event.end = parseDate(event.end);
 	event.end = parseDate(event.end);
-	if (event.end && event.end < event.start) {
+	if (event.end && event.end <= event.start) {
 		event.end = null;
 		event.end = null;
 	}
 	}
 	event._end = event.end ? cloneDate(event.end) : null;
 	event._end = event.end ? cloneDate(event.end) : null;

+ 2 - 5
src/util.js

@@ -3,7 +3,8 @@
 -----------------------------------------------------------------------------*/
 -----------------------------------------------------------------------------*/
 
 
 var DAY_MS = 86400000,
 var DAY_MS = 86400000,
-	HOUR_MS = 3600000;
+	HOUR_MS = 3600000,
+	MINUTE_MS = 60000;
 
 
 function addYears(d, n, keepTime) {
 function addYears(d, n, keepTime) {
 	d.setFullYear(d.getFullYear() + n);
 	d.setFullYear(d.getFullYear() + n);
@@ -340,7 +341,3 @@ function zeroPad(n) {
 	return (n < 10 ? '0' : '') + n;
 	return (n < 10 ? '0' : '') + n;
 }
 }
 
 
-function strProp(s, prop) {
-	return typeof s == 'string' ? s : s[prop];
-}
-

+ 346 - 264
src/view.js

@@ -1,264 +1,346 @@
-
-/* Methods & Utilities for All Views
------------------------------------------------------------------------------*/
-
-var viewMethods = {
-
-	/*
-	 * Objects inheriting these methods must implement the following properties/methods:
-	 * - title
-	 * - start
-	 * - end
-	 * - visStart
-	 * - visEnd
-	 * - defaultEventEnd(event)
-	 * - visEventEnd(event)
-	 * - render(events)
-	 * - rerenderEvents()
-	 *
-	 *
-	 * z-index reservations:
-	 * 1. day-overlay
-	 * 2. events
-	 * 3. dragging/resizing events
-	 *
-	 */
-	
-	
-
-	init: function(element, options) {
-		this.element = element;
-		this.options = options;
-		this.cachedEvents = [];
-		this.eventsByID = {};
-		this.eventElements = [];
-		this.eventElementsByID = {};
-	},
-	
-	
-	
-	// triggers an event handler, always append view as last arg
-	
-	trigger: function(name, thisObj) {
-		if (this.options[name]) {
-			return this.options[name].apply(thisObj || this, Array.prototype.slice.call(arguments, 2).concat([this]));
-		}
-	},
-	
-	
-	
-	// returns a Date object for an event's end
-	
-	eventEnd: function(event) {
-		return event.end || this.defaultEventEnd(event);
-	},
-	
-	
-	
-	// report when view receives new events
-	
-	reportEvents: function(events) { // events are already normalized at this point
-		var i, len=events.length, event,
-			eventsByID = this.eventsByID = {},
-			cachedEvents = this.cachedEvents = [];
-		for (i=0; i<len; i++) {
-			event = events[i];
-			if (eventsByID[event._id]) {
-				eventsByID[event._id].push(event);
-			}else{
-				eventsByID[event._id] = [event];
-			}
-			cachedEvents.push(event);
-		}
-	},
-	
-	
-	
-	// report when view creates an element for an event
-
-	reportEventElement: function(event, element) {
-		this.eventElements.push(element);
-		var eventElementsByID = this.eventElementsByID;
-		if (eventElementsByID[event._id]) {
-			eventElementsByID[event._id].push(element);
-		}else{
-			eventElementsByID[event._id] = [element];
-		}
-	},
-	
-	
-	
-	// event element manipulation
-	
-	clearEvents: function() { // only remove ELEMENTS
-		$.each(this.eventElements, function() {
-			this.remove();
-		});
-		this.eventElements = [];
-		this.eventElementsByID = {};
-	},
-	
-	showEvents: function(event, exceptElement) {
-		this._eee(event, exceptElement, 'show');
-	},
-	
-	hideEvents: function(event, exceptElement) {
-		this._eee(event, exceptElement, 'hide');
-	},
-	
-	_eee: function(event, exceptElement, funcName) { // event-element-each
-		var elements = this.eventElementsByID[event._id],
-			i, len = elements.length;
-		for (i=0; i<len; i++) {
-			if (elements[i] != exceptElement) {
-				elements[i][funcName]();
-			}
-		}
-	},
-	
-	
-	
-	// event modification reporting
-	
-	moveEvent: function(event, days, minutes) { // actually DO the date changes
-		minutes = minutes || 0;
-		var events = this.eventsByID[event._id],
-			i, len=events.length, e;
-		for (i=0; i<len; i++) {
-			e = events[i];
-			e.allDay = event.allDay;
-			addMinutes(addDays(e.start, days, true), minutes);
-			if (e.end) {
-				e.end = addMinutes(addDays(e.end, days, true), minutes);
-			}
-			normalizeEvent(e, this.options);
-		}
-		this.eventsChanged = true;
-	},
-	
-	resizeEvent: function(event, days, minutes) { // actually DO the date changes
-		minutes = minutes || 0;
-		var events = this.eventsByID[event._id],
-			i, len=events.length, e;
-		for (i=0; i<len; i++) {
-			e = events[i];
-			e.end = addMinutes(addDays(this.eventEnd(e), days, true), minutes);
-			normalizeEvent(e, this.options);
-		}
-		this.eventsChanged = true;
-	},
-	
-	
-	
-	// semi-transparent overlay (while dragging)
-	
-	showOverlay: function(props) {
-		if (!this.dayOverlay) {
-			this.dayOverlay = $("<div class='fc-cell-overlay' style='position:absolute;z-index:1;display:none'/>")
-				.appendTo(this.element);
-		}
-		var o = this.element.offset();
-		this.dayOverlay
-			.css({
-				top: props.top - o.top,
-				left: props.left - o.left,
-				width: props.width,
-				height: props.height
-			})
-			.show();
-	},
-	
-	hideOverlay: function() {
-		if (this.dayOverlay) {
-			this.dayOverlay.hide();
-		}
-	},
-	
-	
-	
-	// event rendering utilities
-	
-	sliceSegs: function(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 = this.visEventEnd(event);
-			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);
-	}
-
-};
-
-
-// more event rendering utilities
-
-function stackSegs(segs) {
-	var levels = [],
-		i, len = segs.length, seg,
-		j, collide, k;
-	for (i=0; i<len; i++) {
-		seg = segs[i];
-		j = 0; // the level index where seg should belong
-		while (true) {
-			collide = false;
-			if (levels[j]) {
-				for (k=0; k<levels[j].length; k++) {
-					if (segsCollide(levels[j][k], seg)) {
-						collide = true;
-						break;
-					}
-				}
-			}
-			if (collide) {
-				j++;
-			}else{
-				break;
-			}
-		}
-		if (levels[j]) {
-			levels[j].push(seg);
-		}else{
-			levels[j] = [seg];
-		}
-		//seg.after = 0;
-	}
-	return levels;
-}
-
-function segCmp(a, b) {
-	return  (b.msLength - a.msLength) * 100 + (a.event.start - b.event.start);
-}
-
-function segsCollide(seg1, seg2) {
-	return seg1.end > seg2.start && seg1.start < seg2.end;
-}
-
+
+/* Methods & Utilities for All Views
+-----------------------------------------------------------------------------*/
+
+var viewMethods = {
+
+	// TODO: maybe change the 'vis' variables to 'excl'
+
+	/*
+	 * Objects inheriting these methods must implement the following properties/methods:
+	 * - title
+	 * - start
+	 * - end
+	 * - visStart
+	 * - visEnd
+	 * - defaultEventEnd(event)
+	 * - visEventEnd(event)
+	 * - render(events)
+	 * - rerenderEvents()
+	 *
+	 *
+	 * z-index reservations:
+	 * 3 - day-overlay
+	 * 8 - events
+	 * 9 - dragging/resizing events
+	 *
+	 */
+	
+	
+
+	init: function(element, options) {
+		this.element = element;
+		this.options = options;
+		this.cachedEvents = [];
+		this.eventsByID = {};
+		this.eventElements = [];
+		this.eventElementsByID = {};
+	},
+	
+	
+	
+	// triggers an event handler, always append view as last arg
+	
+	trigger: function(name, thisObj) {
+		if (this.options[name]) {
+			return this.options[name].apply(thisObj || this, Array.prototype.slice.call(arguments, 2).concat([this]));
+		}
+	},
+	
+	
+	
+	// returns a Date object for an event's end
+	
+	eventEnd: function(event) {
+		return event.end ? cloneDate(event.end) : this.defaultEventEnd(event); // TODO: make sure always using copies
+	},
+	
+	
+	
+	// report when view receives new events
+	
+	reportEvents: function(events) { // events are already normalized at this point
+		var i, len=events.length, event,
+			eventsByID = this.eventsByID = {},
+			cachedEvents = this.cachedEvents = [];
+		for (i=0; i<len; i++) {
+			event = events[i];
+			if (eventsByID[event._id]) {
+				eventsByID[event._id].push(event);
+			}else{
+				eventsByID[event._id] = [event];
+			}
+			cachedEvents.push(event);
+		}
+	},
+	
+	
+	
+	// report when view creates an element for an event
+
+	reportEventElement: function(event, element) {
+		this.eventElements.push(element);
+		var eventElementsByID = this.eventElementsByID;
+		if (eventElementsByID[event._id]) {
+			eventElementsByID[event._id].push(element);
+		}else{
+			eventElementsByID[event._id] = [element];
+		}
+	},
+	
+	
+	
+	// event element manipulation
+	
+	clearEvents: function() { // only remove ELEMENTS
+		$.each(this.eventElements, function() {
+			this.remove();
+		});
+		this.eventElements = [];
+		this.eventElementsByID = {};
+	},
+	
+	showEvents: function(event, exceptElement) {
+		this._eee(event, exceptElement, 'show');
+	},
+	
+	hideEvents: function(event, exceptElement) {
+		this._eee(event, exceptElement, 'hide');
+	},
+	
+	_eee: function(event, exceptElement, funcName) { // event-element-each
+		var elements = this.eventElementsByID[event._id],
+			i, len = elements.length;
+		for (i=0; i<len; i++) {
+			if (elements[i] != exceptElement) {
+				elements[i][funcName]();
+			}
+		}
+	},
+	
+	
+	
+	// event modification reporting
+	
+	eventDrop: function(e, event, dayDelta, minuteDelta, allDay, ev, ui) {
+		var view = this,
+			oldAllDay = event.allDay;
+		view.moveEvents(view.eventsByID[event._id], dayDelta, minuteDelta, allDay);
+		view.trigger('eventDrop', e, event, dayDelta, minuteDelta, allDay, function() { // TODO: change docs
+			// TODO: investigate cases where this inverse technique might not work
+			view.moveEvents(view.eventsByID[event._id], -dayDelta, -minuteDelta, oldAllDay);
+			view.rerenderEvents();
+		}, ev, ui);
+		view.eventsChanged = true;
+		view.rerenderEvents();
+	},
+	
+	eventResize: function(e, event, dayDelta, minuteDelta, ev, ui) {
+		var view = this;
+		view.elongateEvents(view.eventsByID[event._id], dayDelta, minuteDelta);
+		view.trigger('eventResize', e, event, dayDelta, minuteDelta, function() {
+			// TODO: investigate cases where this inverse technique might not work
+			view.elongateEvents(view.eventsByID[event._id], -dayDelta, -minuteDelta);
+			view.rerenderEvents();
+		}, ev, ui);
+		view.eventsChanged = true;
+		view.rerenderEvents();
+	},
+	
+	
+	
+	// event modification
+	
+	moveEvents: function(events, dayDelta, minuteDelta, allDay) {
+		minuteDelta = minuteDelta || 0;
+		for (var e, len=events.length, i=0; i<len; i++) {
+			e = events[i];
+			if (allDay != undefined) {
+				e.allDay = allDay;
+			}
+			addMinutes(addDays(e.start, dayDelta, true), minuteDelta);
+			if (e.end) {
+				e.end = addMinutes(addDays(e.end, dayDelta, true), minuteDelta);
+			}
+			normalizeEvent(e, this.options);
+		}
+	},
+	
+	elongateEvents: function(events, dayDelta, minuteDelta) {
+		minuteDelta = minuteDelta || 0;
+		for (var e, len=events.length, i=0; i<len; i++) {
+			e = events[i];
+			e.end = addMinutes(addDays(this.eventEnd(e), dayDelta, true), minuteDelta);
+			normalizeEvent(e, this.options);
+		}
+	},
+	
+	
+	
+	// semi-transparent overlay (while dragging)
+	
+	showOverlay: function(props) {
+		if (!this.dayOverlay) {
+			this.dayOverlay = $("<div class='fc-cell-overlay' style='position:absolute;z-index:3;display:none'/>")
+				.appendTo(this.element);
+		}
+		var o = this.element.offset();
+		this.dayOverlay
+			.css({
+				top: props.top - o.top,
+				left: props.left - o.left,
+				width: props.width,
+				height: props.height
+			})
+			.show();
+	},
+	
+	hideOverlay: function() {
+		if (this.dayOverlay) {
+			this.dayOverlay.hide();
+		}
+	},
+	
+	
+	
+	// common horizontal event resizing
+
+	resizableDayEvent: function(event, eventElement, colWidth) {
+		var view = this;
+		if (!view.options.disableResizing && eventElement.resizable) {
+			eventElement.resizable({
+				handles: view.options.isRTL ? 'w' : 'e',
+				grid: colWidth,
+				minWidth: colWidth/2, // need this or else IE throws errors when too small
+				containment: view.element,
+				start: function(ev, ui) {
+					eventElement.css('z-index', 9);
+					view.hideEvents(event, eventElement);
+					view.trigger('eventResizeStart', this, event, ev, ui);
+				},
+				stop: function(ev, ui) {
+					view.trigger('eventResizeStop', this, event, ev, ui);
+					// ui.size.width wasn't working with grid correctly, use .width()
+					var dayDelta = Math.round((eventElement.width() - ui.originalSize.width) / colWidth);
+					if (dayDelta) {
+						view.eventResize(this, event, dayDelta, 0, ev, ui);
+					}else{
+						eventElement.css('z-index', 8);
+						view.showEvents(event, eventElement);
+					}
+				}
+			});
+		}
+	},
+	
+	
+	
+	// get a property from the 'options' object, using smart view naming
+	
+	option: function(name) {
+		var v = this.options[name];
+		if (typeof v == 'object') {
+			var parts = this.name.split(/(?=[A-Z])/),
+				i=parts.length-1, res;
+			for (; i>=0; i--) {
+				res = v[parts[i].toLowerCase()];
+				if (res != undefined) {
+					return res;
+				}
+			}
+			return v[''];
+		}
+		return v;
+	},
+	
+	
+	
+	// event rendering utilities
+	
+	sliceSegs: function(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 = this.visEventEnd(event);
+			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);
+	}
+	
+
+};
+
+
+
+
+// event rendering calculation utilities
+
+function stackSegs(segs) {
+	var levels = [],
+		i, len = segs.length, seg,
+		j, collide, k;
+	for (i=0; i<len; i++) {
+		seg = segs[i];
+		j = 0; // the level index where seg should belong
+		while (true) {
+			collide = false;
+			if (levels[j]) {
+				for (k=0; k<levels[j].length; k++) {
+					if (segsCollide(levels[j][k], seg)) {
+						collide = true;
+						break;
+					}
+				}
+			}
+			if (collide) {
+				j++;
+			}else{
+				break;
+			}
+		}
+		if (levels[j]) {
+			levels[j].push(seg);
+		}else{
+			levels[j] = [seg];
+		}
+		//seg.after = 0;
+	}
+	return levels;
+}
+
+function segCmp(a, b) {
+	return  (b.msLength - a.msLength) * 100 + (a.event.start - b.event.start);
+}
+
+function segsCollide(seg1, seg2) {
+	return seg1.end > seg2.start && seg1.start < seg2.end;
+}
+

+ 18 - 8
tests/basic.html

@@ -12,10 +12,14 @@
 		var m = d.getMonth();
 		var m = d.getMonth();
 		
 		
 		$('#calendar').fullCalendar({
 		$('#calendar').fullCalendar({
+			slotMinutes: 30,
+			//allDayHeader: false,
 			//weekMode: 'variable',
 			//weekMode: 'variable',
-			theme: true,
+			//theme: true,
+			//firstDay: 1,
 			//isRTL: true,
 			//isRTL: true,
 			editable: true,
 			editable: true,
+			//dragOpacity: .5,
 			defaultView: 'agendaWeek',
 			defaultView: 'agendaWeek',
 			header: {
 			header: {
 				left: 'prev,next today',
 				left: 'prev,next today',
@@ -27,20 +31,26 @@
 					id: 1,
 					id: 1,
 					title: "Long Event",
 					title: "Long Event",
 					start: new Date(y, m, 6, 14, 0),
 					start: new Date(y, m, 6, 14, 0),
-					end: new Date(y, m, 11),
-					allDay: false
+					end: new Date(y, m, 11)
+					//allDay: false
 				},
 				},
 				{
 				{
 					id: 2,
 					id: 2,
-					title: "Repeating Event",
-					start: new Date(y, m, 2),
+					title: "Repeating Event111",
+					start: new Date(y, m, 8),
 					allDay: true
 					allDay: true
 				},
 				},
 				{
 				{
 					id: 2,
 					id: 2,
-					title: "Repeating Event",
-					start: new Date(y, m, 9),
-					allDay: true
+					title: "Repeating Event222",
+					start: new Date(y, m, 9, 5, 0),
+					allDay: false
+				},
+				{
+					id: 345,
+					title: "Hey Hey",
+					start: new Date(y, m, 9, 4, 0),
+					allDay: false
 				},
 				},
 				{
 				{
 					id: 3,
 					id: 3,