Calendar.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738
  1. function Calendar(element, instanceOptions) {
  2. var t = this;
  3. // Build options object
  4. // -----------------------------------------------------------------------------------
  5. // Precedence (lowest to highest): defaults, rtlDefaults, langOptions, instanceOptions
  6. instanceOptions = instanceOptions || {};
  7. var options = mergeOptions({}, defaults, instanceOptions);
  8. var langOptions;
  9. // determine language options
  10. if (options.lang in langOptionHash) {
  11. langOptions = langOptionHash[options.lang];
  12. }
  13. else {
  14. langOptions = langOptionHash[defaults.lang];
  15. }
  16. if (langOptions) { // if language options exist, rebuild...
  17. options = mergeOptions({}, defaults, langOptions, instanceOptions);
  18. }
  19. if (options.isRTL) { // is isRTL, rebuild...
  20. options = mergeOptions({}, defaults, rtlDefaults, langOptions || {}, instanceOptions);
  21. }
  22. // Exports
  23. // -----------------------------------------------------------------------------------
  24. t.options = options;
  25. t.render = render;
  26. t.destroy = destroy;
  27. t.refetchEvents = refetchEvents;
  28. t.reportEvents = reportEvents;
  29. t.reportEventChange = reportEventChange;
  30. t.rerenderEvents = rerenderEvents;
  31. t.changeView = changeView;
  32. t.select = select;
  33. t.unselect = unselect;
  34. t.prev = prev;
  35. t.next = next;
  36. t.prevYear = prevYear;
  37. t.nextYear = nextYear;
  38. t.today = today;
  39. t.gotoDate = gotoDate;
  40. t.incrementDate = incrementDate;
  41. t.getDate = getDate;
  42. t.getCalendar = getCalendar;
  43. t.getView = getView;
  44. t.option = option;
  45. t.trigger = trigger;
  46. // Language-data Internals
  47. // -----------------------------------------------------------------------------------
  48. // Apply overrides to the current language's data
  49. var langData = createObject( // make a cheap clone
  50. moment.langData(options.lang)
  51. );
  52. if (options.monthNames) {
  53. langData._months = options.monthNames;
  54. }
  55. if (options.monthNamesShort) {
  56. langData._monthsShort = options.monthNamesShort;
  57. }
  58. if (options.dayNames) {
  59. langData._weekdays = options.dayNames;
  60. }
  61. if (options.dayNamesShort) {
  62. langData._weekdaysShort = options.dayNamesShort;
  63. }
  64. if (options.firstDay != null) {
  65. var _week = createObject(langData._week); // _week: { dow: # }
  66. _week.dow = options.firstDay;
  67. langData._week = _week;
  68. }
  69. // Calendar-specific Date Utilities
  70. // -----------------------------------------------------------------------------------
  71. t.defaultAllDayEventDuration = moment.duration(options.defaultAllDayEventDuration);
  72. t.defaultTimedEventDuration = moment.duration(options.defaultTimedEventDuration);
  73. // Builds a moment using the settings of the current calendar: timezone and language.
  74. // Accepts anything the vanilla moment() constructor accepts.
  75. t.moment = function() {
  76. var mom;
  77. if (options.timezone === 'local') {
  78. mom = fc.moment.apply(null, arguments);
  79. // Force the moment to be local, because fc.moment doesn't guarantee it.
  80. if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone
  81. mom.local();
  82. }
  83. }
  84. else if (options.timezone === 'UTC') {
  85. mom = fc.moment.utc.apply(null, arguments); // process as UTC
  86. }
  87. else {
  88. mom = fc.moment.parseZone.apply(null, arguments); // let the input decide the zone
  89. }
  90. mom._lang = langData;
  91. return mom;
  92. };
  93. // Returns a boolean about whether or not the calendar knows how to calculate
  94. // the timezone offset of arbitrary dates in the current timezone.
  95. t.getIsAmbigTimezone = function() {
  96. return options.timezone !== 'local' && options.timezone !== 'UTC';
  97. };
  98. // Returns a copy of the given date in the current timezone of it is ambiguously zoned.
  99. // This will also give the date an unambiguous time.
  100. t.rezoneDate = function(date) {
  101. return t.moment(date.toArray());
  102. };
  103. // Returns a moment for the current date, as defined by the client's computer,
  104. // or overridden by the `now` option.
  105. t.getNow = function() {
  106. var now = options.now;
  107. if (typeof now === 'function') {
  108. now = now();
  109. }
  110. return t.moment(now);
  111. };
  112. // Calculates the week number for a moment according to the calendar's
  113. // `weekNumberCalculation` setting.
  114. t.calculateWeekNumber = function(mom) {
  115. var calc = options.weekNumberCalculation;
  116. if (typeof calc === 'function') {
  117. return calc(mom);
  118. }
  119. else if (calc === 'local') {
  120. return mom.week();
  121. }
  122. else if (calc.toUpperCase() === 'ISO') {
  123. return mom.isoWeek();
  124. }
  125. };
  126. // Get an event's normalized end date. If not present, calculate it from the defaults.
  127. t.getEventEnd = function(event) {
  128. if (event.end) {
  129. return event.end.clone();
  130. }
  131. else {
  132. return t.getDefaultEventEnd(event.allDay, event.start);
  133. }
  134. };
  135. // Given an event's allDay status and start date, return swhat its fallback end date should be.
  136. t.getDefaultEventEnd = function(allDay, start) { // TODO: rename to computeDefaultEventEnd
  137. var end = start.clone();
  138. if (allDay) {
  139. end.stripTime().add(t.defaultAllDayEventDuration);
  140. }
  141. else {
  142. end.add(t.defaultTimedEventDuration);
  143. }
  144. if (t.getIsAmbigTimezone()) {
  145. end.stripZone(); // we don't know what the tzo should be
  146. }
  147. return end;
  148. };
  149. // Date-formatting Utilities
  150. // -----------------------------------------------------------------------------------
  151. // Like the vanilla formatRange, but with calendar-specific settings applied.
  152. t.formatRange = function(m1, m2, formatStr) {
  153. // a function that returns a formatStr // TODO: in future, precompute this
  154. if (typeof formatStr === 'function') {
  155. formatStr = formatStr.call(t, options, langData);
  156. }
  157. return formatRange(m1, m2, formatStr, null, options.isRTL);
  158. };
  159. // Like the vanilla formatDate, but with calendar-specific settings applied.
  160. t.formatDate = function(mom, formatStr) {
  161. // a function that returns a formatStr // TODO: in future, precompute this
  162. if (typeof formatStr === 'function') {
  163. formatStr = formatStr.call(t, options, langData);
  164. }
  165. return formatDate(mom, formatStr);
  166. };
  167. // Imports
  168. // -----------------------------------------------------------------------------------
  169. EventManager.call(t, options);
  170. var isFetchNeeded = t.isFetchNeeded;
  171. var fetchEvents = t.fetchEvents;
  172. // Locals
  173. // -----------------------------------------------------------------------------------
  174. var _element = element[0];
  175. var header;
  176. var headerElement;
  177. var content;
  178. var tm; // for making theme classes
  179. var currentView;
  180. var elementOuterWidth;
  181. var suggestedViewHeight;
  182. var resizeUID = 0;
  183. var ignoreWindowResize = 0;
  184. var date;
  185. var events = [];
  186. var _dragElement;
  187. // Main Rendering
  188. // -----------------------------------------------------------------------------------
  189. if (options.defaultDate != null) {
  190. date = t.moment(options.defaultDate);
  191. }
  192. else {
  193. date = t.getNow();
  194. }
  195. function render(inc) {
  196. if (!content) {
  197. initialRender();
  198. }
  199. else if (elementVisible()) {
  200. // mainly for the public API
  201. calcSize();
  202. _renderView(inc);
  203. }
  204. }
  205. function initialRender() {
  206. tm = options.theme ? 'ui' : 'fc';
  207. element.addClass('fc');
  208. if (options.isRTL) {
  209. element.addClass('fc-rtl');
  210. }
  211. else {
  212. element.addClass('fc-ltr');
  213. }
  214. if (options.theme) {
  215. element.addClass('ui-widget');
  216. }
  217. content = $("<div class='fc-content' />")
  218. .prependTo(element);
  219. header = new Header(t, options);
  220. headerElement = header.render();
  221. if (headerElement) {
  222. element.prepend(headerElement);
  223. }
  224. changeView(options.defaultView);
  225. if (options.handleWindowResize) {
  226. $(window).resize(windowResize);
  227. }
  228. // needed for IE in a 0x0 iframe, b/c when it is resized, never triggers a windowResize
  229. if (!bodyVisible()) {
  230. lateRender();
  231. }
  232. }
  233. // called when we know the calendar couldn't be rendered when it was initialized,
  234. // but we think it's ready now
  235. function lateRender() {
  236. setTimeout(function() { // IE7 needs this so dimensions are calculated correctly
  237. if (!currentView.start && bodyVisible()) { // !currentView.start makes sure this never happens more than once
  238. renderView();
  239. }
  240. },0);
  241. }
  242. function destroy() {
  243. if (currentView) {
  244. trigger('viewDestroy', currentView, currentView, currentView.element);
  245. currentView.triggerEventDestroy();
  246. }
  247. $(window).unbind('resize', windowResize);
  248. header.destroy();
  249. content.remove();
  250. element.removeClass('fc fc-rtl ui-widget');
  251. }
  252. function elementVisible() {
  253. return element.is(':visible');
  254. }
  255. function bodyVisible() {
  256. return $('body').is(':visible');
  257. }
  258. // View Rendering
  259. // -----------------------------------------------------------------------------------
  260. function changeView(newViewName) {
  261. if (!currentView || newViewName != currentView.name) {
  262. _changeView(newViewName);
  263. }
  264. }
  265. function _changeView(newViewName) {
  266. ignoreWindowResize++;
  267. if (currentView) {
  268. trigger('viewDestroy', currentView, currentView, currentView.element);
  269. unselect();
  270. currentView.triggerEventDestroy(); // trigger 'eventDestroy' for each event
  271. freezeContentHeight();
  272. currentView.element.remove();
  273. header.deactivateButton(currentView.name);
  274. }
  275. header.activateButton(newViewName);
  276. currentView = new fcViews[newViewName](
  277. $("<div class='fc-view fc-view-" + newViewName + "' />")
  278. .appendTo(content),
  279. t // the calendar object
  280. );
  281. renderView();
  282. unfreezeContentHeight();
  283. ignoreWindowResize--;
  284. }
  285. function renderView(inc) {
  286. if (
  287. !currentView.start || // never rendered before
  288. inc || // explicit date window change
  289. !date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change
  290. ) {
  291. if (elementVisible()) {
  292. _renderView(inc);
  293. }
  294. }
  295. }
  296. function _renderView(inc) { // assumes elementVisible
  297. ignoreWindowResize++;
  298. if (currentView.start) { // already been rendered?
  299. trigger('viewDestroy', currentView, currentView, currentView.element);
  300. unselect();
  301. clearEvents();
  302. }
  303. freezeContentHeight();
  304. if (inc) {
  305. date = currentView.incrementDate(date, inc);
  306. }
  307. currentView.render(date.clone()); // the view's render method ONLY renders the skeleton, nothing else
  308. setSize();
  309. unfreezeContentHeight();
  310. (currentView.afterRender || noop)();
  311. updateTitle();
  312. updateTodayButton();
  313. trigger('viewRender', currentView, currentView, currentView.element);
  314. ignoreWindowResize--;
  315. getAndRenderEvents();
  316. }
  317. // Resizing
  318. // -----------------------------------------------------------------------------------
  319. function updateSize() {
  320. if (elementVisible()) {
  321. unselect();
  322. clearEvents();
  323. calcSize();
  324. setSize();
  325. renderEvents();
  326. }
  327. }
  328. function calcSize() { // assumes elementVisible
  329. if (options.contentHeight) {
  330. suggestedViewHeight = options.contentHeight;
  331. }
  332. else if (options.height) {
  333. suggestedViewHeight = options.height - (headerElement ? headerElement.height() : 0) - vsides(content);
  334. }
  335. else {
  336. suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5));
  337. }
  338. }
  339. function setSize() { // assumes elementVisible
  340. if (suggestedViewHeight === undefined) {
  341. calcSize(); // for first time
  342. // NOTE: we don't want to recalculate on every renderView because
  343. // it could result in oscillating heights due to scrollbars.
  344. }
  345. ignoreWindowResize++;
  346. currentView.setHeight(suggestedViewHeight);
  347. currentView.setWidth(content.width());
  348. ignoreWindowResize--;
  349. elementOuterWidth = element.outerWidth();
  350. }
  351. function windowResize() {
  352. if (!ignoreWindowResize) {
  353. if (currentView.start) { // view has already been rendered
  354. var uid = ++resizeUID;
  355. setTimeout(function() { // add a delay
  356. if (uid == resizeUID && !ignoreWindowResize && elementVisible()) {
  357. if (elementOuterWidth != (elementOuterWidth = element.outerWidth())) {
  358. ignoreWindowResize++; // in case the windowResize callback changes the height
  359. updateSize();
  360. currentView.trigger('windowResize', _element);
  361. ignoreWindowResize--;
  362. }
  363. }
  364. }, 200);
  365. }else{
  366. // calendar must have been initialized in a 0x0 iframe that has just been resized
  367. lateRender();
  368. }
  369. }
  370. }
  371. /* Event Fetching/Rendering
  372. -----------------------------------------------------------------------------*/
  373. // TODO: going forward, most of this stuff should be directly handled by the view
  374. function refetchEvents() { // can be called as an API method
  375. clearEvents();
  376. fetchAndRenderEvents();
  377. }
  378. function rerenderEvents(modifiedEventID) { // can be called as an API method
  379. clearEvents();
  380. renderEvents(modifiedEventID);
  381. }
  382. function renderEvents(modifiedEventID) { // TODO: remove modifiedEventID hack
  383. if (elementVisible()) {
  384. currentView.renderEvents(events, modifiedEventID); // actually render the DOM elements
  385. currentView.trigger('eventAfterAllRender');
  386. }
  387. }
  388. function clearEvents() {
  389. currentView.triggerEventDestroy(); // trigger 'eventDestroy' for each event
  390. currentView.clearEvents(); // actually remove the DOM elements
  391. currentView.clearEventData(); // for View.js, TODO: unify with clearEvents
  392. }
  393. function getAndRenderEvents() {
  394. if (!options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) {
  395. fetchAndRenderEvents();
  396. }
  397. else {
  398. renderEvents();
  399. }
  400. }
  401. function fetchAndRenderEvents() {
  402. fetchEvents(currentView.start, currentView.end);
  403. // ... will call reportEvents
  404. // ... which will call renderEvents
  405. }
  406. // called when event data arrives
  407. function reportEvents(_events) {
  408. events = _events;
  409. renderEvents();
  410. }
  411. // called when a single event's data has been changed
  412. function reportEventChange(eventID) {
  413. rerenderEvents(eventID);
  414. }
  415. /* Header Updating
  416. -----------------------------------------------------------------------------*/
  417. function updateTitle() {
  418. header.updateTitle(currentView.title);
  419. }
  420. function updateTodayButton() {
  421. var now = t.getNow();
  422. if (now.isWithin(currentView.intervalStart, currentView.intervalEnd)) {
  423. header.disableButton('today');
  424. }
  425. else {
  426. header.enableButton('today');
  427. }
  428. }
  429. /* Selection
  430. -----------------------------------------------------------------------------*/
  431. function select(start, end) {
  432. currentView.select(start, end);
  433. }
  434. function unselect() { // safe to be called before renderView
  435. if (currentView) {
  436. currentView.unselect();
  437. }
  438. }
  439. /* Date
  440. -----------------------------------------------------------------------------*/
  441. function prev() {
  442. renderView(-1);
  443. }
  444. function next() {
  445. renderView(1);
  446. }
  447. function prevYear() {
  448. date.add('years', -1);
  449. renderView();
  450. }
  451. function nextYear() {
  452. date.add('years', 1);
  453. renderView();
  454. }
  455. function today() {
  456. date = t.getNow();
  457. renderView();
  458. }
  459. function gotoDate(dateInput) {
  460. date = t.moment(dateInput);
  461. renderView();
  462. }
  463. function incrementDate(delta) {
  464. date.add(moment.duration(delta));
  465. renderView();
  466. }
  467. function getDate() {
  468. return date.clone();
  469. }
  470. /* Height "Freezing"
  471. -----------------------------------------------------------------------------*/
  472. function freezeContentHeight() {
  473. content.css({
  474. width: '100%',
  475. height: content.height(),
  476. overflow: 'hidden'
  477. });
  478. }
  479. function unfreezeContentHeight() {
  480. content.css({
  481. width: '',
  482. height: '',
  483. overflow: ''
  484. });
  485. }
  486. /* Misc
  487. -----------------------------------------------------------------------------*/
  488. function getCalendar() {
  489. return t;
  490. }
  491. function getView() {
  492. return currentView;
  493. }
  494. function option(name, value) {
  495. if (value === undefined) {
  496. return options[name];
  497. }
  498. if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') {
  499. options[name] = value;
  500. updateSize();
  501. }
  502. }
  503. function trigger(name, thisObj) {
  504. if (options[name]) {
  505. return options[name].apply(
  506. thisObj || _element,
  507. Array.prototype.slice.call(arguments, 2)
  508. );
  509. }
  510. }
  511. /* External Dragging
  512. ------------------------------------------------------------------------*/
  513. if (options.droppable) {
  514. // TODO: unbind on destroy
  515. $(document)
  516. .bind('dragstart', function(ev, ui) {
  517. var _e = ev.target;
  518. var e = $(_e);
  519. if (!e.parents('.fc').length) { // not already inside a calendar
  520. var accept = options.dropAccept;
  521. if ($.isFunction(accept) ? accept.call(_e, e) : e.is(accept)) {
  522. _dragElement = _e;
  523. currentView.dragStart(_dragElement, ev, ui);
  524. }
  525. }
  526. })
  527. .bind('dragstop', function(ev, ui) {
  528. if (_dragElement) {
  529. currentView.dragStop(_dragElement, ev, ui);
  530. _dragElement = null;
  531. }
  532. });
  533. }
  534. }