EventManager.js 35 KB

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