EventManager.js 16 KB

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