Calendar.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966
  1. var Calendar = FC.Calendar = Class.extend({
  2. dirDefaults: null, // option defaults related to LTR or RTL
  3. langDefaults: null, // option defaults related to current locale
  4. overrides: null, // option overrides given to the fullCalendar constructor
  5. options: null, // all defaults combined with overrides
  6. viewSpecCache: null, // cache of view definitions
  7. view: null, // current View object
  8. header: null,
  9. loadingLevel: 0, // number of simultaneous loading tasks
  10. // a lot of this class' OOP logic is scoped within this constructor function,
  11. // but in the future, write individual methods on the prototype.
  12. constructor: Calendar_constructor,
  13. // Subclasses can override this for initialization logic after the constructor has been called
  14. initialize: function() {
  15. },
  16. // Initializes `this.options` and other important options-related objects
  17. initOptions: function(overrides) {
  18. var lang, langDefaults;
  19. var isRTL, dirDefaults;
  20. // converts legacy options into non-legacy ones.
  21. // in the future, when this is removed, don't use `overrides` reference. make a copy.
  22. overrides = massageOverrides(overrides);
  23. lang = overrides.lang;
  24. langDefaults = langOptionHash[lang];
  25. if (!langDefaults) {
  26. lang = Calendar.defaults.lang;
  27. langDefaults = langOptionHash[lang] || {};
  28. }
  29. isRTL = firstDefined(
  30. overrides.isRTL,
  31. langDefaults.isRTL,
  32. Calendar.defaults.isRTL
  33. );
  34. dirDefaults = isRTL ? Calendar.rtlDefaults : {};
  35. this.dirDefaults = dirDefaults;
  36. this.langDefaults = langDefaults;
  37. this.overrides = overrides;
  38. this.options = mergeOptions([ // merge defaults and overrides. lowest to highest precedence
  39. Calendar.defaults, // global defaults
  40. dirDefaults,
  41. langDefaults,
  42. overrides
  43. ]);
  44. populateInstanceComputableOptions(this.options);
  45. this.viewSpecCache = {}; // somewhat unrelated
  46. },
  47. // Gets information about how to create a view. Will use a cache.
  48. getViewSpec: function(viewType) {
  49. var cache = this.viewSpecCache;
  50. return cache[viewType] || (cache[viewType] = this.buildViewSpec(viewType));
  51. },
  52. // Given a duration singular unit, like "week" or "day", finds a matching view spec.
  53. // Preference is given to views that have corresponding buttons.
  54. getUnitViewSpec: function(unit) {
  55. var viewTypes;
  56. var i;
  57. var spec;
  58. if ($.inArray(unit, intervalUnits) != -1) {
  59. // put views that have buttons first. there will be duplicates, but oh well
  60. viewTypes = this.header.getViewsWithButtons();
  61. $.each(FC.views, function(viewType) { // all views
  62. viewTypes.push(viewType);
  63. });
  64. for (i = 0; i < viewTypes.length; i++) {
  65. spec = this.getViewSpec(viewTypes[i]);
  66. if (spec) {
  67. if (spec.singleUnit == unit) {
  68. return spec;
  69. }
  70. }
  71. }
  72. }
  73. },
  74. // Builds an object with information on how to create a given view
  75. buildViewSpec: function(requestedViewType) {
  76. var viewOverrides = this.overrides.views || {};
  77. var specChain = []; // for the view. lowest to highest priority
  78. var defaultsChain = []; // for the view. lowest to highest priority
  79. var overridesChain = []; // for the view. lowest to highest priority
  80. var viewType = requestedViewType;
  81. var spec; // for the view
  82. var overrides; // for the view
  83. var duration;
  84. var unit;
  85. // iterate from the specific view definition to a more general one until we hit an actual View class
  86. while (viewType) {
  87. spec = fcViews[viewType];
  88. overrides = viewOverrides[viewType];
  89. viewType = null; // clear. might repopulate for another iteration
  90. if (typeof spec === 'function') { // TODO: deprecate
  91. spec = { 'class': spec };
  92. }
  93. if (spec) {
  94. specChain.unshift(spec);
  95. defaultsChain.unshift(spec.defaults || {});
  96. duration = duration || spec.duration;
  97. viewType = viewType || spec.type;
  98. }
  99. if (overrides) {
  100. overridesChain.unshift(overrides); // view-specific option hashes have options at zero-level
  101. duration = duration || overrides.duration;
  102. viewType = viewType || overrides.type;
  103. }
  104. }
  105. spec = mergeProps(specChain);
  106. spec.type = requestedViewType;
  107. if (!spec['class']) {
  108. return false;
  109. }
  110. if (duration) {
  111. duration = moment.duration(duration);
  112. if (duration.valueOf()) { // valid?
  113. spec.duration = duration;
  114. unit = computeIntervalUnit(duration);
  115. // view is a single-unit duration, like "week" or "day"
  116. // incorporate options for this. lowest priority
  117. if (duration.as(unit) === 1) {
  118. spec.singleUnit = unit;
  119. overridesChain.unshift(viewOverrides[unit] || {});
  120. }
  121. }
  122. }
  123. spec.defaults = mergeOptions(defaultsChain);
  124. spec.overrides = mergeOptions(overridesChain);
  125. this.buildViewSpecOptions(spec);
  126. this.buildViewSpecButtonText(spec, requestedViewType);
  127. return spec;
  128. },
  129. // Builds and assigns a view spec's options object from its already-assigned defaults and overrides
  130. buildViewSpecOptions: function(spec) {
  131. spec.options = mergeOptions([ // lowest to highest priority
  132. Calendar.defaults, // global defaults
  133. spec.defaults, // view's defaults (from ViewSubclass.defaults)
  134. this.dirDefaults,
  135. this.langDefaults, // locale and dir take precedence over view's defaults!
  136. this.overrides, // calendar's overrides (options given to constructor)
  137. spec.overrides // view's overrides (view-specific options)
  138. ]);
  139. populateInstanceComputableOptions(spec.options);
  140. },
  141. // Computes and assigns a view spec's buttonText-related options
  142. buildViewSpecButtonText: function(spec, requestedViewType) {
  143. // given an options object with a possible `buttonText` hash, lookup the buttonText for the
  144. // requested view, falling back to a generic unit entry like "week" or "day"
  145. function queryButtonText(options) {
  146. var buttonText = options.buttonText || {};
  147. return buttonText[requestedViewType] ||
  148. (spec.singleUnit ? buttonText[spec.singleUnit] : null);
  149. }
  150. // highest to lowest priority
  151. spec.buttonTextOverride =
  152. queryButtonText(this.overrides) || // constructor-specified buttonText lookup hash takes precedence
  153. spec.overrides.buttonText; // `buttonText` for view-specific options is a string
  154. // highest to lowest priority. mirrors buildViewSpecOptions
  155. spec.buttonTextDefault =
  156. queryButtonText(this.langDefaults) ||
  157. queryButtonText(this.dirDefaults) ||
  158. spec.defaults.buttonText || // a single string. from ViewSubclass.defaults
  159. queryButtonText(Calendar.defaults) ||
  160. (spec.duration ? this.humanizeDuration(spec.duration) : null) || // like "3 days"
  161. requestedViewType; // fall back to given view name
  162. },
  163. // Given a view name for a custom view or a standard view, creates a ready-to-go View object
  164. instantiateView: function(viewType) {
  165. var spec = this.getViewSpec(viewType);
  166. return new spec['class'](this, viewType, spec.options, spec.duration);
  167. },
  168. // Returns a boolean about whether the view is okay to instantiate at some point
  169. isValidViewType: function(viewType) {
  170. return Boolean(this.getViewSpec(viewType));
  171. },
  172. // Should be called when any type of async data fetching begins
  173. pushLoading: function() {
  174. if (!(this.loadingLevel++)) {
  175. this.trigger('loading', null, true, this.view);
  176. }
  177. },
  178. // Should be called when any type of async data fetching completes
  179. popLoading: function() {
  180. if (!(--this.loadingLevel)) {
  181. this.trigger('loading', null, false, this.view);
  182. }
  183. },
  184. // Given arguments to the select method in the API, returns a span (unzoned start/end and other info)
  185. buildSelectSpan: function(zonedStartInput, zonedEndInput) {
  186. var start = this.moment(zonedStartInput).stripZone();
  187. var end;
  188. if (zonedEndInput) {
  189. end = this.moment(zonedEndInput).stripZone();
  190. }
  191. else if (start.hasTime()) {
  192. end = start.clone().add(this.defaultTimedEventDuration);
  193. }
  194. else {
  195. end = start.clone().add(this.defaultAllDayEventDuration);
  196. }
  197. return { start: start, end: end };
  198. }
  199. });
  200. Calendar.mixin(Emitter);
  201. function Calendar_constructor(element, overrides) {
  202. var t = this;
  203. t.initOptions(overrides || {});
  204. var options = this.options;
  205. // Exports
  206. // -----------------------------------------------------------------------------------
  207. t.render = render;
  208. t.destroy = destroy;
  209. t.refetchEvents = refetchEvents;
  210. t.reportEvents = reportEvents;
  211. t.reportEventChange = reportEventChange;
  212. t.rerenderEvents = renderEvents; // `renderEvents` serves as a rerender. an API method
  213. t.changeView = renderView; // `renderView` will switch to another view
  214. t.select = select;
  215. t.unselect = unselect;
  216. t.prev = prev;
  217. t.next = next;
  218. t.prevYear = prevYear;
  219. t.nextYear = nextYear;
  220. t.today = today;
  221. t.gotoDate = gotoDate;
  222. t.incrementDate = incrementDate;
  223. t.zoomTo = zoomTo;
  224. t.getDate = getDate;
  225. t.getCalendar = getCalendar;
  226. t.getView = getView;
  227. t.option = option;
  228. t.trigger = trigger;
  229. // Language-data Internals
  230. // -----------------------------------------------------------------------------------
  231. // Apply overrides to the current language's data
  232. var localeData = createObject( // make a cheap copy
  233. getMomentLocaleData(options.lang) // will fall back to en
  234. );
  235. if (options.monthNames) {
  236. localeData._months = options.monthNames;
  237. }
  238. if (options.monthNamesShort) {
  239. localeData._monthsShort = options.monthNamesShort;
  240. }
  241. if (options.dayNames) {
  242. localeData._weekdays = options.dayNames;
  243. }
  244. if (options.dayNamesShort) {
  245. localeData._weekdaysShort = options.dayNamesShort;
  246. }
  247. if (options.firstDay != null) {
  248. var _week = createObject(localeData._week); // _week: { dow: # }
  249. _week.dow = options.firstDay;
  250. localeData._week = _week;
  251. }
  252. // assign a normalized value, to be used by our .week() moment extension
  253. localeData._fullCalendar_weekCalc = (function(weekCalc) {
  254. if (typeof weekCalc === 'function') {
  255. return weekCalc;
  256. }
  257. else if (weekCalc === 'local') {
  258. return weekCalc;
  259. }
  260. else if (weekCalc === 'iso' || weekCalc === 'ISO') {
  261. return 'ISO';
  262. }
  263. })(options.weekNumberCalculation);
  264. // Store the first day of the week according to the week number
  265. // *calculation* method, not the week *display*.
  266. // With ISO: Always 1 (Monday). Is not influenced by the firstDay
  267. // option.
  268. // With custom function: Test a sequence of 7 days with the custom
  269. // function to determine on which day of the week the week number
  270. // changes. Is not influenced by the firstDay option.
  271. // With local: Set to the value of localeData._week.dow, which will
  272. // contain the value of firstDay, or else will return the dow value
  273. // from a protype, such as the locale or moment's Locale object
  274. // default 0 (Sunday).
  275. localeData._fullCalendar_weekCalcFirstDoW = (function(weekCalc, localeDoW) {
  276. var weekCalcFirstDoW;
  277. if (weekCalc === 'ISO') {
  278. weekCalcFirstDoW = 1;
  279. }
  280. else if (typeof weekCalc === 'function') {
  281. var m;
  282. var wkNr;
  283. var wkNrPrev;
  284. // Test for first day of week. Start comparing with Sunday (0).
  285. m = moment.utc("2015-01-04T00:00Z");
  286. wkNrPrev = weekCalc(m);
  287. weekCalcFirstDoW = 0; // Assume Sunday.
  288. // Check Monday through Saturday for change in week number.
  289. for (i = 1; i < 7; i++) {
  290. m.add(1, 'days');
  291. wkNr = weekCalc(m);
  292. if (wkNr != wkNrPrev) {
  293. weekCalcFirstDoW = i;
  294. break;
  295. }
  296. wkNrPrev = wkNr;
  297. }
  298. }
  299. else if (weekCalc === 'local') {
  300. weekCalcFirstDoW = localeDoW;
  301. }
  302. return weekCalcFirstDoW;
  303. })(localeData._fullCalendar_weekCalc, localeData._week.dow);
  304. // Calendar-specific Date Utilities
  305. // -----------------------------------------------------------------------------------
  306. t.defaultAllDayEventDuration = moment.duration(options.defaultAllDayEventDuration);
  307. t.defaultTimedEventDuration = moment.duration(options.defaultTimedEventDuration);
  308. // Builds a moment using the settings of the current calendar: timezone and language.
  309. // Accepts anything the vanilla moment() constructor accepts.
  310. t.moment = function() {
  311. var mom;
  312. if (options.timezone === 'local') {
  313. mom = FC.moment.apply(null, arguments);
  314. // Force the moment to be local, because FC.moment doesn't guarantee it.
  315. if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone
  316. mom.local();
  317. }
  318. }
  319. else if (options.timezone === 'UTC') {
  320. mom = FC.moment.utc.apply(null, arguments); // process as UTC
  321. }
  322. else {
  323. mom = FC.moment.parseZone.apply(null, arguments); // let the input decide the zone
  324. }
  325. if ('_locale' in mom) { // moment 2.8 and above
  326. mom._locale = localeData;
  327. }
  328. else { // pre-moment-2.8
  329. mom._lang = localeData;
  330. }
  331. return mom;
  332. };
  333. // Returns a boolean about whether or not the calendar knows how to calculate
  334. // the timezone offset of arbitrary dates in the current timezone.
  335. t.getIsAmbigTimezone = function() {
  336. return options.timezone !== 'local' && options.timezone !== 'UTC';
  337. };
  338. // Returns a copy of the given date in the current timezone. Has no effect on dates without times.
  339. t.applyTimezone = function(date) {
  340. if (!date.hasTime()) {
  341. return date.clone();
  342. }
  343. var zonedDate = t.moment(date.toArray());
  344. var timeAdjust = date.time() - zonedDate.time();
  345. var adjustedZonedDate;
  346. // Safari sometimes has problems with this coersion when near DST. Adjust if necessary. (bug #2396)
  347. if (timeAdjust) { // is the time result different than expected?
  348. adjustedZonedDate = zonedDate.clone().add(timeAdjust); // add milliseconds
  349. if (date.time() - adjustedZonedDate.time() === 0) { // does it match perfectly now?
  350. zonedDate = adjustedZonedDate;
  351. }
  352. }
  353. return zonedDate;
  354. };
  355. // Returns a moment for the current date, as defined by the client's computer or from the `now` option.
  356. // Will return an moment with an ambiguous timezone.
  357. t.getNow = function() {
  358. var now = options.now;
  359. if (typeof now === 'function') {
  360. now = now();
  361. }
  362. return t.moment(now).stripZone();
  363. };
  364. // Get an event's normalized end date. If not present, calculate it from the defaults.
  365. t.getEventEnd = function(event) {
  366. if (event.end) {
  367. return event.end.clone();
  368. }
  369. else {
  370. return t.getDefaultEventEnd(event.allDay, event.start);
  371. }
  372. };
  373. // Given an event's allDay status and start date, return what its fallback end date should be.
  374. // TODO: rename to computeDefaultEventEnd
  375. t.getDefaultEventEnd = function(allDay, zonedStart) {
  376. var end = zonedStart.clone();
  377. if (allDay) {
  378. end.stripTime().add(t.defaultAllDayEventDuration);
  379. }
  380. else {
  381. end.add(t.defaultTimedEventDuration);
  382. }
  383. if (t.getIsAmbigTimezone()) {
  384. end.stripZone(); // we don't know what the tzo should be
  385. }
  386. return end;
  387. };
  388. // Produces a human-readable string for the given duration.
  389. // Side-effect: changes the locale of the given duration.
  390. t.humanizeDuration = function(duration) {
  391. return (duration.locale || duration.lang).call(duration, options.lang) // works moment-pre-2.8
  392. .humanize();
  393. };
  394. // Imports
  395. // -----------------------------------------------------------------------------------
  396. EventManager.call(t, options);
  397. var isFetchNeeded = t.isFetchNeeded;
  398. var fetchEvents = t.fetchEvents;
  399. // Locals
  400. // -----------------------------------------------------------------------------------
  401. var _element = element[0];
  402. var header;
  403. var headerElement;
  404. var content;
  405. var tm; // for making theme classes
  406. var currentView; // NOTE: keep this in sync with this.view
  407. var viewsByType = {}; // holds all instantiated view instances, current or not
  408. var suggestedViewHeight;
  409. var windowResizeProxy; // wraps the windowResize function
  410. var ignoreWindowResize = 0;
  411. var events = [];
  412. var date; // unzoned
  413. // Main Rendering
  414. // -----------------------------------------------------------------------------------
  415. // compute the initial ambig-timezone date
  416. if (options.defaultDate != null) {
  417. date = t.moment(options.defaultDate).stripZone();
  418. }
  419. else {
  420. date = t.getNow(); // getNow already returns unzoned
  421. }
  422. function render() {
  423. if (!content) {
  424. initialRender();
  425. }
  426. else if (elementVisible()) {
  427. // mainly for the public API
  428. calcSize();
  429. renderView();
  430. }
  431. }
  432. function initialRender() {
  433. tm = options.theme ? 'ui' : 'fc';
  434. element.addClass('fc');
  435. if (options.isRTL) {
  436. element.addClass('fc-rtl');
  437. }
  438. else {
  439. element.addClass('fc-ltr');
  440. }
  441. if (options.theme) {
  442. element.addClass('ui-widget');
  443. }
  444. else {
  445. element.addClass('fc-unthemed');
  446. }
  447. content = $("<div class='fc-view-container'/>").prependTo(element);
  448. header = t.header = new Header(t, options);
  449. headerElement = header.render();
  450. if (headerElement) {
  451. element.prepend(headerElement);
  452. }
  453. renderView(options.defaultView);
  454. if (options.handleWindowResize) {
  455. windowResizeProxy = debounce(windowResize, options.windowResizeDelay); // prevents rapid calls
  456. $(window).resize(windowResizeProxy);
  457. }
  458. }
  459. function destroy() {
  460. if (currentView) {
  461. currentView.removeElement();
  462. // NOTE: don't null-out currentView/t.view in case API methods are called after destroy.
  463. // It is still the "current" view, just not rendered.
  464. }
  465. header.removeElement();
  466. content.remove();
  467. element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');
  468. if (windowResizeProxy) {
  469. $(window).unbind('resize', windowResizeProxy);
  470. }
  471. }
  472. function elementVisible() {
  473. return element.is(':visible');
  474. }
  475. // View Rendering
  476. // -----------------------------------------------------------------------------------
  477. // Renders a view because of a date change, view-type change, or for the first time.
  478. // If not given a viewType, keep the current view but render different dates.
  479. function renderView(viewType) {
  480. ignoreWindowResize++;
  481. // if viewType is changing, remove the old view's rendering
  482. if (currentView && viewType && currentView.type !== viewType) {
  483. header.deactivateButton(currentView.type);
  484. freezeContentHeight(); // prevent a scroll jump when view element is removed
  485. currentView.removeElement();
  486. currentView = t.view = null;
  487. }
  488. // if viewType changed, or the view was never created, create a fresh view
  489. if (!currentView && viewType) {
  490. currentView = t.view =
  491. viewsByType[viewType] ||
  492. (viewsByType[viewType] = t.instantiateView(viewType));
  493. currentView.setElement(
  494. $("<div class='fc-view fc-" + viewType + "-view' />").appendTo(content)
  495. );
  496. header.activateButton(viewType);
  497. }
  498. if (currentView) {
  499. // in case the view should render a period of time that is completely hidden
  500. date = currentView.massageCurrentDate(date);
  501. // render or rerender the view
  502. if (
  503. !currentView.displaying ||
  504. !date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change
  505. ) {
  506. if (elementVisible()) {
  507. currentView.display(date); // will call freezeContentHeight
  508. unfreezeContentHeight(); // immediately unfreeze regardless of whether display is async
  509. // need to do this after View::render, so dates are calculated
  510. updateHeaderTitle();
  511. updateTodayButton();
  512. getAndRenderEvents();
  513. }
  514. }
  515. }
  516. unfreezeContentHeight(); // undo any lone freezeContentHeight calls
  517. ignoreWindowResize--;
  518. }
  519. // Resizing
  520. // -----------------------------------------------------------------------------------
  521. t.getSuggestedViewHeight = function() {
  522. if (suggestedViewHeight === undefined) {
  523. calcSize();
  524. }
  525. return suggestedViewHeight;
  526. };
  527. t.isHeightAuto = function() {
  528. return options.contentHeight === 'auto' || options.height === 'auto';
  529. };
  530. function updateSize(shouldRecalc) {
  531. if (elementVisible()) {
  532. if (shouldRecalc) {
  533. _calcSize();
  534. }
  535. ignoreWindowResize++;
  536. currentView.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto()
  537. ignoreWindowResize--;
  538. return true; // signal success
  539. }
  540. }
  541. function calcSize() {
  542. if (elementVisible()) {
  543. _calcSize();
  544. }
  545. }
  546. function _calcSize() { // assumes elementVisible
  547. if (typeof options.contentHeight === 'number') { // exists and not 'auto'
  548. suggestedViewHeight = options.contentHeight;
  549. }
  550. else if (typeof options.height === 'number') { // exists and not 'auto'
  551. suggestedViewHeight = options.height - (headerElement ? headerElement.outerHeight(true) : 0);
  552. }
  553. else {
  554. suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5));
  555. }
  556. }
  557. function windowResize(ev) {
  558. if (
  559. !ignoreWindowResize &&
  560. ev.target === window && // so we don't process jqui "resize" events that have bubbled up
  561. currentView.start // view has already been rendered
  562. ) {
  563. if (updateSize(true)) {
  564. currentView.trigger('windowResize', _element);
  565. }
  566. }
  567. }
  568. /* Event Fetching/Rendering
  569. -----------------------------------------------------------------------------*/
  570. // TODO: going forward, most of this stuff should be directly handled by the view
  571. function refetchEvents() { // can be called as an API method
  572. destroyEvents(); // so that events are cleared before user starts waiting for AJAX
  573. fetchAndRenderEvents();
  574. }
  575. function renderEvents() { // destroys old events if previously rendered
  576. if (elementVisible()) {
  577. freezeContentHeight();
  578. currentView.displayEvents(events);
  579. unfreezeContentHeight();
  580. }
  581. }
  582. function destroyEvents() {
  583. freezeContentHeight();
  584. currentView.clearEvents();
  585. unfreezeContentHeight();
  586. }
  587. function getAndRenderEvents() {
  588. if (!options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) {
  589. fetchAndRenderEvents();
  590. }
  591. else {
  592. renderEvents();
  593. }
  594. }
  595. function fetchAndRenderEvents() {
  596. fetchEvents(currentView.start, currentView.end);
  597. // ... will call reportEvents
  598. // ... which will call renderEvents
  599. }
  600. // called when event data arrives
  601. function reportEvents(_events) {
  602. events = _events;
  603. renderEvents();
  604. }
  605. // called when a single event's data has been changed
  606. function reportEventChange() {
  607. renderEvents();
  608. }
  609. /* Header Updating
  610. -----------------------------------------------------------------------------*/
  611. function updateHeaderTitle() {
  612. header.updateTitle(currentView.title);
  613. }
  614. function updateTodayButton() {
  615. var now = t.getNow();
  616. if (now.isWithin(currentView.intervalStart, currentView.intervalEnd)) {
  617. header.disableButton('today');
  618. }
  619. else {
  620. header.enableButton('today');
  621. }
  622. }
  623. /* Selection
  624. -----------------------------------------------------------------------------*/
  625. // this public method receives start/end dates in any format, with any timezone
  626. function select(zonedStartInput, zonedEndInput) {
  627. currentView.select(
  628. t.buildSelectSpan.apply(t, arguments)
  629. );
  630. }
  631. function unselect() { // safe to be called before renderView
  632. if (currentView) {
  633. currentView.unselect();
  634. }
  635. }
  636. /* Date
  637. -----------------------------------------------------------------------------*/
  638. function prev() {
  639. date = currentView.computePrevDate(date);
  640. renderView();
  641. }
  642. function next() {
  643. date = currentView.computeNextDate(date);
  644. renderView();
  645. }
  646. function prevYear() {
  647. date.add(-1, 'years');
  648. renderView();
  649. }
  650. function nextYear() {
  651. date.add(1, 'years');
  652. renderView();
  653. }
  654. function today() {
  655. date = t.getNow();
  656. renderView();
  657. }
  658. function gotoDate(zonedDateInput) {
  659. date = t.moment(zonedDateInput).stripZone();
  660. renderView();
  661. }
  662. function incrementDate(delta) {
  663. date.add(moment.duration(delta));
  664. renderView();
  665. }
  666. // Forces navigation to a view for the given date.
  667. // `viewType` can be a specific view name or a generic one like "week" or "day".
  668. function zoomTo(newDate, viewType) {
  669. var spec;
  670. viewType = viewType || 'day'; // day is default zoom
  671. spec = t.getViewSpec(viewType) || t.getUnitViewSpec(viewType);
  672. date = newDate.clone();
  673. renderView(spec ? spec.type : null);
  674. }
  675. // for external API
  676. function getDate() {
  677. return t.applyTimezone(date); // infuse the calendar's timezone
  678. }
  679. /* Height "Freezing"
  680. -----------------------------------------------------------------------------*/
  681. // TODO: move this into the view
  682. t.freezeContentHeight = freezeContentHeight;
  683. t.unfreezeContentHeight = unfreezeContentHeight;
  684. function freezeContentHeight() {
  685. content.css({
  686. width: '100%',
  687. height: content.height(),
  688. overflow: 'hidden'
  689. });
  690. }
  691. function unfreezeContentHeight() {
  692. content.css({
  693. width: '',
  694. height: '',
  695. overflow: ''
  696. });
  697. }
  698. /* Misc
  699. -----------------------------------------------------------------------------*/
  700. function getCalendar() {
  701. return t;
  702. }
  703. function getView() {
  704. return currentView;
  705. }
  706. function option(name, value) {
  707. if (value === undefined) {
  708. return options[name];
  709. }
  710. if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') {
  711. options[name] = value;
  712. updateSize(true); // true = allow recalculation of height
  713. }
  714. }
  715. function trigger(name, thisObj) { // overrides the Emitter's trigger method :(
  716. var args = Array.prototype.slice.call(arguments, 2);
  717. thisObj = thisObj || _element;
  718. this.triggerWith(name, thisObj, args); // Emitter's method
  719. if (options[name]) {
  720. return options[name].apply(thisObj, args);
  721. }
  722. }
  723. t.initialize();
  724. }