Przeglądaj źródła

Beefed up event sources: Control over all $.ajax options. Event coloring through options. All event-level options available as "source" options. Event fetching more resilient to errors.

Adam Shaw 15 lat temu
rodzic
commit
06e4734b05

+ 154 - 53
src/EventManager.js

@@ -1,7 +1,15 @@
 
+fc.sourceNormalizers = [];
+fc.sourceFetchers = [];
+
+var ajaxDefaults = {
+	dataType: 'json',
+	cache: true // because we are using the cacheParam option (TODO: deprecate)
+};
+
 var eventGUID = 1;
 
-function EventManager(options, sources) {
+function EventManager(options, _sources) {
 	var t = this;
 	
 	
@@ -24,6 +32,8 @@ function EventManager(options, sources) {
 	
 	
 	// locals
+	var stickySource = { events: [] };
+	var sources = [ stickySource ];
 	var rangeStart, rangeEnd;
 	var currentFetchID = 0;
 	var pendingSourceCnt = 0;
@@ -31,6 +41,11 @@ function EventManager(options, sources) {
 	var cache = [];
 	
 	
+	for (var i=0; i<_sources.length; i++) {
+		_addEventSource(_sources[i]);
+	}
+	
+	
 	
 	/* Fetching
 	-----------------------------------------------------------------------------*/
@@ -57,11 +72,13 @@ function EventManager(options, sources) {
 	function fetchEventSource(source, fetchID) {
 		_fetchEventSource(source, function(events) {
 			if (fetchID == currentFetchID) {
-				for (var i=0; i<events.length; i++) {
-					normalizeEvent(events[i]);
-					events[i].source = source;
+				if (events) {
+					for (var i=0; i<events.length; i++) {
+						normalizeEvent(events[i], source);
+						events[i].source = source;
+					}
+					cache = cache.concat(events);
 				}
-				cache = cache.concat(events);
 				pendingSourceCnt--;
 				if (!pendingSourceCnt) {
 					reportEvents(cache);
@@ -72,35 +89,76 @@ function EventManager(options, sources) {
 	
 	
 	function _fetchEventSource(source, callback) {
-		if (typeof source == 'string') {
-			var params = {};
-			params[options.startParam] = Math.round(rangeStart.getTime() / 1000);
-			params[options.endParam] = Math.round(rangeEnd.getTime() / 1000);
-			if (options.cacheParam) {
-				params[options.cacheParam] = (new Date()).getTime(); // TODO: deprecate cacheParam
+		var i;
+		var fetchers = fc.sourceFetchers;
+		var res;
+		for (i=0; i<fetchers.length; i++) {
+			res = fetchers[i](source, rangeStart, rangeEnd, callback);
+			if (res === true) {
+				// the fetcher is in charge. made its own async request
+				return;
+			}
+			else if (typeof res == 'object') {
+				// the fetcher returned a new source. process it
+				_fetchEventSource(res, callback);
+				return;
 			}
-			pushLoading();
-			// TODO: respect cache param in ajaxSetup
-			$.ajax({
-				url: source,
-				dataType: 'json',
-				data: params,
-				cache: options.cacheParam || false, // don't let jquery prevent caching if cacheParam is being used
-				success: function(events) {
-					popLoading();
-					callback(events);
-				}
-			});
 		}
-		else if ($.isFunction(source)) {
-			pushLoading();
-			source(cloneDate(rangeStart), cloneDate(rangeEnd), function(events) {
-				popLoading();
+		var events = source.events;
+		if (events) {
+			if ($.isFunction(events)) {
+				pushLoading();
+				events(cloneDate(rangeStart), cloneDate(rangeEnd), function(events) {
+					callback(events);
+					popLoading();
+				});
+			}
+			else if ($.isArray(events)) {
 				callback(events);
-			});
-		}
-		else {
-			callback(source); // src is an array
+			}
+			else {
+				callback();
+			}
+		}else{
+			var url = source.url;
+			if (url) {
+				var success = source.success;
+				var error = source.error;
+				var complete = source.complete;
+				var data = $.extend({}, source.data || {});
+				var startParam = firstDefined(source.startParam, options.startParam);
+				var endParam = firstDefined(source.endParam, options.endParam);
+				var cacheParam = firstDefined(source.cacheParam, options.cacheParam);
+				if (startParam) {
+					data[startParam] = Math.round(+rangeStart / 1000);
+				}
+				if (endParam) {
+					data[endParam] = Math.round(+rangeEnd / 1000);
+				}
+				if (cacheParam) {
+					data[cacheParam] = +new Date();
+				}
+				pushLoading();
+				$.ajax($.extend({}, ajaxDefaults, source, {
+					data: data,
+					success: function(events) {
+						events = events || [];
+						var res = applyAll(success, this, arguments);
+						if ($.isArray(res)) {
+							events = res;
+						}
+						callback(events);
+					},
+					error: function() {
+						applyAll(error, this, arguments);
+						callback();
+					},
+					complete: function() {
+						applyAll(complete, this, arguments);
+						popLoading();
+					}
+				}));
+			}
 		}
 	}
 	
@@ -109,25 +167,38 @@ function EventManager(options, sources) {
 	/* Sources
 	-----------------------------------------------------------------------------*/
 	
-	
-	// first event source is reserved for "sticky" events
-	sources.unshift([]);
-	
 
 	function addEventSource(source) {
-		sources.push(source);
-		pendingSourceCnt++;
-		fetchEventSource(source, currentFetchID); // will eventually call reportEvents
+		source = _addEventSource(source);
+		if (source) {
+			pendingSourceCnt++;
+			fetchEventSource(source, currentFetchID); // will eventually call reportEvents
+		}
+	}
+	
+	
+	function _addEventSource(source) {
+		if ($.isFunction(source) || $.isArray(source)) {
+			source = { events: source };
+		}
+		else if (typeof source == 'string') {
+			source = { url: source };
+		}
+		if (typeof source == 'object') {
+			normalizeSource(source);
+			sources.push(source);
+			return source;
+		}
 	}
 	
 
 	function removeEventSource(source) {
 		sources = $.grep(sources, function(src) {
-			return src != source;
+			return !isSourcesEqual(src, source);
 		});
 		// remove all client events from that source
 		cache = $.grep(cache, function(e) {
-			return e.source != source;
+			return !isSourcesEqual(e.source, source);
 		});
 		reportEvents(cache);
 	}
@@ -163,20 +234,20 @@ function EventManager(options, sources) {
 				e.allDay = event.allDay;
 				e.className = event.className;
 				e.editable = event.editable;
-				normalizeEvent(e);
+				normalizeEvent(e, e.source);
 			}
 		}
-		normalizeEvent(event);
+		normalizeEvent(event, event.source);
 		reportEvents(cache);
 	}
 	
 	
 	function renderEvent(event, stick) {
-		normalizeEvent(event);
+		normalizeEvent(event, event.source || stickySource);
 		if (!event.source) {
 			if (stick) {
-				sources[0].push(event);
-				event.source = sources[0];
+				stickySource.events.push(event);
+				event.source = stickySource;
 			}
 			cache.push(event);
 		}
@@ -189,8 +260,8 @@ function EventManager(options, sources) {
 			cache = [];
 			// clear all array sources
 			for (var i=0; i<sources.length; i++) {
-				if (typeof sources[i] == 'object') {
-					sources[i] = [];
+				if ($.isArray(sources[i].events)) {
+					sources[i].events = [];
 				}
 			}
 		}else{
@@ -203,9 +274,8 @@ function EventManager(options, sources) {
 			cache = $.grep(cache, filter, true);
 			// remove events from array sources
 			for (var i=0; i<sources.length; i++) {
-				if (typeof sources[i] == 'object') {
-					sources[i] = $.grep(sources[i], filter, true);
-					// TODO: event objects' sources will no longer be correct reference :(
+				if ($.isArray(sources[i].events)) {
+					sources[i].events = $.grep(sources[i].events, filter, true);
 				}
 			}
 		}
@@ -251,7 +321,7 @@ function EventManager(options, sources) {
 	-----------------------------------------------------------------------------*/
 	
 	
-	function normalizeEvent(event) {
+	function normalizeEvent(event, source) {
 		event._id = event._id || (event.id === undefined ? '_fc' + eventGUID++ : event.id + '');
 		if (event.date) {
 			if (!event.start) {
@@ -259,14 +329,14 @@ function EventManager(options, sources) {
 			}
 			delete event.date;
 		}
-		event._start = cloneDate(event.start = parseDate(event.start, options.ignoreTimezone));
+		event._start = cloneDate(event.start = parseDate(event.start, firstDefined(source.ignoreTimezone, options.ignoreTimezone)));
 		event.end = parseDate(event.end, options.ignoreTimezone);
 		if (event.end && event.end <= event.start) {
 			event.end = null;
 		}
 		event._end = event.end ? cloneDate(event.end) : null;
 		if (event.allDay === undefined) {
-			event.allDay = options.allDayDefault;
+			event.allDay = firstDefined(source.allDayDefault, options.allDayDefault);
 		}
 		if (event.className) {
 			if (typeof event.className == 'string') {
@@ -277,6 +347,37 @@ function EventManager(options, sources) {
 		}
 		// TODO: if there is no start date, return false to indicate an invalid event
 	}
+	
+	
+	
+	/* Utils
+	------------------------------------------------------------------------------*/
+	
+	
+	function normalizeSource(source) {
+		if (source.className) {
+			// TODO: repeate code, same code for event classNames
+			if (typeof source.className == 'string') {
+				source.className = source.className.split(/\s+/);
+			}
+		}else{
+			source.className = [];
+		}
+		var normalizers = fc.sourceNormalizers;
+		for (var i=0; i<normalizers.length; i++) {
+			normalizers[i](source);
+		}
+	}
+	
+	
+	function isSourcesEqual(source1, source2) {
+		return getSourcePrimitive(source1) == getSourcePrimitive(source2);
+	}
+	
+	
+	function getSourcePrimitive(source) {
+		return ((typeof source == 'object') ? (source.events || source.url) : '') || source;
+	}
 
 
 }

+ 34 - 13
src/agenda/AgendaEventRenderer.js

@@ -139,7 +139,7 @@ function AgendaEventRenderer() {
 	
 		var i, segCnt=segs.length, seg,
 			event,
-			className,
+			classes,
 			top, bottom,
 			colI, levelI, forward,
 			leftmost,
@@ -171,12 +171,12 @@ function AgendaEventRenderer() {
 		for (i=0; i<segCnt; i++) {
 			seg = segs[i];
 			event = seg.event;
-			className = 'fc-event fc-event-vert ';
+			classes = ['fc-event', 'fc-event-vert'];
 			if (seg.isStart) {
-				className += 'fc-corner-top ';
+				classes.push('fc-corner-top');
 			}
 			if (seg.isEnd) {
-				className += 'fc-corner-bottom ';
+				classes.push('fc-corner-bottom');
 			}
 			top = timePosition(seg.start, seg.start);
 			bottom = timePosition(seg.start, seg.end);
@@ -205,7 +205,7 @@ function AgendaEventRenderer() {
 			seg.left = left;
 			seg.outerWidth = outerWidth;
 			seg.outerHeight = bottom - top;
-			html += slotSegHtml(event, seg, className);
+			html += slotSegHtml(event, seg, classes);
 		}
 		slotSegmentContainer[0].innerHTML = html; // faster than html()
 		eventElements = slotSegmentContainer.children();
@@ -278,10 +278,16 @@ function AgendaEventRenderer() {
 	}
 	
 	
-	function slotSegHtml(event, seg, className) {
-		return "<div class='" + className + event.className.join(' ') + "' style='position:absolute;z-index:8;top:" + seg.top + "px;left:" + seg.left + "px'>" +
-			"<a class='fc-event-inner'" + (event.url ? " href='" + htmlEscape(event.url) + "'" : '') + ">" + // good for escaping quotes?
-				"<div class='fc-event-head'>" +
+	function slotSegHtml(event, seg, classes) {
+		classes = classes.concat(event.className, event.source.className);
+		var skinCss = getSkinCss(event, opt);
+		var skinCssAttr = (skinCss ? " style='" + skinCss + "'" : '');
+		return "<div class='" + classes.join(' ') + "' style='position:absolute;z-index:8;top:" + seg.top + "px;left:" + seg.left + "px;" + skinCss + "'>" +
+			"<a class='fc-event-inner'" +
+				(event.url ? " href='" + htmlEscape(event.url) + "'" : '') + // good for escaping quotes?
+				skinCssAttr +
+				">" +
+				"<div class='fc-event-head'" + skinCssAttr + ">" +
 					"<div class='fc-event-time'>" +
 						htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) +
 					"</div>" +
@@ -293,16 +299,22 @@ function AgendaEventRenderer() {
 				"</div>" +
 				"<div class='fc-event-bg'></div>" +
 			"</a>" +
-			((event.editable || event.editable === undefined && opt('editable')) && !opt('disableResizing') && $.fn.resizable ?
+			((seg.isEnd &&
+				isEventEditable(event) &&
+				!opt('disableResizing') && // TODO: make like other source properties
+				$.fn.resizable)
+				?
 				"<div class='ui-resizable-handle ui-resizable-s'>=</div>"
-				: '') +
+				:
+				''
+				) +
 		"</div>";
 	}
 	
 	
 	function bindDaySeg(event, eventElement, seg) {
 		eventElementHandlers(event, eventElement);
-		if (event.editable || event.editable === undefined && opt('editable')) {
+		if (isEventEditable(event)) {
 			draggableDayEvent(event, eventElement, seg.isStart);
 			if (seg.isEnd) {
 				resizableDayEvent(event, eventElement, seg);
@@ -313,7 +325,7 @@ function AgendaEventRenderer() {
 	
 	function bindSlotSeg(event, eventElement, seg) {
 		eventElementHandlers(event, eventElement);
-		if (event.editable || event.editable === undefined && opt('editable')) {
+		if (isEventEditable(event)) {
 			var timeElement = eventElement.find('span.fc-event-time');
 			draggableSlotEvent(event, eventElement, timeElement);
 			if (seg.isEnd) {
@@ -323,6 +335,11 @@ function AgendaEventRenderer() {
 	}
 	
 	
+	function isEventEditable(event) {
+		return firstDefined(event.editable, event.source.editable, opt('editable'));
+	}
+	
+	
 	
 	/* Dragging
 	-----------------------------------------------------------------------------------*/
@@ -330,6 +347,10 @@ function AgendaEventRenderer() {
 	
 	// when event starts out FULL-DAY
 	
+	// TODO: bug when dragging an event that occupies first day, but is not the event's start (no rounded left side)
+	
+	// TODO: bug when dragging from day to slot, outer container doesn't seem to change height
+	
 	function draggableDayEvent(event, eventElement, isStart) {
 		if (!opt('disableDragging') && eventElement.draggable) {
 			var origWidth;

+ 3 - 2
src/agenda/AgendaView.js

@@ -701,10 +701,11 @@ function AgendaView(element, calendar, viewName) {
 								start: startDate,
 								end: endDate,
 								className: [],
-								editable: false
+								editable: false,
+								source: {}
 							},
 							rect,
-							'fc-event fc-event-vert fc-corner-top fc-corner-bottom '
+							['fc-event', 'fc-event-vert', 'fc-corner-top', 'fc-corner-bottom']
 						));
 						if ($.browser.msie) {
 							selectionHelper.find('span.fc-event-bg').hide(); // nested opacities mess up in IE, just hide

+ 1 - 1
src/basic/BasicEventRenderer.js

@@ -77,7 +77,7 @@ function BasicEventRenderer() {
 	
 	function bindDaySeg(event, eventElement, seg) {
 		eventElementHandlers(event, eventElement);
-		if (event.editable || event.editable === undefined && opt('editable')) {
+		if (firstDefined(event.editable, event.source.editable, opt('editable'))) {
 			draggableDayEvent(event, eventElement);
 			if (seg.isEnd) {
 				resizableDayEvent(event, eventElement, seg);

+ 23 - 10
src/common/DayEventRenderer.js

@@ -119,25 +119,26 @@ function DayEventRenderer() {
 		var segCnt=segs.length;
 		var seg;
 		var event;
-		var className;
+		var classes;
 		var bounds = allDayBounds();
 		var minLeft = bounds.left;
 		var maxLeft = bounds.right;
 		var cols = []; // don't really like this system (but have to do this b/c RTL works differently in basic vs agenda)
 		var left;
 		var right;
+		var skinCss;
 		var html = '';
 		// calculate desired position/dimensions, create html
 		for (i=0; i<segCnt; i++) {
 			seg = segs[i];
 			event = seg.event;
-			className = 'fc-event fc-event-hori ';
+			classes = ['fc-event', 'fc-event-hori'];
 			if (rtl) {
 				if (seg.isStart) {
-					className += 'fc-corner-right ';
+					classes.push('fc-corner-right');
 				}
 				if (seg.isEnd) {
-					className += 'fc-corner-left ';
+					classes.push('fc-corner-left');
 				}
 				cols[0] = dayOfWeekCol(seg.end.getDay()-1);
 				cols[1] = dayOfWeekCol(seg.start.getDay());
@@ -145,19 +146,26 @@ function DayEventRenderer() {
 				right = seg.isStart ? colContentRight(cols[1]) : maxLeft;
 			}else{
 				if (seg.isStart) {
-					className += 'fc-corner-left ';
+					classes.push('fc-corner-left');
 				}
 				if (seg.isEnd) {
-					className += 'fc-corner-right ';
+					classes.push('fc-corner-right');
 				}
 				cols[0] = dayOfWeekCol(seg.start.getDay());
 				cols[1] = dayOfWeekCol(seg.end.getDay()-1);
 				left = seg.isStart ? colContentLeft(cols[0]) : minLeft;
 				right = seg.isEnd ? colContentRight(cols[1]) : maxLeft;
 			}
+			classes = classes.concat(event.className, event.source.className);
+			skinCss = getSkinCss(event, opt);
 			html +=
-				"<div class='" + className + event.className.join(' ') + "' style='position:absolute;z-index:8;left:"+left+"px'>" +
-					"<a class='fc-event-inner'" + (event.url ? " href='" + htmlEscape(event.url) + "'" : '') + ">" +
+				"<div class='" + classes.join(' ') + "' " +
+					"style='position:absolute;z-index:8;left:"+left+"px;" + skinCss + "'" +
+					">" +
+					"<a class='fc-event-inner'" +
+						(event.url ? " href='" + htmlEscape(event.url) + "'" : '') +
+						(skinCss ? " style='" + skinCss + "'" : '') +
+						">" +
 						(!event.allDay && seg.isStart ?
 							"<span class='fc-event-time'>" +
 								htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) +
@@ -165,9 +173,14 @@ function DayEventRenderer() {
 						:'') +
 						"<span class='fc-event-title'>" + htmlEscape(event.title) + "</span>" +
 					"</a>" +
-					(seg.isEnd && (event.editable || event.editable === undefined && opt('editable')) && !opt('disableResizing') ?
+					((seg.isEnd &&
+						firstDefined(event.editable, event.source.editable, opt('editable')) &&
+						!opt('disableResizing')) // TODO: make this like the other source options
+						?
 						"<div class='ui-resizable-handle ui-resizable-" + (rtl ? 'w' : 'e') + "'></div>"
-						: '') +
+						:
+						''
+						) +
 				"</div>";
 			seg.left = left;
 			seg.outerWidth = right - left;

+ 63 - 0
src/util.js

@@ -1,4 +1,6 @@
 
+fc.applyAll = applyAll;
+
 
 /* Event Date Math
 -----------------------------------------------------------------------------*/
@@ -305,3 +307,64 @@ function setDayID(cell, date) {
 }
 
 
+function getSkinCss(event, opt) {
+	var source = event.source;
+	var eventColor = event.color;
+	var sourceColor = source.color;
+	var optionColor = opt('eventColor');
+	var backgroundColor =
+		event.backgroundColor ||
+		eventColor ||
+		source.backgroundColor ||
+		sourceColor ||
+		opt('eventBackgroundColor') ||
+		optionColor;
+	var borderColor =
+		event.borderColor ||
+		eventColor ||
+		source.borderColor ||
+		sourceColor ||
+		opt('eventBorderColor') ||
+		optionColor;
+	var textColor =
+		event.textColor ||
+		source.textColor ||
+		opt('textColor');
+	var statements = [];
+	if (backgroundColor) {
+		statements.push('background-color:' + backgroundColor);
+	}
+	if (borderColor) {
+		statements.push('border-color:' + borderColor);
+	}
+	if (textColor) {
+		statements.push('color:' + textColor);
+	}
+	return statements.join(';');
+}
+
+
+function applyAll(functions, thisObj, args) {
+	if ($.isFunction(functions)) {
+		functions = [ functions ];
+	}
+	if (functions) {
+		var i;
+		var ret;
+		for (i=0; i<functions.length; i++) {
+			ret = functions[i].apply(thisObj, args) || ret;
+		}
+		return ret;
+	}
+}
+
+
+function firstDefined() {
+	for (var i=0; i<arguments.length; i++) {
+		if (arguments[i] !== undefined) {
+			return arguments[i];
+		}
+	}
+}
+
+

+ 171 - 0
tests/sources_new.html

@@ -0,0 +1,171 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<head>
+<script type='text/javascript' src='../src/_loader.js?debug'></script>
+<script type='text/javascript' src='../src/gcal/_loader.js'></script>
+<script type='text/javascript'>
+
+/*
+
+(main options)
+startParam
+endParam
+cacheParam
+ignoreTimezone
+allDayDefault
+editable
+eventColor
+eventTextColor
+eventBorderColor
+eventBackgroundColor
+
+(event source)
+startParam
+endParam
+cacheParam
+ignoreTimezone
+allDayDefault
+className
+editable
+color
+textColor
+borderColor
+backgroundColor
+
+(event)
+className
+editable
+color
+textColor
+borderColor
+backgroundColor
+
+*/
+
+$(document).ready(function() {
+
+	var date = new Date();
+	var d = date.getDate();
+	var m = date.getMonth();
+	var y = date.getFullYear();
+	
+	$('#calendar').fullCalendar({
+		header: {
+			left: 'prev,next today',
+			center: 'title',
+			right: 'month,agendaWeek,basicWeek,agendaDay,basicDay'
+		},
+		editable: true,
+		selectable: true,
+		selectHelper: true,
+		eventSources: [
+		
+			{
+				url: 'http://www.google.com/calendar/feeds/usa__en%40holiday.calendar.google.com/public/basic',
+				color: 'orange',
+				className: 'gcal',
+				success: function(events) {
+					console.log('successfully loaded gcal event data!', events);
+				},
+			},
+			
+			/*
+			$.fullCalendar.gcalFeed('http://www.google.com/calendar/feeds/usa__en%40holiday.calendar.google.com/public/basic', {
+				color: 'orange',
+				className: 'gcal'
+			}),
+			*/
+			
+			{
+				url: "../demos/json-events.php",
+				//editable: false,
+				color: 'red',
+				data: {
+					something: 'cool'
+				},
+				success: function() {
+					console.log('json-events.php is done!!!', arguments);
+				}
+			},
+		
+			{
+				color: 'purple',
+				events: [
+					{
+						title: 'All Day Event',
+						start: new Date(y, m, 1)
+					},
+					{
+						title: 'Long Event',
+						start: new Date(y, m, d-5),
+						end: new Date(y, m, d-2)
+					},
+					{
+						id: 999,
+						title: 'Repeating Event',
+						start: new Date(y, m, d-3, 16, 0),
+						allDay: false
+					},
+					{
+						id: 999,
+						title: 'Repeating Event',
+						start: new Date(y, m, d+4, 16, 0),
+						allDay: false
+					}
+				]
+			},
+			
+			{
+				events: [
+					{
+						title: 'Meeting',
+						start: new Date(y, m, d, 10, 30),
+						allDay: false
+					},
+					{
+						title: 'Lunch',
+						start: new Date(y, m, d, 12, 5),
+						end: new Date(y, m, d, 14, 43),
+						allDay: false
+					},
+					{
+						title: 'Birthday Party',
+						start: new Date(y, m, d+1, 19, 0),
+						end: new Date(y, m, d+1, 22, 30),
+						allDay: false
+					},
+					{
+						title: 'Click for Google',
+						start: new Date(y, m, 28),
+						end: new Date(y, m, 29),
+						url: 'http://google.com/'
+					}
+				]
+			}
+			
+		]
+	});
+	
+});
+
+</script>
+<style type='text/css'>
+
+body {
+	margin-top: 40px;
+	text-align: center;
+	font-size: 13px;
+	font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif;
+	}
+
+#calendar {
+	width: 900px;
+	margin: 0 auto;
+	}
+
+</style>
+</head>
+<body>
+<div id='calendar'></div>
+</body>
+</html>