Pārlūkot izejas kodu

background event rendering

Adam Shaw 11 gadi atpakaļ
vecāks
revīzija
0dfca4234d

+ 24 - 8
src/agenda/agenda.css

@@ -66,22 +66,30 @@
 	z-index: 2;
 }
 
-.fc-time-grid .fc-highlight-skeleton {
-	z-index: 3;
-}
-
+.fc-time-grid .fc-bgevent-skeleton,
 .fc-time-grid .fc-content-skeleton {
 	position: absolute;
-	z-index: 4;
 	top: 0;
 	left: 0;
 	right: 0;
 }
 
-.fc-time-grid > .fc-helper-skeleton {
+.fc-time-grid .fc-bgevent-skeleton {
+	z-index: 3;
+}
+
+.fc-time-grid .fc-highlight-skeleton {
+	z-index: 4;
+}
+
+.fc-time-grid .fc-content-skeleton {
 	z-index: 5;
 }
 
+.fc-time-grid > .fc-helper-skeleton {
+	z-index: 6;
+}
+
 
 /* TimeGrid Slats (lines that run horizontally)
 --------------------------------------------------------------------------------------------------*/
@@ -118,7 +126,8 @@
 /* TimeGrid Event Containment
 --------------------------------------------------------------------------------------------------*/
 
-.fc-time-grid .fc-event-container { /* a div within a cell within the fc-content-skeleton */
+.fc-time-grid .fc-event-container, /* a div within a cell within the fc-content-skeleton */
+.fc-time-grid .fc-bgevent-container { /* a div within a cell within the fc-bgevent-skeleton */
 	position: relative;
 }
 
@@ -130,11 +139,18 @@
 	margin: 0 2px 0 2.5%;
 }
 
-.fc-time-grid .fc-event {
+.fc-time-grid .fc-event,
+.fc-time-grid .fc-bgevent {
 	position: absolute;
 	z-index: 1; /* scope inner z-index's */
 }
 
+.fc-time-grid .fc-bgevent {
+	/* background events always span full width */
+	left: 0;
+	right: 0;
+}
+
 
 /* TimeGrid Event Styling
 ----------------------------------------------------------------------------------------------------

+ 47 - 31
src/common/DayGrid.events.js

@@ -4,67 +4,83 @@
 
 $.extend(DayGrid.prototype, {
 
-	segs: null,
-	rowStructs: null, // an array of objects, each holding information about a row's event-rendering
+	rowStructs: null, // an array of objects, each holding information about a row's foreground event-rendering
 
 
-	// Render the given events onto the Grid and return the rendered segments
-	renderEvents: function(events) {
-		var rowStructs = this.rowStructs = this.renderEventRows(events);
-		var segs = [];
+	// Unrenders all events currently rendered on the grid
+	destroyEvents: function() {
+		this.destroySegPopover(); // removes the "more.." events popover
+		Grid.prototype.destroyEvents.apply(this, arguments); // calls the super-method
+	},
+
+
+	// Retrieves all rendered segment objects currently rendered on the grid
+	getSegs: function() {
+		return Grid.prototype.getSegs.call(this) // get the segments from the super-method
+			.concat(this.popoverSegs || []); // append the segments from the "more..." popover
+	},
+
+
+	// Renders the given background event segments onto the grid
+	renderBgSegs: function(segs) {
+
+		// don't render timed background events
+		var allDaySegs = $.grep(segs, function(seg) {
+			return seg.event.allDay;
+		});
+
+		return Grid.prototype.renderBgSegs.call(this, allDaySegs); // call the super-method
+	},
+
+
+	// Renders the given foreground event segments onto the grid
+	renderFgSegs: function(segs) {
+		var rowStructs;
+
+		// render an `.el` on each seg
+		// returns a subset of the segs. segs that were actually rendered
+		segs = this.renderFgSegEls(segs);
+
+		rowStructs = this.rowStructs = this.renderSegRows(segs);
 
 		// append to each row's content skeleton
 		this.rowEls.each(function(i, rowNode) {
 			$(rowNode).find('.fc-content-skeleton > table').append(
 				rowStructs[i].tbodyEl
 			);
-			segs.push.apply(segs, rowStructs[i].segs);
 		});
 
-		this.segs = segs;
+		return segs; // return only the segs that were actually rendered
 	},
 
 
-	// Retrieves all segment objects that have been rendered
-	getSegs: function() {
-		return (this.segs || []).concat(
-			this.popoverSegs || [] // segs rendered in the "more" events popover
-		);
-	},
-
-
-	// Removes all rendered event elements
-	destroyEvents: function() {
-		var rowStructs;
+	// Unrenders all currently rendered foreground event segments
+	destroyFgSegs: function() {
+		var rowStructs = this.rowStructs || [];
 		var rowStruct;
 
-		Grid.prototype.destroyEvents.call(this); // call the super-method
-
-		rowStructs = this.rowStructs || [];
 		while ((rowStruct = rowStructs.pop())) {
 			rowStruct.tbodyEl.remove();
 		}
 
-		this.segs = null;
-		this.destroySegPopover(); // removes the "more.." events popover
+		this.rowStructs = null;
 	},
 
 
 	// Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton.
-	// Returns an array of rowStruct objects (see the bottom of `renderEventRow`).
-	renderEventRows: function(events) {
-		var segs = this.eventsToSegs(events);
+	// Returns an array of rowStruct objects (see the bottom of `renderSegRow`).
+	// PRECONDITION: each segment shoud already have a rendered and assigned `.el`
+	renderSegRows: function(segs) {
 		var rowStructs = [];
 		var segRows;
 		var row;
 
-		segs = this.renderSegs(segs); // returns a new array with only visible segments
 		segRows = this.groupSegRows(segs); // group into nested arrays
 
 		// iterate each row of segment groupings
 		for (row = 0; row < segRows.length; row++) {
 			rowStructs.push(
-				this.renderEventRow(row, segRows[row])
+				this.renderSegRow(row, segRows[row])
 			);
 		}
 
@@ -73,7 +89,7 @@ $.extend(DayGrid.prototype, {
 
 
 	// Builds the HTML to be used for the default element for an individual segment
-	renderSegHtml: function(seg, disableResizing) {
+	fgSegHtml: function(seg, disableResizing) {
 		var view = this.view;
 		var isRTL = view.opt('isRTL');
 		var event = seg.event;
@@ -122,7 +138,7 @@ $.extend(DayGrid.prototype, {
 
 	// Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains
 	// the segments. Returns object with a bunch of internal data about how the render was calculated.
-	renderEventRow: function(row, rowSegs) {
+	renderSegRow: function(row, rowSegs) {
 		var view = this.view;
 		var colCnt = view.colCnt;
 		var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels

+ 70 - 26
src/common/DayGrid.js

@@ -4,6 +4,8 @@
 
 function DayGrid(view) {
 	Grid.call(this, view); // call the super-constructor
+
+	this.elsByFill = {};
 }
 
 
@@ -17,7 +19,7 @@ $.extend(DayGrid.prototype, {
 	rowEls: null, // set of fake row elements
 	dayEls: null, // set of whole-day elements comprising the row's background
 	helperEls: null, // set of cell skeleton elements for rendering the mock event "helper"
-	highlightEls: null, // set of cell skeleton elements for rendering the highlight
+	elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.
 
 
 	// Renders the rows and columns into the component's `this.el`, which should already be assigned.
@@ -203,7 +205,11 @@ $.extend(DayGrid.prototype, {
 	// Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null.
 	renderHelper: function(event, sourceSeg) {
 		var helperNodes = [];
-		var rowStructs = this.renderEventRows([ event ]);
+		var segs = this.eventsToSegs([ event ]);
+		var rowStructs;
+
+		segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered
+		rowStructs = this.renderSegRows(segs);
 
 		// inject each new event skeleton into each associated row
 		this.rowEls.each(function(row, rowNode) {
@@ -246,53 +252,90 @@ $.extend(DayGrid.prototype, {
 
 	// Renders an emphasis on the given date range. `start` is an inclusive, `end` is exclusive.
 	renderHighlight: function(start, end) {
-		var segs = this.rangeToSegs(start, end);
-		var highlightNodes = [];
-		var i, seg;
-		var el;
+		this.renderFill('highlight', this.rangeToSegs(start, end));
+	},
+
+
+	// Unrenders any visual emphasis on a date range
+	destroyHighlight: function() {
+		this.destroyFill('highlight');
+	},
+
+
+	/* "Fill" Rendering (rectangles covering a specified period of days)
+	------------------------------------------------------------------------------------------------------------------*/
+
+
+	// Renders a set of rectangles over the given segments of days.
+	// The `type` is used for destroying later. Also allows for special-cased behavior via strategically-named methods.
+	// CAUTION: the segs' `.el` property DOES NOT get assigned (like it does with TimeGrid)
+	renderFill: function(type, segs) {
+		var html = '';
+		var i;
+		var els;
 
-		// build an event skeleton for each row that needs it
+		// concatenate all the rows' html
 		for (i = 0; i < segs.length; i++) {
-			seg = segs[i];
-			el = $(
-				this.highlightSkeletonHtml(seg.leftCol, seg.rightCol + 1) // make end exclusive
-			);
-			el.appendTo(this.rowEls[seg.row]);
-			highlightNodes.push(el[0]);
+			html += this.fillRowHtml(type, segs[i]);
 		}
 
-		this.highlightEls = $(highlightNodes); // array -> jQuery set
+		els = $(html);
+
+		// inject each fill row's element into the correct row container of the grid
+		for (i = 0; i < segs.length; i++) {
+			this.rowEls.eq(segs[i].row).append(els[i]);
+		}
+
+		this.elsByFill[type] = els;
 	},
 
 
-	// Unrenders any visual emphasis on a date range
-	destroyHighlight: function() {
-		if (this.highlightEls) {
-			this.highlightEls.remove();
-			this.highlightEls = null;
+	// Unrenders a specific type of fill that is currently rendered on the grid
+	destroyFill: function(type) {
+		var els = this.elsByFill[type];
+
+		if (els) {
+			els.remove();
+			delete this.elsByFill[type];
 		}
 	},
 
 
-	// Generates the HTML used to build a single-row "highlight skeleton", a table that frames highlight cells
-	highlightSkeletonHtml: function(startCol, endCol) {
+	// Generates the HTML needed for one row of a fill
+	fillRowHtml: function(type, seg) {
+		var typeLower = type.toLowerCase();
 		var colCnt = this.view.colCnt;
+		var startCol = seg.leftCol;
+		var endCol = seg.rightCol + 1;
 		var cellHtml = '';
 
+		// TODO: a better system for this
+		var extraClassesMethod = this[type + 'SegClasses'];
+		var extraStylesMethod = this[type + 'SegStyles'];
+		var extraClasses = extraClassesMethod ? extraClassesMethod.call(this, seg) : '';
+		var extraStyles = extraStylesMethod ? extraStylesMethod.call(this, seg) : '';
+
 		if (startCol > 0) {
 			cellHtml += '<td colspan="' + startCol + '"/>';
 		}
-		if (endCol > startCol) {
-			cellHtml += '<td colspan="' + (endCol - startCol) + '" class="fc-highlight" />';
-		}
-		if (colCnt > endCol) {
+
+		cellHtml += '<td' +
+			' colspan="' + (endCol - startCol) + '"' +
+			' class="fc-' + typeLower + ' ' + extraClasses + '"' +
+			(extraStyles ?
+				' style="' + extraStyles + '"' :
+				''
+				) +
+			'/>';
+
+		if (endCol < colCnt) {
 			cellHtml += '<td colspan="' + (colCnt - endCol) + '"/>';
 		}
 
 		cellHtml = this.bookendCells(cellHtml, 'highlight');
 
 		return '' +
-			'<div class="fc-highlight-skeleton">' +
+			'<div class="fc-' + typeLower + '-skeleton">' +
 				'<table>' +
 					'<tr>' +
 						cellHtml +
@@ -301,4 +344,5 @@ $.extend(DayGrid.prototype, {
 			'</div>';
 	}
 
+
 });

+ 12 - 2
src/common/DayGrid.limit.js

@@ -280,7 +280,7 @@ $.extend(DayGrid.prototype, {
 		var i;
 
 		// render each seg's `el` and only return the visible segs
-		segs = this.renderSegs(segs, true); // disableResizing=true
+		segs = this.renderFgSegEls(segs, true); // disableResizing=true // TODO: filter by only foreground!!!!!
 		this.popoverSegs = segs;
 
 		for (i = 0; i < segs.length; i++) {
@@ -298,13 +298,23 @@ $.extend(DayGrid.prototype, {
 
 	// Given the events within an array of segment objects, reslice them to be in a single day
 	resliceDaySegs: function(segs, dayDate) {
+
+		// build an array of the original events
 		var events = $.map(segs, function(seg) {
 			return seg.event;
 		});
+
 		var dayStart = dayDate.clone().stripTime();
 		var dayEnd = dayStart.clone().add(1, 'days');
 
-		return this.eventsToSegs(events, dayStart, dayEnd);
+		// slice the events with a custom slicing function
+		return this.eventsToSegs(
+			events,
+			function(rangeStart, rangeEnd) {
+				var seg = intersectionToSeg(rangeStart, rangeEnd, dayStart, dayEnd); // if no intersection, undefined
+				return seg ? [ seg ] : []; // must return an array of segments
+			}
+		);
 	},
 
 

+ 270 - 40
src/common/Grid.events.js

@@ -7,28 +7,73 @@ $.extend(Grid.prototype, {
 	mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing
 	isDraggingSeg: false, // is a segment being dragged? boolean
 	isResizingSeg: false, // is a segment being resized? boolean
+	segs: null, // the event segments currently rendered in the grid
 
 
 	// Renders the given events onto the grid
 	renderEvents: function(events) {
-		// subclasses must implement
+		var segs = this.eventsToSegs(events);
+		var bgSegs = [];
+		var fgSegs = [];
+		var i, seg;
+
+		for (i = 0; i < segs.length; i++) {
+			seg = segs[i];
+
+			if (isBgEvent(seg.event)) {
+				bgSegs.push(seg);
+			}
+			else {
+				fgSegs.push(seg);
+			}
+		}
+
+		// Render each different type of segment.
+		// Each function may return a subset of the segs, segs that were actually rendered.
+		bgSegs = this.renderBgSegs(bgSegs) || bgSegs;
+		fgSegs = this.renderFgSegs(fgSegs) || fgSegs;
+
+		this.segs = bgSegs.concat(fgSegs);
 	},
 
 
-	// Retrieves all rendered segment objects in this grid
+	// Unrenders all events currently rendered on the grid
+	destroyEvents: function() {
+		this.triggerSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
+
+		this.destroyFgSegs();
+		this.destroyBgSegs();
+
+		this.segs = null;
+	},
+
+
+	// Retrieves all rendered segment objects currently rendered on the grid
 	getSegs: function() {
+		return this.segs || [];
+	},
+
+
+	/* Foreground Segment Rendering
+	------------------------------------------------------------------------------------------------------------------*/
+
+
+	// Renders foreground event segments onto the grid. May return a subset of segs that were rendered.
+	renderFgSegs: function(segs) {
 		// subclasses must implement
 	},
 
 
-	// Unrenders all events. Subclasses should implement, calling this super-method first.
-	destroyEvents: function() {
-		this.triggerSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
+	// Unrenders all currently rendered foreground segments
+	destroyFgSegs: function() {
+		// subclasses must implement
 	},
 
 
-	// Renders a `el` property for each seg, and only returns segments that successfully rendered
-	renderSegs: function(segs, disableResizing) {
+	// Renders and assigns an `el` property for each foreground event segment.
+	// Only returns segments that successfully rendered.
+	// A utility that subclasses may use.
+	renderFgSegEls: function(segs, disableResizing) {
 		var view = this.view;
 		var html = '';
 		var renderedSegs = [];
@@ -36,7 +81,7 @@ $.extend(Grid.prototype, {
 
 		// build a large concatenation of event segment HTML
 		for (i = 0; i < segs.length; i++) {
-			html += this.renderSegHtml(segs[i], disableResizing);
+			html += this.fgSegHtml(segs[i], disableResizing);
 		}
 
 		// Grab individual elements from the combined HTML string. Use each as the default rendering.
@@ -55,48 +100,65 @@ $.extend(Grid.prototype, {
 	},
 
 
-	// Generates the HTML for the default rendering of a segment
-	renderSegHtml: function(seg, disableResizing) {
-		// subclasses must implement
+	// Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls()
+	fgSegHtml: function(seg, disableResizing) {
+		// subclasses should implement
 	},
 
 
-	// Converts an array of event objects into an array of segment objects
-	eventsToSegs: function(events, intervalStart, intervalEnd) {
-		var _this = this;
+	/* Background Segment Rendering
+	------------------------------------------------------------------------------------------------------------------*/
 
-		return $.map(events, function(event) {
-			return _this.eventToSegs(event, intervalStart, intervalEnd); // $.map flattens all returned arrays together
-		});
+
+	// Renders the given background event segments onto the grid
+	// TODO: should probably be abstract, but do this for immediate code reuse
+	renderBgSegs: function(segs) {
+		this.renderFill('bgEvent', segs); // relies on the subclass having a renderFill method!
 	},
 
 
-	// Slices a single event into an array of event segments.
-	// When `intervalStart` and `intervalEnd` are specified, intersect the events with that interval.
-	// Otherwise, let the subclass decide how it wants to slice the segments over the grid.
-	eventToSegs: function(event, intervalStart, intervalEnd) {
-		var eventStart = event.start.clone().stripZone(); // normalize
-		var eventEnd = this.view.calendar.getEventEnd(event).stripZone(); // compute (if necessary) and normalize
-		var segs;
-		var i, seg;
+	// Unrenders all the currently rendered background event segments
+	// TODO: should probably be abstract, but do this for immediate code reuse
+	destroyBgSegs: function() {
+		this.destroyFill('bgEvent'); // relies on the subclass having a destroyFill method!
+	},
 
-		if (intervalStart && intervalEnd) {
-			seg = intersectionToSeg(eventStart, eventEnd, intervalStart, intervalEnd);
-			segs = seg ? [ seg ] : [];
-		}
-		else {
-			segs = this.rangeToSegs(eventStart, eventEnd); // defined by the subclass
-		}
 
-		// assign extra event-related properties to the segment objects
-		for (i = 0; i < segs.length; i++) {
-			seg = segs[i];
-			seg.event = event;
-			seg.eventStartMS = +eventStart;
-			seg.eventDurationMS = eventEnd - eventStart;
+	// Returns a space-separated string of additional classNames to apply to a background event segment.
+	// Gets called by each subclass' fill-rendering system.
+	// TODO: merge with getSegClasses() somehow
+	bgEventSegClasses: function(seg) {
+		var event = seg.event;
+		var source = event.source || {};
+		var classes = event.className.concat(source.className || []);
+
+		return classes.join(' ');
+	},
+
+
+	// Returns additional CSS properties (as a string) to be applied to a background event segment.
+	// Gets called by each subclass' fill-rendering system.
+	// TODO: merge with getEventSkinCss() somehow
+	bgEventSegStyles: function(seg) {
+		var view = this.view;
+		var event = seg.event;
+		var source = event.source || {};
+		var eventColor = event.color;
+		var sourceColor = source.color;
+		var optionColor = view.opt('eventColor');
+		var backgroundColor =
+			event.backgroundColor ||
+			eventColor ||
+			source.backgroundColor ||
+			sourceColor ||
+			view.opt('eventBackgroundColor') ||
+			optionColor;
+
+		if (backgroundColor) {
+			return 'background:' + backgroundColor;
 		}
 
-		return segs;
+		return '';
 	},
 
 
@@ -402,16 +464,184 @@ $.extend(Grid.prototype, {
 			statements.push('color:' + textColor);
 		}
 		return statements.join(';');
+	},
+
+
+	/* Converting events -> ranges -> segs
+	------------------------------------------------------------------------------------------------------------------*/
+
+
+	// Converts an array of event objects into an array of event segment objects.
+	// A custom `rangeToSegsFunc` may be given for arbitrarily slicing up events.
+	eventsToSegs: function(events, rangeToSegsFunc) {
+		var eventRanges = this.eventsToRanges(events);
+		var segs = [];
+		var i;
+
+		for (i = 0; i < eventRanges.length; i++) {
+			segs.push.apply(
+				segs,
+				this.eventRangeToSegs(eventRanges[i], rangeToSegsFunc)
+			);
+		}
+
+		return segs;
+	},
+
+
+	// Converts an array of events into an array of "range" objects.
+	// A "range" object is a plain object with start/end properties denoting the time it covers. Also an event property.
+	// For "normal" events, this will be identical to the event's start/end, but for "inverse-background" events,
+	// will create an array of ranges that span the time *not* covered by the given event.
+	eventsToRanges: function(events) {
+		var _this = this;
+		var eventsById = groupEventsById(events);
+		var ranges = [];
+
+		// group by ID so that related inverse-background events can be rendered together
+		$.each(eventsById, function(id, eventGroup) {
+			if (eventGroup.length) {
+				ranges.push.apply(
+					ranges,
+					eventGroup[0].rendering === 'inverse-background' ?
+						_this.eventsToInverseRanges(eventGroup) :
+						_this.eventsToNormalRanges(eventGroup)
+				);
+			}
+		});
+
+		return ranges;
+	},
+
+
+	// Converts an array of "normal" events (not inverted rendering) into a parallel array of ranges
+	eventsToNormalRanges: function(events) {
+		var calendar = this.view.calendar;
+		var ranges = [];
+		var i, event;
+		var eventStart, eventEnd;
+
+		for (i = 0; i < events.length; i++) {
+			event = events[i];
+
+			// make copies and normalize by stripping timezone
+			eventStart = event.start.clone().stripZone();
+			eventEnd = calendar.getEventEnd(event).stripZone();
+
+			ranges.push({
+				event: event,
+				start: eventStart,
+				end: eventEnd,
+				eventStartMS: +eventStart,
+				eventDurationMS: eventEnd - eventStart
+			});
+		}
+
+		return ranges;
+	},
+
+
+	// Converts an array of events, with inverse-background rendering, into an array of range objects.
+	// The range objects will cover all the time NOT covered by the events.
+	eventsToInverseRanges: function(events) {
+		var view = this.view;
+		var calendar = view.calendar;
+		var viewStart = view.start.clone().stripZone(); // normalize timezone
+		var viewEnd = view.end.clone().stripZone(); // normalize timezone
+		var normalRanges = this.eventsToNormalRanges(events); // will give us normalized dates we can use w/o copies
+		var inverseRanges = [];
+		var event0 = events[0]; // assign this to each range's `.event`
+		var start = viewStart; // the end of the previous range. the start of the new range
+		var i, normalRange;
+
+		// ranges need to be in order. required for our date-walking algorithm
+		normalRanges.sort(compareNormalRanges);
+
+		for (i = 0; i < normalRanges.length; i++) {
+			normalRange = normalRanges[i];
+
+			// add the span of time before the event (if there is any)
+			if (normalRange.start > start) { // compare millisecond time (skip any ambig logic)
+				inverseRanges.push({
+					event: event0,
+					start: start,
+					end: normalRange.start
+				});
+			}
+
+			start = normalRange.end;
+		}
+
+		// add the span of time after the last event (if there is any)
+		if (start < viewEnd) { // compare millisecond time (skip any ambig logic)
+			inverseRanges.push({
+				event: event0,
+				start: start,
+				end: viewEnd
+			});
+		}
+
+		return inverseRanges;
+	},
+
+
+	// Slices the given event range into one or more segment objects.
+	// A `rangeToSegsFunc` custom slicing function can be given.
+	eventRangeToSegs: function(eventRange, rangeToSegsFunc) {
+		var event = eventRange.event;
+		var segs;
+		var i, seg;
+
+		if (rangeToSegsFunc) {
+			segs = rangeToSegsFunc(eventRange.start, eventRange.end);
+		}
+		else {
+			segs = this.rangeToSegs(eventRange.start, eventRange.end); // defined by the subclass
+		}
+
+		for (i = 0; i < segs.length; i++) {
+			seg = segs[i];
+			seg.event = event;
+			seg.eventStartMS = eventRange.eventStartMS;
+			seg.eventDurationMS = eventRange.eventDurationMS;
+		}
+
+		return segs;
 	}
 
 });
 
 
-/* Event Segment Utilities
+/* Utilities
 ----------------------------------------------------------------------------------------------------------------------*/
 
 
+function isBgEvent(event) {
+	return event.rendering === 'background' || event.rendering === 'inverse-background';
+}
+
+
+function groupEventsById(events) {
+	var eventsById = {};
+	var i, event;
+
+	for (i = 0; i < events.length; i++) {
+		event = events[i];
+		(eventsById[event._id] || (eventsById[event._id] = [])).push(event);
+	}
+
+	return eventsById;
+}
+
+
+// A cmp function for determining which non-inverted "ranges" (see above) happen earlier
+function compareNormalRanges(range1, range2) {
+	return range1.eventStartMS - range2.eventStartMS; // earlier ranges go first
+}
+
+
 // A cmp function for determining which segments should take visual priority
+// DOES NOT WORK ON INVERTED BACKGROUND EVENTS because they have no eventStartMS/eventDurationMS
 function compareSegs(seg1, seg2) {
 	return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first
 		seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first

+ 1 - 1
src/common/RowRenderer.js

@@ -89,7 +89,7 @@ RowRenderer.prototype = {
 		}
 
 		if (typeof renderer === 'function') {
-			return function(row) {
+			return function() {
 				return renderer.apply(provider, arguments) || ''; // use correct `this` and always return a string
 			};
 		}

+ 13 - 27
src/common/TimeGrid.events.js

@@ -4,52 +4,41 @@
 
 $.extend(TimeGrid.prototype, {
 
-	segs: null, // segment objects rendered in the component. null of events haven't been rendered yet
 	eventSkeletonEl: null, // has cells with event-containers, which contain absolutely positioned event elements
 
 
-	// Renders the events onto the grid and returns an array of segments that have been rendered
-	renderEvents: function(events) {
-		var res = this.renderEventTable(events);
+	// Renders the given foreground event segments onto the grid
+	renderFgSegs: function(segs) {
+		segs = this.renderFgSegEls(segs); // returns a subset of the segs. segs that were actually rendered
 
-		this.eventSkeletonEl = $('<div class="fc-content-skeleton"/>').append(res.tableEl);
-		this.el.append(this.eventSkeletonEl);
+		this.el.append(
+			this.eventSkeletonEl = $('<div class="fc-content-skeleton"/>')
+				.append(this.renderSegTable(segs))
+		);
 
-		this.segs = res.segs;
+		return segs; // return only the segs that were actually rendered
 	},
 
 
-	// Retrieves rendered segment objects
-	getSegs: function() {
-		return this.segs || [];
-	},
-
-
-	// Removes all event segment elements from the view
-	destroyEvents: function() {
-		Grid.prototype.destroyEvents.call(this); // call the super-method
-
+	// Unrenders all currently rendered foreground event segments
+	destroyFgSegs: function(segs) {
 		if (this.eventSkeletonEl) {
 			this.eventSkeletonEl.remove();
 			this.eventSkeletonEl = null;
 		}
-
-		this.segs = null;
 	},
 
 
 	// Renders and returns the <table> portion of the event-skeleton.
 	// Returns an object with properties 'tbodyEl' and 'segs'.
-	renderEventTable: function(events) {
+	renderSegTable: function(segs) {
 		var tableEl = $('<table><tr/></table>');
 		var trEl = tableEl.find('tr');
-		var segs = this.eventsToSegs(events);
 		var segCols;
 		var i, seg;
 		var col, colSegs;
 		var containerEl;
 
-		segs = this.renderSegs(segs); // returns only the visible segs
 		segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg
 
 		this.computeSegVerticals(segs); // compute and assign top/bottom
@@ -78,10 +67,7 @@ $.extend(TimeGrid.prototype, {
 
 		this.bookendCells(trEl, 'eventSkeleton');
 
-		return  {
-			tableEl: tableEl,
-			segs: segs
-		};
+		return tableEl;
 	},
 
 
@@ -115,7 +101,7 @@ $.extend(TimeGrid.prototype, {
 
 
 	// Renders the HTML for a single event segment's default rendering
-	renderSegHtml: function(seg, disableResizing) {
+	fgSegHtml: function(seg, disableResizing) {
 		var view = this.view;
 		var event = seg.event;
 		var isDraggable = view.isEventDraggable(event);

+ 86 - 45
src/common/TimeGrid.js

@@ -4,6 +4,8 @@
 
 function TimeGrid(view) {
 	Grid.call(this, view); // call the super-constructor
+
+	this.elsByFill = {};
 }
 
 
@@ -21,8 +23,8 @@ $.extend(TimeGrid.prototype, {
 
 	slatTops: null, // an array of top positions, relative to the container. last item holds bottom of last slot
 
-	highlightEl: null, // cell skeleton element for rendering the highlight
 	helperEl: null, // cell skeleton element for rendering the mock event "helper"
+	elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.
 
 
 	// Renders the time grid into `this.el`, which should already be assigned.
@@ -326,12 +328,14 @@ $.extend(TimeGrid.prototype, {
 
 	// Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag)
 	renderHelper: function(event, sourceSeg) {
-		var res = this.renderEventTable([ event ]);
-		var tableEl = res.tableEl;
-		var segs = res.segs;
+		var segs = this.eventsToSegs([ event ]);
+		var tableEl;
 		var i, seg;
 		var sourceEl;
 
+		segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered
+		tableEl = this.renderSegTable(segs);
+
 		// Try to make the segment that is in the same row as sourceSeg look the same
 		for (i = 0; i < segs.length; i++) {
 			seg = segs[i];
@@ -389,73 +393,110 @@ $.extend(TimeGrid.prototype, {
 
 	// Renders an emphasis on the given date range. `start` is inclusive. `end` is exclusive.
 	renderHighlight: function(start, end) {
-		this.highlightEl = $(
-			this.highlightSkeletonHtml(start, end)
-		).appendTo(this.el);
+		this.renderFill('highlight', this.rangeToSegs(start, end));
 	},
 
 
 	// Unrenders the emphasis on a date range
 	destroyHighlight: function() {
-		if (this.highlightEl) {
-			this.highlightEl.remove();
-			this.highlightEl = null;
-		}
+		this.destroyFill('highlight');
 	},
 
 
-	// Generates HTML for a table element with containers in each column, responsible for absolutely positioning the
-	// highlight elements to cover the highlighted slots.
-	highlightSkeletonHtml: function(start, end) {
+	/* "Fill" Rendering (rectangles covering a specified period of days)
+	------------------------------------------------------------------------------------------------------------------*/
+
+
+	// Renders a set of rectangles over the given time segments.
+	// The `type` is used for destroying later. Also allows for special-cased behavior via strategically-named methods.
+	renderFill: function(type, segs) {
 		var view = this.view;
-		var segs = this.rangeToSegs(start, end);
+		var extraClassesMethod = this[type + 'SegClasses']; // TODO: better system for this
+		var extraStylesMethod = this[type + 'SegStyles']; //
+		var typeLower = type.toLowerCase();
 		var cellHtml = '';
-		var col = 0;
+		var segCols;
+		var col, colSegs;
 		var i, seg;
 		var dayDate;
 		var top, bottom;
+		var extraClasses;
+		var extraStyles;
+		var el;
+		var segEls;
+		var j;
 
-		for (i = 0; i < segs.length; i++) { // loop through the segments. one per column
-			seg = segs[i];
+		segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg
 
-			// need empty cells beforehand?
-			if (col < seg.col) {
-				cellHtml += '<td colspan="' + (seg.col - col) + '"/>';
-				col = seg.col;
-			}
+		for (col = 0; col < segCols.length; col++) {
+			colSegs = segCols[col];
 
-			// compute vertical position
-			dayDate = view.cellToDate(0, col);
-			top = this.computeDateTop(seg.start, dayDate);
-			bottom = this.computeDateTop(seg.end, dayDate); // the y position of the bottom edge
-
-			// generate the cell HTML. bottom becomes negative because it needs to be a CSS value relative to the
-			// bottom edge of the zero-height container.
-			cellHtml +=
-				'<td>' +
-					'<div class="fc-highlight-container">' +
-						'<div class="fc-highlight" style="top:' + top + 'px;bottom:-' + bottom + 'px"/>' +
-					'</div>' +
-				'</td>';
+			cellHtml += '<td>';
 
-			col++;
-		}
+			if (colSegs.length) {
+				cellHtml += '<div class="fc-' + typeLower + '-container">';
+
+				for (i = 0; i < colSegs.length; i++) {
+					seg = colSegs[i];
+
+					// compute vertical position
+					dayDate = view.cellToDate(0, col);
+					top = this.computeDateTop(seg.start, dayDate);
+					bottom = this.computeDateTop(seg.end, dayDate); // the y position of the bottom edge
+
+					// TODO: better system for this
+					extraClasses = extraClassesMethod ? extraClassesMethod.call(this, seg) : '';
+					extraStyles = extraStylesMethod ? extraStylesMethod.call(this, seg) : '';
+
+					cellHtml += '<div' +
+						' class="fc-' + typeLower + ' ' + extraClasses + '"' +
+						' style="top:' + top + 'px;bottom:-' + bottom + 'px;' + extraStyles + '"' +
+						'/>';
+				}
 
-		// need empty cells after the last segment?
-		if (col < view.colCnt) {
-			cellHtml += '<td colspan="' + (view.colCnt - col) + '"/>';
+				cellHtml += '</div>';
+			}
+
+			cellHtml += '</td>';
 		}
 
-		cellHtml = this.bookendCells(cellHtml, 'highlight');
+		cellHtml = this.bookendCells(cellHtml, type);
 
-		return '' +
-			'<div class="fc-highlight-skeleton">' +
+		el = $(
+			'<div class="fc-' + typeLower + '-skeleton">' +
 				'<table>' +
 					'<tr>' +
 						cellHtml +
 					'</tr>' +
 				'</table>' +
-			'</div>';
+			'</div>'
+		);
+
+		// assign each segment's el. TODO: there's gotta be a better way
+		segEls = el.find('.fc-' + typeLower);
+		j = 0;
+		for (col = 0; col < segCols.length; col++) {
+			colSegs = segCols[col];
+			for (i = 0; i < colSegs.length; i++) {
+				seg = colSegs[i];
+				seg.el = segEls.eq(j);
+				j++;
+			}
+		}
+
+		this.el.append(el);
+		this.elsByFill[type] = el;
+	},
+
+
+	// Unrenders a specific type of fill that is currently rendered on the grid
+	destroyFill: function(type) {
+		var el = this.elsByFill[type];
+
+		if (el) {
+			el.remove();
+			delete this.elsByFill[type];
+		}
 	}
 
 });

+ 8 - 4
src/common/View.js

@@ -167,7 +167,7 @@ View.prototype = {
 	renderEvents: function(events) {
 		this.segEach(function(seg) {
 			this.trigger('eventAfterRender', seg.event, seg.event, seg.el);
-		});
+		}, null, false); // isBg=false
 		this.trigger('eventAfterAllRender');
 	},
 
@@ -177,7 +177,7 @@ View.prototype = {
 	destroyEvents: function() {
 		this.segEach(function(seg) {
 			this.trigger('eventDestroy', seg.event, seg.event, seg.el);
-		});
+		}, null, false); // isBg=false
 	},
 
 
@@ -215,13 +215,17 @@ View.prototype = {
 
 	// Iterates through event segments. Goes through all by default.
 	// If the optional `event` argument is specified, only iterates through segments linked to that event.
+	// If the optional `isBg` argument is a boolean, only iterates through segments that are background/foreground.
 	// The `this` value of the callback function will be the view.
-	segEach: function(func, event) {
+	segEach: function(func, event, isBg) {
 		var segs = this.getSegs();
 		var i;
 
 		for (i = 0; i < segs.length; i++) {
-			if (!event || segs[i].event._id === event._id) {
+			if (
+				(!event || segs[i].event._id === event._id) &&
+				(isBg == null || isBgEvent(segs[i].event) === isBg)
+			) {
 				func.call(this, segs[i]);
 			}
 		}

+ 23 - 5
src/common/common.css

@@ -55,6 +55,12 @@ body .fc { /* extra precedence to overcome jqui */
 	filter: alpha(opacity=30); /* for IE */
 }
 
+.fc-bgevent { /* default look for background events */
+	background: green;
+	opacity: .3;
+	filter: alpha(opacity=30); /* for IE */
+}
+
 
 /* Icons (inline elements with styled text that mock arrow icons)
 --------------------------------------------------------------------------------------------------*/
@@ -276,6 +282,7 @@ previous button's border...
 }
 
 .fc-bg,
+.fc-bgevent-skeleton,
 .fc-highlight-skeleton,
 .fc-helper-skeleton {
 	/* these element should always cling to top-left/right corners */
@@ -357,21 +364,32 @@ previous button's border...
 	z-index: 1;
 }
 
-/* highlighting cells */
+/* highlighting cells & background event skeleton */
 
+.fc-row .fc-bgevent-skeleton,
 .fc-row .fc-highlight-skeleton {
-	z-index: 2;
 	bottom: 0; /* stretch skeleton to bottom of row */
 }
 
+.fc-row .fc-bgevent-skeleton table,
 .fc-row .fc-highlight-skeleton table {
 	height: 100%; /* stretch skeleton to bottom of row */
 }
 
-.fc-row .fc-highlight-skeleton td {
+.fc-row .fc-highlight-skeleton td,
+.fc-row .fc-bgevent-skeleton td {
 	border-color: transparent;
 }
 
+.fc-row .fc-bgevent-skeleton {
+	z-index: 2;
+
+}
+
+.fc-row .fc-highlight-skeleton {
+	z-index: 3;
+}
+
 /*
 row content (which contains day/week numbers and events) as well as "helper" (which contains
 temporary rendered events).
@@ -379,12 +397,12 @@ temporary rendered events).
 
 .fc-row .fc-content-skeleton {
 	position: relative;
-	z-index: 3;
+	z-index: 4;
 	padding-bottom: 2px; /* matches the space above the events */
 }
 
 .fc-row .fc-helper-skeleton {
-	z-index: 4;
+	z-index: 5;
 }
 
 .fc-row .fc-content-skeleton td,