Sfoglia il codice sorgente

refactor a bit, make Theme plugin system

Adam Shaw 8 anni fa
parent
commit
abf3f8005c

+ 2 - 1
demos/theme-bootstrap.html

@@ -57,7 +57,8 @@
 		}
 
 		$('#calendar').fullCalendar({
-			theme: 'bootstrap3_glyphicons',
+			theme: 'bootstrap3',
+			//bootstrapGlyphicons: false,
 			customButtons: {
 				pickTheme: {
 					text: 'Pick Theme',

+ 5 - 1
src.json

@@ -27,7 +27,6 @@
     "common/DayGrid.limit.js",
     "common/TimeGrid.js",
     "common/TimeGrid.events.js",
-    "common/Theme.js",
     "common/View.js",
     "common/View.date-range.js",
     "common/Scroller.js",
@@ -43,6 +42,11 @@
     "locale.js",
     "Header.js",
     "EventManager.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",

+ 1 - 2
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
 
 
@@ -21,8 +22,6 @@ var Calendar = FC.Calendar = Class.extend(EmitterMixin, {
 		this.initMomentInternals(); // needs to happen after options hash initialized
 		this.initCurrentDate();
 
-		this.theme = new FC.Theme(this.option('theme'));
-
 		EventManager.call(this); // needs options immediately
 		this.initialize();
 	},

+ 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++;

+ 18 - 4
src/Calendar.render.js

@@ -48,10 +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.theme.setTheme(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.

+ 39 - 38
src/Toolbar.js

@@ -60,6 +60,7 @@ function Toolbar(calendar, toolbarOptions) {
 		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) {
@@ -72,113 +73,113 @@ 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 iconClass;
-					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 = calendar.theme.querySingularIconClass(customButtonProps)) ||
+							(buttonIcon = calendar.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 = calendar.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 = calendar.theme.getIconClass(buttonName)) ||
+							(buttonText = calendarButtonText[buttonName]); // jshint ignore:line
+							//            ^ everything else is considered default
 						}
 
 						if (buttonClick) {
-							iconClass = calendar.theme.getIconClassWithOverride(buttonName, customButtonProps, calendar);
-
-							if (overrideText) {
-								innerHtml = htmlEscape(overrideText);
-							}
-							else if (iconClass) {
-								innerHtml = "<span class='" + iconClass + "'></span>";
-							}
-							else {
-								innerHtml = htmlEscape(defaultText);
-							}
 
-							classes = [
+							buttonClasses = [
 								'fc-' + buttonName + '-button',
 								calendar.theme.getClass('button'),
 								calendar.theme.getClass('stateDefault')
 							];
 
-							button = $( // type="button" so that it doesn't submit a form
-								'<button type="button" class="' + classes.join(' ') + '">' +
-									innerHtml +
+							if (buttonText) {
+								buttonInnerHtml = htmlEscape(buttonText);
+							}
+							else if (buttonIcon) {
+								buttonInnerHtml = "<span class='" + buttonIcon + "'></span>";
+							}
+
+							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(calendar.theme.getClass('stateDisabled'))) {
+									if (!buttonEl.hasClass(calendar.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(calendar.theme.getClass('stateActive')) ||
-											button.hasClass(calendar.theme.getClass('stateDisabled'))
+											buttonEl.hasClass(calendar.theme.getClass('stateActive')) ||
+											buttonEl.hasClass(calendar.theme.getClass('stateDisabled'))
 										) {
-											button.removeClass(calendar.theme.getClass('stateHover'));
+											buttonEl.removeClass(calendar.theme.getClass('stateHover'));
 										}
 									}
 								})
 								.mousedown(function() {
 									// the *down* effect (mouse pressed in).
 									// only on buttons that are not the "active" tab, or disabled
-									button
+									buttonEl
 										.not('.' + calendar.theme.getClass('stateActive'))
 										.not('.' + calendar.theme.getClass('stateDisabled'))
 										.addClass(calendar.theme.getClass('stateDown'));
 								})
 								.mouseup(function() {
 									// undo the *down* effect
-									button.removeClass(calendar.theme.getClass('stateDown'));
+									buttonEl.removeClass(calendar.theme.getClass('stateDown'));
 								})
 								.hover(
 									function() {
 										// the *hover* effect.
 										// only on buttons that are not the "active" tab, or disabled
-										button
+										buttonEl
 											.not('.' + calendar.theme.getClass('stateActive'))
 											.not('.' + calendar.theme.getClass('stateDisabled'))
 											.addClass(calendar.theme.getClass('stateHover'));
 									},
 									function() {
 										// undo the *hover* effect
-										button
+										buttonEl
 											.removeClass(calendar.theme.getClass('stateHover'))
 											.removeClass(calendar.theme.getClass('stateDown')); // if mouseleave happens before mouseup
 									}
 								);
 
-							groupChildren = groupChildren.add(button);
+							groupChildren = groupChildren.add(buttonEl);
 						}
 					}
 				});

+ 0 - 150
src/common/Theme.js

@@ -1,150 +0,0 @@
-var themes = {
-	builtin: {
-		classes: {
-			listContent: 'fc-widget-content',
-			widgetHeader: 'fc-widget-header',
-			widgetContent: 'fc-widget-content',
-			popoverHeader: 'fc-widget-header',
-			popoverContent: 'fc-widget-content',
-			stateHighlight: 'fc-state-highlight',
-			stateDefault: 'fc-state-default',
-			stateActive: 'fc-state-active',
-			stateDisabled: 'fc-state-disabled',
-			stateHover: 'fc-state-hover',
-			stateDown: 'fc-state-down',
-			button: 'fc-button',
-			cornerLeft: 'fc-corner-left',
-			cornerRight: 'fc-corner-right',
-			buttonGroup: 'fc-button-group',
-			tableHeader: 'fc-widget-header',
-			tableContent: 'fc-widget-content'
-		},
-		iconClasses: {
-			close: 'fc-icon fc-icon-x',
-			prev: 'fc-icon fc-icon-left-single-arrow',
-			next: 'fc-icon fc-icon-right-single-arrow',
-			prevYear: 'fc-icon fc-icon-left-double-arrow',
-			nextYear: 'fc-icon fc-icon-right-double-arrow'
-		}
-	},
-	jQueryUI: {
-		classes: {
-			listContent: 'ui-widget-content',
-			widgetHeader: 'ui-widget-header',
-			widgetContent: 'ui-widget-content',
-			popoverHeader: 'ui-widget-header',
-			popoverContent: 'ui-widget-content',
-			stateHighlight: 'ui-state-highlight',
-			stateDefault: 'ui-state-default',
-			stateActive: 'ui-state-active',
-			stateDisabled: 'ui-state-disabled',
-			stateHover: 'ui-state-hover',
-			stateDown: 'ui-state-down',
-			button: 'ui-button',
-			cornerLeft: 'ui-corner-left',
-			cornerRight: 'ui-corner-right',
-			buttonGroup: 'fc-button-group',
-			tableHeader: 'ui-widget-header',
-			tableContent: 'ui-widget-content'
-		},
-		iconClasses: {
-			close: 'ui-icon ui-icon-closethick',
-			prev: 'ui-icon ui-icon-circle-triangle-w',
-			next: 'ui-icon ui-icon-circle-triangle-e',
-			prevYear: 'ui-icon ui-icon-seek-prev',
-			nextYear: 'ui-icon ui-icon-seek-next'
-		}
-	},
-	bootstrap3: {
-		classes: {
-			listContent: 'panel-default',
-			popover: 'panel panel-default',
-			popoverHeader: 'panel-heading',
-			popoverContent: 'panel-body',
-			stateActive: 'active',
-			stateDisabled: 'disabled',
-			button: 'btn btn-default',
-			buttonGroup: 'btn-group',
-			tableHeader: 'panel-default',
-			tableContent: 'panel-default',
-			tableGrid: 'table-bordered',
-			tableList: 'table'
-		},
-		iconClasses: {
-			close: 'fc-icon fc-icon-x',
-			prev: 'fc-icon fc-icon-left-single-arrow',
-			next: 'fc-icon fc-icon-right-single-arrow',
-			prevYear: 'fc-icon fc-icon-left-double-arrow',
-			nextYear: 'fc-icon fc-icon-right-double-arrow'
-		}
-	},
-	bootstrap3_glyphicons: {
-		classes: {
-			listContent: 'panel-default',
-			popover: 'panel panel-default',
-			popoverHeader: 'panel-heading',
-			popoverContent: 'panel-body',
-			stateActive: 'active',
-			stateDisabled: 'disabled',
-			button: 'btn btn-default',
-			buttonGroup: 'btn-group',
-			tableHeader: 'panel-default',
-			tableContent: 'panel-default',
-			tableGrid: 'table-bordered',
-			tableList: 'table'
-		},
-		iconClasses: {
-			close: 'glyphicon glyphicon-remove',
-			prev: 'glyphicon glyphicon-chevron-left',
-			next: 'glyphicon glyphicon-chevron-right',
-			prevYear: 'glyphicon glyphicon-backward',
-			nextYear: 'glyphicon glyphicon-forward'
-		}
-	}
-};
-
-FC.Theme = Class.extend({
-	constructor: function(theme) {
-		this.setTheme(theme);
-	},
-
-	setTheme: function(theme) {
-		if (theme === true) {
-			this.theme = 'jQueryUI';
-		} else if (themes.hasOwnProperty(theme)) {
-			this.theme = theme;
-		} else {
-			this.theme = 'builtin';
-		}
-	},
-
-	getClass: function(key) {
-		return themes[this.theme].classes[key] || '';
-	},
-
-	getIconClass: function(buttonName) {
-		return themes[this.theme].iconClasses[buttonName] || '';
-	},
-
-	getIconClassWithOverride: function(buttonName, customButtonProps, calendar) {
-		if (this.theme === 'builtin') {
-			if (customButtonProps) {
-				return customButtonProps.icon;
-			} else if (!calendar.opt('buttonIcons')) {
-				return undefined;
-			} else if (calendar.opt('buttonIcons')[buttonName]) {
-				return 'fc-icon fc-icon-' + calendar.opt('buttonIcons')[buttonName];
-			}
-		} else if (this.theme === 'jQueryUI') {
-			if (customButtonProps) {
-				return customButtonProps.themeIcon;
-			} else if (!calendar.opt('themeButtonIcons')) {
-				return undefined;
-			} else if (calendar.opt('themeButtonIcons')[buttonName]) {
-				return 'ui-icon ui-icon-' + calendar.opt('themeButtonIcons')[buttonName];
-			}
-		}
-
-		return this.getIconClass(buttonName);
-	}
-});

+ 2 - 3
src/defaults.js

@@ -55,14 +55,13 @@ Calendar.defaults = {
 		week: 'week',
 		day: 'day'
 	},
-
-	buttonIcons: {},
+	//buttonIcons: null,
 
 	allDayText: 'all-day',
 	
 	// jquery-ui theming
 	theme: false,
-	themeButtonIcons: {},
+	//themeButtonIcons: null,
 
 	//eventResizableFromStart: false,
 	dragOpacity: .75,

+ 34 - 0
src/theme/BootstrapTheme.js

@@ -0,0 +1,34 @@
+
+var BootstrapTheme = Theme.extend({
+
+	classes: {
+		listContent: 'panel-default',
+		popover: 'panel panel-default',
+		popoverHeader: 'panel-heading',
+		popoverContent: 'panel-body',
+		stateActive: 'active',
+		stateDisabled: 'disabled',
+		button: 'btn btn-default',
+		buttonGroup: 'btn-group',
+		tableHeader: 'panel-default',
+		tableContent: 'panel-default',
+		tableGrid: 'table-bordered',
+		tableList: 'table'
+	},
+
+	baseIconClass: 'glyphicon',
+	iconClasses: {
+		close: 'glyphicon-remove',
+		prev: 'glyphicon-chevron-left',
+		next: 'glyphicon-chevron-right',
+		prevYear: 'glyphicon-backward',
+		nextYear: 'glyphicon-forward'
+	},
+
+	iconOverrideSingularOption: 'bootstrapGlyphicon',
+	iconOverridePluralOption: 'bootstrapGlyphicons',
+	iconOverridePrefix: 'glyphicon-'
+
+});
+
+ThemeRegistry.register('bootstrap3', BootstrapTheme);

+ 40 - 0
src/theme/JqueryUiTheme.js

@@ -0,0 +1,40 @@
+
+var JqueryUiTheme = Theme.extend({
+
+	classes: {
+		widget: 'ui-widget',
+		widgetHeader: 'ui-widget-header',
+		widgetContent: 'ui-widget-content',
+		listContent: 'ui-widget-content',
+		popoverHeader: 'ui-widget-header',
+		popoverContent: 'ui-widget-content',
+		stateHighlight: 'ui-state-highlight',
+		stateDefault: 'ui-state-default',
+		stateActive: 'ui-state-active',
+		stateDisabled: 'ui-state-disabled',
+		stateHover: 'ui-state-hover',
+		stateDown: 'ui-state-down',
+		button: 'ui-button',
+		cornerLeft: 'ui-corner-left',
+		cornerRight: 'ui-corner-right',
+		buttonGroup: 'fc-button-group',
+		tableHeader: 'ui-widget-header',
+		tableContent: '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'
+	},
+
+	iconOverrideSingularOption: 'themeButtonIcons',
+	iconOverridePluralOption: 'themeButtonIcons',
+	iconOverridePrefix: 'ui-icon-'
+
+});
+
+ThemeRegistry.register('jquery-ui', JqueryUiTheme);

+ 38 - 0
src/theme/StandardTheme.js

@@ -0,0 +1,38 @@
+
+var StandardTheme = Theme.extend({
+
+	classes: {
+		widget: 'fc-unthemed',
+		widgetHeader: 'fc-widget-header',
+		widgetContent: 'fc-widget-content',
+		listContent: 'fc-widget-content',
+		popoverHeader: 'fc-widget-header',
+		popoverContent: 'fc-widget-content',
+		stateHighlight: 'fc-state-highlight',
+		stateDefault: 'fc-state-default',
+		stateActive: 'fc-state-active',
+		stateDisabled: 'fc-state-disabled',
+		stateHover: 'fc-state-hover',
+		stateDown: 'fc-state-down',
+		button: 'fc-button',
+		cornerLeft: 'fc-corner-left',
+		cornerRight: 'fc-corner-right',
+		buttonGroup: 'fc-button-group',
+		tableHeader: 'fc-widget-header',
+		tableContent: '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'
+	},
+
+	iconOverrideSingularOption: 'buttonIcon',
+	iconOverridePluralOption: 'buttonIcons',
+	iconOverridePrefix: 'fc-icon-'
+
+});

+ 85 - 0
src/theme/Theme.js

@@ -0,0 +1,85 @@
+
+var Theme = Class.extend({
+
+	classes: {},
+	iconClasses: {},
+	baseIconClass: '',
+	iconOverridePluralOption: null,
+	iconOverrideSingularOption: null,
+	iconOverridePrefix: '',
+
+
+	constructor: function(optionsModel) {
+		this.optionsModel = optionsModel;
+		this.processIconOverride();
+	},
+
+
+	processIconOverride: function() {
+		if (this.iconOverridePluralOption) {
+			this.setIconOverride(
+				this.optionsModel.get(this.iconOverridePluralOption)
+			);
+		}
+	},
+
+
+	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;
+		}
+	},
+
+
+	querySingularIconClass: function(hash) {
+		var className;
+
+		if (this.iconOverrideSingularOption) {
+			className = hash[this.iconOverrideSingularOption];
+
+			if (className) {
+				return this.applyIconOverridePrefix(className);
+			}
+		}
+	}
+
+});

+ 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];
+		}
+	}
+
+};