| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344 |
- 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<fetchers.length; i++) {
- res = fetchers[i].call(
- t, // this, the Calendar object
- source,
- rangeStart.clone(),
- rangeEnd.clone(),
- t.options.timezone,
- 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)) {
- t.pushLoading();
- events.call(
- t, // this, the Calendar object
- rangeStart.clone(),
- rangeEnd.clone(),
- t.options.timezone,
- function(events) {
- callback(events);
- t.popLoading();
- }
- );
- }
- else if ($.isArray(events)) {
- callback(events);
- }
- else {
- callback();
- }
- }else{
- var url = source.url;
- if (url) {
- var success = source.success;
- var error = source.error;
- var complete = source.complete;
- // retrieve any outbound GET/POST $.ajax data from the options
- var customData;
- if ($.isFunction(source.data)) {
- // supplied as a function that returns a key/value object
- customData = source.data();
- }
- else {
- // supplied as a straight key/value object
- customData = source.data;
- }
- // use a copy of the custom data so we can modify the parameters
- // and not affect the passed-in object.
- var data = $.extend({}, customData || {});
- var startParam = firstDefined(source.startParam, t.options.startParam);
- var endParam = firstDefined(source.endParam, t.options.endParam);
- var timezoneParam = firstDefined(source.timezoneParam, t.options.timezoneParam);
- if (startParam) {
- data[startParam] = rangeStart.format();
- }
- if (endParam) {
- data[endParam] = rangeEnd.format();
- }
- if (t.options.timezone && t.options.timezone != 'local') {
- data[timezoneParam] = t.options.timezone;
- }
- t.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);
- t.popLoading();
- }
- }));
- }else{
- callback();
- }
- }
- }
- /* Sources
- -----------------------------------------------------------------------------*/
- function addEventSource(sourceInput) {
- var source = buildEventSource(sourceInput);
- if (source) {
- sources.push(source);
- fetchEventSources([ source ], 'add'); // will eventually call reportEvents
- }
- }
- function buildEventSource(sourceInput) { // will return undefined if invalid source
- var normalizers = FC.sourceNormalizers;
- var source;
- var i;
- if ($.isFunction(sourceInput) || $.isArray(sourceInput)) {
- source = { events: sourceInput };
- }
- else if (typeof sourceInput === 'string') {
- source = { url: sourceInput };
- }
- else if (typeof sourceInput === 'object') {
- source = $.extend({}, sourceInput); // shallow copy
- }
- if (source) {
- // TODO: repeat code, same code for event classNames
- if (source.className) {
- if (typeof source.className === 'string') {
- source.className = source.className.split(/\s+/);
- }
- // otherwise, assumed to be an array
- }
- else {
- source.className = [];
- }
- // for array sources, we convert to standard Event Objects up front
- if ($.isArray(source.events)) {
- source.origArray = source.events; // for removeEventSource
- source.events = $.map(source.events, function(eventInput) {
- return buildEventFromInput(eventInput, source);
- });
- }
- for (i=0; i<normalizers.length; i++) {
- normalizers[i].call(t, source);
- }
- return 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 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);
- }
- function getSourcePrimitive(source) {
- return (
- (typeof source === 'object') ? // a normalized event source?
- (source.origArray || source.googleCalendarId || source.url || source.events) : // get the primitive
- null
- ) ||
- 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
- });
- }
- /* Manipulation
- -----------------------------------------------------------------------------*/
- // Only ever called from the externally-facing API
- function updateEvent(event) {
- updateEvents([ event ]);
- }
- // Only ever called from the externally-facing API
- function updateEvents(events) {
- var i, event;
- for (i = 0; i < events.length; i++) {
- event = events[i];
- // massage start/end values, even if date string values
- event.start = t.moment(event.start);
- if (event.end) {
- event.end = t.moment(event.end);
- }
- else {
- event.end = null;
- }
- mutateEvent(event, getMiscEventProps(event)); // will handle start/end/allDay normalization
- }
- reportEvents(cache); // reports event modifications (so we can redraw)
- }
- // Returns a hash of misc event properties that should be copied over to related events.
- function getMiscEventProps(event) {
- var props = {};
- $.each(event, function(name, val) {
- if (isMiscEventPropName(name)) {
- if (val !== undefined && isAtomic(val)) { // a defined non-object
- props[name] = val;
- }
- }
- });
- return props;
- }
- // non-date-related, non-id-related, non-secret
- function isMiscEventPropName(name) {
- return !/^_|^(id|allDay|start|end)$/.test(name);
- }
- // returns the expanded events that were created
- function renderEvent(eventInput, stick) {
- return renderEvents([ eventInput ], stick);
- }
- // returns the expanded events that were created
- function renderEvents(eventInputs, stick) {
- var renderedEvents = [];
- var renderableEvents;
- var abstractEvent;
- var i, j, event;
- for (i = 0; i < eventInputs.length; i++) {
- abstractEvent = buildEventFromInput(eventInputs[i]);
- if (abstractEvent) { // not false (a valid input)
- renderableEvents = expandEvent(abstractEvent);
- for (j = 0; j < renderableEvents.length; j++) {
- event = renderableEvents[j];
- if (!event.source) {
- if (stick) {
- stickySource.events.push(event);
- event.source = stickySource;
- }
- cache.push(event);
- }
- }
- renderedEvents = renderedEvents.concat(renderableEvents);
- }
- }
- if (renderedEvents.length) { // any new events rendered?
- reportEvents(cache);
- }
- return renderedEvents;
- }
- function removeEvents(filter) {
- var eventID;
- var i;
- if (filter == null) { // null or undefined. remove all events
- filter = function() { return true; }; // will always match
- }
- else if (!$.isFunction(filter)) { // an event ID
- eventID = filter + '';
- filter = function(event) {
- return event._id == eventID;
- };
- }
- // Purge event(s) from our local cache
- cache = $.grep(cache, filter, true); // inverse=true
- // Remove events from array sources.
- // This works because they have been converted to official Event Objects up front.
- // (and as a result, event._id has been calculated).
- for (i=0; i<sources.length; i++) {
- if ($.isArray(sources[i].events)) {
- sources[i].events = $.grep(sources[i].events, filter, true);
- }
- }
- reportEvents(cache);
- }
- function clientEvents(filter) {
- if ($.isFunction(filter)) {
- return $.grep(cache, filter);
- }
- else if (filter != null) { // not null, not undefined. an event ID
- filter += '';
- return $.grep(cache, function(e) {
- return e._id == filter;
- });
- }
- return cache; // else, return all
- }
- // Makes sure all array event sources have their internal event objects
- // converted over to the Calendar's current timezone.
- t.rezoneArrayEventSources = function() {
- var i;
- var events;
- var j;
- for (i = 0; i < sources.length; i++) {
- events = sources[i].events;
- if ($.isArray(events)) {
- for (j = 0; j < events.length; j++) {
- rezoneEventDates(events[j]);
- }
- }
- }
- };
- function rezoneEventDates(event) {
- event.start = t.moment(event.start);
- if (event.end) {
- event.end = t.moment(event.end);
- }
- backupEventDates(event);
- }
- /* Event Normalization
- -----------------------------------------------------------------------------*/
- // Given a raw object with key/value properties, returns an "abstract" Event object.
- // An "abstract" event is an event that, if recurring, will not have been expanded yet.
- // Will return `false` when input is invalid.
- // `source` is optional
- function buildEventFromInput(input, source) {
- var out = {};
- var start, end;
- var allDay;
- if (t.options.eventDataTransform) {
- input = t.options.eventDataTransform(input);
- }
- if (source && source.eventDataTransform) {
- input = source.eventDataTransform(input);
- }
- // Copy all properties over to the resulting object.
- // The special-case properties will be copied over afterwards.
- $.extend(out, input);
- if (source) {
- out.source = source;
- }
- out._id = input._id || (input.id === undefined ? '_fc' + eventGUID++ : input.id + '');
- if (input.className) {
- if (typeof input.className == 'string') {
- out.className = input.className.split(/\s+/);
- }
- else { // assumed to be an array
- out.className = input.className;
- }
- }
- else {
- out.className = [];
- }
- start = input.start || input.date; // "date" is an alias for "start"
- end = input.end;
- // parse as a time (Duration) if applicable
- if (isTimeString(start)) {
- start = moment.duration(start);
- }
- if (isTimeString(end)) {
- end = moment.duration(end);
- }
- if (input.dow || moment.isDuration(start) || moment.isDuration(end)) {
- // the event is "abstract" (recurring) so don't calculate exact start/end dates just yet
- out.start = start ? moment.duration(start) : null; // will be a Duration or null
- out.end = end ? moment.duration(end) : null; // will be a Duration or null
- out._recurring = true; // our internal marker
- }
- else {
- if (start) {
- start = t.moment(start);
- if (!start.isValid()) {
- return false;
- }
- }
- if (end) {
- end = t.moment(end);
- if (!end.isValid()) {
- end = null; // let defaults take over
- }
- }
- allDay = input.allDay;
- if (allDay === undefined) { // still undefined? fallback to default
- allDay = firstDefined(
- source ? source.allDayDefault : undefined,
- t.options.allDayDefault
- );
- // still undefined? normalizeEventDates will calculate it
- }
- assignDatesToEvent(start, end, allDay, out);
- }
- t.normalizeEvent(out); // hook for external use. a prototype method
- return out;
- }
- t.buildEventFromInput = buildEventFromInput;
- // Normalizes and assigns the given dates to the given partially-formed event object.
- // NOTE: mutates the given start/end moments. does not make a copy.
- function assignDatesToEvent(start, end, allDay, event) {
- event.start = start;
- event.end = end;
- event.allDay = allDay;
- normalizeEventDates(event);
- backupEventDates(event);
- }
- // Ensures proper values for allDay/start/end. Accepts an Event object, or a plain object with event-ish properties.
- // NOTE: Will modify the given object.
- function normalizeEventDates(eventProps) {
- normalizeEventTimes(eventProps);
- if (eventProps.end && !eventProps.end.isAfter(eventProps.start)) {
- eventProps.end = null;
- }
- if (!eventProps.end) {
- if (t.options.forceEventDuration) {
- eventProps.end = t.getDefaultEventEnd(eventProps.allDay, eventProps.start);
- }
- else {
- eventProps.end = null;
- }
- }
- }
- // Ensures the allDay property exists and the timeliness of the start/end dates are consistent
- function normalizeEventTimes(eventProps) {
- if (eventProps.allDay == null) {
- eventProps.allDay = !(eventProps.start.hasTime() || (eventProps.end && eventProps.end.hasTime()));
- }
- if (eventProps.allDay) {
- eventProps.start.stripTime();
- if (eventProps.end) {
- // TODO: consider nextDayThreshold here? If so, will require a lot of testing and adjustment
- eventProps.end.stripTime();
- }
- }
- else {
- if (!eventProps.start.hasTime()) {
- eventProps.start = t.applyTimezone(eventProps.start.time(0)); // will assign a 00:00 time
- }
- if (eventProps.end && !eventProps.end.hasTime()) {
- eventProps.end = t.applyTimezone(eventProps.end.time(0)); // will assign a 00:00 time
- }
- }
- }
- // If the given event is a recurring event, break it down into an array of individual instances.
- // If not a recurring event, return an array with the single original event.
- // If given a falsy input (probably because of a failed buildEventFromInput call), returns an empty array.
- // HACK: can override the recurring window by providing custom rangeStart/rangeEnd (for businessHours).
- function expandEvent(abstractEvent, _rangeStart, _rangeEnd) {
- var events = [];
- var dowHash;
- var dow;
- var i;
- var date;
- var startTime, endTime;
- var start, end;
- var event;
- _rangeStart = _rangeStart || rangeStart;
- _rangeEnd = _rangeEnd || rangeEnd;
- if (abstractEvent) {
- if (abstractEvent._recurring) {
- // make a boolean hash as to whether the event occurs on each day-of-week
- if ((dow = abstractEvent.dow)) {
- dowHash = {};
- for (i = 0; i < dow.length; i++) {
- dowHash[dow[i]] = true;
- }
- }
- // iterate through every day in the current range
- date = _rangeStart.clone().stripTime(); // holds the date of the current day
- while (date.isBefore(_rangeEnd)) {
- if (!dowHash || dowHash[date.day()]) { // if everyday, or this particular day-of-week
- startTime = abstractEvent.start; // the stored start and end properties are times (Durations)
- endTime = abstractEvent.end; // "
- start = date.clone();
- end = null;
- if (startTime) {
- start = start.time(startTime);
- }
- if (endTime) {
- end = date.clone().time(endTime);
- }
- event = $.extend({}, abstractEvent); // make a copy of the original
- assignDatesToEvent(
- start, end,
- !startTime && !endTime, // allDay?
- event
- );
- events.push(event);
- }
- date.add(1, 'days');
- }
- }
- else {
- events.push(abstractEvent); // return the original event. will be a one-item array
- }
- }
- return events;
- }
- t.expandEvent = expandEvent;
- /* Event Modification Math
- -----------------------------------------------------------------------------------------*/
- // Modifies an event and all related events by applying the given properties.
- // Special date-diffing logic is used for manipulation of dates.
- // If `props` does not contain start/end dates, the updated values are assumed to be the event's current start/end.
- // All date comparisons are done against the event's pristine _start and _end dates.
- // Returns an object with delta information and a function to undo all operations.
- // For making computations in a granularity greater than day/time, specify largeUnit.
- // NOTE: The given `newProps` might be mutated for normalization purposes.
- function mutateEvent(event, newProps, largeUnit) {
- var miscProps = {};
- var oldProps;
- var clearEnd;
- var startDelta;
- var endDelta;
- var durationDelta;
- var undoFunc;
- // diffs the dates in the appropriate way, returning a duration
- function diffDates(date1, date0) { // date1 - date0
- if (largeUnit) {
- return diffByUnit(date1, date0, largeUnit);
- }
- else if (newProps.allDay) {
- return diffDay(date1, date0);
- }
- else {
- return diffDayTime(date1, date0);
- }
- }
- newProps = newProps || {};
- // normalize new date-related properties
- if (!newProps.start) {
- newProps.start = event.start.clone();
- }
- if (newProps.end === undefined) {
- newProps.end = event.end ? event.end.clone() : null;
- }
- if (newProps.allDay == null) { // is null or undefined?
- newProps.allDay = event.allDay;
- }
- normalizeEventDates(newProps);
- // create normalized versions of the original props to compare against
- // need a real end value, for diffing
- oldProps = {
- start: event._start.clone(),
- end: event._end ? event._end.clone() : t.getDefaultEventEnd(event._allDay, event._start),
- allDay: newProps.allDay // normalize the dates in the same regard as the new properties
- };
- normalizeEventDates(oldProps);
- // need to clear the end date if explicitly changed to null
- clearEnd = event._end !== null && newProps.end === null;
- // compute the delta for moving the start date
- startDelta = diffDates(newProps.start, oldProps.start);
- // compute the delta for moving the end date
- if (newProps.end) {
- endDelta = diffDates(newProps.end, oldProps.end);
- durationDelta = endDelta.subtract(startDelta);
- }
- else {
- durationDelta = null;
- }
- // gather all non-date-related properties
- $.each(newProps, function(name, val) {
- if (isMiscEventPropName(name)) {
- if (val !== undefined) {
- miscProps[name] = val;
- }
- }
- });
- // apply the operations to the event and all related events
- undoFunc = mutateEvents(
- clientEvents(event._id), // get events with this ID
- clearEnd,
- newProps.allDay,
- startDelta,
- durationDelta,
- miscProps
- );
- return {
- dateDelta: startDelta,
- durationDelta: durationDelta,
- undo: undoFunc
- };
- }
- // Modifies an array of events in the following ways (operations are in order):
- // - clear the event's `end`
- // - convert the event to allDay
- // - add `dateDelta` to the start and end
- // - add `durationDelta` to the event's duration
- // - assign `miscProps` to the event
- //
- // Returns a function that can be called to undo all the operations.
- //
- // TODO: don't use so many closures. possible memory issues when lots of events with same ID.
- //
- function mutateEvents(events, clearEnd, allDay, dateDelta, durationDelta, miscProps) {
- var isAmbigTimezone = t.getIsAmbigTimezone();
- var undoFunctions = [];
- // normalize zero-length deltas to be null
- if (dateDelta && !dateDelta.valueOf()) { dateDelta = null; }
- if (durationDelta && !durationDelta.valueOf()) { durationDelta = null; }
- $.each(events, function(i, event) {
- var oldProps;
- var newProps;
- // build an object holding all the old values, both date-related and misc.
- // for the undo function.
- oldProps = {
- start: event.start.clone(),
- end: event.end ? event.end.clone() : null,
- allDay: event.allDay
- };
- $.each(miscProps, function(name) {
- oldProps[name] = event[name];
- });
- // new date-related properties. work off the original date snapshot.
- // ok to use references because they will be thrown away when backupEventDates is called.
- newProps = {
- start: event._start,
- end: event._end,
- allDay: allDay // normalize the dates in the same regard as the new properties
- };
- normalizeEventDates(newProps); // massages start/end/allDay
- // strip or ensure the end date
- if (clearEnd) {
- newProps.end = null;
- }
- else if (durationDelta && !newProps.end) { // the duration translation requires an end date
- newProps.end = t.getDefaultEventEnd(newProps.allDay, newProps.start);
- }
- if (dateDelta) {
- newProps.start.add(dateDelta);
- if (newProps.end) {
- newProps.end.add(dateDelta);
- }
- }
- if (durationDelta) {
- newProps.end.add(durationDelta); // end already ensured above
- }
- // if the dates have changed, and we know it is impossible to recompute the
- // timezone offsets, strip the zone.
- if (
- isAmbigTimezone &&
- !newProps.allDay &&
- (dateDelta || durationDelta)
- ) {
- newProps.start.stripZone();
- if (newProps.end) {
- newProps.end.stripZone();
- }
- }
- $.extend(event, miscProps, newProps); // copy over misc props, then date-related props
- backupEventDates(event); // regenerate internal _start/_end/_allDay
- undoFunctions.push(function() {
- $.extend(event, oldProps);
- backupEventDates(event); // regenerate internal _start/_end/_allDay
- });
- });
- return function() {
- for (var i = 0; i < undoFunctions.length; i++) {
- undoFunctions[i]();
- }
- };
- }
- t.getEventCache = function() {
- return cache;
- };
- }
- // hook for external libs to manipulate event properties upon creation.
- // should manipulate the event in-place.
- Calendar.prototype.normalizeEvent = function(event) {
- };
- // Does the given span (start, end, and other location information)
- // fully contain the other?
- Calendar.prototype.spanContainsSpan = function(outerSpan, innerSpan) {
- var eventStart = outerSpan.start.clone().stripZone();
- var eventEnd = this.getEventEnd(outerSpan).stripZone();
- return innerSpan.start >= 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;
- };
|