소스 검색

Merge branch 'master' of https://github.com/axelduch/fullcalendar into next-release

Adam Shaw 9 년 전
부모
커밋
a1d1499c35

+ 2 - 0
src.json

@@ -25,6 +25,8 @@
     "common/TimeGrid.events.js",
     "common/TimeGrid.events.js",
     "common/View.js",
     "common/View.js",
     "common/Scroller.js",
     "common/Scroller.js",
+    "common/Iterator.js",
+    "Toolbar.js",
     "Calendar.js",
     "Calendar.js",
     "Calendar.options.js",
     "Calendar.options.js",
     "defaults.js",
     "defaults.js",

+ 123 - 77
src/Calendar.js

@@ -9,6 +9,7 @@ var Calendar = FC.Calendar = Class.extend({
 	viewSpecCache: null, // cache of view definitions
 	viewSpecCache: null, // cache of view definitions
 	view: null, // current View object
 	view: null, // current View object
 	header: null,
 	header: null,
+	footer: null,
 	loadingLevel: 0, // number of simultaneous loading tasks
 	loadingLevel: 0, // number of simultaneous loading tasks
 
 
 
 
@@ -483,7 +484,7 @@ function Calendar_constructor(element, overrides) {
 	};
 	};
 
 
 
 
-	
+
 	// Imports
 	// Imports
 	// -----------------------------------------------------------------------------------
 	// -----------------------------------------------------------------------------------
 
 
@@ -500,7 +501,9 @@ function Calendar_constructor(element, overrides) {
 
 
 
 
 	var _element = element[0];
 	var _element = element[0];
+	var toolbarsManager;
 	var header;
 	var header;
+	var footer;
 	var content;
 	var content;
 	var tm; // for making theme classes
 	var tm; // for making theme classes
 	var currentView; // NOTE: keep this in sync with this.view
 	var currentView; // NOTE: keep this in sync with this.view
@@ -510,9 +513,9 @@ function Calendar_constructor(element, overrides) {
 	var ignoreWindowResize = 0;
 	var ignoreWindowResize = 0;
 	var events = [];
 	var events = [];
 	var date; // unzoned
 	var date; // unzoned
-	
-	
-	
+
+
+
 	// Main Rendering
 	// Main Rendering
 	// -----------------------------------------------------------------------------------
 	// -----------------------------------------------------------------------------------
 
 
@@ -524,8 +527,8 @@ function Calendar_constructor(element, overrides) {
 	else {
 	else {
 		date = t.getNow(); // getNow already returns unzoned
 		date = t.getNow(); // getNow already returns unzoned
 	}
 	}
-	
-	
+
+
 	function render() {
 	function render() {
 		if (!content) {
 		if (!content) {
 			initialRender();
 			initialRender();
@@ -536,8 +539,8 @@ function Calendar_constructor(element, overrides) {
 			renderView();
 			renderView();
 		}
 		}
 	}
 	}
-	
-	
+
+
 	function initialRender() {
 	function initialRender() {
 		element.addClass('fc');
 		element.addClass('fc');
 
 
@@ -578,9 +581,14 @@ function Calendar_constructor(element, overrides) {
 
 
 		content = $("<div class='fc-view-container'/>").prependTo(element);
 		content = $("<div class='fc-view-container'/>").prependTo(element);
 
 
-		header = t.header = new Header(t);
-		renderHeader();
+		var toolbars = buildToolbars();
+		toolbarsManager = new Iterator(toolbars);
+
+		header = t.header = toolbars[0];
+		footer = t.footer = toolbars[1];
 
 
+		renderHeader();
+		renderFooter();
 		renderView(t.options.defaultView);
 		renderView(t.options.defaultView);
 
 
 		if (t.options.handleWindowResize) {
 		if (t.options.handleWindowResize) {
@@ -590,15 +598,50 @@ function Calendar_constructor(element, overrides) {
 	}
 	}
 
 
 
 
+	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
 	// can be called repeatedly and Header will rerender
 	function renderHeader() {
 	function renderHeader() {
+		header.setToolbarOptions(computeHeaderOptions());
 		header.render();
 		header.render();
 		if (header.el) {
 		if (header.el) {
 			element.prepend(header.el);
 			element.prepend(header.el);
 		}
 		}
 	}
 	}
-	
-	
+
+
+	// can be called repeatedly and Footer will rerender
+	function renderFooter() {
+		footer.setToolbarOptions(computeFooterOptions());
+		footer.render();
+		if (footer.el) {
+			element.append(footer.el);
+		}
+	}
+
+
 	function destroy() {
 	function destroy() {
 
 
 		if (currentView) {
 		if (currentView) {
@@ -608,7 +651,7 @@ function Calendar_constructor(element, overrides) {
 			// It is still the "current" view, just not rendered.
 			// It is still the "current" view, just not rendered.
 		}
 		}
 
 
-		header.removeElement();
+		toolbarsManager.proxyCall('removeElement');
 		content.remove();
 		content.remove();
 		element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');
 		element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');
 
 
@@ -618,13 +661,13 @@ function Calendar_constructor(element, overrides) {
 			$(window).unbind('resize', windowResizeProxy);
 			$(window).unbind('resize', windowResizeProxy);
 		}
 		}
 	}
 	}
-	
-	
+
+
 	function elementVisible() {
 	function elementVisible() {
 		return element.is(':visible');
 		return element.is(':visible');
 	}
 	}
-	
-	
+
+
 
 
 	// View Rendering
 	// View Rendering
 	// -----------------------------------------------------------------------------------
 	// -----------------------------------------------------------------------------------
@@ -651,7 +694,7 @@ function Calendar_constructor(element, overrides) {
 			currentView.setElement(
 			currentView.setElement(
 				$("<div class='fc-view fc-" + viewType + "-view' />").appendTo(content)
 				$("<div class='fc-view fc-" + viewType + "-view' />").appendTo(content)
 			);
 			);
-			header.activateButton(viewType);
+			toolbarsManager.proxyCall('activateButton', viewType);
 		}
 		}
 
 
 		if (currentView) {
 		if (currentView) {
@@ -673,8 +716,8 @@ function Calendar_constructor(element, overrides) {
 					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
-					updateHeaderTitle();
-					updateTodayButton();
+					updateToolbarsTitle();
+					updateToolbarsTodayButton();
 
 
 					getAndRenderEvents();
 					getAndRenderEvents();
 				}
 				}
@@ -689,7 +732,7 @@ function Calendar_constructor(element, overrides) {
 	// Unrenders the current view and reflects this change in the Header.
 	// Unrenders the current view and reflects this change in the Header.
 	// Unregsiters the `currentView`, but does not remove from viewByType hash.
 	// Unregsiters the `currentView`, but does not remove from viewByType hash.
 	function clearView() {
 	function clearView() {
-		header.deactivateButton(currentView.type);
+		toolbarsManager.proxyCall('deactivateButton', currentView.type);
 		currentView.removeElement();
 		currentView.removeElement();
 		currentView = t.view = null;
 		currentView = t.view = null;
 	}
 	}
@@ -711,7 +754,7 @@ function Calendar_constructor(element, overrides) {
 		ignoreWindowResize--;
 		ignoreWindowResize--;
 	}
 	}
 
 
-	
+
 
 
 	// Resizing
 	// Resizing
 	// -----------------------------------------------------------------------------------
 	// -----------------------------------------------------------------------------------
@@ -728,8 +771,8 @@ function Calendar_constructor(element, overrides) {
 	t.isHeightAuto = function() {
 	t.isHeightAuto = function() {
 		return t.options.contentHeight === 'auto' || t.options.height === 'auto';
 		return t.options.contentHeight === 'auto' || t.options.height === 'auto';
 	};
 	};
-	
-	
+
+
 	function updateSize(shouldRecalc) {
 	function updateSize(shouldRecalc) {
 		if (elementVisible()) {
 		if (elementVisible()) {
 
 
@@ -751,8 +794,8 @@ function Calendar_constructor(element, overrides) {
 			_calcSize();
 			_calcSize();
 		}
 		}
 	}
 	}
-	
-	
+
+
 	function _calcSize() { // assumes elementVisible
 	function _calcSize() { // assumes elementVisible
 		var contentHeightInput = t.options.contentHeight;
 		var contentHeightInput = t.options.contentHeight;
 		var heightInput = t.options.height;
 		var heightInput = t.options.height;
@@ -764,13 +807,13 @@ function Calendar_constructor(element, overrides) {
 			suggestedViewHeight = contentHeightInput();
 			suggestedViewHeight = contentHeightInput();
 		}
 		}
 		else if (typeof heightInput === 'number') { // exists and not 'auto'
 		else if (typeof heightInput === 'number') { // exists and not 'auto'
-			suggestedViewHeight = heightInput - queryHeaderHeight();
+			suggestedViewHeight = heightInput - queryToolbarsHeight();
 		}
 		}
 		else if (typeof heightInput === 'function') { // exists and is a function
 		else if (typeof heightInput === 'function') { // exists and is a function
-			suggestedViewHeight = heightInput() - queryHeaderHeight();
+			suggestedViewHeight = heightInput() - queryToolbarsHeight();
 		}
 		}
 		else if (heightInput === 'parent') { // set to height of parent element
 		else if (heightInput === 'parent') { // set to height of parent element
-			suggestedViewHeight = element.parent().height() - queryHeaderHeight();
+			suggestedViewHeight = element.parent().height() - queryToolbarsHeight();
 		}
 		}
 		else {
 		else {
 			suggestedViewHeight = Math.round(content.width() / Math.max(t.options.aspectRatio, .5));
 			suggestedViewHeight = Math.round(content.width() / Math.max(t.options.aspectRatio, .5));
@@ -778,11 +821,14 @@ function Calendar_constructor(element, overrides) {
 	}
 	}
 
 
 
 
-	function queryHeaderHeight() {
-		return header.el ? header.el.outerHeight(true) : 0; // includes margin
+	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) {
 	function windowResize(ev) {
 		if (
 		if (
 			!ignoreWindowResize &&
 			!ignoreWindowResize &&
@@ -794,9 +840,9 @@ function Calendar_constructor(element, overrides) {
 			}
 			}
 		}
 		}
 	}
 	}
-	
-	
-	
+
+
+
 	/* Event Fetching/Rendering
 	/* Event Fetching/Rendering
 	-----------------------------------------------------------------------------*/
 	-----------------------------------------------------------------------------*/
 	// TODO: going forward, most of this stuff should be directly handled by the view
 	// TODO: going forward, most of this stuff should be directly handled by the view
@@ -820,7 +866,7 @@ function Calendar_constructor(element, overrides) {
 			unfreezeContentHeight();
 			unfreezeContentHeight();
 		}
 		}
 	}
 	}
-	
+
 
 
 	function getAndRenderEvents() {
 	function getAndRenderEvents() {
 		if (!t.options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) {
 		if (!t.options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) {
@@ -838,7 +884,7 @@ function Calendar_constructor(element, overrides) {
 			// ... which will call renderEvents
 			// ... which will call renderEvents
 	}
 	}
 
 
-	
+
 	// called when event data arrives
 	// called when event data arrives
 	function reportEvents(_events) {
 	function reportEvents(_events) {
 		events = _events;
 		events = _events;
@@ -853,31 +899,30 @@ function Calendar_constructor(element, overrides) {
 
 
 
 
 
 
-	/* Header Updating
+	/* Toolbars Updating
 	-----------------------------------------------------------------------------*/
 	-----------------------------------------------------------------------------*/
 
 
 
 
-	function updateHeaderTitle() {
-		header.updateTitle(currentView.title);
+	function updateToolbarsTitle() {
+		toolbarsManager.proxyCall('updateTitle', currentView.title);
 	}
 	}
 
 
 
 
-	function updateTodayButton() {
+	function updateToolbarsTodayButton() {
 		var now = t.getNow();
 		var now = t.getNow();
-
 		if (now >= currentView.intervalStart && now < currentView.intervalEnd) {
 		if (now >= currentView.intervalStart && now < currentView.intervalEnd) {
-			header.disableButton('today');
+			toolbarsManager.proxyCall('disableButton', 'today');
 		}
 		}
 		else {
 		else {
-			header.enableButton('today');
+			toolbarsManager.proxyCall('enableButton', 'today');
 		}
 		}
 	}
 	}
-	
+
 
 
 
 
 	/* Selection
 	/* Selection
 	-----------------------------------------------------------------------------*/
 	-----------------------------------------------------------------------------*/
-	
+
 
 
 	// this public method receives start/end dates in any format, with any timezone
 	// this public method receives start/end dates in any format, with any timezone
 	function select(zonedStartInput, zonedEndInput) {
 	function select(zonedStartInput, zonedEndInput) {
@@ -885,56 +930,56 @@ function Calendar_constructor(element, overrides) {
 			t.buildSelectSpan.apply(t, arguments)
 			t.buildSelectSpan.apply(t, arguments)
 		);
 		);
 	}
 	}
-	
+
 
 
 	function unselect() { // safe to be called before renderView
 	function unselect() { // safe to be called before renderView
 		if (currentView) {
 		if (currentView) {
 			currentView.unselect();
 			currentView.unselect();
 		}
 		}
 	}
 	}
-	
-	
-	
+
+
+
 	/* Date
 	/* Date
 	-----------------------------------------------------------------------------*/
 	-----------------------------------------------------------------------------*/
-	
-	
+
+
 	function prev() {
 	function prev() {
 		date = currentView.computePrevDate(date);
 		date = currentView.computePrevDate(date);
 		renderView();
 		renderView();
 	}
 	}
-	
-	
+
+
 	function next() {
 	function next() {
 		date = currentView.computeNextDate(date);
 		date = currentView.computeNextDate(date);
 		renderView();
 		renderView();
 	}
 	}
-	
-	
+
+
 	function prevYear() {
 	function prevYear() {
 		date.add(-1, 'years');
 		date.add(-1, 'years');
 		renderView();
 		renderView();
 	}
 	}
-	
-	
+
+
 	function nextYear() {
 	function nextYear() {
 		date.add(1, 'years');
 		date.add(1, 'years');
 		renderView();
 		renderView();
 	}
 	}
-	
-	
+
+
 	function today() {
 	function today() {
 		date = t.getNow();
 		date = t.getNow();
 		renderView();
 		renderView();
 	}
 	}
-	
-	
+
+
 	function gotoDate(zonedDateInput) {
 	function gotoDate(zonedDateInput) {
 		date = t.moment(zonedDateInput).stripZone();
 		date = t.moment(zonedDateInput).stripZone();
 		renderView();
 		renderView();
 	}
 	}
-	
-	
+
+
 	function incrementDate(delta) {
 	function incrementDate(delta) {
 		date.add(moment.duration(delta));
 		date.add(moment.duration(delta));
 		renderView();
 		renderView();
@@ -952,8 +997,8 @@ function Calendar_constructor(element, overrides) {
 		date = newDate.clone();
 		date = newDate.clone();
 		renderView(spec ? spec.type : null);
 		renderView(spec ? spec.type : null);
 	}
 	}
-	
-	
+
+
 	// for external API
 	// for external API
 	function getDate() {
 	function getDate() {
 		return t.applyTimezone(date); // infuse the calendar's timezone
 		return t.applyTimezone(date); // infuse the calendar's timezone
@@ -985,23 +1030,23 @@ function Calendar_constructor(element, overrides) {
 			overflow: ''
 			overflow: ''
 		});
 		});
 	}
 	}
-	
-	
-	
+
+
+
 	/* Misc
 	/* Misc
 	-----------------------------------------------------------------------------*/
 	-----------------------------------------------------------------------------*/
-	
+
 
 
 	function getCalendar() {
 	function getCalendar() {
 		return t;
 		return t;
 	}
 	}
 
 
-	
+
 	function getView() {
 	function getView() {
 		return currentView;
 		return currentView;
 	}
 	}
-	
-	
+
+
 	function option(name, value) {
 	function option(name, value) {
 		var newOptionHash;
 		var newOptionHash;
 
 
@@ -1062,13 +1107,14 @@ function Calendar_constructor(element, overrides) {
 			}
 			}
 		}
 		}
 
 
-		// catch-all. rerender the header and rebuild/rerender the current view
+		// catch-all. rerender the header and footer and rebuild/rerender the current view
 		renderHeader();
 		renderHeader();
+		renderFooter();
 		viewsByType = {}; // even non-current views will be affected by this option change. do before rerender
 		viewsByType = {}; // even non-current views will be affected by this option change. do before rerender
 		reinitView();
 		reinitView();
 	}
 	}
-	
-	
+
+
 	function trigger(name, thisObj) { // overrides the Emitter's trigger method :(
 	function trigger(name, thisObj) { // overrides the Emitter's trigger method :(
 		var args = Array.prototype.slice.call(arguments, 2);
 		var args = Array.prototype.slice.call(arguments, 2);
 
 

+ 32 - 31
src/Header.js → src/Toolbar.js

@@ -1,12 +1,12 @@
 
 
-/* Top toolbar area with buttons and title
+/* Toolbar with buttons and title
 ----------------------------------------------------------------------------------------------------------------------*/
 ----------------------------------------------------------------------------------------------------------------------*/
-// TODO: rename all header-related things to "toolbar"
 
 
-function Header(calendar) {
+function Toolbar(calendar, toolbarOptions) {
 	var t = this;
 	var t = this;
-	
+
 	// exports
 	// exports
+	t.setToolbarOptions = setToolbarOptions;
 	t.render = render;
 	t.render = render;
 	t.removeElement = removeElement;
 	t.removeElement = removeElement;
 	t.updateTitle = updateTitle;
 	t.updateTitle = updateTitle;
@@ -16,23 +16,25 @@ function Header(calendar) {
 	t.enableButton = enableButton;
 	t.enableButton = enableButton;
 	t.getViewsWithButtons = getViewsWithButtons;
 	t.getViewsWithButtons = getViewsWithButtons;
 	t.el = null; // mirrors local `el`
 	t.el = null; // mirrors local `el`
-	
+
 	// locals
 	// locals
 	var el;
 	var el;
 	var viewsWithButtons = [];
 	var viewsWithButtons = [];
 	var tm;
 	var tm;
 
 
+	function setToolbarOptions(newToolbarOptions) {
+		toolbarOptions = newToolbarOptions;
+	}
 
 
 	// can be called repeatedly and will rerender
 	// can be called repeatedly and will rerender
 	function render() {
 	function render() {
-		var options = calendar.options;
-		var sections = options.header;
+		var sections = toolbarOptions.layout;
 
 
-		tm = options.theme ? 'ui' : 'fc';
+		tm = calendar.options.theme ? 'ui' : 'fc';
 
 
 		if (sections) {
 		if (sections) {
 			if (!el) {
 			if (!el) {
-				el = this.el = $("<div class='fc-toolbar'/>");
+				el = this.el = $("<div class='fc-toolbar "+ toolbarOptions.extraClasses + "'/>");
 			}
 			}
 			else {
 			else {
 				el.empty();
 				el.empty();
@@ -46,20 +48,19 @@ function Header(calendar) {
 			removeElement();
 			removeElement();
 		}
 		}
 	}
 	}
-	
-	
+
+
 	function removeElement() {
 	function removeElement() {
 		if (el) {
 		if (el) {
 			el.remove();
 			el.remove();
 			el = t.el = null;
 			el = t.el = null;
 		}
 		}
 	}
 	}
-	
-	
+
+
 	function renderSection(position) {
 	function renderSection(position) {
 		var sectionEl = $('<div class="fc-' + position + '"/>');
 		var sectionEl = $('<div class="fc-' + position + '"/>');
-		var options = calendar.options;
-		var buttonStr = options.header[position];
+		var buttonStr = toolbarOptions.layout[position];
 
 
 		if (buttonStr) {
 		if (buttonStr) {
 			$.each(buttonStr.split(' '), function(i) {
 			$.each(buttonStr.split(' '), function(i) {
@@ -84,7 +85,7 @@ function Header(calendar) {
 						isOnlyButtons = false;
 						isOnlyButtons = false;
 					}
 					}
 					else {
 					else {
-						if ((customButtonProps = (options.customButtons || {})[buttonName])) {
+						if ((customButtonProps = (calendar.options.customButtons || {})[buttonName])) {
 							buttonClick = function(ev) {
 							buttonClick = function(ev) {
 								if (customButtonProps.click) {
 								if (customButtonProps.click) {
 									customButtonProps.click.call(button[0], ev);
 									customButtonProps.click.call(button[0], ev);
@@ -106,7 +107,7 @@ function Header(calendar) {
 								calendar[buttonName]();
 								calendar[buttonName]();
 							};
 							};
 							overrideText = (calendar.overrides.buttonText || {})[buttonName];
 							overrideText = (calendar.overrides.buttonText || {})[buttonName];
-							defaultText = options.buttonText[buttonName]; // everything else is considered default
+							defaultText = calendar.options.buttonText[buttonName]; // everything else is considered default
 						}
 						}
 
 
 						if (buttonClick) {
 						if (buttonClick) {
@@ -114,20 +115,20 @@ function Header(calendar) {
 							themeIcon =
 							themeIcon =
 								customButtonProps ?
 								customButtonProps ?
 									customButtonProps.themeIcon :
 									customButtonProps.themeIcon :
-									options.themeButtonIcons[buttonName];
+									calendar.options.themeButtonIcons[buttonName];
 
 
 							normalIcon =
 							normalIcon =
 								customButtonProps ?
 								customButtonProps ?
 									customButtonProps.icon :
 									customButtonProps.icon :
-									options.buttonIcons[buttonName];
+									calendar.options.buttonIcons[buttonName];
 
 
 							if (overrideText) {
 							if (overrideText) {
 								innerHtml = htmlEscape(overrideText);
 								innerHtml = htmlEscape(overrideText);
 							}
 							}
-							else if (themeIcon && options.theme) {
+							else if (themeIcon && calendar.options.theme) {
 								innerHtml = "<span class='ui-icon ui-icon-" + themeIcon + "'></span>";
 								innerHtml = "<span class='ui-icon ui-icon-" + themeIcon + "'></span>";
 							}
 							}
-							else if (normalIcon && !options.theme) {
+							else if (normalIcon && !calendar.options.theme) {
 								innerHtml = "<span class='fc-icon fc-icon-" + normalIcon + "'></span>";
 								innerHtml = "<span class='fc-icon fc-icon-" + normalIcon + "'></span>";
 							}
 							}
 							else {
 							else {
@@ -217,31 +218,31 @@ function Header(calendar) {
 
 
 		return sectionEl;
 		return sectionEl;
 	}
 	}
-	
-	
+
+
 	function updateTitle(text) {
 	function updateTitle(text) {
 		if (el) {
 		if (el) {
 			el.find('h2').text(text);
 			el.find('h2').text(text);
 		}
 		}
 	}
 	}
-	
-	
+
+
 	function activateButton(buttonName) {
 	function activateButton(buttonName) {
 		if (el) {
 		if (el) {
 			el.find('.fc-' + buttonName + '-button')
 			el.find('.fc-' + buttonName + '-button')
 				.addClass(tm + '-state-active');
 				.addClass(tm + '-state-active');
 		}
 		}
 	}
 	}
-	
-	
+
+
 	function deactivateButton(buttonName) {
 	function deactivateButton(buttonName) {
 		if (el) {
 		if (el) {
 			el.find('.fc-' + buttonName + '-button')
 			el.find('.fc-' + buttonName + '-button')
 				.removeClass(tm + '-state-active');
 				.removeClass(tm + '-state-active');
 		}
 		}
 	}
 	}
-	
-	
+
+
 	function disableButton(buttonName) {
 	function disableButton(buttonName) {
 		if (el) {
 		if (el) {
 			el.find('.fc-' + buttonName + '-button')
 			el.find('.fc-' + buttonName + '-button')
@@ -249,8 +250,8 @@ function Header(calendar) {
 				.addClass(tm + '-state-disabled');
 				.addClass(tm + '-state-disabled');
 		}
 		}
 	}
 	}
-	
-	
+
+
 	function enableButton(buttonName) {
 	function enableButton(buttonName) {
 		if (el) {
 		if (el) {
 			el.find('.fc-' + buttonName + '-button')
 			el.find('.fc-' + buttonName + '-button')

+ 1 - 1
src/common/DayGrid.limit.js

@@ -277,7 +277,7 @@ DayGrid.mixin({
 		var isTheme = view.opt('theme');
 		var isTheme = view.opt('theme');
 		var title = this.getCellDate(row, col).format(view.opt('dayPopoverFormat'));
 		var title = this.getCellDate(row, col).format(view.opt('dayPopoverFormat'));
 		var content = $(
 		var content = $(
-			'<div class="fc-header ' + view.widgetHeaderClass + '">' +
+			'<div class="fc-header-toolbar ' + view.widgetHeaderClass + '">' +
 				'<span class="fc-close ' +
 				'<span class="fc-close ' +
 					(isTheme ? 'ui-icon ui-icon-closethick' : 'fc-icon fc-icon-x') +
 					(isTheme ? 'ui-icon ui-icon-closethick' : 'fc-icon fc-icon-x') +
 				'"></span>' +
 				'"></span>' +

+ 16 - 0
src/common/Iterator.js

@@ -0,0 +1,16 @@
+function Iterator(items) {
+    this.items = items || [];
+}
+
+
+/* Calls a method on every item passing the arguments through */
+Iterator.prototype.proxyCall = function(methodName) {
+    var args = Array.prototype.slice.call(arguments, 1);
+    var results = [];
+
+    this.items.forEach(function(item) {
+        results.push(item[methodName].apply(item, args));
+    });
+
+    return results;
+};

+ 7 - 0
src/main.css

@@ -4,9 +4,16 @@
 
 
 .fc-toolbar {
 .fc-toolbar {
 	text-align: center;
 	text-align: center;
+}
+
+.fc-toolbar.fc-header-toolbar {
 	margin-bottom: 1em;
 	margin-bottom: 1em;
 }
 }
 
 
+.fc-toolbar.fc-footer-toolbar {
+	margin-top: 1em;
+}
+
 .fc-toolbar .fc-left {
 .fc-toolbar .fc-left {
 	float: left;
 	float: left;
 }
 }

+ 2 - 1
src/main.js

@@ -34,13 +34,14 @@ $.fn.fullCalendar = function(options) {
 			calendar.render();
 			calendar.render();
 		}
 		}
 	});
 	});
-	
+
 	return res;
 	return res;
 };
 };
 
 
 
 
 var complexOptions = [ // names of options that are objects whose properties should be combined
 var complexOptions = [ // names of options that are objects whose properties should be combined
 	'header',
 	'header',
+	'footer',
 	'buttonText',
 	'buttonText',
 	'buttonIcons',
 	'buttonIcons',
 	'themeButtonIcons'
 	'themeButtonIcons'

+ 3 - 3
tests/automated/dayPopoverFormat.js

@@ -25,19 +25,19 @@ describe('dayPopoverFormat', function() {
 	it('can be set to a custom value', function() {
 	it('can be set to a custom value', function() {
 		options.dayPopoverFormat = 'ddd, MMMM';
 		options.dayPopoverFormat = 'ddd, MMMM';
 		init();
 		init();
-		expect($('.fc-more-popover > .fc-header .fc-title')).toHaveText('Tue, July');
+		expect($('.fc-more-popover > .fc-header-toolbar .fc-title')).toHaveText('Tue, July');
 	});
 	});
 
 
 	it('is affected by the current locale when the value is default', function() {
 	it('is affected by the current locale when the value is default', function() {
 		options.locale = 'fr';
 		options.locale = 'fr';
 		init();
 		init();
-		expect($('.fc-more-popover > .fc-header .fc-title')).toHaveText('29 juillet 2014');
+		expect($('.fc-more-popover > .fc-header-toolbar .fc-title')).toHaveText('29 juillet 2014');
 	});
 	});
 
 
 	it('still maintains the same format when explicitly set, and there is a locale', function() {
 	it('still maintains the same format when explicitly set, and there is a locale', function() {
 		options.locale = 'fr';
 		options.locale = 'fr';
 		options.dayPopoverFormat = 'YYYY';
 		options.dayPopoverFormat = 'YYYY';
 		init();
 		init();
-		expect($('.fc-more-popover > .fc-header .fc-title')).toHaveText('2014');
+		expect($('.fc-more-popover > .fc-header-toolbar .fc-title')).toHaveText('2014');
 	});
 	});
 });
 });

+ 2 - 2
tests/automated/eventLimit-popover.js

@@ -189,8 +189,8 @@ describe('eventLimit popover', function() {
 	it('doesn\'t close when user clicks somewhere inside of the popover', function() {
 	it('doesn\'t close when user clicks somewhere inside of the popover', function() {
 		init();
 		init();
 		expect($('.fc-more-popover')).toBeVisible();
 		expect($('.fc-more-popover')).toBeVisible();
-		expect($('.fc-more-popover .fc-header')).toBeInDOM();
-		$('.fc-more-popover .fc-header').simulate('mousedown').simulate('click');
+		expect($('.fc-more-popover .fc-header-toolbar')).toBeInDOM();
+		$('.fc-more-popover .fc-header-toolbar').simulate('mousedown').simulate('click');
 		expect($('.fc-more-popover')).toBeVisible();
 		expect($('.fc-more-popover')).toBeVisible();
 	});
 	});
 
 

+ 61 - 0
tests/automated/footer-navigation.js

@@ -0,0 +1,61 @@
+
+describe('footer navigation', function() {
+
+	beforeEach(function() {
+		affix('#calendar');
+		var options = {
+			footer: {
+				left: 'next,prev,prevYear,nextYear today',
+				center: '',
+				right: 'title'
+			}
+		};
+		$('#calendar').fullCalendar(options);
+	});
+
+	describe('and click next', function() {
+		it('should change view to next month', function() {
+			$('#calendar').fullCalendar('gotoDate', '2010-02-01');
+			$('.fc-footer-toolbar .fc-next-button').simulate('click');
+			var newDate = $('#calendar').fullCalendar('getDate');
+			expect(newDate).toEqualMoment('2010-03-01');
+		});
+	});
+
+	describe('and click prev', function() {
+		it('should change view to prev month', function() {
+			$('#calendar').fullCalendar('gotoDate', '2010-02-01');
+			$('.fc-footer-toolbar .fc-prev-button').simulate('click');
+			var newDate = $('#calendar').fullCalendar('getDate');
+			expect(newDate).toEqualMoment('2010-01-01');
+		});
+	});
+
+	describe('and click prevYear', function() {
+		it('should change view to prev month', function() {
+			$('#calendar').fullCalendar('gotoDate', '2010-02-01');
+			$('.fc-footer-toolbar .fc-prevYear-button').simulate('click');
+			var newDate = $('#calendar').fullCalendar('getDate');
+			expect(newDate).toEqualMoment('2009-02-01');
+		});
+	});
+
+	describe('and click nextYear', function() {
+		it('should change view to prev month', function() {
+			$('#calendar').fullCalendar('gotoDate', '2010-02-01');
+			$('.fc-footer-toolbar .fc-nextYear-button').simulate('click');
+			var newDate = $('#calendar').fullCalendar('getDate');
+			expect(newDate).toEqualMoment('2011-02-01');
+		});
+	});
+
+	describe('and click today', function() {
+		it('should change view to prev month', function() {
+			$('#calendar').fullCalendar('gotoDate', '2010-02-01');
+			$('.fc-footer-toolbar .fc-today-button').simulate('click');
+			var newDate = $('#calendar').fullCalendar('getDate'); // will be ambig zone
+			newDate.local(); // assign the local timezone
+			expect(newDate).toEqualNow();
+		});
+	});
+});

+ 51 - 0
tests/automated/footer-rendering.js

@@ -0,0 +1,51 @@
+
+describe('footer rendering', function() {
+
+	beforeEach(function() {
+		affix('#calendar');
+	});
+
+	describe('when supplying footer options', function() {
+		beforeEach(function() {
+			var options = {
+				footer: {
+					left: 'next,prev',
+					center: 'prevYear today nextYear agendaView,dayView',
+					right: 'title'
+				}
+			};
+			$('#calendar').fullCalendar(options);
+		});
+		it('should append a .fc-footer-toolbar to the DOM', function() {
+			var footer = $('#calendar .fc-footer-toolbar');
+			expect(footer.length).toBe(1);
+		});
+	});
+
+	describe('when setting footer to false', function() {
+		beforeEach(function() {
+			var options = {
+				footer: false
+			};
+			$('#calendar').fullCalendar(options);
+		});
+		it('should not have footer table', function() {
+			expect($('.fc-footer-toolbar')).not.toBeInDOM();
+		});
+	});
+
+	it('allow for dynamically changing', function() {
+		var options = {
+			footer: {
+				left: 'next,prev',
+				center: 'prevYear today nextYear agendaView,dayView',
+				right: 'title'
+			}
+		};
+		$('#calendar').fullCalendar(options);
+		expect($('.fc-footer-toolbar')).toBeInDOM();
+		$('#calendar').fullCalendar('option', 'footer', false);
+		expect($('.fc-footer-toolbar')).not.toBeInDOM();
+	});
+
+});

+ 8 - 3
tests/manual/fullheight.html

@@ -9,18 +9,23 @@
 <script>
 <script>
 
 
 	$(document).ready(function() {
 	$(document).ready(function() {
-	
+
 		var date = new Date();
 		var date = new Date();
 		var d = date.getDate();
 		var d = date.getDate();
 		var m = date.getMonth();
 		var m = date.getMonth();
 		var y = date.getFullYear();
 		var y = date.getFullYear();
-		
+
 		$('#calendar').fullCalendar({
 		$('#calendar').fullCalendar({
 			header: {
 			header: {
 				left: 'prev,next today',
 				left: 'prev,next today',
 				center: 'title',
 				center: 'title',
 				right: 'month,agendaWeek,basicWeek,agendaDay,basicDay'
 				right: 'month,agendaWeek,basicWeek,agendaDay,basicDay'
 			},
 			},
+			footer: {
+				left: 'prev,next today',
+				center: 'title',
+				right: 'month,agendaWeek,basicWeek,agendaDay,basicDay'
+			},
 			editable: true,
 			editable: true,
 			events: [
 			events: [
 				{
 				{
@@ -71,7 +76,7 @@
 			fixedWeekCount: false,
 			fixedWeekCount: false,
 			height: 'parent'
 			height: 'parent'
 		});
 		});
-		
+
 	});
 	});
 
 
 </script>
 </script>