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

better refactor of View date/event binding, etc

Adam Shaw 9 лет назад
Родитель
Сommit
3a9846fecf
2 измененных файлов с 180 добавлено и 135 удалено
  1. 1 1
      src/Calendar.js
  2. 179 134
      src/common/View.js

+ 1 - 1
src/Calendar.js

@@ -803,7 +803,7 @@ function Calendar_constructor(element, overrides) {
 
 	function rerenderEvents() { // API method. destroys old events if previously rendered.
 		if (elementVisible()) {
-			currentView.displayEvents();
+			t.reportEventChange(); // will cause a reset, and thus a rerender
 		}
 	}
 

+ 179 - 134
src/common/View.js

@@ -13,11 +13,10 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
 	el: null, // the view's containing element. set by Calendar
 
 	isDateSet: false,
-	dateSetQueue: null,
+	dateRenderQueue: null,
 
-	displayingEvents: null, // a promise
+	isEventsBound: false,
 	isEventsSet: false,
-	isEventsBounds: false,
 	eventRenderQueue: null,
 
 	// range the view is actually displaying (moments)
@@ -68,7 +67,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
 
 		this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder'));
 
-		this.dateSetQueue = new RunQueue();
+		this.dateRenderQueue = new RunQueue();
 		this.eventRenderQueue = new RunQueue();
 
 		this.initialize();
@@ -313,10 +312,11 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
 	// Removes the view's container element from the DOM, clearing any content beforehand.
 	// Undoes any other DOM-related attachments.
 	removeElement: function() {
-		this.unbindGlobalHandlers();
 		this.unsetDate();
 		this.unrenderSkeleton();
 
+		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,
@@ -340,96 +340,101 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
 	// ------------------------------------------------------------------
 
 
-	// Renders ALL date related content, including events. Guaranteed to redraw content.
-	// async
 	setDate: function(date, forcedScroll) {
 		var _this = this;
 
-		// do this before unsetDate, which is destructive
-		this.captureScroll();
-		this.freezeHeight();
+		if (this.isDateSet) {
+			return this.resetDate(date, forcedScroll);
+		}
+		else {
+			this.isDateSet = true;
 
-		this.unsetDate();
-		this.isDateSet = true;
+			return this.requestRenderDate(date, forcedScroll).then(function() {
+				// i wish we could start earlier, but setRange/computeRange needs to execute first
+				_this.bindEvents();
+			});
+		}
+	},
 
-		return this.dateSetQueue.push(function() {
-			_this.setRange(_this.computeRange(date));
-			_this.displayDateVisuals();
 
-			_this.thawHeight();
-			if (forcedScroll) {
-				_this.discardScroll();
-				_this.setScroll(forcedScroll);
-			}
-			else {
-				_this.releaseInitialScroll();
-			}
+	resetDate: function(date, forcedScroll) {
+		var _this = this;
+
+		this.freezeHeight(); // because requestUnrenderDate will kill height
+		// we don't care about the scroll tho because we know it will be reset by requestRenderDate
 
-			_this.triggerDateVisualsRendered();
-		}).then(function() {
-			return _this.displayEvents();
-		}, function() {
-			// failure. TODO: implement in RunQueue
+		this.unsetDate();
+		return this.setDate(date, forcedScroll).then(function() {
 			_this.thawHeight();
-			_this.discardScroll();
 		});
 	},
 
 
-	// sync
 	unsetDate: function() {
 		if (this.isDateSet) {
-			this.isDateSet = false; // important to do first
-			this.dateSetQueue.clear();
-
-			this.stopDisplayingEvents();
-			this.stopDisplayingDateVisuals();
+			this.isDateSet = false;
+			this.unbindEvents();
+			return this.requestUnrenderDate();
 		}
 	},
 
 
-	// sync
-	displayDateVisuals: function() {
-		this.stopDisplayingDateVisuals();
-		this.isDisplayingDateVisuals = true;
+	requestRenderDate: function(date, forcedScroll) {
+		var _this = this;
 
-		if (this.render) {
-			this.render(); // TODO: deprecate
-		}
+		return this.dateRenderQueue.add(function() {
+			_this.setRange(_this.computeRange(date));
+
+			_this.captureInitialScroll(forcedScroll);
+			_this.freezeHeight();
+
+			if (_this.render) {
+				_this.render(); // TODO: deprecate
+			}
+
+			_this.renderDates();
+			_this.updateSize();
+			_this.renderBusinessHours(); // might need coordinates, so should go after updateSize()
+			_this.startNowIndicator();
+
+			_this.thawHeight();
+			_this.releaseScroll();
 
-		this.renderDates();
-		this.updateSize();
-		this.renderBusinessHours(); // might need coordinates, so should go after updateSize()
-		this.startNowIndicator();
+			_this.triggerDateRender();
+		});
 	},
 
 
-	// sync
-	stopDisplayingDateVisuals: function() {
-		if (this.isDisplayingDateVisuals) {
-			this.isDisplayingDateVisuals = false;
+	requestUnrenderDate: function() { // assumes already at least one render job
+		var _this = this;
 
-			this.unselect();
-			this.stopNowIndicator();
-			this.triggerUnrender();
-			this.unrenderBusinessHours();
-			this.unrenderDates();
+		return this.dateRenderQueue.add(function() {
+			return _this.requestUnrenderEvents().then(function() {
+				// there will be so much destruction, we don't care about freezing height or maintaining scroll
 
-			if (this.destroy) {
-				this.destroy(); // TODO: deprecate
-			}
-		}
+				_this.dateRenderQueue.forgetCompleted();
+
+				_this.unselect();
+				_this.stopNowIndicator();
+				_this.triggerUnrender();
+				_this.unrenderBusinessHours();
+				_this.unrenderDates();
+
+				if (_this.destroy) {
+					_this.destroy(); // TODO: deprecate
+				}
+			});
+		});
 	},
 
 
-	// Renders the view's date-related content.
-	// Assumes setRange has already been called and the skeleton has already been rendered.
+	// date-cell content only
 	renderDates: function() {
 		// subclasses should implement
 	},
 
 
-	// Unrenders the view's date-related content
+	// date-cell content only
 	unrenderDates: function() {
 		// subclasses should override
 	},
@@ -439,8 +444,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
 	// --------------------
 
 
-	// Can be extended to rely on other things
-	triggerDateVisualsRendered: function() {
+	triggerDateRender: function() {
 		this.triggerRender();
 	},
 
@@ -634,48 +638,71 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	capturedScrollDepth: 0,
 	capturedScroll: null,
+	capturedScrollDepth: 0,
 
 
 	captureScroll: function() {
 		if (!(this.capturedScrollDepth++)) {
-			this.capturedScroll = this.isDisplayingDateVisuals ? this.queryScroll() : {};
+			this.capturedScroll =
+				this.dateRenderQueue.completed() ? // guarantee a render before querying scroll
+					this.queryScroll() :
+					{};
+			return true; // root?
 		}
+		return false;
 	},
 
 
-	releaseScroll: function() {
-		var scroll = this.capturedScroll;
+	captureInitialScroll: function(forcedScroll) {
+		if (this.captureScroll()) {
+			this.capturedScroll.isInitial = true;
 
-		this.discardScroll();
-		// we always act on a releaseScroll operation, as opposed to captureScroll.
-		// if capture/release wraps a render operation that screws up the scroll,
-		// we still want to restore it a good state after, regardless of depth.
-		this.setScroll(scroll);
+			if (forcedScroll) {
+				$.extend(this.capturedScroll, forcedScroll);
+			}
+			else {
+				this.capturedScroll.isComputed = true;
+			}
+		}
 	},
 
 
-	releaseInitialScroll: function() {
-		var scroll = $.extend({}, this.capturedScroll, this.computeInitialScroll());
+	releaseScroll: function() {
+		var scroll = this.capturedScroll;
+		var isRoot = this.discardScroll();
 
-		this.discardScroll();
-		this.hardSetScroll(scroll); // since it's the initial rendering, we need to outsmart DOM
-	},
+		if (scroll.isComputed) {
+			if (isRoot) {
+				// only compute initial scroll if it will actually be used (is the root capture)
+				$.extend(scroll, this.computeInitialScroll());
+			}
+			else {
+				scroll = null; // scroll couldn't be computed. don't apply it to the DOM
+			}
+		}
 
+		if (scroll) {
+			// we act immediately on a releaseScroll operation, as opposed to captureScroll.
+			// if capture/release wraps a render operation that screws up the scroll,
+			// we still want to restore it a good state after, regardless of depth.
 
-	hardSetScroll: function(scroll) {
-		var _this = this;
-		var exec = function() { _this.setScroll(scroll); };
-		exec();
-		setTimeout(exec, 0); // to surely clear the browser's initial scroll for the DOM
+			if (scroll.isInitial) {
+				this.hardSetScroll(scroll); // outsmart how browsers set scroll on initial DOM
+			}
+			else {
+				this.setScroll(scroll);
+			}
+		}
 	},
 
 
 	discardScroll: function() {
 		if (!(--this.capturedScrollDepth)) {
 			this.capturedScroll = null;
+			return true; // root?
 		}
+		return false;
 	},
 
 
@@ -689,6 +716,14 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
 	},
 
 
+	hardSetScroll: function(scroll) {
+		var _this = this;
+		var exec = function() { _this.setScroll(scroll); };
+		exec();
+		setTimeout(exec, 0); // to surely clear the browser's initial scroll for the DOM
+	},
+
+
 	setScroll: function(scroll) {
 	},
 
@@ -711,25 +746,27 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	// Does everything necessary to display the given events onto the current view. Guaranteed to redraw content.
-	// async. promise might not resolve if rendering cancelled.
-	// Assumes date visuals already displayed.
-	displayEvents: function() {
+	bindEvents: function() {
 		var _this = this;
 
-		return this.displayingEvents = this.requestEvents().then(function(events) {
-			_this.bindEvents(); // listen to changes. do this before the setEvents, because might trigger a reset itself
-			return _this.setEvents(events);
-		});
+		if (!this.isEventsBound) {
+			this.isEventsBound = true;
+			this.requestEvents().then(function(events) {
+				if (_this.isEventsBound) { // in case it was unbound in the meantime
+					_this.listenTo(_this.calendar, 'resetEvents', _this.resetEvents);
+					_this.setEvents(events);
+				}
+			});
+		}
 	},
 
 
-	// Does everything necessary to clear the view's currently-rendered events.
-	// sync
-	stopDisplayingEvents: function() {
-		this.displayingEvents = null;
-		this.unbindEvents();
-		this.unsetEvents();
+	unbindEvents: function() {
+		if (this.isEventsBound) {
+			this.isEventsBound = false;
+			this.stopListeningTo(this.calendar, 'resetEvents');
+			this.unsetEvents();
+		}
 	},
 
 
@@ -738,23 +775,20 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
 	},
 
 
-	bindEvents: function() {
-		if (!this.isEventsBounds) {
-			this.listenTo(this.calendar, 'resetEvents', this.resetEvents);
-			this.isEventsBounds = true;
-		}
-	},
+	setEvents: function(events) {
+		var _this = this;
 
+		if (this.isEventsSet) {
+			return this.resetEvents(events);
+		}
+		else {
+			this.isEventsSet = true;
 
-	unbindEvents: function() {
-		if (this.isEventsBounds) {
-			this.stopListeningTo(this.calendar, 'resetEvents');
-			this.isEventsBounds = false;
+			return this.requestRenderEvents(events);
 		}
 	},
 
 
-	// async
 	resetEvents: function(events) {
 		var _this = this;
 
@@ -763,55 +797,66 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
 		this.freezeHeight();
 
 		this.unsetEvents();
-
 		return this.setEvents(events).then(function() {
-			_this.releaseScroll();
 			_this.thawHeight();
+			_this.releaseScroll();
 		});
 	},
 
 
-	// async
-	setEvents: function(events) {
+	unsetEvents: function() {
 		var _this = this;
 
 		if (this.isEventsSet) {
-			return this.resetEvents(events);
+			this.isEventsSet = false;
+			return this.requestUnrenderEvents();
 		}
-		else {
-			this.isEventsSet = true;
+	},
 
-			return this.eventRenderQueue.push(function() {
-				_this.captureScroll();
-				_this.freezeHeight();
 
-				_this.renderEvents(events);
+	// assumes any previous event renders have been cleared already
+	requestRenderEvents: function(events) {
+		var _this = this;
 
-				_this.thawHeight();
-				_this.releaseScroll();
+		return this.eventRenderQueue.add(function() {
+			_this.captureScroll();
+			_this.freezeHeight();
 
-				_this.triggerEventRender();
-			});
-		}
+			_this.renderEvents(events);
+
+			_this.thawHeight();
+			_this.releaseScroll();
+
+			_this.triggerEventRender();
+		});
 	},
 
 
-	// sync
-	unsetEvents: function() {
+	requestUnrenderEvents: function() { // assumes there was already at least one job in eventRenderQueue
 		var _this = this;
 
-		if (this.isEventsSet) {
-			this.isEventsSet = false; // must go first. so triggers don't reinvoke unsetEvents
-			this.eventRenderQueue.clear(); // kill in-progress renders
+		return this.eventRenderQueue.add(function() {
+			_this.eventRenderQueue.forgetCompleted(); // for ensureRenderEvents
 
-			this.triggerEventUnrender();
+			_this.triggerEventUnrender();
 
-			if (this.destroyEvents) {
-				this.destroyEvents(); // TODO: deprecate
+			_this.captureScroll();
+			_this.freezeHeight();
+
+			if (_this.destroyEvents) {
+				_this.destroyEvents(); // TODO: deprecate
 			}
 
-			this.unrenderEvents();
-		}
+			_this.unrenderEvents();
+
+			_this.thawHeight();
+			_this.releaseScroll();
+		});
+	},
+
+
+	ensureRenderEvents: function() {
+		return this.eventRenderQueue.promise();
 	},