diff options
Diffstat (limited to 'library/fullcalendar/fullcalendar.js')
-rw-r--r-- | library/fullcalendar/fullcalendar.js | 283 |
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 + }); + } |