text: 
  /* An abstract class comprised of a "grid" of cells that each represent a specific datetime
  ----------------------------------------------------------------------------------------------------------------------*/
  
  var Grid = fc.Grid = RowRenderer.extend({
  
  	start: null, // the date of the first cell
  	end: null, // the date after the last cell
  
  	rowCnt: 0, // number of rows
  	colCnt: 0, // number of cols
  	rowData: null, // array of objects, holding misc data for each row
  	colData: null, // array of objects, holding misc data for each column
  
  	el: null, // the containing element
  	coordMap: null, // a GridCoordMap that converts pixel values to datetimes
  	elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.
  
  	documentDragStartProxy: null, // binds the Grid's scope to documentDragStart (in DayGrid.events)
  
  	// derived from options
  	colHeadFormat: null, // TODO: move to another class. not applicable to all Grids
  	eventTimeFormat: null,
  	displayEventEnd: null,
  
  
  	constructor: function() {
  		RowRenderer.apply(this, arguments); // call the super-constructor
  
  		this.coordMap = new GridCoordMap(this);
  		this.elsByFill = {};
  		this.documentDragStartProxy = $.proxy(this, 'documentDragStart');
  	},
  
  
  	// Renders the grid into the `el` element.
  	// Subclasses should override and call this super-method when done.
  	render: function() {
  		this.bindHandlers();
  	},
  
  
  	// Called when the grid's resources need to be cleaned up
  	destroy: function() {
  		this.unbindHandlers();
  	},
  
  
  	/* Options
  	------------------------------------------------------------------------------------------------------------------*/
  
  
  	// Generates the format string used for the text in column headers, if not explicitly defined by 'columnFormat'
  	// TODO: move to another class. not applicable to all Grids
  	computeColHeadFormat: function() {
  		// subclasses must implement if they want to use headHtml()
  	},
  
  
  	// Generates the format string used for event time text, if not explicitly defined by 'timeFormat'
  	computeEventTimeFormat: function() {
  		return this.view.opt('smallTimeFormat');
  	},
  
  
  	// Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd'
  	computeDisplayEventEnd: function() {
  		return false;
  	},
  
  
  	/* Dates
  	------------------------------------------------------------------------------------------------------------------*/
  
  
  	// Tells the grid about what period of time to display. Grid will subsequently compute dates for cell system.
  	setRange: function(range) {
  		var view = this.view;
  
  		this.start = range.start.clone();
  		this.end = range.end.clone();
  
  		this.rowData = [];
  		this.colData = [];
  		this.updateCells();
  
  		// Populate option-derived settings. Look for override first, then compute if necessary.
  		this.colHeadFormat = view.opt('columnFormat') || this.computeColHeadFormat();
  		this.eventTimeFormat = view.opt('timeFormat') || this.computeEventTimeFormat();
  		this.displayEventEnd = view.opt('displayEventEnd');
  		if (this.displayEventEnd == null) {
  			this.displayEventEnd = this.computeDisplayEventEnd();
  		}
  	},
  
  
  	// Responsible for setting rowCnt/colCnt and any other row/col data
  	updateCells: function() {
  		// subclasses must implement
  	},
  
  
  	// Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects
  	rangeToSegs: function(range) {
  		// subclasses must implement
  	},
  
  
  	/* Cells
  	------------------------------------------------------------------------------------------------------------------*/
  	// NOTE: columns are ordered left-to-right
  
  
  	// Gets an object containing row/col number, misc data, and range information about the cell.
  	// Accepts row/col values, an object with row/col properties, or a single-number offset from the first cell.
  	getCell: function(row, col) {
  		var cell;
  
  		if (col == null) {
  			if (typeof row === 'number') { // a single-number offset
  				col = row % this.colCnt;
  				row = Math.floor(row / this.colCnt);
  			}
  			else { // an object with row/col properties
  				col = row.col;
  				row = row.row;
  			}
  		}
  
  		cell = { row: row, col: col };
  
  		$.extend(cell, this.getRowData(row), this.getColData(col));
  		$.extend(cell, this.computeCellRange(cell));
  
  		return cell;
  	},
  
  
  	// Given a cell object with index and misc data, generates a range object
  	computeCellRange: function(cell) {
  		// subclasses must implement
  	},
  
  
  	// Retrieves misc data about the given row
  	getRowData: function(row) {
  		return this.rowData[row] || {};
  	},
  
  
  	// Retrieves misc data baout the given column
  	getColData: function(col) {
  		return this.colData[col] || {};
  	},
  
  
  	// Retrieves the element representing the given row
  	getRowEl: function(row) {
  		// subclasses should implement if leveraging the default getCellDayEl() or computeRowCoords()
  	},
  
  
  	// Retrieves the element representing the given column
  	getColEl: function(col) {
  		// subclasses should implement if leveraging the default getCellDayEl() or computeColCoords()
  	},
  
  
  	// Given a cell object, returns the element that represents the cell's whole-day
  	getCellDayEl: function(cell) {
  		return this.getColEl(cell.col) || this.getRowEl(cell.row);
  	},
  
  
  	/* Cell Coordinates
  	------------------------------------------------------------------------------------------------------------------*/
  
  
  	// Computes the top/bottom coordinates of all rows.
  	// By default, queries the dimensions of the element provided by getRowEl().
  	computeRowCoords: function() {
  		var items = [];
  		var i, el;
  		var item;
  
  		for (i = 0; i < this.rowCnt; i++) {
  			el = this.getRowEl(i);
  			item = {
  				top: el.offset().top
  			};
  			if (i > 0) {
  				items[i - 1].bottom = item.top;
  			}
  			items.push(item);
  		}
  		item.bottom = item.top + el.outerHeight();
  
  		return items;
  	},
  
  
  	// Computes the left/right coordinates of all rows.
  	// By default, queries the dimensions of the element provided by getColEl().
  	computeColCoords: function() {
  		var items = [];
  		var i, el;
  		var item;
  
  		for (i = 0; i < this.colCnt; i++) {
  			el = this.getColEl(i);
  			item = {
  				left: el.offset().left
  			};
  			if (i > 0) {
  				items[i - 1].right = item.left;
  			}
  			items.push(item);
  		}
  		item.right = item.left + el.outerWidth();
  
  		return items;
  	},
  
  
  	/* Handlers
  	------------------------------------------------------------------------------------------------------------------*/
  
  
  	// Attaches handlers to DOM
  	bindHandlers: function() {
  		var _this = this;
  
  		// attach a handler to the grid's root element.
  		// we don't need to clean up in unbindHandlers or destroy, because when jQuery removes the element from the
  		// DOM it automatically unregisters the handlers.
  		this.el.on('mousedown', function(ev) {
  			if (
  				!$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link
  				!$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one)
  			) {
  				_this.dayMousedown(ev);
  			}
  		});
  
  		// attach event-element-related handlers. in Grid.events
  		// same garbage collection note as above.
  		this.bindSegHandlers();
  
  		$(document).on('dragstart', this.documentDragStartProxy); // jqui drag
  	},
  
  
  	// Unattaches handlers from the DOM
  	unbindHandlers: function() {
  		$(document).off('dragstart', this.documentDragStartProxy); // jqui drag
  	},
  
  
  	// Process a mousedown on an element that represents a day. For day clicking and selecting.
  	dayMousedown: function(ev) {
  		var _this = this;
  		var view = this.view;
  		var isSelectable = view.opt('selectable');
  		var dayClickCell; // null if invalid dayClick
  		var selectionRange; // null if invalid selection
  
  		// this listener tracks a mousedown on a day element, and a subsequent drag.
  		// if the drag ends on the same day, it is a 'dayClick'.
  		// if 'selectable' is enabled, this listener also detects selections.
  		var dragListener = new DragListener(this.coordMap, {
  			//distance: 5, // needs more work if we want dayClick to fire correctly
  			scroll: view.opt('dragScroll'),
  			dragStart: function() {
  				// on click on a Day background...
  				view.unselect(); // since we could be rendering a new selection, we want to clear any old one
  			},
  			cellOver: function(cell, isOrig) {
  				var origCell = dragListener.origCell;
  				if (origCell) { // click needs to have started on a cell
  					dayClickCell = isOrig ? cell : null; // single-cell selection is a day click
  					if (isSelectable) {
  						selectionRange = _this.computeSelection(origCell, cell);
  						if (selectionRange) {
  							_this.renderSelection(selectionRange);
  						}
  						else {
  							disableCursor();
  						}
  					}
  				}
  			},
  			cellOut: function(cell) {
  				dayClickCell = null;
  				selectionRange = null;
  				_this.destroySelection();
  				enableCursor();
  			},
  			listenStop: function(ev) {
  				if (dayClickCell) {
  					view.trigger('dayClick', _this.getCellDayEl(dayClickCell), dayClickCell.start, ev);
  				}
  				if (selectionRange) {
  					// the selection will already have been rendered. just report it
  					view.reportSelection(selectionRange, ev);
  				}
  				enableCursor();
  			}
  		});
  
  		dragListener.mousedown(ev); // start listening, which will eventually initiate a dragStart
  	},
  
  
  	/* Event Helper
  	------------------------------------------------------------------------------------------------------------------*/
  	// TODO: should probably move this to Grid.events, like we did event dragging / resizing
  
  
  	// Renders a mock event over the given range.
  	// The range's end can be null, in which case the mock event that is rendered will have a null end time.
  	// `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging.
  	renderRangeHelper: function(range, sourceSeg) {
  		var fakeEvent;
  
  		fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible
  		fakeEvent.start = range.start.clone();
  		fakeEvent.end = range.end ? range.end.clone() : null;
  		fakeEvent.allDay = null; // force it to be freshly computed by normalizeEventDateProps
  		this.view.calendar.normalizeEventDateProps(fakeEvent);
  
  		// this extra className will be useful for differentiating real events from mock events in CSS
  		fakeEvent.className = (fakeEvent.className || []).concat('fc-helper');
  
  		// if something external is being dragged in, don't render a resizer
  		if (!sourceSeg) {
  			fakeEvent.editable = false;
  		}
  
  		this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering
  	},
  
  
  	// Renders a mock event
  	renderHelper: function(event, sourceSeg) {
  		// subclasses must implement
  	},
  
  
  	// Unrenders a mock event
  	destroyHelper: function() {
  		// subclasses must implement
  	},
  
  
  	/* Selection
  	------------------------------------------------------------------------------------------------------------------*/
  
  
  	// Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses.
  	renderSelection: function(range) {
  		if (this.view.name == 'year' && !range.rendered) {
  			this.view.destroySelection();
  			range.rendered = true; /* prevent loops */
  			this.view.renderSelection(range, this);
  		}
  		this.renderHighlight(range);
  	},
  
  
  	// Unrenders any visual indications of a selection. Will unrender a highlight by default.
  	destroySelection: function() {
  		this.destroyHighlight();
  	},
  
  
  	// Given the first and last cells of a selection, returns a range object.
  	// Will return something falsy if the selection is invalid (when outside of selectionConstraint for example).
  	// Subclasses can override and provide additional data in the range object. Will be passed to renderSelection().
  	computeSelection: function(firstCell, lastCell) {
  		var dates = [
  			firstCell.start,
  			firstCell.end,
  			lastCell.start,
  			lastCell.end
  		];
  		var range;
  
  		dates.sort(compareNumbers); // sorts chronologically. works with Moments
  
  		range = {
  			start: dates[0].clone(),
  			end: dates[3].clone()
  		};
  
  		if (!this.view.calendar.isSelectionRangeAllowed(range)) {
  			return null;
  		}
  
  		return range;
  	},
  
  
  	/* Highlight
  	------------------------------------------------------------------------------------------------------------------*/
  
  
  	// Renders an emphasis on the given date range. `start` is inclusive. `end` is exclusive.
  	renderHighlight: function(range) {
  		this.renderFill('highlight', this.rangeToSegs(range));
  	},
  
  
  	// Unrenders the emphasis on a date range
  	destroyHighlight: function() {
  		this.destroyFill('highlight');
  	},
  
  
  	// Generates an array of classNames for rendering the highlight. Used by the fill system.
  	highlightSegClasses: function() {
  		return [ 'fc-highlight' ];
  	},
  
  
  	/* Fill System (highlight, background events, business hours)
  	------------------------------------------------------------------------------------------------------------------*/
  
  
  	// Renders a set of rectangles over the given segments of time.
  	// Returns a subset of segs, the segs that were actually rendered.
  	// Responsible for populating this.elsByFill. TODO: better API for expressing this requirement
  	renderFill: function(type, segs) {
  		// subclasses must implement
  	},
  
  
  	// 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];
  		}
  	},
  
  
  	// Renders and assigns an `el` property for each fill segment. Generic enough to work with different types.
  	// Only returns segments that successfully rendered.
  	// To be harnessed by renderFill (implemented by subclasses).
  	// Analagous to renderFgSegEls.
  	renderFillSegEls: function(type, segs) {
  		var _this = this;
  		var segElMethod = this[type + 'SegEl'];
  		var html = '';
  		var renderedSegs = [];
  		var i;
  
  		if (segs.length) {
  
  			// build a large concatenation of segment HTML
  			for (i = 0; i < segs.length; i++) {
  				html += this.fillSegHtml(type, segs[i]);
  			}
  
  			// Grab individual elements from the combined HTML string. Use each as the default rendering.
  			// Then, compute the 'el' for each segment.
  			$(html).each(function(i, node) {
  				var seg = segs[i];
  				var el = $(node);
  
  				// allow custom filter methods per-type
  				if (segElMethod) {
  					el = segElMethod.call(_this, seg, el);
  				}
  
  				if (el) { // custom filters did not cancel the render
  					el = $(el); // allow custom filter to return raw DOM node
  
  					// correct element type? (would be bad if a non-TD were inserted into a table for example)
  					if (el.is(_this.fillSegTag)) {
  						seg.el = el;
  						renderedSegs.push(seg);
  					}
  				}
  			});
  		}
  
  		return renderedSegs;
  	},
  
  
  	fillSegTag: 'div', // subclasses can override
  
  
  	// Builds the HTML needed for one fill segment. Generic enought o work with different types.
  	fillSegHtml: function(type, seg) {
  		var classesMethod = this[type + 'SegClasses']; // custom hooks per-type
  		var stylesMethod = this[type + 'SegStyles']; //
  		var classes = classesMethod ? classesMethod.call(this, seg) : [];
  		var styles = stylesMethod ? stylesMethod.call(this, seg) : ''; // a semi-colon separated CSS property string
  
  		return '<' + this.fillSegTag +
  			(classes.length ? ' class="' + classes.join(' ') + '"' : '') +
  			(styles ? ' style="' + styles + '"' : '') +
  			' />';
  	},
  
  
  	/* Generic rendering utilities for subclasses
  	------------------------------------------------------------------------------------------------------------------*/
  
  
  	// Renders a day-of-week header row.
  	// TODO: move to another class. not applicable to all Grids
  	headHtml: function() {
  		return '' +
  			'<div class="fc-row ' + this.view.widgetHeaderClass + '">' +
  				'<table>' +
  					'<thead>' +
  						this.rowHtml('head') + // leverages RowRenderer
  					'</thead>' +
  				'</table>' +
  			'</div>';
  	},
  
  
  	// Used by the `headHtml` method, via RowRenderer, for rendering the HTML of a day-of-week header cell
  	// TODO: move to another class. not applicable to all Grids
  	headCellHtml: function(cell) {
  		var view = this.view;
  		var date = cell.start;
  
  		return '' +
  			'<th class="fc-day-header ' + view.widgetHeaderClass + ' fc-' + dayIDs[date.day()] + '">' +
  				htmlEscape(date.format(this.colHeadFormat)) +
  			'</th>';
  	},
  
  
  	// Renders the HTML for a single-day background cell
  	bgCellHtml: function(cell) {
  		var view = this.view;
  		var date = cell.start;
  		var classes = this.getDayClasses(date);
  
  		classes.unshift('fc-day', view.widgetContentClass);
  
  		return '<td class="' + classes.join(' ') + '"' +
  			' data-date="' + date.format('YYYY-MM-DD') + '"' + // if date has a time, won't format it
  			'></td>';
  	},
  
  
  	// Computes HTML classNames for a single-day cell
  	getDayClasses: function(date) {
  		var view = this.view;
  		var today = view.calendar.getNow().stripTime();
  		var classes = [ 'fc-' + dayIDs[date.day()] ];
  
  		if (
  			view.name === 'month' &&
  			date.month() != view.intervalStart.month()
  		) {
  			classes.push('fc-other-month');
  		}
  
  		if (date.isSame(today, 'day')) {
  			classes.push(
  				'fc-today',
  				view.highlightStateClass
  			);
  		}
  		else if (date < today) {
  			classes.push('fc-past');
  		}
  		else {
  			classes.push('fc-future');
  		}
  
  		return classes;
  	}
  
  });
  