EventManager.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101
  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.fetchEventSources = fetchEventSources;
  14. t.addEventSource = addEventSource;
  15. t.removeEventSource = removeEventSource;
  16. t.updateEvent = updateEvent;
  17. t.renderEvent = renderEvent;
  18. t.removeEvents = removeEvents;
  19. t.clientEvents = clientEvents;
  20. t.mutateEvent = mutateEvent;
  21. t.normalizeEventDates = normalizeEventDates;
  22. t.normalizeEventTimes = normalizeEventTimes;
  23. // imports
  24. var reportEvents = t.reportEvents;
  25. // locals
  26. var stickySource = { events: [] };
  27. var sources = [ stickySource ];
  28. var rangeStart, rangeEnd;
  29. var currentFetchID = 0;
  30. var pendingSourceCnt = 0;
  31. var cache = []; // holds events that have already been expanded
  32. $.each(
  33. (options.events ? [ options.events ] : []).concat(options.eventSources || []),
  34. function(i, sourceInput) {
  35. var source = buildEventSource(sourceInput);
  36. if (source) {
  37. sources.push(source);
  38. }
  39. }
  40. );
  41. /* Fetching
  42. -----------------------------------------------------------------------------*/
  43. // start and end are assumed to be unzoned
  44. function isFetchNeeded(start, end) {
  45. return !rangeStart || // nothing has been fetched yet?
  46. start < rangeStart || end > rangeEnd; // is part of the new range outside of the old range?
  47. }
  48. function fetchEvents(start, end) {
  49. rangeStart = start;
  50. rangeEnd = end;
  51. fetchEventSources(sources, true);
  52. }
  53. function fetchEventSources(specificSources, shouldClearAll) {
  54. if (shouldClearAll) {
  55. cache = [];
  56. }
  57. var fetchID = ++currentFetchID;
  58. var len = specificSources.length;
  59. pendingSourceCnt += len;
  60. function checkSources(e) {
  61. return e.source !== specificSources[i];
  62. }
  63. for (var i=0; i<len; i++) {
  64. if (!shouldClearAll) {
  65. // remove events from the cache that belong to the source being refetched
  66. cache = $.grep(cache, checkSources);
  67. }
  68. fetchEventSource(specificSources[i], fetchID);
  69. }
  70. }
  71. function fetchEventSource(source, fetchID) {
  72. _fetchEventSource(source, function(eventInputs) {
  73. var isArraySource = $.isArray(source.events);
  74. var i, eventInput;
  75. var abstractEvent;
  76. if (fetchID == currentFetchID) {
  77. if (eventInputs) {
  78. for (i = 0; i < eventInputs.length; i++) {
  79. eventInput = eventInputs[i];
  80. if (isArraySource) { // array sources have already been convert to Event Objects
  81. abstractEvent = eventInput;
  82. }
  83. else {
  84. abstractEvent = buildEventFromInput(eventInput, source);
  85. }
  86. if (abstractEvent) { // not false (an invalid event)
  87. cache.push.apply(
  88. cache,
  89. expandEvent(abstractEvent) // add individual expanded events to the cache
  90. );
  91. }
  92. }
  93. }
  94. pendingSourceCnt--;
  95. if (!pendingSourceCnt) {
  96. reportEvents(cache);
  97. }
  98. }
  99. });
  100. }
  101. function _fetchEventSource(source, callback) {
  102. var i;
  103. var fetchers = FC.sourceFetchers;
  104. var res;
  105. for (i=0; i<fetchers.length; i++) {
  106. res = fetchers[i].call(
  107. t, // this, the Calendar object
  108. source,
  109. rangeStart.clone(),
  110. rangeEnd.clone(),
  111. options.timezone,
  112. callback
  113. );
  114. if (res === true) {
  115. // the fetcher is in charge. made its own async request
  116. return;
  117. }
  118. else if (typeof res == 'object') {
  119. // the fetcher returned a new source. process it
  120. _fetchEventSource(res, callback);
  121. return;
  122. }
  123. }
  124. var events = source.events;
  125. if (events) {
  126. if ($.isFunction(events)) {
  127. t.pushLoading();
  128. events.call(
  129. t, // this, the Calendar object
  130. rangeStart.clone(),
  131. rangeEnd.clone(),
  132. options.timezone,
  133. function(events) {
  134. callback(events);
  135. t.popLoading();
  136. }
  137. );
  138. }
  139. else if ($.isArray(events)) {
  140. callback(events);
  141. }
  142. else {
  143. callback();
  144. }
  145. }else{
  146. var url = source.url;
  147. if (url) {
  148. var success = source.success;
  149. var error = source.error;
  150. var complete = source.complete;
  151. // retrieve any outbound GET/POST $.ajax data from the options
  152. var customData;
  153. if ($.isFunction(source.data)) {
  154. // supplied as a function that returns a key/value object
  155. customData = source.data();
  156. }
  157. else {
  158. // supplied as a straight key/value object
  159. customData = source.data;
  160. }
  161. // use a copy of the custom data so we can modify the parameters
  162. // and not affect the passed-in object.
  163. var data = $.extend({}, customData || {});
  164. var startParam = firstDefined(source.startParam, options.startParam);
  165. var endParam = firstDefined(source.endParam, options.endParam);
  166. var timezoneParam = firstDefined(source.timezoneParam, options.timezoneParam);
  167. if (startParam) {
  168. data[startParam] = rangeStart.format();
  169. }
  170. if (endParam) {
  171. data[endParam] = rangeEnd.format();
  172. }
  173. if (options.timezone && options.timezone != 'local') {
  174. data[timezoneParam] = options.timezone;
  175. }
  176. t.pushLoading();
  177. $.ajax($.extend({}, ajaxDefaults, source, {
  178. data: data,
  179. success: function(events) {
  180. events = events || [];
  181. var res = applyAll(success, this, arguments);
  182. if ($.isArray(res)) {
  183. events = res;
  184. }
  185. callback(events);
  186. },
  187. error: function() {
  188. applyAll(error, this, arguments);
  189. callback();
  190. },
  191. complete: function() {
  192. applyAll(complete, this, arguments);
  193. t.popLoading();
  194. }
  195. }));
  196. }else{
  197. callback();
  198. }
  199. }
  200. }
  201. /* Sources
  202. -----------------------------------------------------------------------------*/
  203. function addEventSource(sourceInput) {
  204. var source = buildEventSource(sourceInput);
  205. if (source) {
  206. sources.push(source);
  207. pendingSourceCnt++;
  208. fetchEventSource(source, currentFetchID); // will eventually call reportEvents
  209. }
  210. }
  211. function buildEventSource(sourceInput) { // will return undefined if invalid source
  212. var normalizers = FC.sourceNormalizers;
  213. var source;
  214. var i;
  215. if ($.isFunction(sourceInput) || $.isArray(sourceInput)) {
  216. source = { events: sourceInput };
  217. }
  218. else if (typeof sourceInput === 'string') {
  219. source = { url: sourceInput };
  220. }
  221. else if (typeof sourceInput === 'object') {
  222. source = $.extend({}, sourceInput); // shallow copy
  223. }
  224. if (source) {
  225. // TODO: repeat code, same code for event classNames
  226. if (source.className) {
  227. if (typeof source.className === 'string') {
  228. source.className = source.className.split(/\s+/);
  229. }
  230. // otherwise, assumed to be an array
  231. }
  232. else {
  233. source.className = [];
  234. }
  235. // for array sources, we convert to standard Event Objects up front
  236. if ($.isArray(source.events)) {
  237. source.origArray = source.events; // for removeEventSource
  238. source.events = $.map(source.events, function(eventInput) {
  239. return buildEventFromInput(eventInput, source);
  240. });
  241. }
  242. for (i=0; i<normalizers.length; i++) {
  243. normalizers[i].call(t, source);
  244. }
  245. return source;
  246. }
  247. }
  248. function removeEventSource(source) {
  249. sources = $.grep(sources, function(src) {
  250. return !isSourcesEqual(src, source);
  251. });
  252. // remove all client events from that source
  253. cache = $.grep(cache, function(e) {
  254. return !isSourcesEqual(e.source, source);
  255. });
  256. reportEvents(cache);
  257. }
  258. function isSourcesEqual(source1, source2) {
  259. return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2);
  260. }
  261. function getSourcePrimitive(source) {
  262. return (
  263. (typeof source === 'object') ? // a normalized event source?
  264. (source.origArray || source.googleCalendarId || source.url || source.events) : // get the primitive
  265. null
  266. ) ||
  267. source; // the given argument *is* the primitive
  268. }
  269. /* Manipulation
  270. -----------------------------------------------------------------------------*/
  271. // Only ever called from the externally-facing API
  272. function updateEvent(event) {
  273. // massage start/end values, even if date string values
  274. event.start = t.moment(event.start);
  275. if (event.end) {
  276. event.end = t.moment(event.end);
  277. }
  278. else {
  279. event.end = null;
  280. }
  281. mutateEvent(event, getMiscEventProps(event)); // will handle start/end/allDay normalization
  282. reportEvents(cache); // reports event modifications (so we can redraw)
  283. }
  284. // Returns a hash of misc event properties that should be copied over to related events.
  285. function getMiscEventProps(event) {
  286. var props = {};
  287. $.each(event, function(name, val) {
  288. if (isMiscEventPropName(name)) {
  289. if (val !== undefined && isAtomic(val)) { // a defined non-object
  290. props[name] = val;
  291. }
  292. }
  293. });
  294. return props;
  295. }
  296. // non-date-related, non-id-related, non-secret
  297. function isMiscEventPropName(name) {
  298. return !/^_|^(id|allDay|start|end)$/.test(name);
  299. }
  300. // returns the expanded events that were created
  301. function renderEvent(eventInput, stick) {
  302. var abstractEvent = buildEventFromInput(eventInput);
  303. var events;
  304. var i, event;
  305. if (abstractEvent) { // not false (a valid input)
  306. events = expandEvent(abstractEvent);
  307. for (i = 0; i < events.length; i++) {
  308. event = events[i];
  309. if (!event.source) {
  310. if (stick) {
  311. stickySource.events.push(event);
  312. event.source = stickySource;
  313. }
  314. cache.push(event);
  315. }
  316. }
  317. reportEvents(cache);
  318. return events;
  319. }
  320. return [];
  321. }
  322. function removeEvents(filter) {
  323. var eventID;
  324. var i;
  325. if (filter == null) { // null or undefined. remove all events
  326. filter = function() { return true; }; // will always match
  327. }
  328. else if (!$.isFunction(filter)) { // an event ID
  329. eventID = filter + '';
  330. filter = function(event) {
  331. return event._id == eventID;
  332. };
  333. }
  334. // Purge event(s) from our local cache
  335. cache = $.grep(cache, filter, true); // inverse=true
  336. // Remove events from array sources.
  337. // This works because they have been converted to official Event Objects up front.
  338. // (and as a result, event._id has been calculated).
  339. for (i=0; i<sources.length; i++) {
  340. if ($.isArray(sources[i].events)) {
  341. sources[i].events = $.grep(sources[i].events, filter, true);
  342. }
  343. }
  344. reportEvents(cache);
  345. }
  346. function clientEvents(filter) {
  347. if ($.isFunction(filter)) {
  348. return $.grep(cache, filter);
  349. }
  350. else if (filter != null) { // not null, not undefined. an event ID
  351. filter += '';
  352. return $.grep(cache, function(e) {
  353. return e._id == filter;
  354. });
  355. }
  356. return cache; // else, return all
  357. }
  358. /* Event Normalization
  359. -----------------------------------------------------------------------------*/
  360. // Given a raw object with key/value properties, returns an "abstract" Event object.
  361. // An "abstract" event is an event that, if recurring, will not have been expanded yet.
  362. // Will return `false` when input is invalid.
  363. // `source` is optional
  364. function buildEventFromInput(input, source) {
  365. var out = {};
  366. var start, end;
  367. var allDay;
  368. if (options.eventDataTransform) {
  369. input = options.eventDataTransform(input);
  370. }
  371. if (source && source.eventDataTransform) {
  372. input = source.eventDataTransform(input);
  373. }
  374. // Copy all properties over to the resulting object.
  375. // The special-case properties will be copied over afterwards.
  376. $.extend(out, input);
  377. if (source) {
  378. out.source = source;
  379. }
  380. out._id = input._id || (input.id === undefined ? '_fc' + eventGUID++ : input.id + '');
  381. if (input.className) {
  382. if (typeof input.className == 'string') {
  383. out.className = input.className.split(/\s+/);
  384. }
  385. else { // assumed to be an array
  386. out.className = input.className;
  387. }
  388. }
  389. else {
  390. out.className = [];
  391. }
  392. start = input.start || input.date; // "date" is an alias for "start"
  393. end = input.end;
  394. // parse as a time (Duration) if applicable
  395. if (isTimeString(start)) {
  396. start = moment.duration(start);
  397. }
  398. if (isTimeString(end)) {
  399. end = moment.duration(end);
  400. }
  401. if (input.dow || moment.isDuration(start) || moment.isDuration(end)) {
  402. // the event is "abstract" (recurring) so don't calculate exact start/end dates just yet
  403. out.start = start ? moment.duration(start) : null; // will be a Duration or null
  404. out.end = end ? moment.duration(end) : null; // will be a Duration or null
  405. out._recurring = true; // our internal marker
  406. }
  407. else {
  408. if (start) {
  409. start = t.moment(start);
  410. if (!start.isValid()) {
  411. return false;
  412. }
  413. }
  414. if (end) {
  415. end = t.moment(end);
  416. if (!end.isValid()) {
  417. end = null; // let defaults take over
  418. }
  419. }
  420. allDay = input.allDay;
  421. if (allDay === undefined) { // still undefined? fallback to default
  422. allDay = firstDefined(
  423. source ? source.allDayDefault : undefined,
  424. options.allDayDefault
  425. );
  426. // still undefined? normalizeEventDates will calculate it
  427. }
  428. assignDatesToEvent(start, end, allDay, out);
  429. }
  430. t.normalizeEvent(out); // hook for external use. a prototype method
  431. return out;
  432. }
  433. // Normalizes and assigns the given dates to the given partially-formed event object.
  434. // NOTE: mutates the given start/end moments. does not make a copy.
  435. function assignDatesToEvent(start, end, allDay, event) {
  436. event.start = start;
  437. event.end = end;
  438. event.allDay = allDay;
  439. normalizeEventDates(event);
  440. backupEventDates(event);
  441. }
  442. // Ensures proper values for allDay/start/end. Accepts an Event object, or a plain object with event-ish properties.
  443. // NOTE: Will modify the given object.
  444. function normalizeEventDates(eventProps) {
  445. normalizeEventTimes(eventProps);
  446. if (eventProps.end && !eventProps.end.isAfter(eventProps.start)) {
  447. eventProps.end = null;
  448. }
  449. if (!eventProps.end) {
  450. if (options.forceEventDuration) {
  451. eventProps.end = t.getDefaultEventEnd(eventProps.allDay, eventProps.start);
  452. }
  453. else {
  454. eventProps.end = null;
  455. }
  456. }
  457. }
  458. // Ensures the allDay property exists and the timeliness of the start/end dates are consistent
  459. function normalizeEventTimes(eventProps) {
  460. if (eventProps.allDay == null) {
  461. eventProps.allDay = !(eventProps.start.hasTime() || (eventProps.end && eventProps.end.hasTime()));
  462. }
  463. if (eventProps.allDay) {
  464. eventProps.start.stripTime();
  465. if (eventProps.end) {
  466. // TODO: consider nextDayThreshold here? If so, will require a lot of testing and adjustment
  467. eventProps.end.stripTime();
  468. }
  469. }
  470. else {
  471. if (!eventProps.start.hasTime()) {
  472. eventProps.start = t.applyTimezone(eventProps.start.time(0)); // will assign a 00:00 time
  473. }
  474. if (eventProps.end && !eventProps.end.hasTime()) {
  475. eventProps.end = t.applyTimezone(eventProps.end.time(0)); // will assign a 00:00 time
  476. }
  477. }
  478. }
  479. // If the given event is a recurring event, break it down into an array of individual instances.
  480. // If not a recurring event, return an array with the single original event.
  481. // If given a falsy input (probably because of a failed buildEventFromInput call), returns an empty array.
  482. // HACK: can override the recurring window by providing custom rangeStart/rangeEnd (for businessHours).
  483. function expandEvent(abstractEvent, _rangeStart, _rangeEnd) {
  484. var events = [];
  485. var dowHash;
  486. var dow;
  487. var i;
  488. var date;
  489. var startTime, endTime;
  490. var start, end;
  491. var event;
  492. _rangeStart = _rangeStart || rangeStart;
  493. _rangeEnd = _rangeEnd || rangeEnd;
  494. if (abstractEvent) {
  495. if (abstractEvent._recurring) {
  496. // make a boolean hash as to whether the event occurs on each day-of-week
  497. if ((dow = abstractEvent.dow)) {
  498. dowHash = {};
  499. for (i = 0; i < dow.length; i++) {
  500. dowHash[dow[i]] = true;
  501. }
  502. }
  503. // iterate through every day in the current range
  504. date = _rangeStart.clone().stripTime(); // holds the date of the current day
  505. while (date.isBefore(_rangeEnd)) {
  506. if (!dowHash || dowHash[date.day()]) { // if everyday, or this particular day-of-week
  507. startTime = abstractEvent.start; // the stored start and end properties are times (Durations)
  508. endTime = abstractEvent.end; // "
  509. start = date.clone();
  510. end = null;
  511. if (startTime) {
  512. start = start.time(startTime);
  513. }
  514. if (endTime) {
  515. end = date.clone().time(endTime);
  516. }
  517. event = $.extend({}, abstractEvent); // make a copy of the original
  518. assignDatesToEvent(
  519. start, end,
  520. !startTime && !endTime, // allDay?
  521. event
  522. );
  523. events.push(event);
  524. }
  525. date.add(1, 'days');
  526. }
  527. }
  528. else {
  529. events.push(abstractEvent); // return the original event. will be a one-item array
  530. }
  531. }
  532. return events;
  533. }
  534. /* Event Modification Math
  535. -----------------------------------------------------------------------------------------*/
  536. // Modifies an event and all related events by applying the given properties.
  537. // Special date-diffing logic is used for manipulation of dates.
  538. // If `props` does not contain start/end dates, the updated values are assumed to be the event's current start/end.
  539. // All date comparisons are done against the event's pristine _start and _end dates.
  540. // Returns an object with delta information and a function to undo all operations.
  541. // For making computations in a granularity greater than day/time, specify largeUnit.
  542. // NOTE: The given `newProps` might be mutated for normalization purposes.
  543. function mutateEvent(event, newProps, largeUnit) {
  544. var miscProps = {};
  545. var oldProps;
  546. var clearEnd;
  547. var startDelta;
  548. var endDelta;
  549. var durationDelta;
  550. var undoFunc;
  551. // diffs the dates in the appropriate way, returning a duration
  552. function diffDates(date1, date0) { // date1 - date0
  553. if (largeUnit) {
  554. return diffByUnit(date1, date0, largeUnit);
  555. }
  556. else if (newProps.allDay) {
  557. return diffDay(date1, date0);
  558. }
  559. else {
  560. return diffDayTime(date1, date0);
  561. }
  562. }
  563. newProps = newProps || {};
  564. // normalize new date-related properties
  565. if (!newProps.start) {
  566. newProps.start = event.start.clone();
  567. }
  568. if (newProps.end === undefined) {
  569. newProps.end = event.end ? event.end.clone() : null;
  570. }
  571. if (newProps.allDay == null) { // is null or undefined?
  572. newProps.allDay = event.allDay;
  573. }
  574. normalizeEventDates(newProps);
  575. // create normalized versions of the original props to compare against
  576. // need a real end value, for diffing
  577. oldProps = {
  578. start: event._start.clone(),
  579. end: event._end ? event._end.clone() : t.getDefaultEventEnd(event._allDay, event._start),
  580. allDay: newProps.allDay // normalize the dates in the same regard as the new properties
  581. };
  582. normalizeEventDates(oldProps);
  583. // need to clear the end date if explicitly changed to null
  584. clearEnd = event._end !== null && newProps.end === null;
  585. // compute the delta for moving the start date
  586. startDelta = diffDates(newProps.start, oldProps.start);
  587. // compute the delta for moving the end date
  588. if (newProps.end) {
  589. endDelta = diffDates(newProps.end, oldProps.end);
  590. durationDelta = endDelta.subtract(startDelta);
  591. }
  592. else {
  593. durationDelta = null;
  594. }
  595. // gather all non-date-related properties
  596. $.each(newProps, function(name, val) {
  597. if (isMiscEventPropName(name)) {
  598. if (val !== undefined) {
  599. miscProps[name] = val;
  600. }
  601. }
  602. });
  603. // apply the operations to the event and all related events
  604. undoFunc = mutateEvents(
  605. clientEvents(event._id), // get events with this ID
  606. clearEnd,
  607. newProps.allDay,
  608. startDelta,
  609. durationDelta,
  610. miscProps
  611. );
  612. return {
  613. dateDelta: startDelta,
  614. durationDelta: durationDelta,
  615. undo: undoFunc
  616. };
  617. }
  618. // Modifies an array of events in the following ways (operations are in order):
  619. // - clear the event's `end`
  620. // - convert the event to allDay
  621. // - add `dateDelta` to the start and end
  622. // - add `durationDelta` to the event's duration
  623. // - assign `miscProps` to the event
  624. //
  625. // Returns a function that can be called to undo all the operations.
  626. //
  627. // TODO: don't use so many closures. possible memory issues when lots of events with same ID.
  628. //
  629. function mutateEvents(events, clearEnd, allDay, dateDelta, durationDelta, miscProps) {
  630. var isAmbigTimezone = t.getIsAmbigTimezone();
  631. var undoFunctions = [];
  632. // normalize zero-length deltas to be null
  633. if (dateDelta && !dateDelta.valueOf()) { dateDelta = null; }
  634. if (durationDelta && !durationDelta.valueOf()) { durationDelta = null; }
  635. $.each(events, function(i, event) {
  636. var oldProps;
  637. var newProps;
  638. // build an object holding all the old values, both date-related and misc.
  639. // for the undo function.
  640. oldProps = {
  641. start: event.start.clone(),
  642. end: event.end ? event.end.clone() : null,
  643. allDay: event.allDay
  644. };
  645. $.each(miscProps, function(name) {
  646. oldProps[name] = event[name];
  647. });
  648. // new date-related properties. work off the original date snapshot.
  649. // ok to use references because they will be thrown away when backupEventDates is called.
  650. newProps = {
  651. start: event._start,
  652. end: event._end,
  653. allDay: allDay // normalize the dates in the same regard as the new properties
  654. };
  655. normalizeEventDates(newProps); // massages start/end/allDay
  656. // strip or ensure the end date
  657. if (clearEnd) {
  658. newProps.end = null;
  659. }
  660. else if (durationDelta && !newProps.end) { // the duration translation requires an end date
  661. newProps.end = t.getDefaultEventEnd(newProps.allDay, newProps.start);
  662. }
  663. if (dateDelta) {
  664. newProps.start.add(dateDelta);
  665. if (newProps.end) {
  666. newProps.end.add(dateDelta);
  667. }
  668. }
  669. if (durationDelta) {
  670. newProps.end.add(durationDelta); // end already ensured above
  671. }
  672. // if the dates have changed, and we know it is impossible to recompute the
  673. // timezone offsets, strip the zone.
  674. if (
  675. isAmbigTimezone &&
  676. !newProps.allDay &&
  677. (dateDelta || durationDelta)
  678. ) {
  679. newProps.start.stripZone();
  680. if (newProps.end) {
  681. newProps.end.stripZone();
  682. }
  683. }
  684. $.extend(event, miscProps, newProps); // copy over misc props, then date-related props
  685. backupEventDates(event); // regenerate internal _start/_end/_allDay
  686. undoFunctions.push(function() {
  687. $.extend(event, oldProps);
  688. backupEventDates(event); // regenerate internal _start/_end/_allDay
  689. });
  690. });
  691. return function() {
  692. for (var i = 0; i < undoFunctions.length; i++) {
  693. undoFunctions[i]();
  694. }
  695. };
  696. }
  697. /* Business Hours
  698. -----------------------------------------------------------------------------------------*/
  699. t.getBusinessHoursEvents = getBusinessHoursEvents;
  700. // Returns an array of events as to when the business hours occur in the given view.
  701. // Abuse of our event system :(
  702. function getBusinessHoursEvents(wholeDay) {
  703. var optionVal = options.businessHours;
  704. var defaultVal = {
  705. className: 'fc-nonbusiness',
  706. start: '09:00',
  707. end: '17:00',
  708. dow: [ 1, 2, 3, 4, 5 ], // monday - friday
  709. rendering: 'inverse-background'
  710. };
  711. var view = t.getView();
  712. var eventInput;
  713. if (optionVal) { // `true` (which means "use the defaults") or an override object
  714. eventInput = $.extend(
  715. {}, // copy to a new object in either case
  716. defaultVal,
  717. typeof optionVal === 'object' ? optionVal : {} // override the defaults
  718. );
  719. }
  720. if (eventInput) {
  721. // if a whole-day series is requested, clear the start/end times
  722. if (wholeDay) {
  723. eventInput.start = null;
  724. eventInput.end = null;
  725. }
  726. return expandEvent(
  727. buildEventFromInput(eventInput),
  728. view.start,
  729. view.end
  730. );
  731. }
  732. return [];
  733. }
  734. /* Overlapping / Constraining
  735. -----------------------------------------------------------------------------------------*/
  736. t.isEventSpanAllowed = isEventSpanAllowed;
  737. t.isExternalSpanAllowed = isExternalSpanAllowed;
  738. t.isSelectionSpanAllowed = isSelectionSpanAllowed;
  739. // Determines if the given event can be relocated to the given span (unzoned start/end with other misc data)
  740. function isEventSpanAllowed(span, event) {
  741. var source = event.source || {};
  742. var constraint = firstDefined(
  743. event.constraint,
  744. source.constraint,
  745. options.eventConstraint
  746. );
  747. var overlap = firstDefined(
  748. event.overlap,
  749. source.overlap,
  750. options.eventOverlap
  751. );
  752. return isSpanAllowed(span, constraint, overlap, event);
  753. }
  754. // Determines if an external event can be relocated to the given span (unzoned start/end with other misc data)
  755. function isExternalSpanAllowed(eventSpan, eventLocation, eventProps) {
  756. var eventInput;
  757. var event;
  758. // note: very similar logic is in View's reportExternalDrop
  759. if (eventProps) {
  760. eventInput = $.extend({}, eventProps, eventLocation);
  761. event = expandEvent(buildEventFromInput(eventInput))[0];
  762. }
  763. if (event) {
  764. return isEventSpanAllowed(eventSpan, event);
  765. }
  766. else { // treat it as a selection
  767. return isSelectionSpanAllowed(eventSpan);
  768. }
  769. }
  770. // Determines the given span (unzoned start/end with other misc data) can be selected.
  771. function isSelectionSpanAllowed(span) {
  772. return isSpanAllowed(span, options.selectConstraint, options.selectOverlap);
  773. }
  774. // Returns true if the given span (caused by an event drop/resize or a selection) is allowed to exist
  775. // according to the constraint/overlap settings.
  776. // `event` is not required if checking a selection.
  777. function isSpanAllowed(span, constraint, overlap, event) {
  778. var constraintEvents;
  779. var anyContainment;
  780. var peerEvents;
  781. var i, peerEvent;
  782. var peerOverlap;
  783. // the range must be fully contained by at least one of produced constraint events
  784. if (constraint != null) {
  785. // not treated as an event! intermediate data structure
  786. // TODO: use ranges in the future
  787. constraintEvents = constraintToEvents(constraint);
  788. anyContainment = false;
  789. for (i = 0; i < constraintEvents.length; i++) {
  790. if (eventContainsRange(constraintEvents[i], span)) {
  791. anyContainment = true;
  792. break;
  793. }
  794. }
  795. if (!anyContainment) {
  796. return false;
  797. }
  798. }
  799. peerEvents = t.getPeerEvents(span, event);
  800. for (i = 0; i < peerEvents.length; i++) {
  801. peerEvent = peerEvents[i];
  802. // there needs to be an actual intersection before disallowing anything
  803. if (eventIntersectsRange(peerEvent, span)) {
  804. // evaluate overlap for the given range and short-circuit if necessary
  805. if (overlap === false) {
  806. return false;
  807. }
  808. // if the event's overlap is a test function, pass the peer event in question as the first param
  809. else if (typeof overlap === 'function' && !overlap(peerEvent, event)) {
  810. return false;
  811. }
  812. // if we are computing if the given range is allowable for an event, consider the other event's
  813. // EventObject-specific or Source-specific `overlap` property
  814. if (event) {
  815. peerOverlap = firstDefined(
  816. peerEvent.overlap,
  817. (peerEvent.source || {}).overlap
  818. // we already considered the global `eventOverlap`
  819. );
  820. if (peerOverlap === false) {
  821. return false;
  822. }
  823. // if the peer event's overlap is a test function, pass the subject event as the first param
  824. if (typeof peerOverlap === 'function' && !peerOverlap(event, peerEvent)) {
  825. return false;
  826. }
  827. }
  828. }
  829. }
  830. return true;
  831. }
  832. // Given an event input from the API, produces an array of event objects. Possible event inputs:
  833. // 'businessHours'
  834. // An event ID (number or string)
  835. // An object with specific start/end dates or a recurring event (like what businessHours accepts)
  836. function constraintToEvents(constraintInput) {
  837. if (constraintInput === 'businessHours') {
  838. return getBusinessHoursEvents();
  839. }
  840. if (typeof constraintInput === 'object') {
  841. return expandEvent(buildEventFromInput(constraintInput));
  842. }
  843. return clientEvents(constraintInput); // probably an ID
  844. }
  845. // Does the event's date range fully contain the given range?
  846. // start/end already assumed to have stripped zones :(
  847. function eventContainsRange(event, range) {
  848. var eventStart = event.start.clone().stripZone();
  849. var eventEnd = t.getEventEnd(event).stripZone();
  850. return range.start >= eventStart && range.end <= eventEnd;
  851. }
  852. // Does the event's date range intersect with the given range?
  853. // start/end already assumed to have stripped zones :(
  854. function eventIntersectsRange(event, range) {
  855. var eventStart = event.start.clone().stripZone();
  856. var eventEnd = t.getEventEnd(event).stripZone();
  857. return range.start < eventEnd && range.end > eventStart;
  858. }
  859. t.getEventCache = function() {
  860. return cache;
  861. };
  862. }
  863. // hook for external libs to manipulate event properties upon creation.
  864. // should manipulate the event in-place.
  865. Calendar.prototype.normalizeEvent = function(event) {
  866. };
  867. // Returns a list of events that the given event should be compared against when being considered for a move to
  868. // the specified span. Attached to the Calendar's prototype because EventManager is a mixin for a Calendar.
  869. Calendar.prototype.getPeerEvents = function(span, event) {
  870. var cache = this.getEventCache();
  871. var peerEvents = [];
  872. var i, otherEvent;
  873. for (i = 0; i < cache.length; i++) {
  874. otherEvent = cache[i];
  875. if (
  876. !event ||
  877. event._id !== otherEvent._id // don't compare the event to itself or other related [repeating] events
  878. ) {
  879. peerEvents.push(otherEvent);
  880. }
  881. }
  882. return peerEvents;
  883. };
  884. // updates the "backup" properties, which are preserved in order to compute diffs later on.
  885. function backupEventDates(event) {
  886. event._allDay = event.allDay;
  887. event._start = event.start.clone();
  888. event._end = event.end ? event.end.clone() : null;
  889. }