Jelajahi Sumber

move calendar rendering and toolbars into new file

Adam Shaw 8 tahun lalu
induk
melakukan
16ff6945c4
4 mengubah file dengan 397 tambahan dan 392 penghapusan
  1. 2 0
      src.json
  2. 6 392
      src/Calendar.js
  3. 289 0
      src/Calendar.render.js
  4. 100 0
      src/Calendar.toolbar.js

+ 2 - 0
src.json

@@ -35,6 +35,8 @@
     "Calendar.js",
     "Calendar.options.js",
     "Calendar.moment.js",
+    "Calendar.render.js",
+    "Calendar.toolbar.js",
     "defaults.js",
     "locale.js",
     "Header.js",

+ 6 - 392
src/Calendar.js

@@ -5,8 +5,6 @@ var Calendar = FC.Calendar = Class.extend({
 	viewSpecCache: null, // cache of view definitions
 	view: null, // current View object
 	currentDate: null, // unzoned moment. private (public API should use getDate instead)
-	header: null,
-	footer: null,
 	loadingLevel: 0, // number of simultaneous loading tasks
 
 
@@ -353,6 +351,7 @@ function Calendar_constructor(element, overrides) {
 	var t = this;
 
 	GlobalEmitter.needed(); // declare the current calendar instance relies on GlobalEmitter. needed for garbage collection.
+	t.el = element;
 	t.initOptionsInternals(overrides);
 	t.initMomentInternals(); // needs to happen after options hash initialized
 	EventManager.call(t);
@@ -361,8 +360,6 @@ function Calendar_constructor(element, overrides) {
 	// Exports
 	// -----------------------------------------------------------------------------------
 
-	t.render = render;
-	t.destroy = destroy;
 	t.rerenderEvents = rerenderEvents;
 	t.select = select;
 	t.unselect = unselect;
@@ -377,285 +374,12 @@ function Calendar_constructor(element, overrides) {
 
 
 	var _element = element[0];
-	var toolbarsManager;
-	var header;
-	var footer;
-	var content;
-	var tm; // for making theme classes
-	var currentView; // NOTE: keep this in sync with this.view
-	var suggestedViewHeight;
-	var windowResizeProxy; // wraps the windowResize function
-	var ignoreWindowResize = 0;
 
 
 	t.initCurrentDate();
 	t.viewsByType = {};
 
 
-	// Main Rendering
-	// -----------------------------------------------------------------------------------
-
-
-	function render() {
-		if (!content) {
-			initialRender();
-		}
-		else if (elementVisible()) {
-			// mainly for the public API
-			calcSize();
-			t.renderView();
-		}
-	}
-
-
-	function initialRender() {
-		element.addClass('fc');
-
-		// event delegation for nav links
-		element.on('click.fc', 'a[data-goto]', function(ev) {
-			var anchorEl = $(this);
-			var gotoOptions = anchorEl.data('goto'); // will automatically parse JSON
-			var date = t.moment(gotoOptions.date);
-			var viewType = gotoOptions.type;
-
-			// property like "navLinkDayClick". might be a string or a function
-			var customAction = currentView.opt('navLink' + capitaliseFirstLetter(viewType) + 'Click');
-
-			if (typeof customAction === 'function') {
-				customAction(date, ev);
-			}
-			else {
-				if (typeof customAction === 'string') {
-					viewType = customAction;
-				}
-				zoomTo(date, viewType);
-			}
-		});
-
-		// called immediately, and upon option change
-		t.bindOption('theme', function(theme) {
-			tm = theme ? 'ui' : 'fc'; // affects a larger scope
-			element.toggleClass('ui-widget', theme);
-			element.toggleClass('fc-unthemed', !theme);
-		});
-
-		// called immediately, and upon option change.
-		// HACK: locale often affects isRTL, so we explicitly listen to that too.
-		t.bindOptions([ 'isRTL', 'locale' ], function(isRTL) {
-			element.toggleClass('fc-ltr', !isRTL);
-			element.toggleClass('fc-rtl', isRTL);
-		});
-
-		content = $("<div class='fc-view-container'/>").prependTo(element);
-
-		var toolbars = buildToolbars();
-		toolbarsManager = new Iterator(toolbars);
-
-		header = t.header = toolbars[0];
-		footer = t.footer = toolbars[1];
-
-		t.renderHeader();
-		t.renderFooter();
-		t.renderView(t.options.defaultView);
-
-		if (t.options.handleWindowResize) {
-			windowResizeProxy = debounce(windowResize, t.options.windowResizeDelay); // prevents rapid calls
-			$(window).resize(windowResizeProxy);
-		}
-	}
-
-
-	function destroy() {
-
-		if (currentView) {
-			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.
-		}
-
-		toolbarsManager.proxyCall('removeElement');
-		content.remove();
-		element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');
-
-		element.off('.fc'); // unbind nav link handlers
-
-		if (windowResizeProxy) {
-			$(window).unbind('resize', windowResizeProxy);
-		}
-
-		GlobalEmitter.unneeded();
-	}
-
-
-	function elementVisible() {
-		return element.is(':visible');
-	}
-
-
-
-	// View Rendering
-	// -----------------------------------------------------------------------------------
-
-
-	// 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.
-	// Accepts an optional scroll state to restore to.
-	t.renderView = function(viewType, forcedScroll) {
-		ignoreWindowResize++;
-
-		var needsClearView = currentView && viewType && currentView.type !== viewType;
-
-		// if viewType is changing, remove the old view's rendering
-		if (needsClearView) {
-			freezeContentHeight(); // prevent a scroll jump when view element is removed
-			clearView();
-		}
-
-		// if viewType changed, or the view was never created, create a fresh view
-		if (!currentView && viewType) {
-			currentView = t.view =
-				t.viewsByType[viewType] ||
-				(t.viewsByType[viewType] = t.instantiateView(viewType));
-
-			currentView.setElement(
-				$("<div class='fc-view fc-" + viewType + "-view' />").appendTo(content)
-			);
-			toolbarsManager.proxyCall('activateButton', viewType);
-		}
-
-		if (currentView) {
-
-			if (forcedScroll) {
-				currentView.addForcedScroll(forcedScroll);
-			}
-
-			if (elementVisible()) {
-				t.currentDate = currentView.setDate(t.currentDate);
-			}
-		}
-
-		if (needsClearView) {
-			thawContentHeight();
-		}
-
-		ignoreWindowResize--;
-	};
-
-
-	// Unrenders the current view and reflects this change in the Header.
-	// Unregsiters the `currentView`, but does not remove from viewByType hash.
-	function clearView() {
-		toolbarsManager.proxyCall('deactivateButton', currentView.type);
-		currentView.removeElement();
-		currentView = t.view = null;
-	}
-
-
-	// Destroys the view, including the view object. Then, re-instantiates it and renders it.
-	// Maintains the same scroll state.
-	// TODO: maintain any other user-manipulated state.
-	this.reinitView = function() {
-		ignoreWindowResize++;
-		freezeContentHeight();
-
-		var viewType = currentView.type;
-		var scrollState = currentView.queryScroll();
-		clearView();
-		calcSize();
-		t.renderView(viewType, scrollState);
-
-		thawContentHeight();
-		ignoreWindowResize--;
-	};
-
-
-
-	// Resizing
-	// -----------------------------------------------------------------------------------
-
-
-	t.getSuggestedViewHeight = function() {
-		if (suggestedViewHeight === undefined) {
-			calcSize();
-		}
-		return suggestedViewHeight;
-	};
-
-
-	t.isHeightAuto = function() {
-		return t.options.contentHeight === 'auto' || t.options.height === 'auto';
-	};
-
-
-	function updateSize(shouldRecalc) {
-		if (elementVisible()) {
-
-			if (shouldRecalc) {
-				_calcSize();
-			}
-
-			ignoreWindowResize++;
-			currentView.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto()
-			ignoreWindowResize--;
-
-			return true; // signal success
-		}
-	}
-
-
-	function calcSize() {
-		if (elementVisible()) {
-			_calcSize();
-		}
-	}
-
-
-	function _calcSize() { // assumes elementVisible
-		var contentHeightInput = t.options.contentHeight;
-		var heightInput = t.options.height;
-
-		if (typeof contentHeightInput === 'number') { // exists and not 'auto'
-			suggestedViewHeight = contentHeightInput;
-		}
-		else if (typeof contentHeightInput === 'function') { // exists and is a function
-			suggestedViewHeight = contentHeightInput();
-		}
-		else if (typeof heightInput === 'number') { // exists and not 'auto'
-			suggestedViewHeight = heightInput - queryToolbarsHeight();
-		}
-		else if (typeof heightInput === 'function') { // exists and is a function
-			suggestedViewHeight = heightInput() - queryToolbarsHeight();
-		}
-		else if (heightInput === 'parent') { // set to height of parent element
-			suggestedViewHeight = element.parent().height() - queryToolbarsHeight();
-		}
-		else {
-			suggestedViewHeight = Math.round(content.width() / Math.max(t.options.aspectRatio, .5));
-		}
-	}
-
-
-	function queryToolbarsHeight() {
-		return toolbarsManager.items.reduce(function(accumulator, toolbar) {
-			var toolbarHeight = toolbar.el ? toolbar.el.outerHeight(true) : 0; // includes margin
-			return accumulator + toolbarHeight;
-		}, 0);
-	}
-
-
-	function windowResize(ev) {
-		if (
-			!ignoreWindowResize &&
-			ev.target === window && // so we don't process jqui "resize" events that have bubbled up
-			currentView.renderRange // view has already been rendered
-		) {
-			if (updateSize(true)) {
-				currentView.publiclyTrigger('windowResize', _element);
-			}
-		}
-	}
-
 
 
 	/* Event Rendering
@@ -663,111 +387,28 @@ function Calendar_constructor(element, overrides) {
 
 
 	function rerenderEvents() { // API method. destroys old events if previously rendered.
-		if (elementVisible()) {
+		if (t.elementVisible()) {
 			t.reportEventChange(); // will re-trasmit events to the view, causing a rerender
 		}
 	}
 
 
 
-	/* Toolbars
-	-----------------------------------------------------------------------------*/
-
-
-	function buildToolbars() {
-		return [
-			new Toolbar(t, computeHeaderOptions()),
-			new Toolbar(t, computeFooterOptions())
-		];
-	}
-
-
-	function computeHeaderOptions() {
-		return {
-			extraClasses: 'fc-header-toolbar',
-			layout: t.options.header
-		};
-	}
-
-
-	function computeFooterOptions() {
-		return {
-			extraClasses: 'fc-footer-toolbar',
-			layout: t.options.footer
-		};
-	}
-
-
-	// can be called repeatedly and Header will rerender
-	t.renderHeader = function() {
-		header.setToolbarOptions(computeHeaderOptions());
-		header.render();
-		if (header.el) {
-			element.prepend(header.el);
-		}
-	};
-
-
-	// can be called repeatedly and Footer will rerender
-	t.renderFooter = function() {
-		footer.setToolbarOptions(computeFooterOptions());
-		footer.render();
-		if (footer.el) {
-			element.append(footer.el);
-		}
-	};
-
-
-	t.setToolbarsTitle = function(title) {
-		toolbarsManager.proxyCall('updateTitle', title);
-	};
-
-
-	t.updateToolbarButtons = function() {
-		var now = t.getNow();
-		var todayInfo = currentView.buildDateProfile(now);
-		var prevInfo = currentView.buildPrevDateProfile(t.currentDate);
-		var nextInfo = currentView.buildNextDateProfile(t.currentDate);
-
-		toolbarsManager.proxyCall(
-			(todayInfo.isValid && !isDateWithinRange(now, currentView.currentRange)) ?
-				'enableButton' :
-				'disableButton',
-			'today'
-		);
-
-		toolbarsManager.proxyCall(
-			prevInfo.isValid ?
-				'enableButton' :
-				'disableButton',
-			'prev'
-		);
-
-		toolbarsManager.proxyCall(
-			nextInfo.isValid ?
-				'enableButton' :
-				'disableButton',
-			'next'
-		);
-	};
-
-
-
 	/* Selection
 	-----------------------------------------------------------------------------*/
 
 
 	// this public method receives start/end dates in any format, with any timezone
 	function select(zonedStartInput, zonedEndInput) {
-		currentView.select(
+		t.view.select(
 			t.buildSelectSpan.apply(t, arguments)
 		);
 	}
 
 
 	function unselect() { // safe to be called before renderView
-		if (currentView) {
-			currentView.unselect();
+		if (t.view) {
+			t.view.unselect();
 		}
 	}
 
@@ -786,33 +427,6 @@ function Calendar_constructor(element, overrides) {
 
 
 
-	/* Height "Freezing"
-	-----------------------------------------------------------------------------*/
-
-
-	t.freezeContentHeight = freezeContentHeight;
-	t.thawContentHeight = thawContentHeight;
-
-
-	function freezeContentHeight() {
-		content.css({
-			width: '100%',
-			height: content.height(),
-			overflow: 'hidden'
-		});
-	}
-
-
-	function thawContentHeight() {
-		content.css({
-			width: '',
-			height: '',
-			overflow: ''
-		});
-	}
-
-
-
 	/* Misc
 	-----------------------------------------------------------------------------*/
 
@@ -823,7 +437,7 @@ function Calendar_constructor(element, overrides) {
 
 
 	function getView() {
-		return currentView;
+		return t.view;
 	}
 
 

+ 289 - 0
src/Calendar.render.js

@@ -0,0 +1,289 @@
+
+Calendar.mixin({
+
+	// todo: populate
+	el: null,
+	contentEl: null,
+
+	windowResizeProxy: null,
+	ignoreWindowResize: 0,
+	suggestedViewHeight: null,
+
+
+	render: function() {
+		if (!this.contentEl) {
+			this.initialRender();
+		}
+		else if (this.elementVisible()) {
+			// mainly for the public API
+			this.calcSize();
+			this.renderView();
+		}
+	},
+
+
+	initialRender: function() {
+		var _this = this;
+		var el = this.el;
+
+		el.addClass('fc');
+
+		// event delegation for nav links
+		el.on('click.fc', 'a[data-goto]', function(ev) {
+			var anchorEl = $(this);
+			var gotoOptions = anchorEl.data('goto'); // will automatically parse JSON
+			var date = _this.moment(gotoOptions.date);
+			var viewType = gotoOptions.type;
+
+			// property like "navLinkDayClick". might be a string or a function
+			var customAction = this.view.opt('navLink' + capitaliseFirstLetter(viewType) + 'Click');
+
+			if (typeof customAction === 'function') {
+				customAction(date, ev);
+			}
+			else {
+				if (typeof customAction === 'string') {
+					viewType = customAction;
+				}
+				_this.zoomTo(date, viewType);
+			}
+		});
+
+		// called immediately, and upon option change
+		this.bindOption('theme', function(theme) {
+			el.toggleClass('ui-widget', theme);
+			el.toggleClass('fc-unthemed', !theme);
+		});
+
+		// called immediately, and upon option change.
+		// HACK: locale often affects isRTL, so we explicitly listen to that too.
+		this.bindOptions([ 'isRTL', 'locale' ], function(isRTL) {
+			el.toggleClass('fc-ltr', !isRTL);
+			el.toggleClass('fc-rtl', isRTL);
+		});
+
+		this.contentEl = $("<div class='fc-view-container'/>").prependTo(el);
+
+		this.initToolbars();
+		this.renderHeader();
+		this.renderFooter();
+		this.renderView(this.options.defaultView);
+
+		if (this.options.handleWindowResize) {
+			$(window).resize(
+				this.windowResizeProxy = debounce( // prevents rapid calls
+					this.windowResize.bind(this),
+					this.options.windowResizeDelay
+				)
+			);
+		}
+	},
+
+
+	destroy: function() {
+
+		if (this.view) {
+			this.view.removeElement();
+
+			// NOTE: don't null-out this.view in case API methods are called after destroy.
+			// It is still the "current" view, just not rendered.
+		}
+
+		this.toolbarsManager.proxyCall('removeElement');
+		this.contentEl.remove();
+		this.el.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');
+
+		this.el.off('.fc'); // unbind nav link handlers
+
+		if (this.windowResizeProxy) {
+			$(window).unbind('resize', this.windowResizeProxy);
+			this.windowResizeProxy = null;
+		}
+
+		GlobalEmitter.unneeded();
+	},
+
+
+	elementVisible: function() {
+		return this.el.is(':visible');
+	},
+
+
+
+	// View Rendering
+	// -----------------------------------------------------------------------------------
+
+
+	// 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.
+	// Accepts an optional scroll state to restore to.
+	renderView: function(viewType, forcedScroll) {
+
+		this.ignoreWindowResize++;
+
+		var needsClearView = this.view && viewType && this.view.type !== viewType;
+
+		// if viewType is changing, remove the old view's rendering
+		if (needsClearView) {
+			this.freezeContentHeight(); // prevent a scroll jump when view element is removed
+			this.clearView();
+		}
+
+		// if viewType changed, or the view was never created, create a fresh view
+		if (!this.view && viewType) {
+			this.view =
+				this.viewsByType[viewType] ||
+				(this.viewsByType[viewType] = this.instantiateView(viewType));
+
+			this.view.setElement(
+				$("<div class='fc-view fc-" + viewType + "-view' />").appendTo(this.contentEl)
+			);
+			this.toolbarsManager.proxyCall('activateButton', viewType);
+		}
+
+		if (this.view) {
+
+			if (forcedScroll) {
+				this.view.addForcedScroll(forcedScroll);
+			}
+
+			if (this.elementVisible()) {
+				this.currentDate = this.view.setDate(this.currentDate);
+			}
+		}
+
+		if (needsClearView) {
+			this.thawContentHeight();
+		}
+
+		this.ignoreWindowResize--;
+	},
+
+
+	// Unrenders the current view and reflects this change in the Header.
+	// Unregsiters the `view`, but does not remove from viewByType hash.
+	clearView: function() {
+		this.toolbarsManager.proxyCall('deactivateButton', this.view.type);
+		this.view.removeElement();
+		this.view = null;
+	},
+
+
+	// Destroys the view, including the view object. Then, re-instantiates it and renders it.
+	// Maintains the same scroll state.
+	// TODO: maintain any other user-manipulated state.
+	reinitView: function() {
+		this.ignoreWindowResize++;
+		this.freezeContentHeight();
+
+		var viewType = this.view.type;
+		var scrollState = this.view.queryScroll();
+		this.clearView();
+		this.calcSize();
+		this.renderView(viewType, scrollState);
+
+		this.thawContentHeight();
+		this.ignoreWindowResize--;
+	},
+
+
+	// Resizing
+	// -----------------------------------------------------------------------------------
+
+
+	getSuggestedViewHeight: function() {
+		if (this.suggestedViewHeight === null) {
+			this.calcSize();
+		}
+		return this.suggestedViewHeight;
+	},
+
+
+	isHeightAuto: function() {
+		return this.options.contentHeight === 'auto' || this.options.height === 'auto';
+	},
+
+
+	updateSize: function(shouldRecalc) {
+		if (this.elementVisible()) {
+
+			if (shouldRecalc) {
+				this._calcSize();
+			}
+
+			this.ignoreWindowResize++;
+			this.view.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto()
+			this.ignoreWindowResize--;
+
+			return true; // signal success
+		}
+	},
+
+
+	calcSize: function() {
+		if (this.elementVisible()) {
+			this._calcSize();
+		}
+	},
+
+
+	_calcSize: function() { // assumes elementVisible
+		var contentHeightInput = this.options.contentHeight;
+		var heightInput = this.options.height;
+
+		if (typeof contentHeightInput === 'number') { // exists and not 'auto'
+			this.suggestedViewHeight = contentHeightInput;
+		}
+		else if (typeof contentHeightInput === 'function') { // exists and is a function
+			this.suggestedViewHeight = this.contentHeightInput();
+		}
+		else if (typeof heightInput === 'number') { // exists and not 'auto'
+			this.suggestedViewHeight = heightInput - this.queryToolbarsHeight();
+		}
+		else if (typeof heightInput === 'function') { // exists and is a function
+			this.suggestedViewHeight = this.heightInput() - this.queryToolbarsHeight();
+		}
+		else if (heightInput === 'parent') { // set to height of parent element
+			this.suggestedViewHeight = this.el.parent().height() - this.queryToolbarsHeight();
+		}
+		else {
+			this.suggestedViewHeight = Math.round(this.contentEl.width() / Math.max(this.options.aspectRatio, .5));
+		}
+	},
+
+
+	windowResize: function(ev) {
+		if (
+			!this.ignoreWindowResize &&
+			ev.target === window && // so we don't process jqui "resize" events that have bubbled up
+			this.view.renderRange // view has already been rendered
+		) {
+			if (this.updateSize(true)) {
+				this.view.publiclyTrigger('windowResize', this.el[0]);
+			}
+		}
+	},
+
+
+	/* Height "Freezing"
+	-----------------------------------------------------------------------------*/
+
+
+	freezeContentHeight: function() {
+		this.contentEl.css({
+			width: '100%',
+			height: this.contentEl.height(),
+			overflow: 'hidden'
+		});
+	},
+
+
+	thawContentHeight: function() {
+		this.contentEl.css({
+			width: '',
+			height: '',
+			overflow: ''
+		});
+	}
+
+});

+ 100 - 0
src/Calendar.toolbar.js

@@ -0,0 +1,100 @@
+
+Calendar.mixin({
+
+	header: null,
+	footer: null,
+	toolbarsManager: null,
+
+
+	initToolbars: function() {
+		this.header = new Toolbar(this, this.computeHeaderOptions());
+		this.footer = new Toolbar(this, this.computeFooterOptions());
+		this.toolbarsManager = new Iterator([ this.header, this.footer ]);
+	},
+
+
+	computeHeaderOptions: function() {
+		return {
+			extraClasses: 'fc-header-toolbar',
+			layout: this.options.header
+		};
+	},
+
+
+	computeFooterOptions: function() {
+		return {
+			extraClasses: 'fc-footer-toolbar',
+			layout: this.options.footer
+		};
+	},
+
+
+	// can be called repeatedly and Header will rerender
+	renderHeader: function() {
+		var header = this.header;
+
+		header.setToolbarOptions(this.computeHeaderOptions());
+		header.render();
+
+		if (header.el) {
+			this.el.prepend(header.el);
+		}
+	},
+
+
+	// can be called repeatedly and Footer will rerender
+	renderFooter: function() {
+		var footer = this.footer;
+
+		footer.setToolbarOptions(this.computeFooterOptions());
+		footer.render();
+
+		if (footer.el) {
+			this.el.append(footer.el);
+		}
+	},
+
+
+	setToolbarsTitle: function(title) {
+		this.toolbarsManager.proxyCall('updateTitle', title);
+	},
+
+
+	updateToolbarButtons: function() {
+		var now = this.getNow();
+		var view = this.view;
+		var todayInfo = view.buildDateProfile(now);
+		var prevInfo = view.buildPrevDateProfile(this.currentDate);
+		var nextInfo = view.buildNextDateProfile(this.currentDate);
+
+		this.toolbarsManager.proxyCall(
+			(todayInfo.isValid && !isDateWithinRange(now, view.currentRange)) ?
+				'enableButton' :
+				'disableButton',
+			'today'
+		);
+
+		this.toolbarsManager.proxyCall(
+			prevInfo.isValid ?
+				'enableButton' :
+				'disableButton',
+			'prev'
+		);
+
+		this.toolbarsManager.proxyCall(
+			nextInfo.isValid ?
+				'enableButton' :
+				'disableButton',
+			'next'
+		);
+	},
+
+
+	queryToolbarsHeight: function() {
+		return this.toolbarsManager.items.reduce(function(accumulator, toolbar) {
+			var toolbarHeight = toolbar.el ? toolbar.el.outerHeight(true) : 0; // includes margin
+			return accumulator + toolbarHeight;
+		}, 0);
+	}
+
+});