EventManager.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719
  1. fc.sourceNormalizers = [];
  2. fc.sourceFetchers = [];
  3. var ajaxDefaults = {
  4. dataType: 'json',
  5. cache: false
  6. };
  7. var eventGUID = 1;
  8. function EventManager(options) { // assumed to be a calendar
  9. var t = this;
  10. // exports
  11. t.isFetchNeeded = isFetchNeeded;
  12. t.fetchEvents = fetchEvents;
  13. t.addEventSource = addEventSource;
  14. t.removeEventSource = removeEventSource;
  15. t.updateEvent = updateEvent;
  16. t.renderEvent = renderEvent;
  17. t.removeEvents = removeEvents;
  18. t.clientEvents = clientEvents;
  19. t.mutateEvent = mutateEvent;
  20. // imports
  21. var trigger = t.trigger;
  22. var getView = t.getView;
  23. var reportEvents = t.reportEvents;
  24. var getEventEnd = t.getEventEnd;
  25. // locals
  26. var stickySource = { events: [] };
  27. var sources = [ stickySource ];
  28. var rangeStart, rangeEnd;
  29. var currentFetchID = 0;
  30. var pendingSourceCnt = 0;
  31. var loadingLevel = 0;
  32. var cache = [];
  33. $.each(
  34. (options.events ? [ options.events ] : []).concat(options.eventSources || []),
  35. function(i, sourceInput) {
  36. var source = buildEventSource(sourceInput);
  37. if (source) {
  38. sources.push(source);
  39. }
  40. }
  41. );
  42. /* Fetching
  43. -----------------------------------------------------------------------------*/
  44. function isFetchNeeded(start, end) {
  45. return !rangeStart || // nothing has been fetched yet?
  46. // or, a part of the new range is outside of the old range? (after normalizing)
  47. start.clone().stripZone() < rangeStart.clone().stripZone() ||
  48. end.clone().stripZone() > rangeEnd.clone().stripZone();
  49. }
  50. function fetchEvents(start, end) {
  51. rangeStart = start;
  52. rangeEnd = end;
  53. cache = [];
  54. var fetchID = ++currentFetchID;
  55. var len = sources.length;
  56. pendingSourceCnt = len;
  57. for (var i=0; i<len; i++) {
  58. fetchEventSource(sources[i], fetchID);
  59. }
  60. }
  61. function fetchEventSource(source, fetchID) {
  62. _fetchEventSource(source, function(events) {
  63. var isArraySource = $.isArray(source.events);
  64. var i;
  65. var event;
  66. if (fetchID == currentFetchID) {
  67. if (events) {
  68. for (i=0; i<events.length; i++) {
  69. event = events[i];
  70. // event array sources have already been convert to Event Objects
  71. if (!isArraySource) {
  72. event = buildEvent(event, source);
  73. }
  74. if (event) {
  75. cache.push(event);
  76. }
  77. }
  78. }
  79. pendingSourceCnt--;
  80. if (!pendingSourceCnt) {
  81. reportEvents(cache);
  82. }
  83. }
  84. });
  85. }
  86. function _fetchEventSource(source, callback) {
  87. var i;
  88. var fetchers = fc.sourceFetchers;
  89. var res;
  90. for (i=0; i<fetchers.length; i++) {
  91. res = fetchers[i].call(
  92. t, // this, the Calendar object
  93. source,
  94. rangeStart.clone(),
  95. rangeEnd.clone(),
  96. options.timezone,
  97. callback
  98. );
  99. if (res === true) {
  100. // the fetcher is in charge. made its own async request
  101. return;
  102. }
  103. else if (typeof res == 'object') {
  104. // the fetcher returned a new source. process it
  105. _fetchEventSource(res, callback);
  106. return;
  107. }
  108. }
  109. var events = source.events;
  110. if (events) {
  111. if ($.isFunction(events)) {
  112. pushLoading();
  113. events.call(
  114. t, // this, the Calendar object
  115. rangeStart.clone(),
  116. rangeEnd.clone(),
  117. options.timezone,
  118. function(events) {
  119. callback(events);
  120. popLoading();
  121. }
  122. );
  123. }
  124. else if ($.isArray(events)) {
  125. callback(events);
  126. }
  127. else {
  128. callback();
  129. }
  130. }else{
  131. var url = source.url;
  132. if (url) {
  133. var success = source.success;
  134. var error = source.error;
  135. var complete = source.complete;
  136. // retrieve any outbound GET/POST $.ajax data from the options
  137. var customData;
  138. if ($.isFunction(source.data)) {
  139. // supplied as a function that returns a key/value object
  140. customData = source.data();
  141. }
  142. else {
  143. // supplied as a straight key/value object
  144. customData = source.data;
  145. }
  146. // use a copy of the custom data so we can modify the parameters
  147. // and not affect the passed-in object.
  148. var data = $.extend({}, customData || {});
  149. var startParam = firstDefined(source.startParam, options.startParam);
  150. var endParam = firstDefined(source.endParam, options.endParam);
  151. var timezoneParam = firstDefined(source.timezoneParam, options.timezoneParam);
  152. if (startParam) {
  153. data[startParam] = rangeStart.format();
  154. }
  155. if (endParam) {
  156. data[endParam] = rangeEnd.format();
  157. }
  158. if (options.timezone && options.timezone != 'local') {
  159. data[timezoneParam] = options.timezone;
  160. }
  161. pushLoading();
  162. $.ajax($.extend({}, ajaxDefaults, source, {
  163. data: data,
  164. success: function(events) {
  165. events = events || [];
  166. var res = applyAll(success, this, arguments);
  167. if ($.isArray(res)) {
  168. events = res;
  169. }
  170. callback(events);
  171. },
  172. error: function() {
  173. applyAll(error, this, arguments);
  174. callback();
  175. },
  176. complete: function() {
  177. applyAll(complete, this, arguments);
  178. popLoading();
  179. }
  180. }));
  181. }else{
  182. callback();
  183. }
  184. }
  185. }
  186. /* Sources
  187. -----------------------------------------------------------------------------*/
  188. function addEventSource(sourceInput) {
  189. var source = buildEventSource(sourceInput);
  190. if (source) {
  191. sources.push(source);
  192. pendingSourceCnt++;
  193. fetchEventSource(source, currentFetchID); // will eventually call reportEvents
  194. }
  195. }
  196. function buildEventSource(sourceInput) { // will return undefined if invalid source
  197. var normalizers = fc.sourceNormalizers;
  198. var source;
  199. var i;
  200. if ($.isFunction(sourceInput) || $.isArray(sourceInput)) {
  201. source = { events: sourceInput };
  202. }
  203. else if (typeof sourceInput === 'string') {
  204. source = { url: sourceInput };
  205. }
  206. else if (typeof sourceInput === 'object') {
  207. source = $.extend({}, sourceInput); // shallow copy
  208. }
  209. if (source) {
  210. // TODO: repeat code, same code for event classNames
  211. if (source.className) {
  212. if (typeof source.className === 'string') {
  213. source.className = source.className.split(/\s+/);
  214. }
  215. // otherwise, assumed to be an array
  216. }
  217. else {
  218. source.className = [];
  219. }
  220. // for array sources, we convert to standard Event Objects up front
  221. if ($.isArray(source.events)) {
  222. source.events = $.map(source.events, function(eventInput) {
  223. return buildEvent(eventInput, source);
  224. });
  225. }
  226. for (i=0; i<normalizers.length; i++) {
  227. normalizers[i].call(t, source);
  228. }
  229. return source;
  230. }
  231. }
  232. function removeEventSource(source) {
  233. sources = $.grep(sources, function(src) {
  234. return !isSourcesEqual(src, source);
  235. });
  236. // remove all client events from that source
  237. cache = $.grep(cache, function(e) {
  238. return !isSourcesEqual(e.source, source);
  239. });
  240. reportEvents(cache);
  241. }
  242. function isSourcesEqual(source1, source2) {
  243. return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2);
  244. }
  245. function getSourcePrimitive(source) {
  246. return ((typeof source == 'object') ? (source.events || source.url) : '') || source;
  247. }
  248. /* Manipulation
  249. -----------------------------------------------------------------------------*/
  250. function updateEvent(event) {
  251. event.start = t.moment(event.start);
  252. if (event.end) {
  253. event.end = t.moment(event.end);
  254. }
  255. mutateEvent(event);
  256. propagateMiscProperties(event);
  257. reportEvents(cache); // reports event modifications (so we can redraw)
  258. }
  259. var miscCopyableProps = [
  260. 'title',
  261. 'url',
  262. 'allDay',
  263. 'className',
  264. 'editable',
  265. 'color',
  266. 'backgroundColor',
  267. 'borderColor',
  268. 'textColor'
  269. ];
  270. function propagateMiscProperties(event) {
  271. var i;
  272. var cachedEvent;
  273. var j;
  274. var prop;
  275. for (i=0; i<cache.length; i++) {
  276. cachedEvent = cache[i];
  277. if (cachedEvent._id == event._id && cachedEvent !== event) {
  278. for (j=0; j<miscCopyableProps.length; j++) {
  279. prop = miscCopyableProps[j];
  280. if (event[prop] !== undefined) {
  281. cachedEvent[prop] = event[prop];
  282. }
  283. }
  284. }
  285. }
  286. }
  287. function renderEvent(eventData, stick) {
  288. var event = buildEvent(eventData);
  289. if (event) {
  290. if (!event.source) {
  291. if (stick) {
  292. stickySource.events.push(event);
  293. event.source = stickySource;
  294. }
  295. cache.push(event);
  296. }
  297. reportEvents(cache);
  298. }
  299. }
  300. function removeEvents(filter) {
  301. var eventID;
  302. var i;
  303. if (filter == null) { // null or undefined. remove all events
  304. filter = function() { return true; }; // will always match
  305. }
  306. else if (!$.isFunction(filter)) { // an event ID
  307. eventID = filter + '';
  308. filter = function(event) {
  309. return event._id == eventID;
  310. };
  311. }
  312. // Purge event(s) from our local cache
  313. cache = $.grep(cache, filter, true); // inverse=true
  314. // Remove events from array sources.
  315. // This works because they have been converted to official Event Objects up front.
  316. // (and as a result, event._id has been calculated).
  317. for (i=0; i<sources.length; i++) {
  318. if ($.isArray(sources[i].events)) {
  319. sources[i].events = $.grep(sources[i].events, filter, true);
  320. }
  321. }
  322. reportEvents(cache);
  323. }
  324. function clientEvents(filter) {
  325. if ($.isFunction(filter)) {
  326. return $.grep(cache, filter);
  327. }
  328. else if (filter != null) { // not null, not undefined. an event ID
  329. filter += '';
  330. return $.grep(cache, function(e) {
  331. return e._id == filter;
  332. });
  333. }
  334. return cache; // else, return all
  335. }
  336. /* Loading State
  337. -----------------------------------------------------------------------------*/
  338. function pushLoading() {
  339. if (!(loadingLevel++)) {
  340. trigger('loading', null, true, getView());
  341. }
  342. }
  343. function popLoading() {
  344. if (!(--loadingLevel)) {
  345. trigger('loading', null, false, getView());
  346. }
  347. }
  348. /* Event Normalization
  349. -----------------------------------------------------------------------------*/
  350. function buildEvent(data, source) { // source may be undefined!
  351. var out = {};
  352. var start;
  353. var end;
  354. var allDay;
  355. var allDayDefault;
  356. if (options.eventDataTransform) {
  357. data = options.eventDataTransform(data);
  358. }
  359. if (source && source.eventDataTransform) {
  360. data = source.eventDataTransform(data);
  361. }
  362. start = t.moment(data.start || data.date); // "date" is an alias for "start"
  363. if (!start.isValid()) {
  364. return;
  365. }
  366. end = null;
  367. if (data.end) {
  368. end = t.moment(data.end);
  369. if (!end.isValid()) {
  370. return;
  371. }
  372. }
  373. allDay = data.allDay;
  374. if (allDay === undefined) {
  375. allDayDefault = firstDefined(
  376. source ? source.allDayDefault : undefined,
  377. options.allDayDefault
  378. );
  379. if (allDayDefault !== undefined) {
  380. // use the default
  381. allDay = allDayDefault;
  382. }
  383. else {
  384. // all dates need to have ambig time for the event to be considered allDay
  385. allDay = !start.hasTime() && (!end || !end.hasTime());
  386. }
  387. }
  388. // normalize the date based on allDay
  389. if (allDay) {
  390. // neither date should have a time
  391. if (start.hasTime()) {
  392. start.stripTime();
  393. }
  394. if (end && end.hasTime()) {
  395. end.stripTime();
  396. }
  397. }
  398. else {
  399. // force a time/zone up the dates
  400. if (!start.hasTime()) {
  401. start = t.rezoneDate(start);
  402. }
  403. if (end && !end.hasTime()) {
  404. end = t.rezoneDate(end);
  405. }
  406. }
  407. // Copy all properties over to the resulting object.
  408. // The special-case properties will be copied over afterwards.
  409. $.extend(out, data);
  410. if (source) {
  411. out.source = source;
  412. }
  413. out._id = data._id || (data.id === undefined ? '_fc' + eventGUID++ : data.id + '');
  414. if (data.className) {
  415. if (typeof data.className == 'string') {
  416. out.className = data.className.split(/\s+/);
  417. }
  418. else { // assumed to be an array
  419. out.className = data.className;
  420. }
  421. }
  422. else {
  423. out.className = [];
  424. }
  425. out.allDay = allDay;
  426. out.start = start;
  427. out.end = end;
  428. if (options.forceEventDuration && !out.end) {
  429. out.end = getEventEnd(out);
  430. }
  431. backupEventDates(out);
  432. return out;
  433. }
  434. /* Event Modification Math
  435. -----------------------------------------------------------------------------------------*/
  436. // Modify the date(s) of an event and make this change propagate to all other events with
  437. // the same ID (related repeating events).
  438. //
  439. // If `newStart`/`newEnd` are not specified, the "new" dates are assumed to be `event.start` and `event.end`.
  440. // The "old" dates to be compare against are always `event._start` and `event._end` (set by EventManager).
  441. //
  442. // Returns an object with delta information and a function to undo all operations.
  443. //
  444. function mutateEvent(event, newStart, newEnd) {
  445. var oldAllDay = event._allDay;
  446. var oldStart = event._start;
  447. var oldEnd = event._end;
  448. var clearEnd = false;
  449. var newAllDay;
  450. var dateDelta;
  451. var durationDelta;
  452. var undoFunc;
  453. // if no new dates were passed in, compare against the event's existing dates
  454. if (!newStart && !newEnd) {
  455. newStart = event.start;
  456. newEnd = event.end;
  457. }
  458. // NOTE: throughout this function, the initial values of `newStart` and `newEnd` are
  459. // preserved. These values may be undefined.
  460. // detect new allDay
  461. if (event.allDay != oldAllDay) { // if value has changed, use it
  462. newAllDay = event.allDay;
  463. }
  464. else { // otherwise, see if any of the new dates are allDay
  465. newAllDay = !(newStart || newEnd).hasTime();
  466. }
  467. // normalize the new dates based on allDay
  468. if (newAllDay) {
  469. if (newStart) {
  470. newStart = newStart.clone().stripTime();
  471. }
  472. if (newEnd) {
  473. newEnd = newEnd.clone().stripTime();
  474. }
  475. }
  476. // compute dateDelta
  477. if (newStart) {
  478. if (newAllDay) {
  479. dateDelta = dayishDiff(newStart, oldStart.clone().stripTime()); // treat oldStart as allDay
  480. }
  481. else {
  482. dateDelta = dayishDiff(newStart, oldStart);
  483. }
  484. }
  485. if (newAllDay != oldAllDay) {
  486. // if allDay has changed, always throw away the end
  487. clearEnd = true;
  488. }
  489. else if (newEnd) {
  490. durationDelta = dayishDiff(
  491. // new duration
  492. newEnd || t.getDefaultEventEnd(newAllDay, newStart || oldStart),
  493. newStart || oldStart
  494. ).subtract(dayishDiff(
  495. // subtract old duration
  496. oldEnd || t.getDefaultEventEnd(oldAllDay, oldStart),
  497. oldStart
  498. ));
  499. }
  500. undoFunc = mutateEvents(
  501. clientEvents(event._id), // get events with this ID
  502. clearEnd,
  503. newAllDay,
  504. dateDelta,
  505. durationDelta
  506. );
  507. return {
  508. dateDelta: dateDelta,
  509. durationDelta: durationDelta,
  510. undo: undoFunc
  511. };
  512. }
  513. // Modifies an array of events in the following ways (operations are in order):
  514. // - clear the event's `end`
  515. // - convert the event to allDay
  516. // - add `dateDelta` to the start and end
  517. // - add `durationDelta` to the event's duration
  518. //
  519. // Returns a function that can be called to undo all the operations.
  520. //
  521. function mutateEvents(events, clearEnd, forceAllDay, dateDelta, durationDelta) {
  522. var isAmbigTimezone = t.getIsAmbigTimezone();
  523. var undoFunctions = [];
  524. $.each(events, function(i, event) {
  525. var oldAllDay = event._allDay;
  526. var oldStart = event._start;
  527. var oldEnd = event._end;
  528. var newAllDay = forceAllDay != null ? forceAllDay : oldAllDay;
  529. var newStart = oldStart.clone();
  530. var newEnd = (!clearEnd && oldEnd) ? oldEnd.clone() : null;
  531. // NOTE: this function is responsible for transforming `newStart` and `newEnd`,
  532. // which were initialized to the OLD values first. `newEnd` may be null.
  533. // normlize newStart/newEnd to be consistent with newAllDay
  534. if (newAllDay) {
  535. newStart.stripTime();
  536. if (newEnd) {
  537. newEnd.stripTime();
  538. }
  539. }
  540. else {
  541. if (!newStart.hasTime()) {
  542. newStart = t.rezoneDate(newStart);
  543. }
  544. if (newEnd && !newEnd.hasTime()) {
  545. newEnd = t.rezoneDate(newEnd);
  546. }
  547. }
  548. // ensure we have an end date if necessary
  549. if (!newEnd && (options.forceEventDuration || +durationDelta)) {
  550. newEnd = t.getDefaultEventEnd(newAllDay, newStart);
  551. }
  552. // translate the dates
  553. newStart.add(dateDelta);
  554. if (newEnd) {
  555. newEnd.add(dateDelta).add(durationDelta);
  556. }
  557. // if the dates have changed, and we know it is impossible to recompute the
  558. // timezone offsets, strip the zone.
  559. if (isAmbigTimezone) {
  560. if (+dateDelta || +durationDelta) {
  561. newStart.stripZone();
  562. if (newEnd) {
  563. newEnd.stripZone();
  564. }
  565. }
  566. }
  567. event.allDay = newAllDay;
  568. event.start = newStart;
  569. event.end = newEnd;
  570. backupEventDates(event);
  571. undoFunctions.push(function() {
  572. event.allDay = oldAllDay;
  573. event.start = oldStart;
  574. event.end = oldEnd;
  575. backupEventDates(event);
  576. });
  577. });
  578. return function() {
  579. for (var i=0; i<undoFunctions.length; i++) {
  580. undoFunctions[i]();
  581. }
  582. };
  583. }
  584. }
  585. // updates the "backup" properties, which are preserved in order to compute diffs later on.
  586. function backupEventDates(event) {
  587. event._allDay = event.allDay;
  588. event._start = event.start.clone();
  589. event._end = event.end ? event.end.clone() : null;
  590. }