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

Merge branch 'bootstrap-themes-adam'

Adam Shaw 8 лет назад
Родитель
Сommit
d3e80d25f5

+ 179 - 0
demos/theme-bootstrap.html

@@ -0,0 +1,179 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset='utf-8' />
+<link href='../dist/fullcalendar.css' rel='stylesheet' />
+<link href='../dist/fullcalendar.print.css' rel='stylesheet' media='print' />
+<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" data-theme-element>
+<script src='../node_modules/moment/moment.js'></script>
+<script src='../node_modules/jquery/dist/jquery.js'></script>
+<script src='../dist/fullcalendar.js'></script>
+<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
+<script>
+	function bootswatchThemeUrl(name) {
+		return 'http://bootswatch.com/' + name + '/bootstrap.min.css';
+	}
+
+	function $themeButton(name, url) {
+		url = url || $('link[data-theme-element]').prop('href');
+
+		return $('<button type="button" class="btn btn-default">')
+			.text(name)
+			.on('click', function () {
+				$('link[data-theme-element]').prop('href', url);
+				$('#theme-selection-popup').modal('hide');
+				$(this).siblings().removeClass('active').end().addClass('active')
+			});
+	}
+
+	$(document).ready(function() {
+		var themeButtons = [
+			// The default theme
+			$themeButton('Default').addClass('active'),
+
+			// Some additional themes from bootswatch.com
+			$themeButton('Cerulean', bootswatchThemeUrl('cerulean')),
+			$themeButton('Cosmo', bootswatchThemeUrl('cosmo')),
+			$themeButton('Cyborg', bootswatchThemeUrl('cyborg')),
+			$themeButton('Darkly', bootswatchThemeUrl('darkly')),
+			$themeButton('Flatly', bootswatchThemeUrl('flatly')),
+			$themeButton('Journal', bootswatchThemeUrl('journal')),
+			$themeButton('Lumen', bootswatchThemeUrl('lumen')),
+			$themeButton('Paper', bootswatchThemeUrl('paper')),
+			$themeButton('Readable', bootswatchThemeUrl('readable')),
+			$themeButton('Sandstone', bootswatchThemeUrl('sandstone')),
+			$themeButton('Simplex', bootswatchThemeUrl('simplex')),
+			$themeButton('Slate', bootswatchThemeUrl('slate')),
+			$themeButton('Solar', bootswatchThemeUrl('solar')),
+			$themeButton('Spacelab', bootswatchThemeUrl('spacelab')),
+			$themeButton('Superhero', bootswatchThemeUrl('superhero')),
+			$themeButton('United', bootswatchThemeUrl('united')),
+			$themeButton('Yeti', bootswatchThemeUrl('yeti')),
+		];
+
+		for (i = 0; i < themeButtons.length; i++) {
+			$('.btn-group-vertical').append(themeButtons[i]);
+		}
+
+		$('#calendar').fullCalendar({
+			theme: 'bootstrap3',
+			//bootstrapGlyphicons: false,
+			customButtons: {
+				pickTheme: {
+					text: 'Pick Theme',
+					click: function() {
+						$('#theme-selection-popup').modal('show');
+					}
+				},
+			},
+			header: {
+				left: 'prev,next today pickTheme',
+				center: 'title',
+				right: 'month,agendaWeek,agendaDay,listMonth'
+			},
+			defaultDate: '2017-02-12',
+			navLinks: true, // can click day/week names to navigate views
+			editable: true,
+			eventLimit: true, // allow "more" link when too many events
+			events: [
+				{
+					title: 'All Day Event',
+					start: '2017-02-01'
+				},
+				{
+					title: 'Long Event',
+					start: '2017-02-07',
+					end: '2017-02-10'
+				},
+				{
+					id: 999,
+					title: 'Repeating Event',
+					start: '2017-02-09T16:00:00'
+				},
+				{
+					id: 999,
+					title: 'Repeating Event',
+					start: '2017-02-16T16:00:00'
+				},
+				{
+					title: 'Conference',
+					start: '2017-02-11',
+					end: '2017-02-13'
+				},
+				{
+					title: 'Meeting',
+					start: '2017-02-12T10:30:00',
+					end: '2017-02-12T12:30:00'
+				},
+				{
+					title: 'Lunch',
+					start: '2017-02-12T12:00:00'
+				},
+				{
+					title: 'Meeting',
+					start: '2017-02-12T14:30:00'
+				},
+				{
+					title: 'Happy Hour',
+					start: '2017-02-12T17:30:00'
+				},
+				{
+					title: 'Dinner',
+					start: '2017-02-12T20:00:00'
+				},
+				{
+					title: 'Birthday Party',
+					start: '2017-02-13T07:00:00'
+				},
+				{
+					title: 'Click for Google',
+					url: 'http://google.com/',
+					start: '2017-02-28'
+				}
+			]
+		});
+
+	});
+
+</script>
+<style>
+
+	body {
+		margin: 40px 10px;
+		padding: 0;
+		font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif;
+		font-size: 14px;
+	}
+
+	#calendar {
+		max-width: 900px;
+		margin: 0 auto;
+	}
+
+	#theme-selection-popup .btn-group-vertical {
+		width: 100%;
+	}
+
+</style>
+</head>
+<body>
+
+	<div id="theme-selection-popup" class="modal fade" tabindex="-1" role="dialog">
+		<div class="modal-dialog" role="document">
+			<div class="modal-content">
+				<div class="modal-header">
+					<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+					<h4 class="modal-title">Pick Theme</h4>
+				</div>
+				<div class="modal-body">
+					<div class="btn-group-vertical" role="group">
+					</div>
+				</div>
+			</div>
+		</div>
+	</div>
+
+	<div id='calendar'></div>
+
+</body>
+</html>

+ 8 - 0
src.json

@@ -68,6 +68,11 @@
     "models/event-source/ArrayEventSource.js",
     "models/event-source/FuncEventSource.js",
     "models/event-source/JsonFeedEventSource.js",
+    "theme/ThemeRegistry.js",
+    "theme/Theme.js",
+    "theme/StandardTheme.js",
+    "theme/JqueryUiTheme.js",
+    "theme/BootstrapTheme.js",
     "basic/BasicView.js",
     "basic/MonthView.js",
     "basic/config.js",
@@ -79,6 +84,9 @@
   ],
   "fullcalendar.css": [
     "common/common.css",
+    "common/common.standard.css",
+    "common/common.jquery-ui.css",
+    "common/common.bootstrap3.css",
     "main.css",
     "basic/basic.css",
     "agenda/agenda.css",

+ 1 - 0
src/Calendar.js

@@ -4,6 +4,7 @@ var Calendar = FC.Calendar = Class.extend(EmitterMixin, {
 	view: null, // current View object
 	viewsByType: null, // holds all instantiated view instances, current or not
 	currentDate: null, // unzoned moment. private (public API should use getDate instead)
+	theme: null,
 	loadingLevel: 0, // number of simultaneous loading tasks
 
 

+ 1 - 1
src/Calendar.options.js

@@ -49,7 +49,7 @@ Calendar.mixin({
 		var optionCnt = 0;
 		var optionName;
 
-		this.recordOptionOverrides(newOptionHash);
+		this.recordOptionOverrides(newOptionHash); // will trigger optionsModel watchers
 
 		for (optionName in newOptionHash) {
 			optionCnt++;

+ 22 - 4
src/Calendar.render.js

@@ -48,9 +48,24 @@ Calendar.mixin({
 		});
 
 		// called immediately, and upon option change
-		this.optionsModel.watch('applyingThemeClasses', [ '?theme' ], function(opts) {
-			el.toggleClass('ui-widget', opts.theme);
-			el.toggleClass('fc-unthemed', !opts.theme);
+		this.optionsModel.watch('settingTheme', [ '?theme' ], function(opts) {
+			var themeClass = ThemeRegistry.getThemeClass(opts.theme);
+			var theme = new themeClass(_this.optionsModel);
+			var widgetClass = theme.getClass('widget');
+
+			_this.theme = theme;
+
+			if (widgetClass) {
+				el.addClass(widgetClass);
+			}
+		}, function() {
+			var widgetClass = _this.theme.getClass('widget');
+
+			_this.theme = null;
+
+			if (widgetClass) {
+				el.removeClass(widgetClass);
+			}
 		});
 
 		// called immediately, and upon option change.
@@ -89,7 +104,10 @@ Calendar.mixin({
 
 		this.toolbarsManager.proxyCall('removeElement');
 		this.contentEl.remove();
-		this.el.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');
+		this.el.removeClass('fc fc-ltr fc-rtl');
+
+		// removes theme-related root className
+		this.optionsModel.unwatch('settingTheme');
 
 		this.el.off('.fc'); // unbind nav link handlers
 

+ 56 - 70
src/Toolbar.js

@@ -20,7 +20,6 @@ function Toolbar(calendar, toolbarOptions) {
 	// locals
 	var el;
 	var viewsWithButtons = [];
-	var tm;
 
 	// method to update toolbar-specific options, not calendar-wide options
 	function setToolbarOptions(newToolbarOptions) {
@@ -31,8 +30,6 @@ function Toolbar(calendar, toolbarOptions) {
 	function render() {
 		var sections = toolbarOptions.layout;
 
-		tm = calendar.opt('theme') ? 'ui' : 'fc';
-
 		if (sections) {
 			if (!el) {
 				el = this.el = $("<div class='fc-toolbar "+ toolbarOptions.extraClasses + "'/>");
@@ -60,9 +57,11 @@ function Toolbar(calendar, toolbarOptions) {
 
 
 	function renderSection(position) {
+		var theme = calendar.theme;
 		var sectionEl = $('<div class="fc-' + position + '"/>');
 		var buttonStr = toolbarOptions.layout[position];
 		var calendarCustomButtons = calendar.opt('customButtons') || {};
+		var calendarButtonTextOverrides = calendar.overrides.buttonText || {};
 		var calendarButtonText = calendar.opt('buttonText') || {};
 
 		if (buttonStr) {
@@ -75,140 +74,127 @@ function Toolbar(calendar, toolbarOptions) {
 					var customButtonProps;
 					var viewSpec;
 					var buttonClick;
-					var overrideText; // text explicitly set by calendar's constructor options. overcomes icons
-					var defaultText;
-					var themeIcon;
-					var normalIcon;
-					var innerHtml;
-					var classes;
-					var button; // the element
+					var buttonIcon; // only one of these will be set
+					var buttonText; // "
+					var buttonInnerHtml;
+					var buttonClasses;
+					var buttonEl;
 
 					if (buttonName == 'title') {
 						groupChildren = groupChildren.add($('<h2>&nbsp;</h2>')); // we always want it to take up height
 						isOnlyButtons = false;
 					}
 					else {
+
 						if ((customButtonProps = calendarCustomButtons[buttonName])) {
 							buttonClick = function(ev) {
 								if (customButtonProps.click) {
-									customButtonProps.click.call(button[0], ev);
+									customButtonProps.click.call(buttonEl[0], ev);
 								}
 							};
-							overrideText = ''; // icons will override text
-							defaultText = customButtonProps.text;
+							(buttonIcon = theme.getCustomButtonIconClass(customButtonProps)) ||
+							(buttonIcon = theme.getIconClass(buttonName)) ||
+							(buttonText = customButtonProps.text); // jshint ignore:line
 						}
 						else if ((viewSpec = calendar.getViewSpec(buttonName))) {
+							viewsWithButtons.push(buttonName);
 							buttonClick = function() {
 								calendar.changeView(buttonName);
 							};
-							viewsWithButtons.push(buttonName);
-							overrideText = viewSpec.buttonTextOverride;
-							defaultText = viewSpec.buttonTextDefault;
+							(buttonText = viewSpec.buttonTextOverride) ||
+							(buttonIcon = theme.getIconClass(buttonName)) ||
+							(buttonText = viewSpec.buttonTextDefault); // jshint ignore:line
 						}
 						else if (calendar[buttonName]) { // a calendar method
 							buttonClick = function() {
 								calendar[buttonName]();
 							};
-							overrideText = (calendar.overrides.buttonText || {})[buttonName];
-							defaultText = calendarButtonText[buttonName]; // everything else is considered default
+							(buttonText = calendarButtonTextOverrides[buttonName]) ||
+							(buttonIcon = theme.getIconClass(buttonName)) ||
+							(buttonText = calendarButtonText[buttonName]); // jshint ignore:line
+							//            ^ everything else is considered default
 						}
 
 						if (buttonClick) {
 
-							themeIcon =
-								customButtonProps ?
-									customButtonProps.themeIcon :
-									calendar.opt('themeButtonIcons')[buttonName];
-
-							normalIcon =
-								customButtonProps ?
-									customButtonProps.icon :
-									calendar.opt('buttonIcons')[buttonName];
+							buttonClasses = [
+								'fc-' + buttonName + '-button',
+								theme.getClass('button'),
+								theme.getClass('stateDefault')
+							];
 
-							if (overrideText) {
-								innerHtml = htmlEscape(overrideText);
-							}
-							else if (themeIcon && calendar.opt('theme')) {
-								innerHtml = "<span class='ui-icon ui-icon-" + themeIcon + "'></span>";
-							}
-							else if (normalIcon && !calendar.opt('theme')) {
-								innerHtml = "<span class='fc-icon fc-icon-" + normalIcon + "'></span>";
+							if (buttonText) {
+								buttonInnerHtml = htmlEscape(buttonText);
 							}
-							else {
-								innerHtml = htmlEscape(defaultText);
+							else if (buttonIcon) {
+								buttonInnerHtml = "<span class='" + buttonIcon + "'></span>";
 							}
 
-							classes = [
-								'fc-' + buttonName + '-button',
-								tm + '-button',
-								tm + '-state-default'
-							];
-
-							button = $( // type="button" so that it doesn't submit a form
-								'<button type="button" class="' + classes.join(' ') + '">' +
-									innerHtml +
+							buttonEl = $( // type="button" so that it doesn't submit a form
+								'<button type="button" class="' + buttonClasses.join(' ') + '">' +
+									buttonInnerHtml +
 								'</button>'
 								)
 								.click(function(ev) {
 									// don't process clicks for disabled buttons
-									if (!button.hasClass(tm + '-state-disabled')) {
+									if (!buttonEl.hasClass(theme.getClass('stateDisabled'))) {
 
 										buttonClick(ev);
 
 										// after the click action, if the button becomes the "active" tab, or disabled,
 										// it should never have a hover class, so remove it now.
 										if (
-											button.hasClass(tm + '-state-active') ||
-											button.hasClass(tm + '-state-disabled')
+											buttonEl.hasClass(theme.getClass('stateActive')) ||
+											buttonEl.hasClass(theme.getClass('stateDisabled'))
 										) {
-											button.removeClass(tm + '-state-hover');
+											buttonEl.removeClass(theme.getClass('stateHover'));
 										}
 									}
 								})
 								.mousedown(function() {
 									// the *down* effect (mouse pressed in).
 									// only on buttons that are not the "active" tab, or disabled
-									button
-										.not('.' + tm + '-state-active')
-										.not('.' + tm + '-state-disabled')
-										.addClass(tm + '-state-down');
+									buttonEl
+										.not('.' + theme.getClass('stateActive'))
+										.not('.' + theme.getClass('stateDisabled'))
+										.addClass(theme.getClass('stateDown'));
 								})
 								.mouseup(function() {
 									// undo the *down* effect
-									button.removeClass(tm + '-state-down');
+									buttonEl.removeClass(theme.getClass('stateDown'));
 								})
 								.hover(
 									function() {
 										// the *hover* effect.
 										// only on buttons that are not the "active" tab, or disabled
-										button
-											.not('.' + tm + '-state-active')
-											.not('.' + tm + '-state-disabled')
-											.addClass(tm + '-state-hover');
+										buttonEl
+											.not('.' + theme.getClass('stateActive'))
+											.not('.' + theme.getClass('stateDisabled'))
+											.addClass(theme.getClass('stateHover'));
 									},
 									function() {
 										// undo the *hover* effect
-										button
-											.removeClass(tm + '-state-hover')
-											.removeClass(tm + '-state-down'); // if mouseleave happens before mouseup
+										buttonEl
+											.removeClass(theme.getClass('stateHover'))
+											.removeClass(theme.getClass('stateDown')); // if mouseleave happens before mouseup
 									}
 								);
 
-							groupChildren = groupChildren.add(button);
+							groupChildren = groupChildren.add(buttonEl);
 						}
 					}
 				});
 
 				if (isOnlyButtons) {
 					groupChildren
-						.first().addClass(tm + '-corner-left').end()
-						.last().addClass(tm + '-corner-right').end();
+						.first().addClass(theme.getClass('cornerLeft')).end()
+						.last().addClass(theme.getClass('cornerRight')).end();
 				}
 
 				if (groupChildren.length > 1) {
 					groupEl = $('<div/>');
 					if (isOnlyButtons) {
-						groupEl.addClass('fc-button-group');
+						groupEl.addClass(theme.getClass('buttonGroup'));
 					}
 					groupEl.append(groupChildren);
 					sectionEl.append(groupEl);
@@ -233,7 +219,7 @@ function Toolbar(calendar, toolbarOptions) {
 	function activateButton(buttonName) {
 		if (el) {
 			el.find('.fc-' + buttonName + '-button')
-				.addClass(tm + '-state-active');
+				.addClass(calendar.theme.getClass('stateActive'));
 		}
 	}
 
@@ -241,7 +227,7 @@ function Toolbar(calendar, toolbarOptions) {
 	function deactivateButton(buttonName) {
 		if (el) {
 			el.find('.fc-' + buttonName + '-button')
-				.removeClass(tm + '-state-active');
+				.removeClass(calendar.theme.getClass('stateActive'));
 		}
 	}
 
@@ -250,7 +236,7 @@ function Toolbar(calendar, toolbarOptions) {
 		if (el) {
 			el.find('.fc-' + buttonName + '-button')
 				.prop('disabled', true)
-				.addClass(tm + '-state-disabled');
+				.addClass(calendar.theme.getClass('stateDisabled'));
 		}
 	}
 
@@ -259,7 +245,7 @@ function Toolbar(calendar, toolbarOptions) {
 		if (el) {
 			el.find('.fc-' + buttonName + '-button')
 				.prop('disabled', false)
-				.removeClass(tm + '-state-disabled');
+				.removeClass(calendar.theme.getClass('stateDisabled'));
 		}
 	}
 

+ 11 - 9
src/agenda/AgendaView.js

@@ -83,7 +83,7 @@ var AgendaView = FC.AgendaView = View.extend({
 		this.timeGrid.renderDates();
 
 		// the <hr> that sometimes displays under the time-grid
-		this.bottomRuleEl = $('<hr class="fc-divider ' + this.widgetHeaderClass + '"/>')
+		this.bottomRuleEl = $('<hr class="fc-divider ' + this.calendar.theme.getClass('widgetHeader') + '"/>')
 			.appendTo(this.timeGrid.el); // inject it into the time-grid
 
 		if (this.dayGrid) {
@@ -125,19 +125,21 @@ var AgendaView = FC.AgendaView = View.extend({
 	// Builds the HTML skeleton for the view.
 	// The day-grid and time-grid components will render inside containers defined by this HTML.
 	renderSkeletonHtml: function() {
+		var theme = this.calendar.theme;
+
 		return '' +
-			'<table>' +
+			'<table class="' + theme.getClass('tableGrid') + '">' +
 				'<thead class="fc-head">' +
 					'<tr>' +
-						'<td class="fc-head-container ' + this.widgetHeaderClass + '"></td>' +
+						'<td class="fc-head-container ' + theme.getClass('widgetHeader') + '"></td>' +
 					'</tr>' +
 				'</thead>' +
 				'<tbody class="fc-body">' +
 					'<tr>' +
-						'<td class="' + this.widgetContentClass + '">' +
+						'<td class="' + theme.getClass('widgetContent') + '">' +
 							(this.dayGrid ?
 								'<div class="fc-day-grid"/>' +
-								'<hr class="fc-divider ' + this.widgetHeaderClass + '"/>' :
+								'<hr class="fc-divider ' + theme.getClass('widgetHeader') + '"/>' :
 								''
 								) +
 						'</td>' +
@@ -377,7 +379,7 @@ var agendaTimeGridMethods = {
 			weekText = weekStart.format(this.opt('smallWeekFormat'));
 
 			return '' +
-				'<th class="fc-axis fc-week-number ' + view.widgetHeaderClass + '" ' + view.axisStyleAttr() + '>' +
+				'<th class="fc-axis fc-week-number ' + view.calendar.theme.getClass('widgetHeader') + '" ' + view.axisStyleAttr() + '>' +
 					view.buildGotoAnchorHtml( // aside from link, important for matchCellWidths
 						{ date: weekStart, type: 'week', forceOff: this.colCnt > 1 },
 						htmlEscape(weekText) // inner HTML
@@ -385,7 +387,7 @@ var agendaTimeGridMethods = {
 				'</th>';
 		}
 		else {
-			return '<th class="fc-axis ' + view.widgetHeaderClass + '" ' + view.axisStyleAttr() + '></th>';
+			return '<th class="fc-axis ' + view.calendar.theme.getClass('widgetHeader') + '" ' + view.axisStyleAttr() + '></th>';
 		}
 	},
 
@@ -394,7 +396,7 @@ var agendaTimeGridMethods = {
 	renderBgIntroHtml: function() {
 		var view = this.view;
 
-		return '<td class="fc-axis ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '></td>';
+		return '<td class="fc-axis ' + view.calendar.theme.getClass('widgetContent') + '" ' + view.axisStyleAttr() + '></td>';
 	},
 
 
@@ -418,7 +420,7 @@ var agendaDayGridMethods = {
 		var view = this.view;
 
 		return '' +
-			'<td class="fc-axis ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' +
+			'<td class="fc-axis ' + view.calendar.theme.getClass('widgetContent') + '" ' + view.axisStyleAttr() + '>' +
 				'<span>' + // needed for matchCellWidths
 					view.getAllDayHtml() +
 				'</span>' +

+ 0 - 8
src/agenda/agenda.css

@@ -33,10 +33,6 @@
 	text-align: left;
 }
 
-.ui-widget td.fc-axis {
-	font-weight: normal; /* overcome jqui theme making it bold */
-}
-
 
 /* TimeGrid Structure
 --------------------------------------------------------------------------------------------------*/
@@ -121,10 +117,6 @@
 	border-top-style: dotted;
 }
 
-.fc-time-grid .fc-slats .ui-widget-content { /* for jqui theme */
-	background: none; /* see through to fc-bg */
-}
-
 
 /* TimeGrid Highlighting Slots
 --------------------------------------------------------------------------------------------------*/

+ 7 - 5
src/basic/BasicView.js

@@ -116,16 +116,18 @@ var BasicView = FC.BasicView = View.extend({
 	// Builds the HTML skeleton for the view.
 	// The day-grid component will render inside of a container defined by this HTML.
 	renderSkeletonHtml: function() {
+		var theme = this.calendar.theme;
+
 		return '' +
-			'<table>' +
+			'<table class="' + theme.getClass('tableGrid') + '">' +
 				'<thead class="fc-head">' +
 					'<tr>' +
-						'<td class="fc-head-container ' + this.widgetHeaderClass + '"></td>' +
+						'<td class="fc-head-container ' + theme.getClass('widgetHeader') + '"></td>' +
 					'</tr>' +
 				'</thead>' +
 				'<tbody class="fc-body">' +
 					'<tr>' +
-						'<td class="' + this.widgetContentClass + '"></td>' +
+						'<td class="' + theme.getClass('widgetContent') + '"></td>' +
 					'</tr>' +
 				'</tbody>' +
 			'</table>';
@@ -276,7 +278,7 @@ var basicDayGridMethods = {
 
 		if (view.colWeekNumbersVisible) {
 			return '' +
-				'<th class="fc-week-number ' + view.widgetHeaderClass + '" ' + view.weekNumberStyleAttr() + '>' +
+				'<th class="fc-week-number ' + view.calendar.theme.getClass('widgetHeader') + '" ' + view.weekNumberStyleAttr() + '>' +
 					'<span>' + // needed for matchCellWidths
 						htmlEscape(this.opt('weekNumberTitle')) +
 					'</span>' +
@@ -311,7 +313,7 @@ var basicDayGridMethods = {
 		var view = this.view;
 
 		if (view.colWeekNumbersVisible) {
-			return '<td class="fc-week-number ' + view.widgetContentClass + '" ' +
+			return '<td class="fc-week-number ' + view.calendar.theme.getClass('widgetContent') + '" ' +
 				view.weekNumberStyleAttr() + '></td>';
 		}
 

+ 1 - 1
src/common/ChronoComponent.js

@@ -552,7 +552,7 @@ var ChronoComponent = Model.extend({
 				classes.push('fc-today');
 
 				if (noThemeHighlight !== true) {
-					classes.push(view.highlightStateClass);
+					classes.push(view.calendar.theme.getClass('stateHighlight'));
 				}
 			}
 			else if (date < today) {

+ 3 - 3
src/common/DayGrid.js

@@ -78,8 +78,8 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
 	// Generates the HTML for a single row, which is a div that wraps a table.
 	// `row` is the row number.
 	renderDayRowHtml: function(row, isRigid) {
-		var view = this.view;
-		var classes = [ 'fc-row', 'fc-week', view.widgetContentClass ];
+		var theme = this.view.calendar.theme;
+		var classes = [ 'fc-row', 'fc-week', theme.getClass('dayRow') ];
 
 		if (isRigid) {
 			classes.push('fc-rigid');
@@ -88,7 +88,7 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
 		return '' +
 			'<div class="' + classes.join(' ') + '">' +
 				'<div class="fc-bg">' +
-					'<table>' +
+					'<table class="' + theme.getClass('tableGrid') + '">' +
 						this.renderBgTrHtml(row) +
 					'</table>' +
 				'</div>' +

+ 5 - 7
src/common/DayGrid.limit.js

@@ -246,7 +246,7 @@ DayGrid.mixin({
 		}
 
 		options = {
-			className: 'fc-more-popover',
+			className: 'fc-more-popover ' + view.calendar.theme.getClass('popover'),
 			content: this.renderSegPopoverContent(row, col, segs),
 			parentEl: view.el, // attach to root of view. guarantees outside of scrollbars.
 			top: topEl.offset().top,
@@ -297,19 +297,17 @@ DayGrid.mixin({
 	// Builds the inner DOM contents of the segment popover
 	renderSegPopoverContent: function(row, col, segs) {
 		var view = this.view;
-		var isTheme = this.opt('theme');
+		var theme = view.calendar.theme;
 		var title = this.getCellDate(row, col).format(this.opt('dayPopoverFormat'));
 		var content = $(
-			'<div class="fc-header ' + view.widgetHeaderClass + '">' +
-				'<span class="fc-close ' +
-					(isTheme ? 'ui-icon ui-icon-closethick' : 'fc-icon fc-icon-x') +
-				'"></span>' +
+			'<div class="fc-header ' + theme.getClass('popoverHeader') + '">' +
+				'<span class="fc-close ' + theme.getIconClass('close') + '"></span>' +
 				'<span class="fc-title">' +
 					htmlEscape(title) +
 				'</span>' +
 				'<div class="fc-clear"/>' +
 			'</div>' +
-			'<div class="fc-body ' + view.widgetContentClass + '">' +
+			'<div class="fc-body ' + theme.getClass('popoverContent') + '">' +
 				'<div class="fc-event-container"></div>' +
 			'</div>'
 		);

+ 5 - 5
src/common/DayTableMixin.js

@@ -251,11 +251,11 @@ var DayTableMixin = FC.DayTableMixin = {
 
 
 	renderHeadHtml: function() {
-		var view = this.view;
+		var theme = this.view.calendar.theme;
 
 		return '' +
-			'<div class="fc-row ' + view.widgetHeaderClass + '">' +
-				'<table>' +
+			'<div class="fc-row ' + theme.getClass('headerRow') + '">' +
+				'<table class="' + theme.getClass('tableGrid') + '">' +
 					'<thead>' +
 						this.renderHeadTrHtml() +
 					'</thead>' +
@@ -299,7 +299,7 @@ var DayTableMixin = FC.DayTableMixin = {
 		var isDateValid = view.activeUnzonedRange.containsDate(date); // TODO: called too frequently. cache somehow.
 		var classNames = [
 			'fc-day-header',
-			view.widgetHeaderClass
+			view.calendar.theme.getClass('widgetHeader')
 		];
 		var innerHtml = htmlEscape(date.format(this.colHeadFormat));
 
@@ -377,7 +377,7 @@ var DayTableMixin = FC.DayTableMixin = {
 		var isDateValid = view.activeUnzonedRange.containsDate(date); // TODO: called too frequently. cache somehow.
 		var classes = this.getDayClasses(date);
 
-		classes.unshift('fc-day', view.widgetContentClass);
+		classes.unshift('fc-day', view.calendar.theme.getClass('widgetContent'));
 
 		return '<td class="' + classes.join(' ') + '"' +
 			(isDateValid ?

+ 8 - 5
src/common/TimeGrid.js

@@ -51,14 +51,16 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
 
 	// Renders the basic HTML skeleton for the grid
 	renderHtml: function() {
+		var theme = this.view.calendar.theme;
+
 		return '' +
 			'<div class="fc-bg">' +
-				'<table>' +
+				'<table class="' + theme.getClass('tableGrid') + '">' +
 					this.renderBgTrHtml(0) + // row=0
 				'</table>' +
 			'</div>' +
 			'<div class="fc-slats">' +
-				'<table>' +
+				'<table class="' + theme.getClass('tableGrid') + '">' +
 					this.renderSlatRowHtml() +
 				'</table>' +
 			'</div>';
@@ -69,6 +71,7 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
 	renderSlatRowHtml: function() {
 		var view = this.view;
 		var calendar = view.calendar;
+		var theme = calendar.theme;
 		var isRTL = this.isRTL;
 		var html = '';
 		var slotTime = moment.duration(+this.view.minTime); // wish there was .clone() for durations
@@ -77,12 +80,12 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
 		var axisHtml;
 
 		// Calculate the time for each slot
-		while (slotTime < this.view.maxTime) {
+		while (slotTime < view.maxTime) {
 			slotDate = calendar.msToUtcMoment(this.unzonedRange.startMs).time(slotTime);
 			isLabeled = isInt(divideDurationByDuration(slotTime, this.labelInterval));
 
 			axisHtml =
-				'<td class="fc-axis fc-time ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' +
+				'<td class="fc-axis fc-time ' + theme.getClass('widgetContent') + '" ' + view.axisStyleAttr() + '>' +
 					(isLabeled ?
 						'<span>' + // for matchCellWidths
 							htmlEscape(slotDate.format(this.labelFormat)) +
@@ -96,7 +99,7 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
 					(isLabeled ? '' : ' class="fc-minor"') +
 					'>' +
 					(!isRTL ? axisHtml : '') +
-					'<td class="' + view.widgetContentClass + '"/>' +
+					'<td class="' + theme.getClass('widgetContent') + '"/>' +
 					(isRTL ? axisHtml : '') +
 				"</tr>";
 

+ 29 - 0
src/common/common.bootstrap3.css

@@ -0,0 +1,29 @@
+
+.fc.fc-bootstrap3 a {
+	text-decoration: none;
+}
+
+.fc.fc-bootstrap3 a[data-goto]:hover {
+	text-decoration: underline;
+}
+
+.fc-bootstrap3 .fc-divider {
+	border-color: inherit;
+}
+
+
+/* Popover
+--------------------------------------------------------------------------------------------------*/
+
+.fc-bootstrap3 .fc-popover .panel-body {
+	padding: 0; /* undo built-in padding */
+}
+
+
+/* TimeGrid Slats (lines that run horizontally)
+--------------------------------------------------------------------------------------------------*/
+
+.fc-bootstrap3 .fc-time-grid .fc-slats table {
+	/* some themes have background color. see through to slats */
+	background: none;
+}

+ 5 - 145
src/common/common.css

@@ -22,36 +22,6 @@ body .fc { /* extra precedence to overcome jqui */
 /* Colors
 --------------------------------------------------------------------------------------------------*/
 
-.fc-unthemed th,
-.fc-unthemed td,
-.fc-unthemed thead,
-.fc-unthemed tbody,
-.fc-unthemed .fc-divider,
-.fc-unthemed .fc-row,
-.fc-unthemed .fc-content, /* for gutter border */
-.fc-unthemed .fc-popover,
-.fc-unthemed .fc-list-view,
-.fc-unthemed .fc-list-heading td {
-	border-color: #ddd;
-}
-
-.fc-unthemed .fc-popover {
-	background-color: #fff;
-}
-
-.fc-unthemed .fc-divider,
-.fc-unthemed .fc-popover .fc-header,
-.fc-unthemed .fc-list-heading td {
-	background: #eee;
-}
-
-.fc-unthemed .fc-popover .fc-header .fc-close {
-	color: #666;
-}
-
-.fc-unthemed td.fc-today {
-	background: #fcf8e3;
-}
 
 .fc-highlight { /* when user is selecting cells */
 	background: #bce8f1;
@@ -68,99 +38,6 @@ body .fc { /* extra precedence to overcome jqui */
 	background: #d7d7d7;
 }
 
-.fc-unthemed .fc-disabled-day {
-	background: #d7d7d7;
-	opacity: .3;
-}
-
-.ui-widget .fc-disabled-day { /* themed */
-	background-image: none;
-}
-
-
-/* Icons (inline elements with styled text that mock arrow icons)
---------------------------------------------------------------------------------------------------*/
-
-.fc-icon {
-	display: inline-block;
-	height: 1em;
-	line-height: 1em;
-	font-size: 1em;
-	text-align: center;
-	overflow: hidden;
-	font-family: "Courier New", Courier, monospace;
-
-	/* don't allow browser text-selection */
-	-webkit-touch-callout: none;
-	-webkit-user-select: none;
-	-khtml-user-select: none;
-	-moz-user-select: none;
-	-ms-user-select: none;
-	user-select: none;
-	}
-
-/*
-Acceptable font-family overrides for individual icons:
-	"Arial", sans-serif
-	"Times New Roman", serif
-
-NOTE: use percentage font sizes or else old IE chokes
-*/
-
-.fc-icon:after {
-	position: relative;
-}
-
-.fc-icon-left-single-arrow:after {
-	content: "\02039";
-	font-weight: bold;
-	font-size: 200%;
-	top: -7%;
-}
-
-.fc-icon-right-single-arrow:after {
-	content: "\0203A";
-	font-weight: bold;
-	font-size: 200%;
-	top: -7%;
-}
-
-.fc-icon-left-double-arrow:after {
-	content: "\000AB";
-	font-size: 160%;
-	top: -7%;
-}
-
-.fc-icon-right-double-arrow:after {
-	content: "\000BB";
-	font-size: 160%;
-	top: -7%;
-}
-
-.fc-icon-left-triangle:after {
-	content: "\25C4";
-	font-size: 125%;
-	top: 3%;
-}
-
-.fc-icon-right-triangle:after {
-	content: "\25BA";
-	font-size: 125%;
-	top: 3%;
-}
-
-.fc-icon-down-triangle:after {
-	content: "\25BC";
-	font-size: 125%;
-	top: 2%;
-}
-
-.fc-icon-x:after {
-	content: "\000D7";
-	font-size: 200%;
-	top: 6%;
-}
-
 
 /* Buttons (styled <button> tags, normalized to work cross-browser)
 --------------------------------------------------------------------------------------------------*/
@@ -313,24 +190,6 @@ previous button's border...
 	float: right;
 }
 
-/* unthemed */
-
-.fc-unthemed .fc-popover {
-	border-width: 1px;
-	border-style: solid;
-}
-
-.fc-unthemed .fc-popover .fc-header .fc-close {
-	font-size: .9em;
-	margin-top: 2px;
-}
-
-/* jqui themed */
-
-.fc-popover > .ui-widget-header + .ui-widget-content {
-	border-top: 0; /* where they meet, let the header have the border */
-}
-
 
 /* Misc Reusable Components
 --------------------------------------------------------------------------------------------------*/
@@ -488,12 +347,16 @@ temporary rendered events).
 	z-index: 5;
 }
 
+.fc-row .fc-content-skeleton table,
 .fc-row .fc-content-skeleton td,
 .fc-row .fc-helper-skeleton td {
 	/* see-through to the background below */
 	background: none; /* in case <td>s are globally styled */
 	border-color: transparent;
+}
 
+.fc-row .fc-content-skeleton td,
+.fc-row .fc-helper-skeleton td {
 	/* don't put a border between events and/or the day number */
 	border-bottom: 0;
 }
@@ -530,7 +393,6 @@ temporary rendered events).
 	line-height: 1.3;
 	border-radius: 3px;
 	border: 1px solid #3a87ad; /* default BORDER color */
-	font-weight: normal; /* undo jqui's ui-widget-header bold */
 }
 
 .fc-event,
@@ -538,10 +400,8 @@ temporary rendered events).
 	background-color: #3a87ad; /* default BACKGROUND color */
 }
 
-/* overpower some of bootstrap's and jqui's styles on <a> tags */
 .fc-event,
-.fc-event:hover,
-.ui-widget .fc-event {
+.fc-event:hover {
 	color: #fff; /* default TEXT color */
 	text-decoration: none; /* if <a> has an href */
 }

+ 44 - 0
src/common/common.jquery-ui.css

@@ -0,0 +1,44 @@
+
+/* Colors
+--------------------------------------------------------------------------------------------------*/
+
+.ui-widget .fc-disabled-day {
+	background-image: none;
+}
+
+
+/* Popover
+--------------------------------------------------------------------------------------------------*/
+
+.fc-popover > .ui-widget-header + .ui-widget-content {
+	border-top: 0; /* where they meet, let the header have the border */
+}
+
+
+/* Global Event Styles
+--------------------------------------------------------------------------------------------------*/
+
+.ui-widget .fc-event {
+	/* overpower jqui's styles on <a> tags. TODO: more DRY */
+	color: #fff; /* default TEXT color */
+	text-decoration: none; /* if <a> has an href */
+
+	/* undo ui-widget-header bold */
+	font-weight: normal;
+}
+
+
+/* TimeGrid axis running down the side (for both the all-day area and the slot area)
+--------------------------------------------------------------------------------------------------*/
+
+.ui-widget td.fc-axis {
+	font-weight: normal; /* overcome bold */
+}
+
+
+/* TimeGrid Slats (lines that run horizontally)
+--------------------------------------------------------------------------------------------------*/
+
+.fc-time-grid .fc-slats .ui-widget-content { 
+	background: none; /* see through to fc-bg */
+}

+ 149 - 0
src/common/common.standard.css

@@ -0,0 +1,149 @@
+
+/*
+TODO: more distinction between this file and common.css
+*/
+
+/* Colors
+--------------------------------------------------------------------------------------------------*/
+
+.fc-unthemed th,
+.fc-unthemed td,
+.fc-unthemed thead,
+.fc-unthemed tbody,
+.fc-unthemed .fc-divider,
+.fc-unthemed .fc-row,
+.fc-unthemed .fc-content, /* for gutter border */
+.fc-unthemed .fc-popover,
+.fc-unthemed .fc-list-view,
+.fc-unthemed .fc-list-heading td {
+	border-color: #ddd;
+}
+
+.fc-unthemed .fc-popover {
+	background-color: #fff;
+}
+
+.fc-unthemed .fc-divider,
+.fc-unthemed .fc-popover .fc-header,
+.fc-unthemed .fc-list-heading td {
+	background: #eee;
+}
+
+.fc-unthemed .fc-popover .fc-header .fc-close {
+	color: #666;
+}
+
+.fc-unthemed td.fc-today {
+	background: #fcf8e3;
+}
+
+.fc-unthemed .fc-disabled-day {
+	background: #d7d7d7;
+	opacity: .3;
+}
+
+
+/* Icons (inline elements with styled text that mock arrow icons)
+--------------------------------------------------------------------------------------------------*/
+
+.fc-icon {
+	display: inline-block;
+	height: 1em;
+	line-height: 1em;
+	font-size: 1em;
+	text-align: center;
+	overflow: hidden;
+	font-family: "Courier New", Courier, monospace;
+
+	/* don't allow browser text-selection */
+	-webkit-touch-callout: none;
+	-webkit-user-select: none;
+	-khtml-user-select: none;
+	-moz-user-select: none;
+	-ms-user-select: none;
+	user-select: none;
+}
+
+/*
+Acceptable font-family overrides for individual icons:
+	"Arial", sans-serif
+	"Times New Roman", serif
+
+NOTE: use percentage font sizes or else old IE chokes
+*/
+
+.fc-icon:after {
+	position: relative;
+}
+
+.fc-icon-left-single-arrow:after {
+	content: "\02039";
+	font-weight: bold;
+	font-size: 200%;
+	top: -7%;
+}
+
+.fc-icon-right-single-arrow:after {
+	content: "\0203A";
+	font-weight: bold;
+	font-size: 200%;
+	top: -7%;
+}
+
+.fc-icon-left-double-arrow:after {
+	content: "\000AB";
+	font-size: 160%;
+	top: -7%;
+}
+
+.fc-icon-right-double-arrow:after {
+	content: "\000BB";
+	font-size: 160%;
+	top: -7%;
+}
+
+.fc-icon-left-triangle:after {
+	content: "\25C4";
+	font-size: 125%;
+	top: 3%;
+}
+
+.fc-icon-right-triangle:after {
+	content: "\25BA";
+	font-size: 125%;
+	top: 3%;
+}
+
+.fc-icon-down-triangle:after {
+	content: "\25BC";
+	font-size: 125%;
+	top: 2%;
+}
+
+.fc-icon-x:after {
+	content: "\000D7";
+	font-size: 200%;
+	top: 6%;
+}
+
+
+/* Popover
+--------------------------------------------------------------------------------------------------*/
+
+.fc-unthemed .fc-popover {
+	border-width: 1px;
+	border-style: solid;
+}
+
+.fc-unthemed .fc-popover .fc-header .fc-close {
+	font-size: .9em;
+	margin-top: 2px;
+}
+
+
+/* List View
+--------------------------------------------------------------------------------------------------*/
+
+.fc-unthemed .fc-list-item:hover td {
+	background-color: #f5f5f5;
+}

+ 2 - 13
src/defaults.js

@@ -55,24 +55,13 @@ Calendar.defaults = {
 		week: 'week',
 		day: 'day'
 	},
-
-	buttonIcons: {
-		prev: 'left-single-arrow',
-		next: 'right-single-arrow',
-		prevYear: 'left-double-arrow',
-		nextYear: 'right-double-arrow'
-	},
+	//buttonIcons: null,
 
 	allDayText: 'all-day',
 	
 	// jquery-ui theming
 	theme: false,
-	themeButtonIcons: {
-		prev: 'circle-triangle-w',
-		next: 'circle-triangle-e',
-		prevYear: 'seek-prev',
-		nextYear: 'seek-next'
-	},
+	//themeButtonIcons: null,
 
 	//eventResizableFromStart: false,
 	dragOpacity: .75,

+ 7 - 6
src/list/ListView.js

@@ -20,7 +20,7 @@ var ListView = View.extend({
 	renderSkeleton: function() {
 		this.el.addClass(
 			'fc-list-view ' +
-			this.widgetContentClass
+			this.calendar.theme.getClass('listView')
 		);
 
 		this.scroller.render();
@@ -183,7 +183,7 @@ var ListViewGrid = Grid.extend({
 		var dayIndex;
 		var daySegs;
 		var i;
-		var tableEl = $('<table class="fc-list-table"><tbody/></table>');
+		var tableEl = $('<table class="fc-list-table ' + this.view.calendar.theme.getClass('tableList') + '"><tbody/></table>');
 		var tbodyEl = tableEl.find('tbody');
 
 		for (dayIndex = 0; dayIndex < segsByDay.length; dayIndex++) {
@@ -226,7 +226,7 @@ var ListViewGrid = Grid.extend({
 		var altFormat = this.opt('listDayAltFormat');
 
 		return '<tr class="fc-list-heading" data-date="' + dayDate.format('YYYY-MM-DD') + '">' +
-			'<td class="' + view.widgetHeaderClass + '" colspan="3">' +
+			'<td class="' + view.calendar.theme.getClass('widgetHeader') + '" colspan="3">' +
 				(mainFormat ?
 					view.buildGotoAnchorHtml(
 						dayDate,
@@ -249,6 +249,7 @@ var ListViewGrid = Grid.extend({
 	fgSegHtml: function(seg) {
 		var view = this.view;
 		var calendar = view.calendar;
+		var theme = calendar.theme;
 		var classes = [ 'fc-list-item' ].concat(this.getSegCustomClasses(seg));
 		var bgColor = this.getSegBackgroundColor(seg);
 		var eventFootprint = seg.footprint;
@@ -284,18 +285,18 @@ var ListViewGrid = Grid.extend({
 
 		return '<tr class="' + classes.join(' ') + '">' +
 			(this.displayEventTime ?
-				'<td class="fc-list-item-time ' + view.widgetContentClass + '">' +
+				'<td class="fc-list-item-time ' + theme.getClass('widgetContent') + '">' +
 					(timeHtml || '') +
 				'</td>' :
 				'') +
-			'<td class="fc-list-item-marker ' + view.widgetContentClass + '">' +
+			'<td class="fc-list-item-marker ' + theme.getClass('widgetContent') + '">' +
 				'<span class="fc-event-dot"' +
 				(bgColor ?
 					' style="background-color:' + bgColor + '"' :
 					'') +
 				'></span>' +
 			'</td>' +
-			'<td class="fc-list-item-title ' + view.widgetContentClass + '">' +
+			'<td class="fc-list-item-title ' + theme.getClass('widgetContent') + '">' +
 				'<a' + (url ? ' href="' + htmlEscape(url) + '"' : '') + '>' +
 					htmlEscape(eventDef.title || '') +
 				'</a>' +

+ 0 - 4
src/list/list.css

@@ -59,10 +59,6 @@
 	cursor: pointer; /* whole row will be clickable */
 }
 
-.fc-list-item:hover td {
-	background-color: #f5f5f5;
-}
-
 .fc-list-item-marker,
 .fc-list-item-time {
 	white-space: nowrap;

+ 42 - 0
src/theme/BootstrapTheme.js

@@ -0,0 +1,42 @@
+
+var BootstrapTheme = Theme.extend({
+
+	classes: {
+		widget: 'fc-bootstrap3',
+
+		tableGrid: 'table-bordered', // avoid `table` class b/c don't want margins. only border color
+		tableList: 'table table-striped', // `table` class creates bottom margin but who cares
+
+		buttonGroup: 'btn-group',
+		button: 'btn btn-default',
+		stateActive: 'active',
+		stateDisabled: 'disabled',
+
+		popover: 'panel panel-default',
+		popoverHeader: 'panel-heading',
+		popoverContent: 'panel-body',
+
+		// day grid
+		headerRow: 'panel-default', // avoid `panel` class b/c don't want margins/radius. only border color
+		dayRow: 'panel-default', // "
+
+		// list view
+		listView: 'panel panel-default'
+	},
+
+	baseIconClass: 'glyphicon',
+	iconClasses: {
+		close: 'glyphicon-remove',
+		prev: 'glyphicon-chevron-left',
+		next: 'glyphicon-chevron-right',
+		prevYear: 'glyphicon-backward',
+		nextYear: 'glyphicon-forward'
+	},
+
+	iconOverrideOption: 'bootstrapGlyphicons',
+	iconOverrideCustomButtonOption: 'bootstrapGlyphicon',
+	iconOverridePrefix: 'glyphicon-'
+
+});
+
+ThemeRegistry.register('bootstrap3', BootstrapTheme);

+ 46 - 0
src/theme/JqueryUiTheme.js

@@ -0,0 +1,46 @@
+
+var JqueryUiTheme = Theme.extend({
+
+	classes: {
+		widget: 'ui-widget',
+		widgetHeader: 'ui-widget-header',
+		widgetContent: 'ui-widget-content',
+
+		buttonGroup: 'fc-button-group',
+		button: 'ui-button',
+		cornerLeft: 'ui-corner-left',
+		cornerRight: 'ui-corner-right',
+		stateHighlight: 'ui-state-highlight',
+		stateDefault: 'ui-state-default',
+		stateActive: 'ui-state-active',
+		stateDisabled: 'ui-state-disabled',
+		stateHover: 'ui-state-hover',
+		stateDown: 'ui-state-down',
+
+		popoverHeader: 'ui-widget-header',
+		popoverContent: 'ui-widget-content',
+
+		// day grid
+		headerRow: 'ui-widget-header',
+		dayRow: 'ui-widget-content',
+
+		// list view
+		listView: 'ui-widget-content'
+	},
+
+	baseIconClass: 'ui-icon',
+	iconClasses: {
+		close: 'ui-icon-closethick',
+		prev: 'ui-icon-circle-triangle-w',
+		next: 'ui-icon-circle-triangle-e',
+		prevYear: 'ui-icon-seek-prev',
+		nextYear: 'ui-icon-seek-next'
+	},
+
+	iconOverrideOption: 'themeButtonIcons',
+	iconOverrideCustomButtonOption: 'themeIcon',
+	iconOverridePrefix: 'ui-icon-'
+
+});
+
+ThemeRegistry.register('jquery-ui', JqueryUiTheme);

+ 44 - 0
src/theme/StandardTheme.js

@@ -0,0 +1,44 @@
+
+var StandardTheme = Theme.extend({
+
+	classes: {
+		widget: 'fc-unthemed',
+		widgetHeader: 'fc-widget-header',
+		widgetContent: 'fc-widget-content',
+
+		buttonGroup: 'fc-button-group',
+		button: 'fc-button',
+		cornerLeft: 'fc-corner-left',
+		cornerRight: 'fc-corner-right',
+		stateHighlight: 'fc-state-highlight',
+		stateDefault: 'fc-state-default',
+		stateActive: 'fc-state-active',
+		stateDisabled: 'fc-state-disabled',
+		stateHover: 'fc-state-hover',
+		stateDown: 'fc-state-down',
+
+		popoverHeader: 'fc-widget-header',
+		popoverContent: 'fc-widget-content',
+
+		// day grid
+		headerRow: 'fc-widget-header',
+		dayRow: 'fc-widget-content',
+
+		// list view
+		listView: 'fc-widget-content'
+	},
+
+	baseIconClass: 'fc-icon',
+	iconClasses: {
+		close: 'fc-icon-x',
+		prev: 'fc-icon-left-single-arrow',
+		next: 'fc-icon-right-single-arrow',
+		prevYear: 'fc-icon-left-double-arrow',
+		nextYear: 'fc-icon-right-double-arrow'
+	},
+
+	iconOverrideOption: 'buttonIcons',
+	iconOverrideCustomButtonOption: 'icon',
+	iconOverridePrefix: 'fc-icon-'
+
+});

+ 89 - 0
src/theme/Theme.js

@@ -0,0 +1,89 @@
+
+var Theme = Class.extend({
+
+	classes: {},
+	iconClasses: {},
+	baseIconClass: '',
+	iconOverrideOption: null,
+	iconOverrideCustomButtonOption: null,
+	iconOverridePrefix: '',
+
+
+	constructor: function(optionsModel) {
+		this.optionsModel = optionsModel;
+		this.processIconOverride();
+	},
+
+
+	processIconOverride: function() {
+		if (this.iconOverrideOption) {
+			this.setIconOverride(
+				this.optionsModel.get(this.iconOverrideOption)
+			);
+		}
+	},
+
+
+	setIconOverride: function(iconOverrideHash) {
+		var iconClassesCopy;
+		var buttonName;
+
+		if ($.isPlainObject(iconOverrideHash)) {
+			iconClassesCopy = $.extend({}, this.iconClasses);
+
+			for (buttonName in iconOverrideHash) {
+				iconClassesCopy[buttonName] = this.applyIconOverridePrefix(
+					iconOverrideHash[buttonName]
+				);
+			}
+
+			this.iconClasses = iconClassesCopy;
+		}
+		else if (iconOverrideHash === false) {
+			this.iconClasses = {};
+		}
+	},
+
+
+	applyIconOverridePrefix: function(className) {
+		var prefix = this.iconOverridePrefix;
+
+		if (prefix && className.indexOf(prefix) !== 0) { // if not already present
+			className = prefix + className;
+		}
+
+		return className;
+	},
+
+
+	getClass: function(key) {
+		return this.classes[key] || '';
+	},
+
+
+	getIconClass: function(buttonName) {
+		var className = this.iconClasses[buttonName];
+
+		if (className) {
+			return this.baseIconClass + ' ' + className;
+		}
+
+		return '';
+	},
+
+
+	getCustomButtonIconClass: function(customButtonProps) {
+		var className;
+
+		if (this.iconOverrideCustomButtonOption) {
+			className = customButtonProps[this.iconOverrideCustomButtonOption];
+
+			if (className) {
+				return this.baseIconClass + ' ' + this.applyIconOverridePrefix(className);
+			}
+		}
+
+		return '';
+	}
+
+});

+ 24 - 0
src/theme/ThemeRegistry.js

@@ -0,0 +1,24 @@
+
+var ThemeRegistry = {
+
+	themeClassHash: {},
+
+
+	register: function(themeName, themeClass) {
+		this.themeClassHash[themeName] = themeClass;
+	},
+
+
+	getThemeClass: function(themeSetting) {
+		if (!themeSetting) {
+			return StandardTheme;
+		}
+		else if (themeSetting === true) {
+			return JqueryUiTheme;
+		}
+		else {
+			return this.themeClassHash[themeSetting];
+		}
+	}
+
+};

+ 11 - 0
tests/legacy/destroy.js

@@ -24,6 +24,17 @@ describe('destroy', function() {
 		});
 	});
 
+	describeOptions('theme', {
+		'when jquery-ui theme': 'jquery-ui',
+		'when bootstrap theme': 'bootstrap3'
+	}, function() {
+		it('cleans up all classNames on the root element', function() {
+			initCalendar();
+			$('#cal').fullCalendar('destroy');
+			expect($('#cal')[0].className).toBe('');
+		});
+	});
+
 	[ 'month', 'basicWeek', 'agendaWeek' ].forEach(function(viewName) {
 
 		describe('when in ' + viewName + ' view', function() {

+ 3 - 2
tests/manual/theming.html

@@ -20,6 +20,7 @@ function initCalendar() {
 		},
 		defaultDate: '2014-08-12',
 		weekNumbers: true,
+		navLinks: true, // can click day/week names to navigate views
 		editable: true,
 		eventLimit: true, // allow "more" link when too many events
 		events: [
@@ -87,7 +88,7 @@ $(document).ready(function() {
 	initCalendar();
 	
 	$('#switcher').themeswitcher({
-		imgPath: '../lib/themeswitcher/images/',
+		imgPath: 'themeswitcher/images/',
 		loadTheme: 'Cupertino',
 		height: 400,
 		onSelect: function(theme) {
@@ -101,7 +102,7 @@ $(document).ready(function() {
 
 </script>
 </head>
-<body style='font-size:13px;margin:0'>
+<body style='font-size:14px;margin:0'>
 <div id="switcher" style='position:absolute;top:5px;left:5px'></div>
 <div id='calendar' style='width:900px;margin:50px auto 0;font-family:arial'></div>
 </body>

+ 33 - 0
tests/theme/bootstrap3.js

@@ -0,0 +1,33 @@
+
+describe('bootstrap3 theme', function() {
+	pushOptions({ theme: 'bootstrap3' });
+
+	describe('glyphicons', function() {
+		pushOptions({
+			header: { left: '', center: '', right: 'next' },
+		});
+
+		it('renders default', function() {
+			initCalendar();
+			expect($('.glyphicon')).toHaveClass('glyphicon-chevron-right');
+		});
+
+		it('renders a customized icon', function() {
+			initCalendar({
+				bootstrapGlyphicons: {
+					next: 'asdf'
+				}
+			});
+			expect($('.glyphicon')).toHaveClass('glyphicon-asdf');
+		});
+
+		it('renders text when specified as false', function() {
+			initCalendar({
+				bootstrapGlyphicons: false
+			});
+			expect($('.glyphicon')).not.toBeInDOM();
+			expect($('.fc-next-button')).toHaveText('next');
+		});
+	});
+
+});

+ 34 - 0
tests/theme/switching.js

@@ -0,0 +1,34 @@
+
+describe('theme switching', function() {
+
+	it('can switch from standard to jquery-ui', function() {
+		initCalendar();
+		verifyStandardTheme();
+		currentCalendar.option('theme', 'jquery-ui');
+		verifyJqueryUiTheme();
+	});
+
+	it('can switch from jquery-ui to boostrap3', function() {
+		initCalendar({ theme: 'jquery-ui' });
+		verifyJqueryUiTheme();
+		currentCalendar.option('theme', 'bootstrap3');
+		verifyBootstrapTheme();
+	});
+
+
+	function verifyStandardTheme() {
+		expect($('.fc-unthemed')).toBeInDOM();
+		expect($('.fc-widget-header')).toBeInDOM();
+	}
+
+	function verifyJqueryUiTheme() {
+		expect($('.fc.ui-widget')).toBeInDOM();
+		expect($('.ui-widget-header')).toBeInDOM();
+	}
+
+	function verifyBootstrapTheme() {
+		expect($('.fc-bootstrap3')).toBeInDOM();
+		expect($('.fc .table-bordered')).toBeInDOM();
+	}
+
+});

+ 49 - 0
tests/toolbar/customButtons.js

@@ -0,0 +1,49 @@
+
+describe('customButtons', function() {
+
+	it('can specify text', function() {
+		initCalendar({
+			customButtons: {
+				mybutton: { text: 'asdf' }
+			},
+			header: { left: 'mybutton', center: '', right: '' }
+		});
+		
+		expect($('.fc-mybutton-button')).toHaveText('asdf');
+	});
+
+	it('can specify an icon', function() {
+		initCalendar({
+			customButtons: {
+				mybutton: { icon: 'asdf' }
+			},
+			header: { left: 'mybutton', center: '', right: '' }
+		});
+
+		expect($('.fc-mybutton-button .fc-icon')).toHaveClass('fc-icon-asdf');
+	});
+
+	it('can specify a jquery-ui icon', function() {
+		initCalendar({
+			theme: 'jquery-ui',
+			customButtons: {
+				mybutton: { themeIcon: 'asdf' }
+			},
+			header: { left: 'mybutton', center: '', right: '' }
+		});
+
+		expect($('.fc-mybutton-button .ui-icon')).toHaveClass('ui-icon-asdf');
+	});
+
+	it('can specify a bootstrap glyphicon', function() {
+		initCalendar({
+			theme: 'bootstrap3',
+			customButtons: {
+				mybutton: { bootstrapGlyphicon: 'asdf' }
+			},
+			header: { left: 'mybutton', center: '', right: '' }
+		});
+
+		expect($('.fc-mybutton-button .glyphicon')).toHaveClass('glyphicon-asdf');
+	});
+});