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;
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) {
if (events) {
for (var i=0; i<events.length; i++) {
normalizeEvent(events[i]);
normalizeEvent(events[i], source);
events[i].source = source;
}
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;
}
}
var events = source.events;
if (events) {
if ($.isFunction(events)) {
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();
events(cloneDate(rangeStart), cloneDate(rangeEnd), function(events) {
callback(events);
}
popLoading();
});
}
else if ($.isFunction(source)) {
pushLoading();
source(cloneDate(rangeStart), cloneDate(rangeEnd), function(events) {
popLoading();
else if ($.isArray(events)) {
callback(events);
});
}
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) {
sources.push(source);
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') {
@ -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,
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;

View file

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

View file

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

View file

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

View file

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