EventManager.js 33 KB

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