Explorar o código

View/Grid rendering, restructure methods

Adam Shaw %!s(int64=11) %!d(string=hai) anos
pai
achega
7d8d2f284d
Modificáronse 7 ficheiros con 252 adicións e 96 borrados
  1. 21 16
      src/Calendar.js
  2. 19 12
      src/agenda/AgendaView.js
  3. 6 5
      src/basic/BasicView.js
  4. 2 5
      src/common/DayGrid.js
  5. 50 21
      src/common/Grid.js
  6. 6 8
      src/common/TimeGrid.js
  7. 148 29
      src/common/View.js

+ 21 - 16
src/Calendar.js

@@ -383,6 +383,7 @@ function Calendar_constructor(element, overrides) {
 	var content;
 	var tm; // for making theme classes
 	var currentView; // NOTE: keep this in sync with this.view
+	var viewsByType = {}; // holds all instantiated view instances, current or not
 	var suggestedViewHeight;
 	var windowResizeProxy; // wraps the windowResize function
 	var ignoreWindowResize = 0;
@@ -403,14 +404,14 @@ function Calendar_constructor(element, overrides) {
 	}
 	
 	
-	function render(inc) {
+	function render() {
 		if (!content) {
 			initialRender();
 		}
 		else if (elementVisible()) {
 			// mainly for the public API
 			calcSize();
-			renderView(inc);
+			renderView();
 		}
 	}
 	
@@ -453,7 +454,10 @@ function Calendar_constructor(element, overrides) {
 	function destroy() {
 
 		if (currentView) {
-			currentView.destroyView();
+			currentView.removeElement();
+
+			// NOTE: don't null-out currentView/t.view in case API methods are called after destroy.
+			// It is still the "current" view, just not rendered.
 		}
 
 		header.destroy();
@@ -474,7 +478,8 @@ function Calendar_constructor(element, overrides) {
 	// -----------------------------------------------------------------------------------
 
 
-	// Renders a view because of a date change, view-type change, or for the first time
+	// Renders a view because of a date change, view-type change, or for the first time.
+	// If not given a viewType, keep the current view but render different dates.
 	function renderView(viewType) {
 		ignoreWindowResize++;
 
@@ -482,14 +487,19 @@ function Calendar_constructor(element, overrides) {
 		if (currentView && viewType && currentView.type !== viewType) {
 			header.deactivateButton(currentView.type);
 			freezeContentHeight(); // prevent a scroll jump when view element is removed
-			currentView.el.remove();
+			currentView.removeElement();
 			currentView = t.view = null;
 		}
 
 		// if viewType changed, or the view was never created, create a fresh view
 		if (!currentView && viewType) {
-			currentView = t.view = t.instantiateView(viewType);
-			currentView.el =  $("<div class='fc-view fc-" + viewType + "-view' />").appendTo(content);
+			currentView = t.view =
+				viewsByType[viewType] ||
+				(viewsByType[viewType] = t.instantiateView(viewType));
+
+			currentView.setElement(
+				$("<div class='fc-view fc-" + viewType + "-view' />").appendTo(content)
+			);
 			header.activateButton(viewType);
 		}
 
@@ -500,17 +510,13 @@ function Calendar_constructor(element, overrides) {
 
 			// render or rerender the view
 			if (
-				!currentView.start || // never rendered before
+				!currentView.isDisplayed ||
 				!date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change
 			) {
 				if (elementVisible()) {
 
 					freezeContentHeight();
-					if (currentView.start) { // rendered before?
-						currentView.destroyView();
-					}
-					currentView.setDate(date);
-					currentView.renderView();
+					currentView.display(date);
 					unfreezeContentHeight();
 
 					// need to do this after View::render, so dates are calculated
@@ -609,8 +615,7 @@ function Calendar_constructor(element, overrides) {
 	function renderEvents() { // destroys old events if previously rendered
 		if (elementVisible()) {
 			freezeContentHeight();
-			currentView.destroyViewEvents(); // no performance cost if never rendered
-			currentView.renderViewEvents(events);
+			currentView.displayEvents(events);
 			unfreezeContentHeight();
 		}
 	}
@@ -618,7 +623,7 @@ function Calendar_constructor(element, overrides) {
 
 	function destroyEvents() {
 		freezeContentHeight();
-		currentView.destroyViewEvents();
+		currentView.clearEvents();
 		unfreezeContentHeight();
 	}
 	

+ 19 - 12
src/agenda/AgendaView.js

@@ -72,16 +72,16 @@ var AgendaView = fcViews.agenda = View.extend({
 		this.scrollerEl = this.el.find('.fc-time-grid-container');
 		this.timeGrid.coordMap.containerEl = this.scrollerEl; // don't accept clicks/etc outside of this
 
-		this.timeGrid.el = this.el.find('.fc-time-grid');
-		this.timeGrid.render();
+		this.timeGrid.setElement(this.el.find('.fc-time-grid'));
+		this.timeGrid.renderDates();
 
 		// the <hr> that sometimes displays under the time-grid
 		this.bottomRuleEl = $('<hr class="' + this.widgetHeaderClass + '"/>')
 			.appendTo(this.timeGrid.el); // inject it into the time-grid
 
 		if (this.dayGrid) {
-			this.dayGrid.el = this.el.find('.fc-day-grid');
-			this.dayGrid.render();
+			this.dayGrid.setElement(this.el.find('.fc-day-grid'));
+			this.dayGrid.renderDates();
 
 			// have the day-grid extend it's coordinate area over the <hr> dividing the two grids
 			this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight();
@@ -91,13 +91,21 @@ var AgendaView = fcViews.agenda = View.extend({
 	},
 
 
-	// Make subcomponents ready for cleanup
+	// Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering,
+	// always completely kill each grid's rendering.
 	destroy: function() {
-		this.timeGrid.destroy();
+		this.timeGrid.destroyDates();
+		this.timeGrid.removeElement();
+
 		if (this.dayGrid) {
-			this.dayGrid.destroy();
+			this.dayGrid.destroyDates();
+			this.dayGrid.removeElement();
 		}
-		View.prototype.destroy.call(this); // call the super-method
+	},
+
+
+	renderBusinessHours: function() {
+		this.timeGrid.renderBusinessHours();
 	},
 
 
@@ -204,10 +212,9 @@ var AgendaView = fcViews.agenda = View.extend({
 
 
 	updateSize: function(isResize) {
-		if (isResize) {
-			this.timeGrid.resize();
-		}
-		View.prototype.updateSize.call(this, isResize);
+		this.timeGrid.updateSize(isResize);
+
+		View.prototype.updateSize.call(this, isResize); // call the super-method
 	},
 
 

+ 6 - 5
src/basic/BasicView.js

@@ -65,15 +65,16 @@ var BasicView = fcViews.basic = View.extend({
 		this.scrollerEl = this.el.find('.fc-day-grid-container');
 		this.dayGrid.coordMap.containerEl = this.scrollerEl; // constrain clicks/etc to the dimensions of the scroller
 
-		this.dayGrid.el = this.el.find('.fc-day-grid');
-		this.dayGrid.render(this.hasRigidRows());
+		this.dayGrid.setElement(this.el.find('.fc-day-grid'));
+		this.dayGrid.renderDates(this.hasRigidRows());
 	},
 
 
-	// Make subcomponents ready for cleanup
+	// Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering,
+	// always completely kill the dayGrid's rendering.
 	destroy: function() {
-		this.dayGrid.destroy();
-		View.prototype.destroy.call(this); // call the super-method
+		this.dayGrid.destroyDates();
+		this.dayGrid.removeElement();
 	},
 
 

+ 2 - 5
src/common/DayGrid.js

@@ -19,7 +19,7 @@ var DayGrid = Grid.extend({
 	// Renders the rows and columns into the component's `this.el`, which should already be assigned.
 	// isRigid determins whether the individual rows should ignore the contents and be a constant height.
 	// Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient.
-	render: function(isRigid) {
+	renderDates: function(isRigid) {
 		var view = this.view;
 		var rowCnt = this.rowCnt;
 		var colCnt = this.colCnt;
@@ -41,14 +41,11 @@ var DayGrid = Grid.extend({
 			cell = this.getCell(i);
 			view.trigger('dayRender', null, cell.start, this.dayEls.eq(i));
 		}
-
-		Grid.prototype.render.call(this); // call the super-method
 	},
 
 
-	destroy: function() {
+	destroyDates: function() {
 		this.destroySegPopover();
-		Grid.prototype.destroy.call(this); // call the super-method
 	},
 
 

+ 50 - 21
src/common/Grid.js

@@ -33,19 +33,6 @@ var Grid = fc.Grid = RowRenderer.extend({
 	},
 
 
-	// 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
 	------------------------------------------------------------------------------------------------------------------*/
 
@@ -222,18 +209,20 @@ var Grid = fc.Grid = RowRenderer.extend({
 	},
 
 
-	/* Handlers
+	/* Rendering
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	// Attaches handlers to DOM
-	bindHandlers: function() {
+	// Sets the container element that the grid should render inside of.
+	// Does other DOM-related initializations.
+	setElement: function(el) {
 		var _this = this;
 
+		this.el = el;
+
 		// 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) {
+		// jQuery will take care of unregistering them when removeElement gets called.
+		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)
@@ -246,12 +235,52 @@ var Grid = fc.Grid = RowRenderer.extend({
 		// same garbage collection note as above.
 		this.bindSegHandlers();
 
+		this.bindGlobalHandlers();
+	},
+
+
+	// Removes the grid's container element from the DOM. Undoes any other DOM-related attachments.
+	// DOES NOT remove any content before hand (doens't clear events or call destroyDates), unlike View
+	removeElement: function() {
+		this.unbindGlobalHandlers();
+
+		this.el.remove();
+
+		// NOTE: we don't null-out this.el for the same reasons we don't do it within View::removeElement
+	},
+
+
+	// Renders the basic structure of grid view before any content is rendered
+	renderSkeleton: function() {
+		// subclasses should implement
+	},
+
+
+	// Renders the grid's date-related content (like cells that represent days/times).
+	// Assumes setRange has already been called and the skeleton has already been rendered.
+	renderDates: function() {
+		// subclasses should implement
+	},
+
+
+	// Unrenders the grid's date-related content
+	destroyDates: function() {
+		// subclasses should implement
+	},
+
+
+	/* Handlers
+	------------------------------------------------------------------------------------------------------------------*/
+
+
+	// Binds DOM handlers to elements that reside outside the grid, such as the document
+	bindGlobalHandlers: function() {
 		$(document).on('dragstart', this.documentDragStartProxy); // jqui drag
 	},
 
 
-	// Unattaches handlers from the DOM
-	unbindHandlers: function() {
+	// Unbinds DOM handlers from elements that reside outside the grid
+	unbindGlobalHandlers: function() {
 		$(document).off('dragstart', this.documentDragStartProxy); // jqui drag
 	},
 

+ 6 - 8
src/common/TimeGrid.js

@@ -30,14 +30,10 @@ var TimeGrid = Grid.extend({
 
 	// Renders the time grid into `this.el`, which should already be assigned.
 	// Relies on the view's colCnt. In the future, this component should probably be self-sufficient.
-	render: function() {
+	renderDates: function() {
 		this.el.html(this.renderHtml());
 		this.dayEls = this.el.find('.fc-day');
 		this.slatEls = this.el.find('.fc-slats tr');
-
-		this.computeSlatTops();
-		this.renderBusinessHours();
-		Grid.prototype.render.call(this); // call the super-method
 	},
 
 
@@ -247,10 +243,12 @@ var TimeGrid = Grid.extend({
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	// Called when there is a window resize/zoom and we need to recalculate coordinates for the grid
-	resize: function() {
+	updateSize: function(isResize) { // NOT a standard Grid method
 		this.computeSlatTops();
-		this.updateSegVerticals();
+
+		if (isResize) {
+			this.updateSegVerticals();
+		}
 	},
 
 

+ 148 - 29
src/common/View.js

@@ -13,6 +13,10 @@ var View = fc.View = Class.extend({
 	coordMap: null, // a CoordMap object for converting pixel regions to dates
 	el: null, // the view's containing element. set by Calendar
 
+	isDisplayed: false,
+	isSkeletonRendered: false,
+	isEventsRendered: false,
+
 	// range the view is actually displaying (moments)
 	start: null,
 	end: null, // exclusive
@@ -227,38 +231,138 @@ var View = fc.View = Class.extend({
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	// Wraps the basic render() method with more View-specific logic. Called by the owner Calendar.
-	renderView: function() {
+	// Sets the container element that the view should render inside of.
+	// Does other DOM-related initializations.
+	setElement: function(el) {
+		this.el = el;
+		this.bindGlobalHandlers();
+	},
+
+
+	// Removes the view's container element from the DOM, clearing any content beforehand.
+	// Undoes any other DOM-related attachments.
+	removeElement: function() {
+		this.clear(); // clears all content
+
+		// clean up the skeleton
+		if (this.isSkeletonRendered) {
+			this.destroySkeleton();
+			this.isSkeletonRendered = false;
+		}
+
+		this.unbindGlobalHandlers();
+
+		this.el.remove();
+
+		// NOTE: don't null-out this.el in case the View was destroyed within an API callback.
+		// We don't null-out the View's other jQuery element references upon destroy, so why should we kill this.el?
+	},
+
+
+	// Does everything necessary to display the view centered around the given date.
+	// Does every type of rendering EXCEPT rendering events.
+	display: function(date) {
+		this.clear(); // clear the old content
+		this.setDate(date);
 		this.render();
 		this.updateSize();
+		this.renderBusinessHours(); // might need coordinates, so should go after updateSize()
 		this.initializeScroll();
-		this.trigger('viewRender', this, this, this.el);
+		this.isDisplayed = true;
+		this.triggerRender();
+	},
 
-		// attach handlers to document. do it here to allow for destroy/rerender
-		$(document).on('mousedown', this.documentMousedownProxy);
+
+	// Does everything necessary to clear the content of the view.
+	// Clears dates and events. Does not clear the skeleton.
+	clear: function() { // clears the view of *content* but not the skeleton
+		if (this.isDisplayed) {
+			this.unselect();
+			this.clearEvents();
+			this.triggerDestroy();
+			this.destroyBusinessHours();
+			this.destroy();
+			this.isDisplayed = false;
+		}
 	},
 
 
-	// Renders the view inside an already-defined `this.el`
+	// Renders the view's date-related content, rendering the view's non-content skeleton if necessary
 	render: function() {
+		if (!this.isSkeletonRendered) {
+			this.renderSkeleton();
+			this.isSkeletonRendered = true;
+		}
+		this.renderDates();
+	},
+
+
+	// Unrenders the view's date-related content.
+	// Call this instead of destroyDates directly in case the View subclass wants to use a render/destroy pattern
+	// where both the skeleton and the content always get rendered/unrendered together.
+	destroy: function() {
+		this.destroyDates();
+	},
+
+
+	// Renders the basic structure of the view before any content is rendered
+	renderSkeleton: function() {
 		// subclasses should implement
 	},
 
 
-	// Wraps the basic destroy() method with more View-specific logic. Called by the owner Calendar.
-	destroyView: function() {
-		this.unselect();
-		this.destroyViewEvents();
-		this.destroy();
+	// Unrenders the basic structure of the view
+	destroySkeleton: function() {
+		// subclasses should implement
+	},
+
+
+	// Renders the view's date-related content (like cells that represent days/times).
+	// Assumes setRange has already been called and the skeleton has already been rendered.
+	renderDates: function() {
+		// subclasses should implement
+	},
+
+
+	// Unrenders the view's date-related content
+	destroyDates: function() {
+		// subclasses should override
+	},
+
+
+	// Renders business-hours onto the view. Assumes updateSize has already been called.
+	renderBusinessHours: function() {
+		// subclasses should implement
+	},
+
+
+	// Unrenders previously-rendered business-hours
+	destroyBusinessHours: function() {
+		// subclasses should implement
+	},
+
+
+	// Signals that the view's content has been rendered
+	triggerRender: function() {
+		this.trigger('viewRender', this, this, this.el);
+	},
+
+
+	// Signals that the view's content is about to be unrendered
+	triggerDestroy: function() {
 		this.trigger('viewDestroy', this, this, this.el);
+	},
 
-		$(document).off('mousedown', this.documentMousedownProxy);
+
+	// Binds DOM handlers to elements that reside outside the view container, such as the document
+	bindGlobalHandlers: function() {
+		$(document).on('mousedown', this.documentMousedownProxy);
 	},
 
 
-	// Clears the view's rendering
-	destroy: function() {
-		this.el.empty(); // removes inner contents but leaves the element intact
+	// Unbinds DOM handlers from elements that reside outside the view container
+	unbindGlobalHandlers: function() {
+		$(document).off('mousedown', this.documentMousedownProxy);
 	},
 
 
@@ -365,14 +469,22 @@ var View = fc.View = Class.extend({
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	// Wraps the basic renderEvents() method with more View-specific logic
-	renderViewEvents: function(events) {
+	// Does everything necessary to display the given events onto the current view
+	displayEvents: function(events) {
+		this.clearEvents();
 		this.renderEvents(events);
+		this.isEventsRendered = true;
+		this.triggerEventRender();
+	},
 
-		this.eventSegEach(function(seg) {
-			this.trigger('eventAfterRender', seg.event, seg.event, seg.el);
-		});
-		this.trigger('eventAfterAllRender');
+
+	// Does everything necessary to clear the view's currently-rendered events
+	clearEvents: function() {
+		if (this.isEventsRendered) {
+			this.triggerEventDestroy();
+			this.destroyEvents();
+			this.isEventsRendered = false;
+		}
 	},
 
 
@@ -382,19 +494,26 @@ var View = fc.View = Class.extend({
 	},
 
 
-	// Wraps the basic destroyEvents() method with more View-specific logic
-	destroyViewEvents: function() {
+	// Removes event elements from the view.
+	destroyEvents: function() {
+		// subclasses should implement
+	},
+
+
+	// Signals that all events have been rendered
+	triggerEventRender: function() {
 		this.eventSegEach(function(seg) {
-			this.trigger('eventDestroy', seg.event, seg.event, seg.el);
+			this.trigger('eventAfterRender', seg.event, seg.event, seg.el);
 		});
-
-		this.destroyEvents();
+		this.trigger('eventAfterAllRender');
 	},
 
 
-	// Removes event elements from the view.
-	destroyEvents: function() {
-		// subclasses should implement
+	// Signals that all event elements are about to be removed
+	triggerEventDestroy: function() {
+		this.eventSegEach(function(seg) {
+			this.trigger('eventDestroy', seg.event, seg.event, seg.el);
+		});
 	},