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.

This commit is contained in:
Adam Shaw 2011-02-10 22:40:16 -08:00
parent f3fcd57530
commit 06e4734b05
7 changed files with 449 additions and 79 deletions

View file

@ -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; var eventGUID = 1;
function EventManager(options, sources) { function EventManager(options, _sources) {
var t = this; var t = this;
@ -24,6 +32,8 @@ function EventManager(options, sources) {
// locals // locals
var stickySource = { events: [] };
var sources = [ stickySource ];
var rangeStart, rangeEnd; var rangeStart, rangeEnd;
var currentFetchID = 0; var currentFetchID = 0;
var pendingSourceCnt = 0; var pendingSourceCnt = 0;
@ -31,6 +41,11 @@ function EventManager(options, sources) {
var cache = []; var cache = [];
for (var i=0; i<_sources.length; i++) {
_addEventSource(_sources[i]);
}
/* Fetching /* Fetching
-----------------------------------------------------------------------------*/ -----------------------------------------------------------------------------*/
@ -57,11 +72,13 @@ function EventManager(options, sources) {
function fetchEventSource(source, fetchID) { function fetchEventSource(source, fetchID) {
_fetchEventSource(source, function(events) { _fetchEventSource(source, function(events) {
if (fetchID == currentFetchID) { if (fetchID == currentFetchID) {
if (events) {
for (var i=0; i<events.length; i++) { for (var i=0; i<events.length; i++) {
normalizeEvent(events[i]); normalizeEvent(events[i], source);
events[i].source = source; events[i].source = source;
} }
cache = cache.concat(events); cache = cache.concat(events);
}
pendingSourceCnt--; pendingSourceCnt--;
if (!pendingSourceCnt) { if (!pendingSourceCnt) {
reportEvents(cache); reportEvents(cache);
@ -72,35 +89,76 @@ function EventManager(options, sources) {
function _fetchEventSource(source, callback) { function _fetchEventSource(source, callback) {
if (typeof source == 'string') { var i;
var params = {}; var fetchers = fc.sourceFetchers;
params[options.startParam] = Math.round(rangeStart.getTime() / 1000); var res;
params[options.endParam] = Math.round(rangeEnd.getTime() / 1000); for (i=0; i<fetchers.length; i++) {
if (options.cacheParam) { res = fetchers[i](source, rangeStart, rangeEnd, callback);
params[options.cacheParam] = (new Date()).getTime(); // TODO: deprecate cacheParam 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;
}
}
var events = source.events;
if (events) {
if ($.isFunction(events)) {
pushLoading(); pushLoading();
// TODO: respect cache param in ajaxSetup events(cloneDate(rangeStart), cloneDate(rangeEnd), function(events) {
$.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); callback(events);
} popLoading();
}); });
} }
else if ($.isFunction(source)) { else if ($.isArray(events)) {
pushLoading();
source(cloneDate(rangeStart), cloneDate(rangeEnd), function(events) {
popLoading();
callback(events); callback(events);
});
} }
else { else {
callback(source); // src is an array 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();
}
}));
}
} }
} }
@ -110,24 +168,37 @@ function EventManager(options, sources) {
-----------------------------------------------------------------------------*/ -----------------------------------------------------------------------------*/
// first event source is reserved for "sticky" events
sources.unshift([]);
function addEventSource(source) { function addEventSource(source) {
sources.push(source); source = _addEventSource(source);
if (source) {
pendingSourceCnt++; pendingSourceCnt++;
fetchEventSource(source, currentFetchID); // will eventually call reportEvents 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) { function removeEventSource(source) {
sources = $.grep(sources, function(src) { sources = $.grep(sources, function(src) {
return src != source; return !isSourcesEqual(src, source);
}); });
// remove all client events from that source // remove all client events from that source
cache = $.grep(cache, function(e) { cache = $.grep(cache, function(e) {
return e.source != source; return !isSourcesEqual(e.source, source);
}); });
reportEvents(cache); reportEvents(cache);
} }
@ -163,20 +234,20 @@ function EventManager(options, sources) {
e.allDay = event.allDay; e.allDay = event.allDay;
e.className = event.className; e.className = event.className;
e.editable = event.editable; e.editable = event.editable;
normalizeEvent(e); normalizeEvent(e, e.source);
} }
} }
normalizeEvent(event); normalizeEvent(event, event.source);
reportEvents(cache); reportEvents(cache);
} }
function renderEvent(event, stick) { function renderEvent(event, stick) {
normalizeEvent(event); normalizeEvent(event, event.source || stickySource);
if (!event.source) { if (!event.source) {
if (stick) { if (stick) {
sources[0].push(event); stickySource.events.push(event);
event.source = sources[0]; event.source = stickySource;
} }
cache.push(event); cache.push(event);
} }
@ -189,8 +260,8 @@ function EventManager(options, sources) {
cache = []; cache = [];
// clear all array sources // clear all array sources
for (var i=0; i<sources.length; i++) { for (var i=0; i<sources.length; i++) {
if (typeof sources[i] == 'object') { if ($.isArray(sources[i].events)) {
sources[i] = []; sources[i].events = [];
} }
} }
}else{ }else{
@ -203,9 +274,8 @@ function EventManager(options, sources) {
cache = $.grep(cache, filter, true); cache = $.grep(cache, filter, true);
// remove events from array sources // remove events from array sources
for (var i=0; i<sources.length; i++) { for (var i=0; i<sources.length; i++) {
if (typeof sources[i] == 'object') { if ($.isArray(sources[i].events)) {
sources[i] = $.grep(sources[i], filter, true); sources[i].events = $.grep(sources[i].events, filter, true);
// TODO: event objects' sources will no longer be correct reference :(
} }
} }
} }
@ -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 + ''); event._id = event._id || (event.id === undefined ? '_fc' + eventGUID++ : event.id + '');
if (event.date) { if (event.date) {
if (!event.start) { if (!event.start) {
@ -259,14 +329,14 @@ function EventManager(options, sources) {
} }
delete event.date; 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); event.end = parseDate(event.end, options.ignoreTimezone);
if (event.end && event.end <= event.start) { if (event.end && event.end <= event.start) {
event.end = null; event.end = null;
} }
event._end = event.end ? cloneDate(event.end) : null; event._end = event.end ? cloneDate(event.end) : null;
if (event.allDay === undefined) { if (event.allDay === undefined) {
event.allDay = options.allDayDefault; event.allDay = firstDefined(source.allDayDefault, options.allDayDefault);
} }
if (event.className) { if (event.className) {
if (typeof event.className == 'string') { if (typeof event.className == 'string') {
@ -279,4 +349,35 @@ function EventManager(options, sources) {
} }
/* 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;
}
} }

View file

@ -139,7 +139,7 @@ function AgendaEventRenderer() {
var i, segCnt=segs.length, seg, var i, segCnt=segs.length, seg,
event, event,
className, classes,
top, bottom, top, bottom,
colI, levelI, forward, colI, levelI, forward,
leftmost, leftmost,
@ -171,12 +171,12 @@ function AgendaEventRenderer() {
for (i=0; i<segCnt; i++) { for (i=0; i<segCnt; i++) {
seg = segs[i]; seg = segs[i];
event = seg.event; event = seg.event;
className = 'fc-event fc-event-vert '; classes = ['fc-event', 'fc-event-vert'];
if (seg.isStart) { if (seg.isStart) {
className += 'fc-corner-top '; classes.push('fc-corner-top');
} }
if (seg.isEnd) { if (seg.isEnd) {
className += 'fc-corner-bottom '; classes.push('fc-corner-bottom');
} }
top = timePosition(seg.start, seg.start); top = timePosition(seg.start, seg.start);
bottom = timePosition(seg.start, seg.end); bottom = timePosition(seg.start, seg.end);
@ -205,7 +205,7 @@ function AgendaEventRenderer() {
seg.left = left; seg.left = left;
seg.outerWidth = outerWidth; seg.outerWidth = outerWidth;
seg.outerHeight = bottom - top; seg.outerHeight = bottom - top;
html += slotSegHtml(event, seg, className); html += slotSegHtml(event, seg, classes);
} }
slotSegmentContainer[0].innerHTML = html; // faster than html() slotSegmentContainer[0].innerHTML = html; // faster than html()
eventElements = slotSegmentContainer.children(); eventElements = slotSegmentContainer.children();
@ -278,10 +278,16 @@ function AgendaEventRenderer() {
} }
function slotSegHtml(event, seg, className) { function slotSegHtml(event, seg, classes) {
return "<div class='" + className + event.className.join(' ') + "' style='position:absolute;z-index:8;top:" + seg.top + "px;left:" + seg.left + "px'>" + classes = classes.concat(event.className, event.source.className);
"<a class='fc-event-inner'" + (event.url ? " href='" + htmlEscape(event.url) + "'" : '') + ">" + // good for escaping quotes? var skinCss = getSkinCss(event, opt);
"<div class='fc-event-head'>" + 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'>" + "<div class='fc-event-time'>" +
htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) + htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) +
"</div>" + "</div>" +
@ -293,16 +299,22 @@ function AgendaEventRenderer() {
"</div>" + "</div>" +
"<div class='fc-event-bg'></div>" + "<div class='fc-event-bg'></div>" +
"</a>" + "</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 class='ui-resizable-handle ui-resizable-s'>=</div>"
: '') + :
''
) +
"</div>"; "</div>";
} }
function bindDaySeg(event, eventElement, seg) { function bindDaySeg(event, eventElement, seg) {
eventElementHandlers(event, eventElement); eventElementHandlers(event, eventElement);
if (event.editable || event.editable === undefined && opt('editable')) { if (isEventEditable(event)) {
draggableDayEvent(event, eventElement, seg.isStart); draggableDayEvent(event, eventElement, seg.isStart);
if (seg.isEnd) { if (seg.isEnd) {
resizableDayEvent(event, eventElement, seg); resizableDayEvent(event, eventElement, seg);
@ -313,7 +325,7 @@ function AgendaEventRenderer() {
function bindSlotSeg(event, eventElement, seg) { function bindSlotSeg(event, eventElement, seg) {
eventElementHandlers(event, eventElement); eventElementHandlers(event, eventElement);
if (event.editable || event.editable === undefined && opt('editable')) { if (isEventEditable(event)) {
var timeElement = eventElement.find('span.fc-event-time'); var timeElement = eventElement.find('span.fc-event-time');
draggableSlotEvent(event, eventElement, timeElement); draggableSlotEvent(event, eventElement, timeElement);
if (seg.isEnd) { if (seg.isEnd) {
@ -323,6 +335,11 @@ function AgendaEventRenderer() {
} }
function isEventEditable(event) {
return firstDefined(event.editable, event.source.editable, opt('editable'));
}
/* Dragging /* Dragging
-----------------------------------------------------------------------------------*/ -----------------------------------------------------------------------------------*/
@ -330,6 +347,10 @@ function AgendaEventRenderer() {
// when event starts out FULL-DAY // 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) { function draggableDayEvent(event, eventElement, isStart) {
if (!opt('disableDragging') && eventElement.draggable) { if (!opt('disableDragging') && eventElement.draggable) {
var origWidth; var origWidth;

View file

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

View file

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

View file

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

View file

@ -1,4 +1,6 @@
fc.applyAll = applyAll;
/* Event Date Math /* 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
tests/sources_new.html Normal file
View file

@ -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>