浏览代码

make proper time/day-grid renderer classes

Adam Shaw 8 年之前
父节点
当前提交
7acf1584b2
共有 5 个文件被更改,包括 689 次插入698 次删除
  1. 2 2
      src.json
  2. 229 259
      src/common/DayGrid.events.js
  3. 33 0
      src/common/DayGrid.js
  4. 209 437
      src/common/TimeGrid.events.js
  5. 216 0
      src/common/TimeGrid.js

+ 2 - 2
src.json

@@ -31,11 +31,11 @@
     "common/EventRenderUtils.js",
     "common/SegChronoComponentMixin.js",
     "common/DayTableMixin.js",
-    "common/DayGrid.js",
     "common/DayGrid.events.js",
+    "common/DayGrid.js",
     "common/DayGrid.limit.js",
-    "common/TimeGrid.js",
     "common/TimeGrid.events.js",
+    "common/TimeGrid.js",
     "common/View.js",
     "common/View.date-range.js",
     "common/Scroller.js",

+ 229 - 259
src/common/DayGrid.events.js

@@ -2,315 +2,285 @@
 /* Event-rendering methods for the DayGrid class
 ----------------------------------------------------------------------------------------------------------------------*/
 
-DayGrid.mixin({
+var DayGridEventRenderer = EventRenderUtils.extend({
 
-	eventRenderUtilsClass: SegChronoComponentMixin.eventRenderUtilsClass.extend({
+	dayGrid: null,
+	rowStructs: null, // an array of objects, each holding information about a row's foreground event-rendering
 
-		dayGrid: null,
-		rowStructs: null, // an array of objects, each holding information about a row's foreground event-rendering
 
+	constructor: function(dayGrid) {
+		EventRenderUtils.call(this, dayGrid);
 
-		constructor: function(dayGrid) {
-			SegChronoComponentMixin.eventRenderUtilsClass.call(this, dayGrid);
-			this.dayGrid = dayGrid;
-		},
+		this.dayGrid = dayGrid;
+	},
 
 
-		// Renders the given foreground event segments onto the grid
-		renderFgSegs: function(segs) {
-			var rowStructs;
+	// 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);
+		// 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);
+		rowStructs = this.rowStructs = this.renderSegRows(segs);
 
-			// append to each row's content skeleton
-			this.dayGrid.rowEls.each(function(i, rowNode) {
-				$(rowNode).find('.fc-content-skeleton > table').append(
-					rowStructs[i].tbodyEl
-				);
-			});
+		// append to each row's content skeleton
+		this.dayGrid.rowEls.each(function(i, rowNode) {
+			$(rowNode).find('.fc-content-skeleton > table').append(
+				rowStructs[i].tbodyEl
+			);
+		});
 
-			return segs; // return only the segs that were actually rendered
-		},
+		return segs; // return only the segs that were actually rendered
+	},
 
 
-		// Unrenders all currently rendered foreground event segments
-		unrenderFgSegs: function() {
-			var rowStructs = this.rowStructs || [];
-			var rowStruct;
+	// Unrenders all currently rendered foreground event segments
+	unrenderFgSegs: function() {
+		var rowStructs = this.rowStructs || [];
+		var rowStruct;
 
-			while ((rowStruct = rowStructs.pop())) {
-				rowStruct.tbodyEl.remove();
-			}
+		while ((rowStruct = rowStructs.pop())) {
+			rowStruct.tbodyEl.remove();
+		}
 
-			this.rowStructs = null;
-		},
+		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 `renderSegRow`).
-		// PRECONDITION: each segment shoud already have a rendered and assigned `.el`
-		renderSegRows: function(segs) {
-			var rowStructs = [];
-			var segRows;
-			var row;
+	// 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 `renderSegRow`).
+	// PRECONDITION: each segment shoud already have a rendered and assigned `.el`
+	renderSegRows: function(segs) {
+		var rowStructs = [];
+		var segRows;
+		var row;
 
-			segRows = this.groupSegRows(segs); // group into nested arrays
+		segRows = this.groupSegRows(segs); // group into nested arrays
 
-			// iterate each row of segment groupings
-			for (row = 0; row < segRows.length; row++) {
-				rowStructs.push(
-					this.renderSegRow(row, segRows[row])
-				);
-			}
+		// iterate each row of segment groupings
+		for (row = 0; row < segRows.length; row++) {
+			rowStructs.push(
+				this.renderSegRow(row, segRows[row])
+			);
+		}
 
-			return rowStructs;
-		},
-
-
-		// 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.
-		// NOTE: modifies rowSegs
-		renderSegRow: function(row, rowSegs) {
-			var colCnt = this.dayGrid.colCnt;
-			var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels
-			var levelCnt = Math.max(1, segLevels.length); // ensure at least one level
-			var tbody = $('<tbody/>');
-			var segMatrix = []; // lookup for which segments are rendered into which level+col cells
-			var cellMatrix = []; // lookup for all <td> elements of the level+col matrix
-			var loneCellMatrix = []; // lookup for <td> elements that only take up a single column
-			var i, levelSegs;
-			var col;
-			var tr;
-			var j, seg;
-			var td;
-
-			// populates empty cells from the current column (`col`) to `endCol`
-			function emptyCellsUntil(endCol) {
-				while (col < endCol) {
-					// try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell
-					td = (loneCellMatrix[i - 1] || [])[col];
-					if (td) {
-						td.attr(
-							'rowspan',
-							parseInt(td.attr('rowspan') || 1, 10) + 1
-						);
-					}
-					else {
-						td = $('<td/>');
-						tr.append(td);
-					}
-					cellMatrix[i][col] = td;
-					loneCellMatrix[i][col] = td;
-					col++;
+		return rowStructs;
+	},
+
+
+	// 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.
+	// NOTE: modifies rowSegs
+	renderSegRow: function(row, rowSegs) {
+		var colCnt = this.dayGrid.colCnt;
+		var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels
+		var levelCnt = Math.max(1, segLevels.length); // ensure at least one level
+		var tbody = $('<tbody/>');
+		var segMatrix = []; // lookup for which segments are rendered into which level+col cells
+		var cellMatrix = []; // lookup for all <td> elements of the level+col matrix
+		var loneCellMatrix = []; // lookup for <td> elements that only take up a single column
+		var i, levelSegs;
+		var col;
+		var tr;
+		var j, seg;
+		var td;
+
+		// populates empty cells from the current column (`col`) to `endCol`
+		function emptyCellsUntil(endCol) {
+			while (col < endCol) {
+				// try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell
+				td = (loneCellMatrix[i - 1] || [])[col];
+				if (td) {
+					td.attr(
+						'rowspan',
+						parseInt(td.attr('rowspan') || 1, 10) + 1
+					);
 				}
+				else {
+					td = $('<td/>');
+					tr.append(td);
+				}
+				cellMatrix[i][col] = td;
+				loneCellMatrix[i][col] = td;
+				col++;
 			}
+		}
 
-			for (i = 0; i < levelCnt; i++) { // iterate through all levels
-				levelSegs = segLevels[i];
-				col = 0;
-				tr = $('<tr/>');
-
-				segMatrix.push([]);
-				cellMatrix.push([]);
-				loneCellMatrix.push([]);
-
-				// levelCnt might be 1 even though there are no actual levels. protect against this.
-				// this single empty row is useful for styling.
-				if (levelSegs) {
-					for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level
-						seg = levelSegs[j];
-
-						emptyCellsUntil(seg.leftCol);
-
-						// create a container that occupies or more columns. append the event element.
-						td = $('<td class="fc-event-container"/>').append(seg.el);
-						if (seg.leftCol != seg.rightCol) {
-							td.attr('colspan', seg.rightCol - seg.leftCol + 1);
-						}
-						else { // a single-column segment
-							loneCellMatrix[i][col] = td;
-						}
-
-						while (col <= seg.rightCol) {
-							cellMatrix[i][col] = td;
-							segMatrix[i][col] = seg;
-							col++;
-						}
-
-						tr.append(td);
-					}
-				}
+		for (i = 0; i < levelCnt; i++) { // iterate through all levels
+			levelSegs = segLevels[i];
+			col = 0;
+			tr = $('<tr/>');
 
-				emptyCellsUntil(colCnt); // finish off the row
-				this.dayGrid.bookendCells(tr);
-				tbody.append(tr);
-			}
+			segMatrix.push([]);
+			cellMatrix.push([]);
+			loneCellMatrix.push([]);
+
+			// levelCnt might be 1 even though there are no actual levels. protect against this.
+			// this single empty row is useful for styling.
+			if (levelSegs) {
+				for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level
+					seg = levelSegs[j];
 
-			return { // a "rowStruct"
-				row: row, // the row number
-				tbodyEl: tbody,
-				cellMatrix: cellMatrix,
-				segMatrix: segMatrix,
-				segLevels: segLevels,
-				segs: rowSegs
-			};
-		},
-
-
-		// Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels.
-		// NOTE: modifies segs
-		buildSegLevels: function(segs) {
-			var levels = [];
-			var i, seg;
-			var j;
-
-			// Give preference to elements with certain criteria, so they have
-			// a chance to be closer to the top.
-			this.sortEventSegs(segs);
-
-			for (i = 0; i < segs.length; i++) {
-				seg = segs[i];
-
-				// loop through levels, starting with the topmost, until the segment doesn't collide with other segments
-				for (j = 0; j < levels.length; j++) {
-					if (!isDaySegCollision(seg, levels[j])) {
-						break;
+					emptyCellsUntil(seg.leftCol);
+
+					// create a container that occupies or more columns. append the event element.
+					td = $('<td class="fc-event-container"/>').append(seg.el);
+					if (seg.leftCol != seg.rightCol) {
+						td.attr('colspan', seg.rightCol - seg.leftCol + 1);
+					}
+					else { // a single-column segment
+						loneCellMatrix[i][col] = td;
 					}
-				}
-				// `j` now holds the desired subrow index
-				seg.level = j;
 
-				// create new level array if needed and append segment
-				(levels[j] || (levels[j] = [])).push(seg);
-			}
+					while (col <= seg.rightCol) {
+						cellMatrix[i][col] = td;
+						segMatrix[i][col] = seg;
+						col++;
+					}
 
-			// order segments left-to-right. very important if calendar is RTL
-			for (j = 0; j < levels.length; j++) {
-				levels[j].sort(compareDaySegCols);
+					tr.append(td);
+				}
 			}
 
-			return levels;
-		},
+			emptyCellsUntil(colCnt); // finish off the row
+			this.dayGrid.bookendCells(tr);
+			tbody.append(tr);
+		}
+
+		return { // a "rowStruct"
+			row: row, // the row number
+			tbodyEl: tbody,
+			cellMatrix: cellMatrix,
+			segMatrix: segMatrix,
+			segLevels: segLevels,
+			segs: rowSegs
+		};
+	},
 
 
-		// Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row
-		groupSegRows: function(segs) {
-			var segRows = [];
-			var i;
+	// Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels.
+	// NOTE: modifies segs
+	buildSegLevels: function(segs) {
+		var levels = [];
+		var i, seg;
+		var j;
 
-			for (i = 0; i < this.dayGrid.rowCnt; i++) {
-				segRows.push([]);
-			}
+		// Give preference to elements with certain criteria, so they have
+		// a chance to be closer to the top.
+		this.sortEventSegs(segs);
 
-			for (i = 0; i < segs.length; i++) {
-				segRows[segs[i].row].push(segs[i]);
-			}
+		for (i = 0; i < segs.length; i++) {
+			seg = segs[i];
 
-			return segRows;
-		},
-
-
-		// Computes a default event time formatting string if `timeFormat` is not explicitly defined
-		computeEventTimeFormat: function() {
-			return this.opt('extraSmallTimeFormat'); // like "6p" or "6:30p"
-		},
-
-
-		// Computes a default `displayEventEnd` value if one is not expliclty defined
-		computeDisplayEventEnd: function() {
-			return this.dayGrid.colCnt === 1; // we'll likely have space if there's only one day
-		},
-
-
-		// Builds the HTML to be used for the default element for an individual segment
-		fgSegHtml: function(seg, disableResizing) {
-			var view = this.view;
-			var eventDef = seg.footprint.eventDef;
-			var isAllDay = seg.footprint.componentFootprint.isAllDay;
-			var isDraggable = view.isEventDefDraggable(eventDef);
-			var isResizableFromStart = !disableResizing && isAllDay &&
-				seg.isStart && view.isEventDefResizableFromStart(eventDef);
-			var isResizableFromEnd = !disableResizing && isAllDay &&
-				seg.isEnd && view.isEventDefResizableFromEnd(eventDef);
-			var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd);
-			var skinCss = cssToStr(this.getSkinCss(seg.footprint));
-			var timeHtml = '';
-			var timeText;
-			var titleHtml;
-
-			classes.unshift('fc-day-grid-event', 'fc-h-event');
-
-			// Only display a timed events time if it is the starting segment
-			if (seg.isStart) {
-				timeText = this.getTimeText(seg.footprint);
-				if (timeText) {
-					timeHtml = '<span class="fc-time">' + htmlEscape(timeText) + '</span>';
+			// loop through levels, starting with the topmost, until the segment doesn't collide with other segments
+			for (j = 0; j < levels.length; j++) {
+				if (!isDaySegCollision(seg, levels[j])) {
+					break;
 				}
 			}
+			// `j` now holds the desired subrow index
+			seg.level = j;
 
-			titleHtml =
-				'<span class="fc-title">' +
-					(htmlEscape(eventDef.title || '') || '&nbsp;') + // we always want one line of height
-				'</span>';
+			// create new level array if needed and append segment
+			(levels[j] || (levels[j] = [])).push(seg);
+		}
 
-			return '<a class="' + classes.join(' ') + '"' +
-					(eventDef.url ?
-						' href="' + htmlEscape(eventDef.url) + '"' :
-						''
-						) +
-					(skinCss ?
-						' style="' + skinCss + '"' :
-						''
-						) +
-				'>' +
-					'<div class="fc-content">' +
-						(this.isRTL ?
-							titleHtml + ' ' + timeHtml : // put a natural space in between
-							timeHtml + ' ' + titleHtml   //
-							) +
-					'</div>' +
-					(isResizableFromStart ?
-						'<div class="fc-resizer fc-start-resizer" />' :
-						''
-						) +
-					(isResizableFromEnd ?
-						'<div class="fc-resizer fc-end-resizer" />' :
-						''
-						) +
-				'</a>';
-		},
+		// order segments left-to-right. very important if calendar is RTL
+		for (j = 0; j < levels.length; j++) {
+			levels[j].sort(compareDaySegCols);
+		}
+
+		return levels;
+	},
 
-	}),
 
+	// Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row
+	groupSegRows: function(segs) {
+		var segRows = [];
+		var i;
 
-	// Unrenders all events currently rendered on the grid
-	unrenderEvents: function() {
-		this.removeSegPopover(); // removes the "more.." events popover
+		for (i = 0; i < this.dayGrid.rowCnt; i++) {
+			segRows.push([]);
+		}
+
+		for (i = 0; i < segs.length; i++) {
+			segRows[segs[i].row].push(segs[i]);
+		}
 
-		SegChronoComponentMixin.unrenderEvents.apply(this, arguments);
+		return segRows;
 	},
 
 
-	// Retrieves all rendered segment objects currently rendered on the grid
-	getEventSegs: function() {
-		return SegChronoComponentMixin.getEventSegs.call(this) // get the segments from the super-method
-			.concat(this.popoverSegs || []); // append the segments from the "more..." popover
+	// Computes a default event time formatting string if `timeFormat` is not explicitly defined
+	computeEventTimeFormat: function() {
+		return this.opt('extraSmallTimeFormat'); // like "6p" or "6:30p"
 	},
 
 
-	// Renders the given background event segments onto the grid
-	renderBgSegs: function(segs) {
+	// Computes a default `displayEventEnd` value if one is not expliclty defined
+	computeDisplayEventEnd: function() {
+		return this.dayGrid.colCnt === 1; // we'll likely have space if there's only one day
+	},
 
-		// don't render timed background events
-		var allDaySegs = $.grep(segs, function(seg) {
-			return seg.footprint.componentFootprint.isAllDay;
-		});
 
-		return SegChronoComponentMixin.renderBgSegs.call(this, allDaySegs); // call the super-method
+	// Builds the HTML to be used for the default element for an individual segment
+	fgSegHtml: function(seg, disableResizing) {
+		var view = this.view;
+		var eventDef = seg.footprint.eventDef;
+		var isAllDay = seg.footprint.componentFootprint.isAllDay;
+		var isDraggable = view.isEventDefDraggable(eventDef);
+		var isResizableFromStart = !disableResizing && isAllDay &&
+			seg.isStart && view.isEventDefResizableFromStart(eventDef);
+		var isResizableFromEnd = !disableResizing && isAllDay &&
+			seg.isEnd && view.isEventDefResizableFromEnd(eventDef);
+		var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd);
+		var skinCss = cssToStr(this.getSkinCss(seg.footprint));
+		var timeHtml = '';
+		var timeText;
+		var titleHtml;
+
+		classes.unshift('fc-day-grid-event', 'fc-h-event');
+
+		// Only display a timed events time if it is the starting segment
+		if (seg.isStart) {
+			timeText = this.getTimeText(seg.footprint);
+			if (timeText) {
+				timeHtml = '<span class="fc-time">' + htmlEscape(timeText) + '</span>';
+			}
+		}
+
+		titleHtml =
+			'<span class="fc-title">' +
+				(htmlEscape(eventDef.title || '') || '&nbsp;') + // we always want one line of height
+			'</span>';
+
+		return '<a class="' + classes.join(' ') + '"' +
+				(eventDef.url ?
+					' href="' + htmlEscape(eventDef.url) + '"' :
+					''
+					) +
+				(skinCss ?
+					' style="' + skinCss + '"' :
+					''
+					) +
+			'>' +
+				'<div class="fc-content">' +
+					(this.isRTL ?
+						titleHtml + ' ' + timeHtml : // put a natural space in between
+						timeHtml + ' ' + titleHtml   //
+						) +
+				'</div>' +
+				(isResizableFromStart ?
+					'<div class="fc-resizer fc-start-resizer" />' :
+					''
+					) +
+				(isResizableFromEnd ?
+					'<div class="fc-resizer fc-end-resizer" />' :
+					''
+					) +
+			'</a>';
 	}
 
 });

+ 33 - 0
src/common/DayGrid.js

@@ -4,6 +4,8 @@
 
 var DayGrid = FC.DayGrid = ChronoComponent.extend(CoordChronoComponentMixin, SegChronoComponentMixin, DayTableMixin, {
 
+	eventRenderUtilsClass: DayGridEventRenderer,
+
 	view: null, // TODO: make more general and/or remove
 
 	numbersVisible: false, // should render a row for day/week numbers? set by outside view. TODO: make internal
@@ -322,6 +324,37 @@ var DayGrid = FC.DayGrid = ChronoComponent.extend(CoordChronoComponentMixin, Seg
 	},
 
 
+	/* Event Rendering
+	------------------------------------------------------------------------------------------------------------------*/
+
+
+	// 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.footprint.componentFootprint.isAllDay;
+		});
+
+		return SegChronoComponentMixin.renderBgSegs.call(this, allDaySegs); // call the super-method
+	},
+
+
+	// Unrenders all events currently rendered on the grid
+	unrenderEvents: function() {
+		this.removeSegPopover(); // removes the "more.." events popover
+
+		SegChronoComponentMixin.unrenderEvents.apply(this, arguments);
+	},
+
+
+	// Retrieves all rendered segment objects currently rendered on the grid
+	getEventSegs: function() {
+		return SegChronoComponentMixin.getEventSegs.call(this) // get the segments from the super-method
+			.concat(this.popoverSegs || []); // append the segments from the "more..." popover
+	},
+
+
 	/* Event Drag Visualization
 	------------------------------------------------------------------------------------------------------------------*/
 	// TODO: move to DayGrid.event, similar to what we did with Grid's drag methods

+ 209 - 437
src/common/TimeGrid.events.js

@@ -1,509 +1,281 @@
 
-/* Methods for rendering SEGMENTS, pieces of content that live on the view
- ( this file is no longer just for events )
-----------------------------------------------------------------------------------------------------------------------*/
+/*
+Only handles foreground segs.
+Does not own rendering. Use for low-level util methods by TimeGrid.
+*/
+var TimeGridEventRenderer = EventRenderUtils.extend({
 
-TimeGrid.mixin({
+	timeGrid: null,
 
-	colContainerEls: null, // containers for each column
 
-	// inner-containers for each column where different types of segs live
-	fgContainerEls: null,
-	bgContainerEls: null,
-	helperContainerEls: null,
-	highlightContainerEls: null,
-	businessContainerEls: null,
+	constructor: function(timeGrid) {
+		EventRenderUtils.call(this, timeGrid);
 
-	// arrays of different types of displayed segments
-	bgSegs: null,
-	helperSegs: null,
-	highlightSegs: null,
-	businessSegs: null,
-
-
-	eventRenderUtilsClass: SegChronoComponentMixin.eventRenderUtilsClass.extend({
-
-		timeGrid: null,
-		fgSegs: null,
-
-
-		constructor: function(timeGrid) {
-			SegChronoComponentMixin.eventRenderUtilsClass.call(this, timeGrid);
-			this.timeGrid = timeGrid;
-		},
-
-
-		// Given an array of foreground segments, render a DOM element for each, computes position,
-		// and attaches to the column inner-container elements.
-		renderFgSegsIntoContainers: function(segs, containerEls) {
-			var segsByCol;
-			var col;
-
-			segs = this.renderFgSegEls(segs);
-			segsByCol = this.timeGrid.groupSegsByCol(segs);
-
-			for (col = 0; col < this.timeGrid.colCnt; col++) {
-				this.updateFgSegCoords(segsByCol[col]);
-			}
-
-			this.timeGrid.attachSegsByCol(segsByCol, containerEls);
-
-			return segs;
-		},
-
-
-		unrenderFgSegs: function() {
-			this.timeGrid.unrenderNamedSegs('fgSegs');
-		},
-
-
-		// Computes a default event time formatting string if `timeFormat` is not explicitly defined
-		computeEventTimeFormat: function() {
-			return this.opt('noMeridiemTimeFormat'); // like "6:30" (no AM/PM)
-		},
-
-
-		// Computes a default `displayEventEnd` value if one is not expliclty defined
-		computeDisplayEventEnd: function() {
-			return true;
-		},
-
-
-		// Renders the HTML for a single event segment's default rendering
-		fgSegHtml: function(seg, disableResizing) {
-			var view = this.view;
-			var calendar = view.calendar;
-			var componentFootprint = seg.footprint.componentFootprint;
-			var isAllDay = componentFootprint.isAllDay;
-			var eventDef = seg.footprint.eventDef;
-			var isDraggable = view.isEventDefDraggable(eventDef);
-			var isResizableFromStart = !disableResizing && seg.isStart && view.isEventDefResizableFromStart(eventDef);
-			var isResizableFromEnd = !disableResizing && seg.isEnd && view.isEventDefResizableFromEnd(eventDef);
-			var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd);
-			var skinCss = cssToStr(this.getSkinCss(seg.footprint));
-			var timeText;
-			var fullTimeText; // more verbose time text. for the print stylesheet
-			var startTimeText; // just the start time text
-
-			classes.unshift('fc-time-grid-event', 'fc-v-event');
-
-			// if the event appears to span more than one day...
-			if (view.isMultiDayRange(componentFootprint.unzonedRange)) {
-				// Don't display time text on segments that run entirely through a day.
-				// That would appear as midnight-midnight and would look dumb.
-				// Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am)
-				if (seg.isStart || seg.isEnd) {
-					var zonedStart = calendar.msToMoment(seg.startMs);
-					var zonedEnd = calendar.msToMoment(seg.endMs);
-					timeText = this._getTimeText(zonedStart, zonedEnd, isAllDay);
-					fullTimeText = this._getTimeText(zonedStart, zonedEnd, isAllDay, 'LT');
-					startTimeText = this._getTimeText(zonedStart, zonedEnd, isAllDay, null, false); // displayEnd=false
-				}
-			}
-			else {
-				// Display the normal time text for the *event's* times
-				timeText = this.getTimeText(seg.footprint);
-				fullTimeText = this.getTimeText(seg.footprint, 'LT');
-				startTimeText = this.getTimeText(seg.footprint, null, false); // displayEnd=false
-			}
-
-			return '<a class="' + classes.join(' ') + '"' +
-				(eventDef.url ?
-					' href="' + htmlEscape(eventDef.url) + '"' :
-					''
-					) +
-				(skinCss ?
-					' style="' + skinCss + '"' :
-					''
-					) +
-				'>' +
-					'<div class="fc-content">' +
-						(timeText ?
-							'<div class="fc-time"' +
-							' data-start="' + htmlEscape(startTimeText) + '"' +
-							' data-full="' + htmlEscape(fullTimeText) + '"' +
-							'>' +
-								'<span>' + htmlEscape(timeText) + '</span>' +
-							'</div>' :
-							''
-							) +
-						(eventDef.title ?
-							'<div class="fc-title">' +
-								htmlEscape(eventDef.title) +
-							'</div>' :
-							''
-							) +
-					'</div>' +
-					'<div class="fc-bg"/>' +
-					/* TODO: write CSS for this
-					(isResizableFromStart ?
-						'<div class="fc-resizer fc-start-resizer" />' :
-						''
-						) +
-					*/
-					(isResizableFromEnd ?
-						'<div class="fc-resizer fc-end-resizer" />' :
-						''
-						) +
-				'</a>';
-		},
-
-
-		// Given segments that are assumed to all live in the *same column*,
-		// compute their verical/horizontal coordinates and assign to their elements.
-		updateFgSegCoords: function(segs) {
-			this.timeGrid.computeSegVerticals(segs); // horizontals relies on this
-			this.computeFgSegHorizontals(segs); // compute horizontal coordinates, z-index's, and reorder the array
-			this.timeGrid.assignSegVerticals(segs);
-			this.assignFgSegHorizontals(segs);
-		},
-
-
-		// Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each.
-		// NOTE: Also reorders the given array by date!
-		computeFgSegHorizontals: function(segs) {
-			var levels;
-			var level0;
-			var i;
-
-			this.sortEventSegs(segs); // order by certain criteria
-			levels = buildSlotSegLevels(segs);
-			computeForwardSlotSegs(levels);
-
-			if ((level0 = levels[0])) {
-
-				for (i = 0; i < level0.length; i++) {
-					computeSlotSegPressures(level0[i]);
-				}
-
-				for (i = 0; i < level0.length; i++) {
-					this.computeFgSegForwardBack(level0[i], 0, 0);
-				}
-			}
-		},
-
-
-		// Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
-		// from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
-		// seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
-		//
-		// The segment might be part of a "series", which means consecutive segments with the same pressure
-		// who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
-		// segments behind this one in the current series, and `seriesBackwardCoord` is the starting
-		// coordinate of the first segment in the series.
-		computeFgSegForwardBack: function(seg, seriesBackwardPressure, seriesBackwardCoord) {
-			var forwardSegs = seg.forwardSegs;
-			var i;
-
-			if (seg.forwardCoord === undefined) { // not already computed
-
-				if (!forwardSegs.length) {
-
-					// if there are no forward segments, this segment should butt up against the edge
-					seg.forwardCoord = 1;
-				}
-				else {
-
-					// sort highest pressure first
-					this.sortForwardSegs(forwardSegs);
-
-					// this segment's forwardCoord will be calculated from the backwardCoord of the
-					// highest-pressure forward segment.
-					this.computeFgSegForwardBack(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord);
-					seg.forwardCoord = forwardSegs[0].backwardCoord;
-				}
-
-				// calculate the backwardCoord from the forwardCoord. consider the series
-				seg.backwardCoord = seg.forwardCoord -
-					(seg.forwardCoord - seriesBackwardCoord) / // available width for series
-					(seriesBackwardPressure + 1); // # of segments in the series
-
-				// use this segment's coordinates to computed the coordinates of the less-pressurized
-				// forward segments
-				for (i=0; i<forwardSegs.length; i++) {
-					this.computeFgSegForwardBack(forwardSegs[i], 0, seg.forwardCoord);
-				}
-			}
-		},
-
-
-		sortForwardSegs: function(forwardSegs) {
-			forwardSegs.sort(proxy(this, 'compareForwardSegs'));
-		},
-
-
-		// A cmp function for determining which forward segment to rely on more when computing coordinates.
-		compareForwardSegs: function(seg1, seg2) {
-			// put higher-pressure first
-			return seg2.forwardPressure - seg1.forwardPressure ||
-				// put segments that are closer to initial edge first (and favor ones with no coords yet)
-				(seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) ||
-				// do normal sorting...
-				this.compareEventSegs(seg1, seg2);
-		},
-
-
-		// Given foreground event segments that have already had their position coordinates computed,
-		// assigns position-related CSS values to their elements.
-		assignFgSegHorizontals: function(segs) {
-			var i, seg;
-
-			for (i = 0; i < segs.length; i++) {
-				seg = segs[i];
-				seg.el.css(this.generateFgSegHorizontalCss(seg));
-
-				// if the height is short, add a className for alternate styling
-				if (seg.bottom - seg.top < 30) {
-					seg.el.addClass('fc-short');
-				}
-			}
-		},
-
-
-		// Generates an object with CSS properties/values that should be applied to an event segment element.
-		// Contains important positioning-related properties that should be applied to any event element, customized or not.
-		generateFgSegHorizontalCss: function(seg) {
-			var shouldOverlap = this.opt('slotEventOverlap');
-			var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point
-			var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point
-			var props = this.timeGrid.generateSegVerticalCss(seg); // get top/bottom first
-			var left; // amount of space from left edge, a fraction of the total width
-			var right; // amount of space from right edge, a fraction of the total width
-
-			if (shouldOverlap) {
-				// double the width, but don't go beyond the maximum forward coordinate (1.0)
-				forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2);
-			}
-
-			if (this.timeGrid.isRTL) {
-				left = 1 - forwardCoord;
-				right = backwardCoord;
-			}
-			else {
-				left = backwardCoord;
-				right = 1 - forwardCoord;
-			}
-
-			props.zIndex = seg.level + 1; // convert from 0-base to 1-based
-			props.left = left * 100 + '%';
-			props.right = right * 100 + '%';
-
-			if (shouldOverlap && seg.forwardPressure) {
-				// add padding to the edge so that forward stacked events don't cover the resizer's icon
-				props[this.isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width
-			}
-
-			return props;
-		}
-
-	}),
-
-
-	// Renders the DOM that the view's content will live in
-	renderContentSkeleton: function() {
-		var cellHtml = '';
-		var i;
-		var skeletonEl;
-
-		for (i = 0; i < this.colCnt; i++) {
-			cellHtml +=
-				'<td>' +
-					'<div class="fc-content-col">' +
-						'<div class="fc-event-container fc-helper-container"></div>' +
-						'<div class="fc-event-container"></div>' +
-						'<div class="fc-highlight-container"></div>' +
-						'<div class="fc-bgevent-container"></div>' +
-						'<div class="fc-business-container"></div>' +
-					'</div>' +
-				'</td>';
-		}
-
-		skeletonEl = $(
-			'<div class="fc-content-skeleton">' +
-				'<table>' +
-					'<tr>' + cellHtml + '</tr>' +
-				'</table>' +
-			'</div>'
-		);
-
-		this.colContainerEls = skeletonEl.find('.fc-content-col');
-		this.helperContainerEls = skeletonEl.find('.fc-helper-container');
-		this.fgContainerEls = skeletonEl.find('.fc-event-container:not(.fc-helper-container)');
-		this.bgContainerEls = skeletonEl.find('.fc-bgevent-container');
-		this.highlightContainerEls = skeletonEl.find('.fc-highlight-container');
-		this.businessContainerEls = skeletonEl.find('.fc-business-container');
-
-		this.bookendCells(skeletonEl.find('tr')); // TODO: do this on string level
-		this.el.append(skeletonEl);
+		this.timeGrid = timeGrid;
 	},
 
 
-	/* Foreground Helper Events
-	------------------------------------------------------------------------------------------------------------------*/
-
-
-	renderHelperSegs: function(segs, sourceSeg) {
-		var helperEls = [];
-		var i, seg;
-		var sourceEl;
+	// Given an array of foreground segments, render a DOM element for each, computes position,
+	// and attaches to the column inner-container elements.
+	renderFgSegsIntoContainers: function(segs, containerEls) {
+		var segsByCol;
+		var col;
 
-		segs = this.eventRenderUtils.renderFgSegsIntoContainers(segs, this.helperContainerEls);
+		segs = this.renderFgSegEls(segs);
+		segsByCol = this.timeGrid.groupSegsByCol(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];
-			if (sourceSeg && sourceSeg.col === seg.col) {
-				sourceEl = sourceSeg.el;
-				seg.el.css({
-					left: sourceEl.css('left'),
-					right: sourceEl.css('right'),
-					'margin-left': sourceEl.css('margin-left'),
-					'margin-right': sourceEl.css('margin-right')
-				});
-			}
-			helperEls.push(seg.el[0]);
+		for (col = 0; col < this.timeGrid.colCnt; col++) {
+			this.updateFgSegCoords(segsByCol[col]);
 		}
 
-		this.helperSegs = segs;
+		this.timeGrid.attachSegsByCol(segsByCol, containerEls);
 
-		return $(helperEls); // must return rendered helpers
+		return segs;
 	},
 
 
-	unrenderHelperSegs: function() {
-		this.unrenderNamedSegs('helperSegs');
+	unrenderFgSegs: function() {
+		this.timeGrid.unrenderNamedSegs('fgSegs');
 	},
 
 
-	/* Foreground Events
-	------------------------------------------------------------------------------------------------------------------*/
-
-
-	renderFgSegs: function(segs) {
-		segs = this.eventRenderUtils.renderFgSegsIntoContainers(segs, this.fgContainerEls);
+	// Computes a default event time formatting string if `timeFormat` is not explicitly defined
+	computeEventTimeFormat: function() {
+		return this.opt('noMeridiemTimeFormat'); // like "6:30" (no AM/PM)
+	},
 
-		this.fgSegs = segs;
 
-		return segs; // needed for Grid::renderEvents
+	// Computes a default `displayEventEnd` value if one is not expliclty defined
+	computeDisplayEventEnd: function() {
+		return true;
 	},
 
 
-	/* Background Events
-	------------------------------------------------------------------------------------------------------------------*/
-
+	// Renders the HTML for a single event segment's default rendering
+	fgSegHtml: function(seg, disableResizing) {
+		var view = this.view;
+		var calendar = view.calendar;
+		var componentFootprint = seg.footprint.componentFootprint;
+		var isAllDay = componentFootprint.isAllDay;
+		var eventDef = seg.footprint.eventDef;
+		var isDraggable = view.isEventDefDraggable(eventDef);
+		var isResizableFromStart = !disableResizing && seg.isStart && view.isEventDefResizableFromStart(eventDef);
+		var isResizableFromEnd = !disableResizing && seg.isEnd && view.isEventDefResizableFromEnd(eventDef);
+		var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd);
+		var skinCss = cssToStr(this.getSkinCss(seg.footprint));
+		var timeText;
+		var fullTimeText; // more verbose time text. for the print stylesheet
+		var startTimeText; // just the start time text
+
+		classes.unshift('fc-time-grid-event', 'fc-v-event');
+
+		// if the event appears to span more than one day...
+		if (view.isMultiDayRange(componentFootprint.unzonedRange)) {
+			// Don't display time text on segments that run entirely through a day.
+			// That would appear as midnight-midnight and would look dumb.
+			// Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am)
+			if (seg.isStart || seg.isEnd) {
+				var zonedStart = calendar.msToMoment(seg.startMs);
+				var zonedEnd = calendar.msToMoment(seg.endMs);
+				timeText = this._getTimeText(zonedStart, zonedEnd, isAllDay);
+				fullTimeText = this._getTimeText(zonedStart, zonedEnd, isAllDay, 'LT');
+				startTimeText = this._getTimeText(zonedStart, zonedEnd, isAllDay, null, false); // displayEnd=false
+			}
+		}
+		else {
+			// Display the normal time text for the *event's* times
+			timeText = this.getTimeText(seg.footprint);
+			fullTimeText = this.getTimeText(seg.footprint, 'LT');
+			startTimeText = this.getTimeText(seg.footprint, null, false); // displayEnd=false
+		}
 
-	renderBgSegs: function(segs) {
-		segs = this.fillSystem.buildSegEls('bgEvent', segs);
-		this.updateSegVerticals(segs);
-		this.attachSegsByCol(this.groupSegsByCol(segs), this.bgContainerEls);
-		this.bgSegs = segs;
-		return segs; // needed for Grid::renderEvents
+		return '<a class="' + classes.join(' ') + '"' +
+			(eventDef.url ?
+				' href="' + htmlEscape(eventDef.url) + '"' :
+				''
+				) +
+			(skinCss ?
+				' style="' + skinCss + '"' :
+				''
+				) +
+			'>' +
+				'<div class="fc-content">' +
+					(timeText ?
+						'<div class="fc-time"' +
+						' data-start="' + htmlEscape(startTimeText) + '"' +
+						' data-full="' + htmlEscape(fullTimeText) + '"' +
+						'>' +
+							'<span>' + htmlEscape(timeText) + '</span>' +
+						'</div>' :
+						''
+						) +
+					(eventDef.title ?
+						'<div class="fc-title">' +
+							htmlEscape(eventDef.title) +
+						'</div>' :
+						''
+						) +
+				'</div>' +
+				'<div class="fc-bg"/>' +
+				/* TODO: write CSS for this
+				(isResizableFromStart ?
+					'<div class="fc-resizer fc-start-resizer" />' :
+					''
+					) +
+				*/
+				(isResizableFromEnd ?
+					'<div class="fc-resizer fc-end-resizer" />' :
+					''
+					) +
+			'</a>';
 	},
 
 
-	unrenderBgSegs: function() {
-		this.unrenderNamedSegs('bgSegs');
+	// Given segments that are assumed to all live in the *same column*,
+	// compute their verical/horizontal coordinates and assign to their elements.
+	updateFgSegCoords: function(segs) {
+		this.timeGrid.computeSegVerticals(segs); // horizontals relies on this
+		this.computeFgSegHorizontals(segs); // compute horizontal coordinates, z-index's, and reorder the array
+		this.timeGrid.assignSegVerticals(segs);
+		this.assignFgSegHorizontals(segs);
 	},
 
 
-	/* Seg Rendering Utils
-	------------------------------------------------------------------------------------------------------------------*/
+	// Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each.
+	// NOTE: Also reorders the given array by date!
+	computeFgSegHorizontals: function(segs) {
+		var levels;
+		var level0;
+		var i;
 
+		this.sortEventSegs(segs); // order by certain criteria
+		levels = buildSlotSegLevels(segs);
+		computeForwardSlotSegs(levels);
 
-	// Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col
-	groupSegsByCol: function(segs) {
-		var segsByCol = [];
-		var i;
+		if ((level0 = levels[0])) {
 
-		for (i = 0; i < this.colCnt; i++) {
-			segsByCol.push([]);
-		}
+			for (i = 0; i < level0.length; i++) {
+				computeSlotSegPressures(level0[i]);
+			}
 
-		for (i = 0; i < segs.length; i++) {
-			segsByCol[segs[i].col].push(segs[i]);
+			for (i = 0; i < level0.length; i++) {
+				this.computeFgSegForwardBack(level0[i], 0, 0);
+			}
 		}
-
-		return segsByCol;
 	},
 
 
-	// Given segments grouped by column, insert the segments' elements into a parallel array of container
-	// elements, each living within a column.
-	attachSegsByCol: function(segsByCol, containerEls) {
-		var col;
-		var segs;
+	// Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
+	// from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
+	// seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
+	//
+	// The segment might be part of a "series", which means consecutive segments with the same pressure
+	// who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
+	// segments behind this one in the current series, and `seriesBackwardCoord` is the starting
+	// coordinate of the first segment in the series.
+	computeFgSegForwardBack: function(seg, seriesBackwardPressure, seriesBackwardCoord) {
+		var forwardSegs = seg.forwardSegs;
 		var i;
 
-		for (col = 0; col < this.colCnt; col++) { // iterate each column grouping
-			segs = segsByCol[col];
+		if (seg.forwardCoord === undefined) { // not already computed
+
+			if (!forwardSegs.length) {
 
-			for (i = 0; i < segs.length; i++) {
-				containerEls.eq(col).append(segs[i].el);
+				// if there are no forward segments, this segment should butt up against the edge
+				seg.forwardCoord = 1;
 			}
-		}
-	},
+			else {
 
+				// sort highest pressure first
+				this.sortForwardSegs(forwardSegs);
 
-	// Given the name of a property of `this` object, assumed to be an array of segments,
-	// loops through each segment and removes from DOM. Will null-out the property afterwards.
-	unrenderNamedSegs: function(propName) {
-		var segs = this[propName];
-		var i;
+				// this segment's forwardCoord will be calculated from the backwardCoord of the
+				// highest-pressure forward segment.
+				this.computeFgSegForwardBack(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord);
+				seg.forwardCoord = forwardSegs[0].backwardCoord;
+			}
+
+			// calculate the backwardCoord from the forwardCoord. consider the series
+			seg.backwardCoord = seg.forwardCoord -
+				(seg.forwardCoord - seriesBackwardCoord) / // available width for series
+				(seriesBackwardPressure + 1); // # of segments in the series
 
-		if (segs) {
-			for (i = 0; i < segs.length; i++) {
-				segs[i].el.remove();
+			// use this segment's coordinates to computed the coordinates of the less-pressurized
+			// forward segments
+			for (i=0; i<forwardSegs.length; i++) {
+				this.computeFgSegForwardBack(forwardSegs[i], 0, seg.forwardCoord);
 			}
-			this[propName] = null;
 		}
 	},
 
 
-	/* Seg Position Utils
-	------------------------------------------------------------------------------------------------------------------*/
+	sortForwardSegs: function(forwardSegs) {
+		forwardSegs.sort(proxy(this, 'compareForwardSegs'));
+	},
 
 
-	// Refreshes the CSS top/bottom coordinates for each segment element.
-	// Works when called after initial render, after a window resize/zoom for example.
-	updateSegVerticals: function(segs) {
-		this.computeSegVerticals(segs);
-		this.assignSegVerticals(segs);
+	// A cmp function for determining which forward segment to rely on more when computing coordinates.
+	compareForwardSegs: function(seg1, seg2) {
+		// put higher-pressure first
+		return seg2.forwardPressure - seg1.forwardPressure ||
+			// put segments that are closer to initial edge first (and favor ones with no coords yet)
+			(seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) ||
+			// do normal sorting...
+			this.compareEventSegs(seg1, seg2);
 	},
 
 
-	// For each segment in an array, computes and assigns its top and bottom properties
-	computeSegVerticals: function(segs) {
+	// Given foreground event segments that have already had their position coordinates computed,
+	// assigns position-related CSS values to their elements.
+	assignFgSegHorizontals: function(segs) {
 		var i, seg;
-		var dayDate;
 
 		for (i = 0; i < segs.length; i++) {
 			seg = segs[i];
-			dayDate = this.dayDates[seg.dayIndex];
+			seg.el.css(this.generateFgSegHorizontalCss(seg));
 
-			seg.top = this.computeDateTop(seg.startMs, dayDate);
-			seg.bottom = this.computeDateTop(seg.endMs, dayDate);
+			// if the height is short, add a className for alternate styling
+			if (seg.bottom - seg.top < 30) {
+				seg.el.addClass('fc-short');
+			}
 		}
 	},
 
 
-	// Given segments that already have their top/bottom properties computed, applies those values to
-	// the segments' elements.
-	assignSegVerticals: function(segs) {
-		var i, seg;
+	// Generates an object with CSS properties/values that should be applied to an event segment element.
+	// Contains important positioning-related properties that should be applied to any event element, customized or not.
+	generateFgSegHorizontalCss: function(seg) {
+		var shouldOverlap = this.opt('slotEventOverlap');
+		var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point
+		var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point
+		var props = this.timeGrid.generateSegVerticalCss(seg); // get top/bottom first
+		var left; // amount of space from left edge, a fraction of the total width
+		var right; // amount of space from right edge, a fraction of the total width
 
-		for (i = 0; i < segs.length; i++) {
-			seg = segs[i];
-			seg.el.css(this.generateSegVerticalCss(seg));
+		if (shouldOverlap) {
+			// double the width, but don't go beyond the maximum forward coordinate (1.0)
+			forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2);
 		}
-	},
 
+		if (this.timeGrid.isRTL) {
+			left = 1 - forwardCoord;
+			right = backwardCoord;
+		}
+		else {
+			left = backwardCoord;
+			right = 1 - forwardCoord;
+		}
+
+		props.zIndex = seg.level + 1; // convert from 0-base to 1-based
+		props.left = left * 100 + '%';
+		props.right = right * 100 + '%';
+
+		if (shouldOverlap && seg.forwardPressure) {
+			// add padding to the edge so that forward stacked events don't cover the resizer's icon
+			props[this.isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width
+		}
 
-	// Generates an object with CSS properties for the top/bottom coordinates of a segment element
-	generateSegVerticalCss: function(seg) {
-		return {
-			top: seg.top,
-			bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container
-		};
+		return props;
 	}
 
 });

+ 216 - 0
src/common/TimeGrid.js

@@ -5,6 +5,8 @@
 
 var TimeGrid = FC.TimeGrid = ChronoComponent.extend(CoordChronoComponentMixin, SegChronoComponentMixin, DayTableMixin, {
 
+	eventRenderUtilsClass: TimeGridEventRenderer,
+
 	view: null, // TODO: make more general and/or remove
 
 	dayRanges: null, // UnzonedRange[], of start-end of each day
@@ -22,6 +24,22 @@ var TimeGrid = FC.TimeGrid = ChronoComponent.extend(CoordChronoComponentMixin, S
 	colCoordCache: null,
 	slatCoordCache: null,
 
+	colContainerEls: null, // containers for each column
+
+	// inner-containers for each column where different types of segs live
+	fgContainerEls: null,
+	bgContainerEls: null,
+	helperContainerEls: null,
+	highlightContainerEls: null,
+	businessContainerEls: null,
+
+	// arrays of different types of displayed segments
+	fgSegs: null,
+	bgSegs: null,
+	helperSegs: null,
+	highlightSegs: null,
+	businessSegs: null,
+
 
 	constructor: function(view) {
 		this.view = view; // do first, for opt calls during initialization
@@ -377,6 +395,79 @@ var TimeGrid = FC.TimeGrid = ChronoComponent.extend(CoordChronoComponentMixin, S
 	},
 
 
+	// Refreshes the CSS top/bottom coordinates for each segment element.
+	// Works when called after initial render, after a window resize/zoom for example.
+	updateSegVerticals: function(segs) {
+		this.computeSegVerticals(segs);
+		this.assignSegVerticals(segs);
+	},
+
+
+	// For each segment in an array, computes and assigns its top and bottom properties
+	computeSegVerticals: function(segs) {
+		var i, seg;
+		var dayDate;
+
+		for (i = 0; i < segs.length; i++) {
+			seg = segs[i];
+			dayDate = this.dayDates[seg.dayIndex];
+
+			seg.top = this.computeDateTop(seg.startMs, dayDate);
+			seg.bottom = this.computeDateTop(seg.endMs, dayDate);
+		}
+	},
+
+
+	// Given segments that already have their top/bottom properties computed, applies those values to
+	// the segments' elements.
+	assignSegVerticals: function(segs) {
+		var i, seg;
+
+		for (i = 0; i < segs.length; i++) {
+			seg = segs[i];
+			seg.el.css(this.generateSegVerticalCss(seg));
+		}
+	},
+
+
+	// Generates an object with CSS properties for the top/bottom coordinates of a segment element
+	generateSegVerticalCss: function(seg) {
+		return {
+			top: seg.top,
+			bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container
+		};
+	},
+
+
+	/* Event Rendering
+	------------------------------------------------------------------------------------------------------------------*/
+
+
+	renderFgSegs: function(segs) {
+		segs = this.eventRenderUtils.renderFgSegsIntoContainers(segs, this.fgContainerEls);
+
+		this.fgSegs = segs;
+
+		return segs; // needed for Grid::renderEvents
+	},
+
+
+	renderBgSegs: function(segs) {
+		segs = this.fillSystem.buildSegEls('bgEvent', segs);
+
+		this.updateSegVerticals(segs);
+		this.attachSegsByCol(this.groupSegsByCol(segs), this.bgContainerEls);
+		this.bgSegs = segs;
+
+		return segs; // needed for Grid::renderEvents
+	},
+
+
+	unrenderBgSegs: function() {
+		this.unrenderNamedSegs('bgSegs');
+	},
+
+
 
 	/* Event Drag Visualization
 	------------------------------------------------------------------------------------------------------------------*/
@@ -446,6 +537,39 @@ var TimeGrid = FC.TimeGrid = ChronoComponent.extend(CoordChronoComponentMixin, S
 	},
 
 
+	renderHelperSegs: function(segs, sourceSeg) {
+		var helperEls = [];
+		var i, seg;
+		var sourceEl;
+
+		segs = this.eventRenderUtils.renderFgSegsIntoContainers(segs, this.helperContainerEls);
+
+		// 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];
+			if (sourceSeg && sourceSeg.col === seg.col) {
+				sourceEl = sourceSeg.el;
+				seg.el.css({
+					left: sourceEl.css('left'),
+					right: sourceEl.css('right'),
+					'margin-left': sourceEl.css('margin-left'),
+					'margin-right': sourceEl.css('margin-right')
+				});
+			}
+			helperEls.push(seg.el[0]);
+		}
+
+		this.helperSegs = segs;
+
+		return $(helperEls); // must return rendered helpers
+	},
+
+
+	unrenderHelperSegs: function() {
+		this.unrenderNamedSegs('helperSegs');
+	},
+
+
 	/* Business Hours
 	------------------------------------------------------------------------------------------------------------------*/
 
@@ -492,6 +616,98 @@ var TimeGrid = FC.TimeGrid = ChronoComponent.extend(CoordChronoComponentMixin, S
 	},
 
 
+	/* Seg Rendering Utils
+	------------------------------------------------------------------------------------------------------------------*/
+
+
+	// Renders the DOM that the view's content will live in
+	renderContentSkeleton: function() {
+		var cellHtml = '';
+		var i;
+		var skeletonEl;
+
+		for (i = 0; i < this.colCnt; i++) {
+			cellHtml +=
+				'<td>' +
+					'<div class="fc-content-col">' +
+						'<div class="fc-event-container fc-helper-container"></div>' +
+						'<div class="fc-event-container"></div>' +
+						'<div class="fc-highlight-container"></div>' +
+						'<div class="fc-bgevent-container"></div>' +
+						'<div class="fc-business-container"></div>' +
+					'</div>' +
+				'</td>';
+		}
+
+		skeletonEl = $(
+			'<div class="fc-content-skeleton">' +
+				'<table>' +
+					'<tr>' + cellHtml + '</tr>' +
+				'</table>' +
+			'</div>'
+		);
+
+		this.colContainerEls = skeletonEl.find('.fc-content-col');
+		this.helperContainerEls = skeletonEl.find('.fc-helper-container');
+		this.fgContainerEls = skeletonEl.find('.fc-event-container:not(.fc-helper-container)');
+		this.bgContainerEls = skeletonEl.find('.fc-bgevent-container');
+		this.highlightContainerEls = skeletonEl.find('.fc-highlight-container');
+		this.businessContainerEls = skeletonEl.find('.fc-business-container');
+
+		this.bookendCells(skeletonEl.find('tr')); // TODO: do this on string level
+		this.el.append(skeletonEl);
+	},
+
+
+	// Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col
+	groupSegsByCol: function(segs) {
+		var segsByCol = [];
+		var i;
+
+		for (i = 0; i < this.colCnt; i++) {
+			segsByCol.push([]);
+		}
+
+		for (i = 0; i < segs.length; i++) {
+			segsByCol[segs[i].col].push(segs[i]);
+		}
+
+		return segsByCol;
+	},
+
+
+	// Given segments grouped by column, insert the segments' elements into a parallel array of container
+	// elements, each living within a column.
+	attachSegsByCol: function(segsByCol, containerEls) {
+		var col;
+		var segs;
+		var i;
+
+		for (col = 0; col < this.colCnt; col++) { // iterate each column grouping
+			segs = segsByCol[col];
+
+			for (i = 0; i < segs.length; i++) {
+				containerEls.eq(col).append(segs[i].el);
+			}
+		}
+	},
+
+
+	// Given the name of a property of `this` object, assumed to be an array of segments,
+	// loops through each segment and removes from DOM. Will null-out the property afterwards.
+	unrenderNamedSegs: function(propName) {
+		var segs = this[propName];
+		var i;
+
+		if (segs) {
+			for (i = 0; i < segs.length; i++) {
+				segs[i].el.remove();
+			}
+			this[propName] = null;
+		}
+	},
+
+
 	/* Now Indicator
 	------------------------------------------------------------------------------------------------------------------*/