aboutsummaryrefslogtreecommitdiffstats
path: root/library/fullcalendar/fullcalendar.js
diff options
context:
space:
mode:
Diffstat (limited to 'library/fullcalendar/fullcalendar.js')
-rw-r--r--library/fullcalendar/fullcalendar.js283
1 files changed, 233 insertions, 50 deletions
diff --git a/library/fullcalendar/fullcalendar.js b/library/fullcalendar/fullcalendar.js
index cbe67697d..2460eb5e7 100644
--- a/library/fullcalendar/fullcalendar.js
+++ b/library/fullcalendar/fullcalendar.js
@@ -1,5 +1,5 @@
/*!
- * FullCalendar v2.7.3
+ * FullCalendar v2.8.0
* Docs & License: http://fullcalendar.io/
* (c) 2016 Adam Shaw
*/
@@ -19,7 +19,7 @@
;;
var FC = $.fullCalendar = {
- version: "2.7.3",
+ version: "2.8.0",
internalApiVersion: 4
};
var fcViews = FC.views = {};
@@ -1054,6 +1054,20 @@ function debounce(func, wait, immediate) {
};
}
+
+// HACK around jQuery's now A+ promises: execute callback synchronously if already resolved.
+// thenFunc shouldn't accept args.
+// similar to whenResources in Scheduler plugin.
+function syncThen(promise, thenFunc) {
+ // not a promise, or an already-resolved promise?
+ if (!promise || !promise.then || promise.state() === 'resolved') {
+ return $.when(thenFunc()); // resolve immediately
+ }
+ else if (thenFunc) {
+ return promise.then(thenFunc);
+ }
+}
+
;;
var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/;
@@ -3960,7 +3974,7 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
fillSegTag: 'div', // subclasses can override
- // Builds the HTML needed for one fill segment. Generic enought o work with different types.
+ // Builds the HTML needed for one fill segment. Generic enough to work with different types.
fillSegHtml: function(type, seg) {
// custom hooks per-type
@@ -8106,15 +8120,14 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
this.calendar.freezeContentHeight();
- return this.clear().then(function() { // clear the content first (async)
+ return syncThen(this.clear(), function() { // clear the content first
return (
_this.displaying =
- $.when(_this.displayView(date)) // displayView might return a promise
- .then(function() {
- _this.forceScroll(_this.computeInitialScroll(scrollState));
- _this.calendar.unfreezeContentHeight();
- _this.triggerRender();
- })
+ syncThen(_this.displayView(date), function() { // displayView might return a promise
+ _this.forceScroll(_this.computeInitialScroll(scrollState));
+ _this.calendar.unfreezeContentHeight();
+ _this.triggerRender();
+ })
);
});
},
@@ -8128,7 +8141,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
var displaying = this.displaying;
if (displaying) { // previously displayed, or in the process of being displayed?
- return displaying.then(function() { // wait for the display to finish
+ return syncThen(displaying, function() { // wait for the display to finish
_this.displaying = null;
_this.clearEvents();
return _this.clearView(); // might return a promise. chain it
@@ -9321,6 +9334,7 @@ function Calendar_constructor(element, overrides) {
t.render = render;
t.destroy = destroy;
t.refetchEvents = refetchEvents;
+ t.refetchEventSources = refetchEventSources;
t.reportEvents = reportEvents;
t.reportEventChange = reportEventChange;
t.rerenderEvents = renderEvents; // `renderEvents` serves as a rerender. an API method
@@ -9511,6 +9525,7 @@ function Calendar_constructor(element, overrides) {
EventManager.call(t, options);
var isFetchNeeded = t.isFetchNeeded;
var fetchEvents = t.fetchEvents;
+ var fetchEventSources = t.fetchEventSources;
@@ -9750,11 +9765,16 @@ function Calendar_constructor(element, overrides) {
function refetchEvents() { // can be called as an API method
- destroyEvents(); // so that events are cleared before user starts waiting for AJAX
fetchAndRenderEvents();
}
+ // TODO: move this into EventManager?
+ function refetchEventSources(matchInputs) {
+ fetchEventSources(t.getEventSourcesByMatchArray(matchInputs));
+ }
+
+
function renderEvents() { // destroys old events if previously rendered
if (elementVisible()) {
freezeContentHeight();
@@ -9762,13 +9782,6 @@ function Calendar_constructor(element, overrides) {
unfreezeContentHeight();
}
}
-
-
- function destroyEvents() {
- freezeContentHeight();
- currentView.clearEvents();
- unfreezeContentHeight();
- }
function getAndRenderEvents() {
@@ -9979,7 +9992,7 @@ function Calendar_constructor(element, overrides) {
Calendar.defaults = {
- titleRangeSeparator: ' \u2014 ', // emphasized dash
+ titleRangeSeparator: ' \u2013 ', // en dash
monthYearFormat: 'MMMM YYYY', // required for en. other languages rely on datepicker computable option
defaultTimedEventDuration: '02:00:00',
@@ -10528,14 +10541,14 @@ function Header(calendar, options) {
function disableButton(buttonName) {
el.find('.fc-' + buttonName + '-button')
- .attr('disabled', 'disabled')
+ .prop('disabled', true)
.addClass(tm + '-state-disabled');
}
function enableButton(buttonName) {
el.find('.fc-' + buttonName + '-button')
- .removeAttr('disabled')
+ .prop('disabled', false)
.removeClass(tm + '-state-disabled');
}
@@ -10566,8 +10579,14 @@ function EventManager(options) { // assumed to be a calendar
// exports
t.isFetchNeeded = isFetchNeeded;
t.fetchEvents = fetchEvents;
+ t.fetchEventSources = fetchEventSources;
+ t.getEventSources = getEventSources;
+ t.getEventSourceById = getEventSourceById;
+ t.getEventSourcesByMatchArray = getEventSourcesByMatchArray;
+ t.getEventSourcesByMatch = getEventSourcesByMatch;
t.addEventSource = addEventSource;
t.removeEventSource = removeEventSource;
+ t.removeEventSources = removeEventSources;
t.updateEvent = updateEvent;
t.renderEvent = renderEvent;
t.removeEvents = removeEvents;
@@ -10585,8 +10604,7 @@ function EventManager(options) { // assumed to be a calendar
var stickySource = { events: [] };
var sources = [ stickySource ];
var rangeStart, rangeEnd;
- var currentFetchID = 0;
- var pendingSourceCnt = 0;
+ var pendingSourceCnt = 0; // outstanding fetch requests, max one per source
var cache = []; // holds events that have already been expanded
@@ -10616,23 +10634,58 @@ function EventManager(options) { // assumed to be a calendar
function fetchEvents(start, end) {
rangeStart = start;
rangeEnd = end;
- cache = [];
- var fetchID = ++currentFetchID;
- var len = sources.length;
- pendingSourceCnt = len;
- for (var i=0; i<len; i++) {
- fetchEventSource(sources[i], fetchID);
+ fetchEventSources(sources, 'reset');
+ }
+
+
+ // expects an array of event source objects (the originals, not copies)
+ // `specialFetchType` is an optimization parameter that affects purging of the event cache.
+ function fetchEventSources(specificSources, specialFetchType) {
+ var i, source;
+
+ if (specialFetchType === 'reset') {
+ cache = [];
+ }
+ else if (specialFetchType !== 'add') {
+ cache = excludeEventsBySources(cache, specificSources);
+ }
+
+ for (i = 0; i < specificSources.length; i++) {
+ source = specificSources[i];
+
+ // already-pending sources have already been accounted for in pendingSourceCnt
+ if (source._status !== 'pending') {
+ pendingSourceCnt++;
+ }
+
+ source._fetchId = (source._fetchId || 0) + 1;
+ source._status = 'pending';
+ }
+
+ for (i = 0; i < specificSources.length; i++) {
+ source = specificSources[i];
+
+ tryFetchEventSource(source, source._fetchId);
}
}
-
-
- function fetchEventSource(source, fetchID) {
+
+
+ // fetches an event source and processes its result ONLY if it is still the current fetch.
+ // caller is responsible for incrementing pendingSourceCnt first.
+ function tryFetchEventSource(source, fetchId) {
_fetchEventSource(source, function(eventInputs) {
var isArraySource = $.isArray(source.events);
var i, eventInput;
var abstractEvent;
- if (fetchID == currentFetchID) {
+ if (
+ // is this the source's most recent fetch?
+ // if not, rely on an upcoming fetch of this source to decrement pendingSourceCnt
+ fetchId === source._fetchId &&
+ // event source no longer valid?
+ source._status !== 'rejected'
+ ) {
+ source._status = 'resolved';
if (eventInputs) {
for (i = 0; i < eventInputs.length; i++) {
@@ -10654,13 +10707,29 @@ function EventManager(options) { // assumed to be a calendar
}
}
- pendingSourceCnt--;
- if (!pendingSourceCnt) {
- reportEvents(cache);
- }
+ decrementPendingSourceCnt();
}
});
}
+
+
+ function rejectEventSource(source) {
+ var wasPending = source._status === 'pending';
+
+ source._status = 'rejected';
+
+ if (wasPending) {
+ decrementPendingSourceCnt();
+ }
+ }
+
+
+ function decrementPendingSourceCnt() {
+ pendingSourceCnt--;
+ if (!pendingSourceCnt) {
+ reportEvents(cache);
+ }
+ }
function _fetchEventSource(source, callback) {
@@ -10776,14 +10845,13 @@ function EventManager(options) { // assumed to be a calendar
/* Sources
-----------------------------------------------------------------------------*/
-
+
function addEventSource(sourceInput) {
var source = buildEventSource(sourceInput);
if (source) {
sources.push(source);
- pendingSourceCnt++;
- fetchEventSource(source, currentFetchID); // will eventually call reportEvents
+ fetchEventSources([ source ], 'add'); // will eventually call reportEvents
}
}
@@ -10833,19 +10901,120 @@ function EventManager(options) { // assumed to be a calendar
}
- function removeEventSource(source) {
- sources = $.grep(sources, function(src) {
- return !isSourcesEqual(src, source);
- });
- // remove all client events from that source
- cache = $.grep(cache, function(e) {
- return !isSourcesEqual(e.source, source);
- });
+ function removeEventSource(matchInput) {
+ removeSpecificEventSources(
+ getEventSourcesByMatch(matchInput)
+ );
+ }
+
+
+ // if called with no arguments, removes all.
+ function removeEventSources(matchInputs) {
+ if (matchInputs == null) {
+ removeSpecificEventSources(sources, true); // isAll=true
+ }
+ else {
+ removeSpecificEventSources(
+ getEventSourcesByMatchArray(matchInputs)
+ );
+ }
+ }
+
+
+ function removeSpecificEventSources(targetSources, isAll) {
+ var i;
+
+ // cancel pending requests
+ for (i = 0; i < targetSources.length; i++) {
+ rejectEventSource(targetSources[i]);
+ }
+
+ if (isAll) { // an optimization
+ sources = [];
+ cache = [];
+ }
+ else {
+ // remove from persisted source list
+ sources = $.grep(sources, function(source) {
+ for (i = 0; i < targetSources.length; i++) {
+ if (source === targetSources[i]) {
+ return false; // exclude
+ }
+ }
+ return true; // include
+ });
+
+ cache = excludeEventsBySources(cache, targetSources);
+ }
+
reportEvents(cache);
}
- function isSourcesEqual(source1, source2) {
+ function getEventSources() {
+ return sources.slice(1); // returns a shallow copy of sources with stickySource removed
+ }
+
+
+ function getEventSourceById(id) {
+ return $.grep(sources, function(source) {
+ return source.id && source.id === id;
+ })[0];
+ }
+
+
+ // like getEventSourcesByMatch, but accepts multple match criteria (like multiple IDs)
+ function getEventSourcesByMatchArray(matchInputs) {
+
+ // coerce into an array
+ if (!matchInputs) {
+ matchInputs = [];
+ }
+ else if (!$.isArray(matchInputs)) {
+ matchInputs = [ matchInputs ];
+ }
+
+ var matchingSources = [];
+ var i;
+
+ // resolve raw inputs to real event source objects
+ for (i = 0; i < matchInputs.length; i++) {
+ matchingSources.push.apply( // append
+ matchingSources,
+ getEventSourcesByMatch(matchInputs[i])
+ );
+ }
+
+ return matchingSources;
+ }
+
+
+ // matchInput can either by a real event source object, an ID, or the function/URL for the source.
+ // returns an array of matching source objects.
+ function getEventSourcesByMatch(matchInput) {
+ var i, source;
+
+ // given an proper event source object
+ for (i = 0; i < sources.length; i++) {
+ source = sources[i];
+ if (source === matchInput) {
+ return [ source ];
+ }
+ }
+
+ // an ID match
+ source = getEventSourceById(matchInput);
+ if (source) {
+ return [ source ];
+ }
+
+ return $.grep(sources, function(source) {
+ return isSourcesEquivalent(matchInput, source);
+ });
+ }
+
+
+ function isSourcesEquivalent(source1, source2) {
return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2);
}
@@ -10858,6 +11027,20 @@ function EventManager(options) { // assumed to be a calendar
) ||
source; // the given argument *is* the primitive
}
+
+
+ // util
+ // returns a filtered array without events that are part of any of the given sources
+ function excludeEventsBySources(specificEvents, specificSources) {
+ return $.grep(specificEvents, function(event) {
+ for (var i = 0; i < specificSources.length; i++) {
+ if (event.source === specificSources[i]) {
+ return false; // exclude
+ }
+ }
+ return true; // keep
+ });
+ }