EventManager.js 28 KB

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