Просмотр исходного кода

redo the fill-rendering system the right way
allows for the eventRender hook for background events

Adam Shaw 11 лет назад
Родитель
Сommit
2e82f4755e
5 измененных файлов с 208 добавлено и 213 удалено
  1. 30 71
      src/common/DayGrid.js
  2. 44 33
      src/common/Grid.events.js
  3. 96 4
      src/common/Grid.js
  4. 35 98
      src/common/TimeGrid.js
  5. 3 7
      src/common/View.js

+ 30 - 71
src/common/DayGrid.js

@@ -4,8 +4,6 @@
 
 function DayGrid(view) {
 	Grid.call(this, view); // call the super-constructor
-
-	this.elsByFill = {};
 }
 
 
@@ -19,7 +17,6 @@ $.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"
-	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.
@@ -246,103 +243,65 @@ $.extend(DayGrid.prototype, {
 	},
 
 
-	/* Highlighting
+	/* Fill System (highlight, background events, business hours)
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	// Renders an emphasis on the given date range. `start` is an inclusive, `end` is exclusive.
-	renderHighlight: function(start, end) {
-		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)
-	------------------------------------------------------------------------------------------------------------------*/
+	fillSegTag: 'td', // override the default tag name
 
 
 	// 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)
+	// Only returns segments that successfully rendered.
 	renderFill: function(type, segs) {
-		var html = '';
-		var i;
-		var els;
+		var nodes = [];
+		var i, seg;
+		var skeletonEl;
 
-		// concatenate all the rows' html
-		for (i = 0; i < segs.length; i++) {
-			html += this.fillRowHtml(type, segs[i]);
-		}
+		segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs
 
-		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]);
+			seg = segs[i];
+			skeletonEl = this.renderFillRow(type, seg);
+			this.rowEls.eq(seg.row).append(skeletonEl);
+			nodes.push(skeletonEl[0]);
 		}
 
-		this.elsByFill[type] = els;
-	},
-
+		this.elsByFill[type] = $(nodes);
 
-	// 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];
-		}
+		return segs;
 	},
 
 
-	// Generates the HTML needed for one row of a fill
-	fillRowHtml: function(type, seg) {
-		var typeLower = type.toLowerCase();
+	// Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered.
+	renderFillRow: function(type, seg) {
 		var colCnt = this.view.colCnt;
 		var startCol = seg.leftCol;
 		var endCol = seg.rightCol + 1;
-		var cellHtml = '';
+		var skeletonEl;
+		var trEl;
 
-		// 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) : '';
+		skeletonEl = $(
+			'<div class="fc-' + type.toLowerCase() + '-skeleton">' +
+				'<table><tr/></table>' +
+			'</div>'
+		);
+		trEl = skeletonEl.find('tr');
 
 		if (startCol > 0) {
-			cellHtml += '<td colspan="' + startCol + '"/>';
+			trEl.append('<td colspan="' + startCol + '"/>');
 		}
 
-		cellHtml += '<td' +
-			' colspan="' + (endCol - startCol) + '"' +
-			' class="fc-' + typeLower + ' ' + extraClasses + '"' +
-			(extraStyles ?
-				' style="' + extraStyles + '"' :
-				''
-				) +
-			'/>';
+		trEl.append(
+			seg.el.attr('colspan', endCol - startCol)
+		);
 
 		if (endCol < colCnt) {
-			cellHtml += '<td colspan="' + (colCnt - endCol) + '"/>';
+			trEl.append('<td colspan="' + (colCnt - endCol) + '"/>');
 		}
 
-		cellHtml = this.bookendCells(cellHtml, 'highlight');
+		this.bookendCells(trEl, type);
 
-		return '' +
-			'<div class="fc-' + typeLower + '-skeleton">' +
-				'<table>' +
-					'<tr>' +
-						cellHtml +
-					'</tr>' +
-				'</table>' +
-			'</div>';
+		return skeletonEl;
 	}
 
-
 });

+ 44 - 33
src/common/Grid.events.js

@@ -79,22 +79,26 @@ $.extend(Grid.prototype, {
 		var renderedSegs = [];
 		var i;
 
-		// build a large concatenation of event segment HTML
-		for (i = 0; i < segs.length; i++) {
-			html += this.fgSegHtml(segs[i], disableResizing);
-		}
+		if (segs.length) { // don't build an empty html string
 
-		// Grab individual elements from the combined HTML string. Use each as the default rendering.
-		// Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
-		$(html).each(function(i, node) {
-			var seg = segs[i];
-			var el = view.resolveEventEl(seg.event, $(node));
-			if (el) {
-				el.data('fc-seg', seg); // used by handlers
-				seg.el = el;
-				renderedSegs.push(seg);
+			// build a large concatenation of event segment HTML
+			for (i = 0; i < segs.length; i++) {
+				html += this.fgSegHtml(segs[i], disableResizing);
 			}
-		});
+
+			// Grab individual elements from the combined HTML string. Use each as the default rendering.
+			// Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
+			$(html).each(function(i, node) {
+				var seg = segs[i];
+				var el = view.resolveEventEl(seg.event, $(node));
+
+				if (el) {
+					el.data('fc-seg', seg); // used by handlers
+					seg.el = el;
+					renderedSegs.push(seg);
+				}
+			});
+		}
 
 		return renderedSegs;
 	},
@@ -110,40 +114,41 @@ $.extend(Grid.prototype, {
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	// Renders the given background event segments onto the grid
-	// TODO: should probably be abstract, but do this for immediate code reuse
+	// Renders the given background event segments onto the grid.
+	// Returns a subset of the segs that were actually rendered.
 	renderBgSegs: function(segs) {
-		this.renderFill('bgEvent', segs); // relies on the subclass having a renderFill method!
+		return this.renderFill('bgEvent', segs);
 	},
 
 
 	// 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!
+		this.destroyFill('bgEvent');
+	},
+
+
+	// Renders a background event element, given the default rendering. Called by the fill system.
+	bgEventSegEl: function(seg, el) {
+		return this.view.resolveEventEl(seg.event, el); // will filter through eventRender
 	},
 
 
-	// 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
+	// Generates an array of classNames to be used for the default rendering of a background event.
+	// Called by the fill system.
 	bgEventSegClasses: function(seg) {
 		var event = seg.event;
 		var source = event.source || {};
-		var classes = event.className.concat(source.className || []);
 
-		return classes.join(' ');
-	},
-
-
-	businessHoursSegClasses: function(seg) {
-		return this.bgEventSegClasses(seg);
+		return [ 'fc-bgevent' ].concat(
+			event.className,
+			source.className || []
+		);
 	},
 
 
-	// 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
+	// Generates a semicolon-separated CSS string to be used for the default rendering of a background event.
+	// Called by the fill system.
+	// TODO: consolidate with getEventSkinCss?
 	bgEventSegStyles: function(seg) {
 		var view = this.view;
 		var event = seg.event;
@@ -160,13 +165,19 @@ $.extend(Grid.prototype, {
 			optionColor;
 
 		if (backgroundColor) {
-			return 'background:' + backgroundColor;
+			return 'background-color:' + backgroundColor;
 		}
 
 		return '';
 	},
 
 
+	// Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system.
+	businessHoursSegClasses: function(seg) {
+		return [ 'fc-nonbusiness', 'fc-bgevent' ];
+	},
+
+
 	/* Handlers
 	------------------------------------------------------------------------------------------------------------------*/
 

+ 96 - 4
src/common/Grid.js

@@ -5,6 +5,7 @@
 function Grid(view) {
 	RowRenderer.call(this, view); // call the super-constructor
 	this.coordMap = new GridCoordMap(this);
+	this.elsByFill = {};
 }
 
 
@@ -14,6 +15,7 @@ $.extend(Grid.prototype, {
 	el: null, // the containing element
 	coordMap: null, // a GridCoordMap that converts pixel values to datetimes
 	cellDuration: null, // a cell's duration. subclasses must assign this ASAP
+	elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.
 
 
 	// Renders the grid into the `el` element.
@@ -243,18 +245,108 @@ $.extend(Grid.prototype, {
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	// Puts visual emphasis on a certain date range
+	// Renders an emphasis on the given date range. `start` is inclusive. `end` is exclusive.
 	renderHighlight: function(start, end) {
-		// subclasses should implement
+		this.renderFill('highlight', this.rangeToSegs(start, end));
 	},
 
 
-	// Removes visual emphasis on a date range
+	// Unrenders the emphasis on a date range
 	destroyHighlight: function() {
-		// subclasses should implement
+		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
+	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
 	------------------------------------------------------------------------------------------------------------------*/

+ 35 - 98
src/common/TimeGrid.js

@@ -4,8 +4,6 @@
 
 function TimeGrid(view) {
 	Grid.call(this, view); // call the super-constructor
-
-	this.elsByFill = {};
 }
 
 
@@ -24,7 +22,6 @@ $.extend(TimeGrid.prototype, {
 	slatTops: null, // an array of top positions, relative to the container. last item holds bottom of last slot
 
 	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.
@@ -395,119 +392,59 @@ $.extend(TimeGrid.prototype, {
 	},
 
 
-	/* Highlight
-	------------------------------------------------------------------------------------------------------------------*/
-
-
-	// Renders an emphasis on the given date range. `start` is inclusive. `end` is exclusive.
-	renderHighlight: function(start, end) {
-		this.renderFill('highlight', this.rangeToSegs(start, end));
-	},
-
-
-	// Unrenders the emphasis on a date range
-	destroyHighlight: function() {
-		this.destroyFill('highlight');
-	},
-
-
-	/* "Fill" Rendering (rectangles covering a specified period of days)
+	/* Fill System (highlight, background events, business hours)
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	// 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.
-	// `classNameStr` is a HACK
-	renderFill: function(type, segs, classNameStr) {
+	// Renders a set of rectangles over the given time segments
+	renderFill: function(type, segs, className) {
 		var view = this.view;
-		var extraClassesMethod = this[type + 'SegClasses']; // TODO: better system for this
-		var extraStylesMethod = this[type + 'SegStyles']; //
-		var cellHtml = '';
 		var segCols;
+		var skeletonEl;
+		var trEl;
 		var col, colSegs;
-		var i, seg;
+		var tdEl;
+		var containerEl;
 		var dayDate;
-		var top, bottom;
-		var extraClasses;
-		var extraStyles;
-		var el;
-		var segEls;
-		var j;
-
-		if (!segs.length) { return; }
-
-		segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg
-
-		classNameStr = classNameStr || type.toLowerCase();
+		var i, seg;
 
-		for (col = 0; col < segCols.length; col++) {
-			colSegs = segCols[col];
+		if (segs.length) {
 
-			cellHtml += '<td>';
+			segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs
+			segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg
 
-			if (colSegs.length) {
-				cellHtml += '<div class="fc-' + classNameStr + '-container">';
+			className = className || type.toLowerCase();
+			skeletonEl = $(
+				'<div class="fc-' + className + '-skeleton">' +
+					'<table><tr/></table>' +
+				'</div>'
+			);
+			trEl = skeletonEl.find('tr');
 
-				for (i = 0; i < colSegs.length; i++) {
-					seg = colSegs[i];
+			for (col = 0; col < segCols.length; col++) {
+				colSegs = segCols[col];
+				tdEl = $('<td/>').appendTo(trEl);
 
-					// compute vertical position
+				if (colSegs.length) {
+					containerEl = $('<div class="fc-' + className + '-container"/>').appendTo(tdEl);
 					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-' + classNameStr + ' ' + extraClasses + '"' +
-						' style="top:' + top + 'px;bottom:-' + bottom + 'px;' + extraStyles + '"' +
-						'/>';
+					for (i = 0; i < colSegs.length; i++) {
+						seg = colSegs[i];
+						containerEl.append(
+							seg.el.css({
+								top: this.computeDateTop(seg.start, dayDate),
+								bottom: -this.computeDateTop(seg.end, dayDate) // the y position of the bottom edge
+							})
+						);
+					}
 				}
-
-				cellHtml += '</div>';
 			}
 
-			cellHtml += '</td>';
-		}
-
-		cellHtml = this.bookendCells(cellHtml, type);
-
-		el = $(
-			'<div class="fc-' + classNameStr + '-skeleton">' +
-				'<table>' +
-					'<tr>' +
-						cellHtml +
-					'</tr>' +
-				'</table>' +
-			'</div>'
-		);
-
-		// assign each segment's el. TODO: there's gotta be a better way
-		segEls = el.find('.fc-' + classNameStr);
-		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];
+			this.bookendCells(trEl, type);
 
-		if (el) {
-			el.remove();
-			delete this.elsByFill[type];
+			this.el.append(skeletonEl);
+			this.elsByFill[type] = skeletonEl;
 		}
 	}
 

+ 3 - 7
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,17 +215,13 @@ 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, isBg) {
 		var segs = this.getSegs();
 		var i;
 
 		for (i = 0; i < segs.length; i++) {
-			if (
-				(!event || segs[i].event._id === event._id) &&
-				(isBg == null || isBgEvent(segs[i].event) === isBg)
-			) {
+			if (!event || segs[i].event._id === event._id) {
 				func.call(this, segs[i]);
 			}
 		}