2
0
Эх сурвалжийг харах

core view rendering flow cleaned up and better asynch

Adam Shaw 9 жил өмнө
parent
commit
3c349d3eba
2 өөрчлөгдсөн 281 нэмэгдсэн , 116 устгасан
  1. 6 8
      src/Calendar.js
  2. 275 108
      src/common/View.js

+ 6 - 8
src/Calendar.js

@@ -668,11 +668,11 @@ function Calendar_constructor(element, overrides) {
 			) {
 			) {
 				if (elementVisible()) {
 				if (elementVisible()) {
 
 
-					currentView.display(date, explicitScrollState); // will call freezeContentHeight
+					currentView.setDate(date, explicitScrollState); // will call freezeContentHeight
 					unfreezeContentHeight(); // immediately unfreeze regardless of whether display is async
 					unfreezeContentHeight(); // immediately unfreeze regardless of whether display is async
 
 
 					// need to do this after View::render, so dates are calculated
 					// need to do this after View::render, so dates are calculated
-					updateToolbarsTitle();
+					// NOTE: view updates title text proactively
 					updateToolbarsTodayButton();
 					updateToolbarsTodayButton();
 
 
 					getAndRenderEvents();
 					getAndRenderEvents();
@@ -818,9 +818,7 @@ function Calendar_constructor(element, overrides) {
 
 
 	function renderEvents() { // destroys old events if previously rendered
 	function renderEvents() { // destroys old events if previously rendered
 		if (elementVisible()) {
 		if (elementVisible()) {
-			freezeContentHeight();
-			currentView.displayEvents(events);
-			unfreezeContentHeight();
+			//currentView.displayEvents(events);
 		}
 		}
 	}
 	}
 
 
@@ -904,9 +902,9 @@ function Calendar_constructor(element, overrides) {
 	}
 	}
 
 
 
 
-	function updateToolbarsTitle() {
-		toolbarsManager.proxyCall('updateTitle', currentView.title);
-	}
+	t.setToolbarsTitle = function(title) {
+		toolbarsManager.proxyCall('updateTitle', title);
+	};
 
 
 
 
 	function updateToolbarsTodayButton() {
 	function updateToolbarsTodayButton() {

+ 275 - 108
src/common/View.js

@@ -12,9 +12,14 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
 	options: null, // hash containing all options. already merged with view-specific-options
 	options: null, // hash containing all options. already merged with view-specific-options
 	el: null, // the view's containing element. set by Calendar
 	el: null, // the view's containing element. set by Calendar
 
 
-	displaying: null, // a promise representing the state of rendering. null if no render requested
-	isSkeletonRendered: false,
-	isEventsRendered: false,
+	// promises
+	settingElement: null,
+	settingDate: null,
+	displayingNonChrono: null,
+	displayingChrono: null,
+	displayingSkeleton: null,
+	displayingDates: null,
+	displayingEvents: null,
 
 
 	// range the view is actually displaying (moments)
 	// range the view is actually displaying (moments)
 	start: null,
 	start: null,
@@ -94,16 +99,10 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
 	},
 	},
 
 
 
 
-	/* Dates
+	/* Date Computation
 	------------------------------------------------------------------------------------------------------------------*/
 	------------------------------------------------------------------------------------------------------------------*/
 
 
 
 
-	// Updates all internal dates to center around the given current unzoned date.
-	setDate: function(date) {
-		this.setRange(this.computeRange(date));
-	},
-
-
 	// Updates all internal dates for displaying the given unzoned range.
 	// Updates all internal dates for displaying the given unzoned range.
 	setRange: function(range) {
 	setRange: function(range) {
 		$.extend(this, range); // assigns every property to this object's member variables
 		$.extend(this, range); // assigns every property to this object's member variables
@@ -186,6 +185,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
 	// Sets the view's title property to the most updated computed value
 	// Sets the view's title property to the most updated computed value
 	updateTitle: function() {
 	updateTitle: function() {
 		this.title = this.computeTitle();
 		this.title = this.computeTitle();
+		this.calendar.setToolbarsTitle(this.title);
 	},
 	},
 
 
 
 
@@ -295,126 +295,224 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
 	------------------------------------------------------------------------------------------------------------------*/
 	------------------------------------------------------------------------------------------------------------------*/
 
 
 
 
-	// Sets the container element that the view should render inside of.
-	// Does other DOM-related initializations.
+	// Non-date-related content, like the view's skeleton
+	// --------------------------------------------------
+
+
+	// Sets the container element that the view should render inside of, does global DOM-related initializations,
+	// and renders all the non-date-related content inside.
+	// Returns a potentially-asynchronous promise.
 	setElement: function(el) {
 	setElement: function(el) {
-		this.el = el;
-		this.bindGlobalHandlers();
+		var _this = this;
+
+		return this.settingElement = this.removeElement().then(function() {
+			_this.el = el;
+			_this.bindGlobalHandlers();
+			return _this.displayNonChrono();
+		});
 	},
 	},
 
 
 
 
 	// Removes the view's container element from the DOM, clearing any content beforehand.
 	// Removes the view's container element from the DOM, clearing any content beforehand.
 	// Undoes any other DOM-related attachments.
 	// Undoes any other DOM-related attachments.
+	// Returns a potentially-asynchronous promise.
 	removeElement: function() {
 	removeElement: function() {
-		this.clear(); // clears all content
+		var _this = this;
+
+		if (this.settingElement) {
+			return this.settingElement.then(function() {
+				return Promise.all([
+					_this.clearNonChrono(),
+					_this.clearChrono()
+				]).then(function() {
+
+					_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 we shouldn't kill this.el either.
+
+					_this.unbindGlobalHandlers();
+					_this.settingElement = null;
+				});
+			});
+		}
+		else {
+			return Promise.resolve();
+		}
+	},
+
+
+	// Returns a potentially-asynchronous promise.
+	ensureNonChrono: function() {
+		return this.displayingNonChrono || this.displayNonChrono();
+	},
+
+
+	// Returns a potentially-asynchronous promise.
+	displayNonChrono: function() {
+		var _this = this;
+
+		return this.displayingNonChrono = this.clearNonChrono().then(function() {
+			return _this.ensureSkeleton();
+		});
+	},
+
+
+	// Returns a potentially-asynchronous promise.
+	clearNonChrono: function() {
+		var _this = this;
 
 
-		// clean up the skeleton
-		if (this.isSkeletonRendered) {
-			this.unrenderSkeleton();
-			this.isSkeletonRendered = false;
+		if (this.displayingNonChrono) {
+			return this.displayingNonChrono.then(function() {
+				return _this.clearSkeleton().then(function() {
+					_this.displayingNonChrono = null;
+				});
+			});
+		}
+		else {
+			return Promise.resolve();
 		}
 		}
+	},
 
 
-		this.unbindGlobalHandlers();
 
 
-		this.el.remove();
+	// Date-related-content, like date cells, date indicators, and events
+	// ------------------------------------------------------------------
 
 
-		// 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 we shouldn't kill this.el either.
+
+	isDateSet: function() {
+		return this.settingDate && this.settingDate.state() === 'resolved';
 	},
 	},
 
 
 
 
-	// Does everything necessary to display the view centered around the given unzoned date.
-	// Does every type of rendering EXCEPT rendering events.
-	// Is asychronous and returns a promise.
-	display: function(date, explicitScrollState) {
+	// Renders ALL date related content, including events. Guaranteed to redraw content.
+	// Returns a potentially-asynchronous promise.
+	setDate: function(date, explicitScrollState) {
 		var _this = this;
 		var _this = this;
 		var prevScrollState = null;
 		var prevScrollState = null;
 
 
-		if (explicitScrollState != null && this.displaying) { // don't need prevScrollState if explicitScrollState
+		if (explicitScrollState != null && this.isDateSet()) { // don't need prevScrollState if explicitScrollState
 			prevScrollState = this.queryScroll();
 			prevScrollState = this.queryScroll();
 		}
 		}
 
 
 		this.calendar.freezeContentHeight();
 		this.calendar.freezeContentHeight();
 
 
-		return syncThen(this.clear(), function() { // clear the content first
-			return (
-				_this.displaying =
-					syncThen(_this.displayView(date), function() { // displayView might return a promise
-
-						// caller of display() wants a specific scroll state?
-						if (explicitScrollState != null) {
-							// we make an assumption that this is NOT the initial render,
-							// and thus don't need forceScroll (is inconveniently asynchronous)
-							_this.setScroll(explicitScrollState);
-						}
-						else {
-							_this.forceScroll(_this.computeInitialScroll(prevScrollState));
-						}
-
-						_this.calendar.unfreezeContentHeight();
-						_this.triggerRender();
-					})
-			);
+		return this.settingDate = this.unsetDate().then(function() {
+			_this.setRange(_this.computeRange(date));
+			_this.displayChrono();
+
+			// hack for freezeContentHeight, scrollState, and triggerRender
+			// TODO: refactor
+			Promise.all([
+				_this.ensureNonChrono(),
+				_this.displayingChrono
+			]).then(function() {
+
+				// caller of display() wants a specific scroll state?
+				if (explicitScrollState != null) {
+					// we make an assumption that this is NOT the initial render,
+					// and thus don't need forceScroll (is inconveniently asynchronous)
+					_this.setScroll(explicitScrollState);
+				}
+				else {
+					_this.forceScroll(_this.computeInitialScroll(prevScrollState));
+				}
+
+				_this.calendar.unfreezeContentHeight();
+				_this.triggerRender();
+			});
+
+			return _this.displayingChrono;
 		});
 		});
 	},
 	},
 
 
 
 
-	// Does everything necessary to clear the content of the view.
-	// Clears dates and events. Does not clear the skeleton.
-	// Is asychronous and returns a promise.
-	clear: function() {
+	// NOTE: triggerUnrender is called in clearChrono -> clearDates
+	unsetDate: function() {
 		var _this = this;
 		var _this = this;
-		var displaying = this.displaying;
 
 
-		if (displaying) { // previously displayed, or in the process of being displayed?
-			return syncThen(displaying, function() { // wait for the display to finish
-				_this.displaying = null;
-				_this.clearEvents();
-				return _this.clearView(); // might return a promise. chain it
+		if (this.settingDate) {
+			return this.settingDate.then(function() {
+				return _this.clearChrono().then(function() {
+					_this.settingDate = null;
+				});
 			});
 			});
 		}
 		}
 		else {
 		else {
-			return $.when(); // an immediately-resolved promise
+			return Promise.resolve();
 		}
 		}
 	},
 	},
 
 
 
 
-	// Displays the view's non-event content, such as date-related content or anything required by events.
-	// Renders the view's non-content skeleton if necessary.
-	// Can be asynchronous and return a promise.
-	displayView: function(date) {
-		if (!this.isSkeletonRendered) {
-			this.renderSkeleton();
-			this.isSkeletonRendered = true;
+	// Renders ALL date related content, including events. Guaranteed to redraw content.
+	// Returns a potentially-asynchronous promise.
+	displayChrono: function() {
+		var _this = this;
+
+		return this.displayingChrono = this.clearChrono().then(function() {
+			return Promise.all([
+				_this.displayDates(),
+				_this.displayEvents()
+			]);
+		});
+	},
+
+
+	// Returns a potentially-asynchronous promise.
+	clearChrono: function() {
+		var _this = this;
+
+		if (this.displayingChrono) {
+			return this.displayingChrono.then(function() {
+				return Promise.all([
+					_this.clearEvents(),
+					_this.clearDates()
+				]).then(function() {
+					_this.displayingChrono = null;
+				});
+			});
 		}
 		}
-		if (date) {
-			this.setDate(date);
+		else {
+			return Promise.resolve();
 		}
 		}
-		if (this.render) {
-			this.render(); // TODO: deprecate
+	},
+
+
+	// Skeleton rendering
+	// ------------------
+
+
+	// Renders the skeleton if it has not already been rendered.
+	// Returns a potentially-asynchronous promise.
+	ensureSkeleton: function() {
+		return this.displayingSkeleton || this.displaySkeleton();
+	},
+
+
+	// Returns a potentially-asynchronous promise. Guaranteed to redraw content.
+	displaySkeleton: function() {
+		var _this = this;
+
+		this.displayingSkeleton = this.clearSkeleton().then(function() {
+			_this.renderSkeleton();
+		});
+	},
+
+
+	// Returns a potentially-asynchronous promise.
+	clearSkeleton: function() {
+		var _this = this;
+
+		if (this.displayingSkeleton) {
+			return this.displayingSkeleton.then(function() {
+				_this.unrenderSkeleton();
+				_this.displayingSkeleton = null;
+			});
 		}
 		}
-		this.renderDates();
-		this.updateSize();
-		this.renderBusinessHours(); // might need coordinates, so should go after updateSize()
-		this.startNowIndicator();
-	},
-
-
-	// Unrenders the view content that was rendered in displayView.
-	// Can be asynchronous and return a promise.
-	clearView: function() {
-		this.unselect();
-		this.stopNowIndicator();
-		this.triggerUnrender();
-		this.unrenderBusinessHours();
-		this.unrenderDates();
-		if (this.destroy) {
-			this.destroy(); // TODO: deprecate
+		else {
+			return Promise.resolve();
 		}
 		}
 	},
 	},
 
 
-
 	// Renders the basic structure of the view before any content is rendered
 	// Renders the basic structure of the view before any content is rendered
 	renderSkeleton: function() {
 	renderSkeleton: function() {
 		// subclasses should implement
 		// subclasses should implement
@@ -427,6 +525,51 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
 	},
 	},
 
 
 
 
+	// Date rendering (usually some type of date cells that events eventually live on)
+	// -------------------------------------------------------------------------------
+
+
+	// Renders date cells ONLY. Does not render events. Guaranteed to redraw content.
+	// Returns a potentially-asynchronous promise.
+	displayDates: function() {
+		var _this = this;
+
+		return this.displayingDates = this.clearDates().then(function() {
+			return _this.ensureSkeleton().then(function() {
+				if (_this.render) {
+					_this.render(); // TODO: deprecate
+				}
+				_this.renderDates();
+				_this.updateSize();
+				_this.renderBusinessHours(); // might need coordinates, so should go after updateSize()
+				_this.startNowIndicator();
+			});
+		});
+	},
+
+
+	clearDates: function() {
+		var _this = this;
+
+		if (this.displayingDates) {
+			return this.displayingDates.then(function() {
+				_this.unselect();
+				_this.stopNowIndicator();
+				_this.triggerUnrender();
+				_this.unrenderBusinessHours();
+				_this.unrenderDates();
+				if (_this.destroy) {
+					_this.destroy(); // TODO: deprecate
+				}
+				_this.displayingDates = null;
+			});
+		}
+		else {
+			return Promise.resolve();
+		}
+	},
+
+
 	// Renders the view's date-related content.
 	// Renders the view's date-related content.
 	// Assumes setRange has already been called and the skeleton has already been rendered.
 	// Assumes setRange has already been called and the skeleton has already been rendered.
 	renderDates: function() {
 	renderDates: function() {
@@ -440,6 +583,10 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
 	},
 	},
 
 
 
 
+	// Misc rendering utils
+	// --------------------
+
+
 	// Signals that the view's content has been rendered
 	// Signals that the view's content has been rendered
 	triggerRender: function() {
 	triggerRender: function() {
 		this.trigger('viewRender', this, this, this.el);
 		this.trigger('viewRender', this, this, this.el);
@@ -663,35 +810,55 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
 	------------------------------------------------------------------------------------------------------------------*/
 	------------------------------------------------------------------------------------------------------------------*/
 
 
 
 
-	// Does everything necessary to display the given events onto the current view
-	displayEvents: function(events) {
+	// Does everything necessary to display the given events onto the current view. Guaranteed to redraw content.
+	// Returns a potentially-asynchronous promise.
+	displayEvents: function() {
+		var _this = this;
 		var scrollState = this.queryScroll();
 		var scrollState = this.queryScroll();
 
 
-		this.clearEvents();
-		this.renderEvents(events);
-		this.isEventsRendered = true;
-		this.setScroll(scrollState);
-		this.triggerEventRender();
+		this.calendar.freezeContentHeight();
+
+		return this.displayingEvents = this.clearEvents().then(function() {
+			return Promise.all([
+				_this.requestEvents(),
+				_this.displayDates()
+			]).then(function(values) {
+				_this.renderEvents(values[0]); // value[0] is the events array
+				_this.calendar.unfreezeContentHeight();
+				_this.setScroll(scrollState);
+				_this.triggerEventRender();
+			});
+		});
 	},
 	},
 
 
 
 
-	// Does everything necessary to clear the view's currently-rendered events
+	// Does everything necessary to clear the view's currently-rendered events.
+	// Returns a potentially-asynchronous promise.
 	clearEvents: function() {
 	clearEvents: function() {
-		var scrollState;
+		var _this = this;
+		// TODO: optimize: if we know this is part of a displayEvents call, don't queryScroll/setScroll
+		var scrollState = this.queryScroll();
 
 
-		if (this.isEventsRendered) {
+		if (this.displayingEvents) {
+			return this.displayingEvents.then(function() {
+				_this.triggerEventUnrender();
+				if (_this.destroyEvents) {
+					_this.destroyEvents(); // TODO: deprecate
+				}
+				_this.unrenderEvents();
+				_this.setScroll(scrollState);
+				_this.displayingEvents = null;
+			});
+		}
+		else {
+			return Promise.resolve();
+		}
+	},
 
 
-			// TODO: optimize: if we know this is part of a displayEvents call, don't queryScroll/setScroll
-			scrollState = this.queryScroll();
 
 
-			this.triggerEventUnrender();
-			if (this.destroyEvents) {
-				this.destroyEvents(); // TODO: deprecate
-			}
-			this.unrenderEvents();
-			this.setScroll(scrollState);
-			this.isEventsRendered = false;
-		}
+	// stub
+	requestEvents: function() {
+		return Promise.resolve([]);
 	},
 	},