FC.sourceNormalizers = []; FC.sourceFetchers = []; var ajaxDefaults = { dataType: 'json', cache: false }; var eventGUID = 1; function EventManager() { // assumed to be a calendar var t = this; // 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.updateEvents = updateEvents; t.renderEvent = renderEvent; t.renderEvents = renderEvents; t.removeEvents = removeEvents; t.clientEvents = clientEvents; t.mutateEvent = mutateEvent; t.normalizeEventDates = normalizeEventDates; t.normalizeEventTimes = normalizeEventTimes; // imports var reportEvents = t.reportEvents; // locals var stickySource = { events: [] }; var sources = [ stickySource ]; var rangeStart, rangeEnd; var pendingSourceCnt = 0; // outstanding fetch requests, max one per source var cache = []; // holds events that have already been expanded $.each( (t.options.events ? [ t.options.events ] : []).concat(t.options.eventSources || []), function(i, sourceInput) { var source = buildEventSource(sourceInput); if (source) { sources.push(source); } } ); /* Fetching -----------------------------------------------------------------------------*/ // start and end are assumed to be unzoned function isFetchNeeded(start, end) { return !rangeStart || // nothing has been fetched yet? start < rangeStart || end > rangeEnd; // is part of the new range outside of the old range? } function fetchEvents(start, end) { rangeStart = start; rangeEnd = end; 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); } } // 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 ( // 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++) { eventInput = eventInputs[i]; if (isArraySource) { // array sources have already been convert to Event Objects abstractEvent = eventInput; } else { abstractEvent = buildEventFromInput(eventInput, source); } if (abstractEvent) { // not false (an invalid event) cache.push.apply( cache, expandEvent(abstractEvent) // add individual expanded events to the 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) { var i; var fetchers = FC.sourceFetchers; var res; for (i=0; i= eventStart && innerSpan.end <= eventEnd; }; // Returns a list of events that the given event should be compared against when being considered for a move to // the specified span. Attached to the Calendar's prototype because EventManager is a mixin for a Calendar. Calendar.prototype.getPeerEvents = function(span, event) { var cache = this.getEventCache(); var peerEvents = []; var i, otherEvent; for (i = 0; i < cache.length; i++) { otherEvent = cache[i]; if ( !event || event._id !== otherEvent._id // don't compare the event to itself or other related [repeating] events ) { peerEvents.push(otherEvent); } } return peerEvents; }; // updates the "backup" properties, which are preserved in order to compute diffs later on. function backupEventDates(event) { event._allDay = event.allDay; event._start = event.start.clone(); event._end = event.end ? event.end.clone() : null; } /* Overlapping / Constraining -----------------------------------------------------------------------------------------*/ // Determines if the given event can be relocated to the given span (unzoned start/end with other misc data) Calendar.prototype.isEventSpanAllowed = function(span, event) { var source = event.source || {}; var constraint = firstDefined( event.constraint, source.constraint, this.options.eventConstraint ); var overlap = firstDefined( event.overlap, source.overlap, this.options.eventOverlap ); return this.isSpanAllowed(span, constraint, overlap, event) && (!this.options.eventAllow || this.options.eventAllow(span, event) !== false); }; // Determines if an external event can be relocated to the given span (unzoned start/end with other misc data) Calendar.prototype.isExternalSpanAllowed = function(eventSpan, eventLocation, eventProps) { var eventInput; var event; // note: very similar logic is in View's reportExternalDrop if (eventProps) { eventInput = $.extend({}, eventProps, eventLocation); event = this.expandEvent( this.buildEventFromInput(eventInput) )[0]; } if (event) { return this.isEventSpanAllowed(eventSpan, event); } else { // treat it as a selection return this.isSelectionSpanAllowed(eventSpan); } }; // Determines the given span (unzoned start/end with other misc data) can be selected. Calendar.prototype.isSelectionSpanAllowed = function(span) { return this.isSpanAllowed(span, this.options.selectConstraint, this.options.selectOverlap) && (!this.options.selectAllow || this.options.selectAllow(span) !== false); }; // Returns true if the given span (caused by an event drop/resize or a selection) is allowed to exist // according to the constraint/overlap settings. // `event` is not required if checking a selection. Calendar.prototype.isSpanAllowed = function(span, constraint, overlap, event) { var constraintEvents; var anyContainment; var peerEvents; var i, peerEvent; var peerOverlap; // the range must be fully contained by at least one of produced constraint events if (constraint != null) { // not treated as an event! intermediate data structure // TODO: use ranges in the future constraintEvents = this.constraintToEvents(constraint); if (constraintEvents) { // not invalid anyContainment = false; for (i = 0; i < constraintEvents.length; i++) { if (this.spanContainsSpan(constraintEvents[i], span)) { anyContainment = true; break; } } if (!anyContainment) { return false; } } } peerEvents = this.getPeerEvents(span, event); for (i = 0; i < peerEvents.length; i++) { peerEvent = peerEvents[i]; // there needs to be an actual intersection before disallowing anything if (this.eventIntersectsRange(peerEvent, span)) { // evaluate overlap for the given range and short-circuit if necessary if (overlap === false) { return false; } // if the event's overlap is a test function, pass the peer event in question as the first param else if (typeof overlap === 'function' && !overlap(peerEvent, event)) { return false; } // if we are computing if the given range is allowable for an event, consider the other event's // EventObject-specific or Source-specific `overlap` property if (event) { peerOverlap = firstDefined( peerEvent.overlap, (peerEvent.source || {}).overlap // we already considered the global `eventOverlap` ); if (peerOverlap === false) { return false; } // if the peer event's overlap is a test function, pass the subject event as the first param if (typeof peerOverlap === 'function' && !peerOverlap(event, peerEvent)) { return false; } } } } return true; }; // Given an event input from the API, produces an array of event objects. Possible event inputs: // 'businessHours' // An event ID (number or string) // An object with specific start/end dates or a recurring event (like what businessHours accepts) Calendar.prototype.constraintToEvents = function(constraintInput) { if (constraintInput === 'businessHours') { return this.getCurrentBusinessHourEvents(); } if (typeof constraintInput === 'object') { if (constraintInput.start != null) { // needs to be event-like input return this.expandEvent(this.buildEventFromInput(constraintInput)); } else { return null; // invalid } } return this.clientEvents(constraintInput); // probably an ID }; // Does the event's date range intersect with the given range? // start/end already assumed to have stripped zones :( Calendar.prototype.eventIntersectsRange = function(event, range) { var eventStart = event.start.clone().stripZone(); var eventEnd = this.getEventEnd(event).stripZone(); return range.start < eventEnd && range.end > eventStart; }; /* Business Hours -----------------------------------------------------------------------------------------*/ var BUSINESS_HOUR_EVENT_DEFAULTS = { id: '_fcBusinessHours', // will relate events from different calls to expandEvent start: '09:00', end: '17:00', dow: [ 1, 2, 3, 4, 5 ], // monday - friday rendering: 'inverse-background' // classNames are defined in businessHoursSegClasses }; // Return events objects for business hours within the current view. // Abuse of our event system :( Calendar.prototype.getCurrentBusinessHourEvents = function(wholeDay) { return this.computeBusinessHourEvents(wholeDay, this.options.businessHours); }; // Given a raw input value from options, return events objects for business hours within the current view. Calendar.prototype.computeBusinessHourEvents = function(wholeDay, input) { if (input === true) { return this.expandBusinessHourEvents(wholeDay, [ {} ]); } else if ($.isPlainObject(input)) { return this.expandBusinessHourEvents(wholeDay, [ input ]); } else if ($.isArray(input)) { return this.expandBusinessHourEvents(wholeDay, input, true); } else { return []; } }; // inputs expected to be an array of objects. // if ignoreNoDow is true, will ignore entries that don't specify a day-of-week (dow) key. Calendar.prototype.expandBusinessHourEvents = function(wholeDay, inputs, ignoreNoDow) { var view = this.getView(); var events = []; var i, input; for (i = 0; i < inputs.length; i++) { input = inputs[i]; if (ignoreNoDow && !input.dow) { continue; } // give defaults. will make a copy input = $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, input); // if a whole-day series is requested, clear the start/end times if (wholeDay) { input.start = null; input.end = null; } events.push.apply(events, // append this.expandEvent( this.buildEventFromInput(input), view.start, view.end ) ); } return events; };