main.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793
  1. var fc = $.fullCalendar = {};
  2. var views = fc.views = {};
  3. /* Defaults
  4. -----------------------------------------------------------------------------*/
  5. var defaults = {
  6. // display
  7. defaultView: 'month',
  8. aspectRatio: 1.35,
  9. header: {
  10. left: 'title',
  11. center: '',
  12. right: 'today prev,next'
  13. },
  14. weekends: true,
  15. // editing
  16. //editable: false,
  17. //disableDragging: false,
  18. //disableResizing: false,
  19. allDayDefault: true,
  20. // event ajax
  21. startParam: 'start',
  22. endParam: 'end',
  23. cacheParam: '_',
  24. // time formats
  25. titleFormat: {
  26. month: 'MMMM yyyy',
  27. week: "MMM d[ yyyy]{ '—'[ MMM] d yyyy}",
  28. day: 'dddd, MMM d, yyyy'
  29. },
  30. columnFormat: {
  31. month: 'ddd',
  32. week: 'ddd M/d',
  33. day: 'dddd M/d'
  34. },
  35. timeFormat: { // for event elements
  36. '': 'h(:mm)t' // default
  37. },
  38. // locale
  39. isRTL: false,
  40. firstDay: 0,
  41. monthNames: ['January','February','March','April','May','June','July','August','September','October','November','December'],
  42. monthNamesShort: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
  43. dayNames: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
  44. dayNamesShort: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'],
  45. buttonText: {
  46. prev: ' ◄ ',
  47. next: ' ► ',
  48. prevYear: ' << ',
  49. nextYear: ' >> ',
  50. today: 'today',
  51. month: 'month',
  52. week: 'week',
  53. day: 'day'
  54. },
  55. // jquery-ui theming
  56. theme: false,
  57. buttonIcons: {
  58. prev: 'circle-triangle-w',
  59. next: 'circle-triangle-e'
  60. }
  61. };
  62. // right-to-left defaults
  63. var rtlDefaults = {
  64. header: {
  65. left: 'next,prev today',
  66. center: '',
  67. right: 'title'
  68. },
  69. buttonText: {
  70. prev: ' ► ',
  71. next: ' ◄ ',
  72. prevYear: ' >> ',
  73. nextYear: ' << '
  74. },
  75. buttonIcons: {
  76. prev: 'circle-triangle-e',
  77. next: 'circle-triangle-w'
  78. }
  79. };
  80. // function for adding/overriding defaults
  81. var setDefaults = fc.setDefaults = function(d) {
  82. $.extend(true, defaults, d);
  83. }
  84. /* .fullCalendar jQuery function
  85. -----------------------------------------------------------------------------*/
  86. $.fn.fullCalendar = function(options) {
  87. // method calling
  88. if (typeof options == 'string') {
  89. var args = Array.prototype.slice.call(arguments, 1),
  90. res;
  91. this.each(function() {
  92. var r = $.data(this, 'fullCalendar')[options].apply(this, args);
  93. if (res == undefined) {
  94. res = r;
  95. }
  96. });
  97. if (res != undefined) {
  98. return res;
  99. }
  100. return this;
  101. }
  102. // pluck the 'events' and 'eventSources' options
  103. var eventSources = options.eventSources || [];
  104. delete options.eventSources;
  105. if (options.events) {
  106. eventSources.push(options.events);
  107. delete options.events;
  108. }
  109. // first event source reserved for 'sticky' events
  110. eventSources.unshift([]);
  111. // initialize options
  112. options = $.extend(true, {},
  113. defaults,
  114. (options.isRTL || options.isRTL==undefined && defaults.isRTL) ? rtlDefaults : {},
  115. options
  116. );
  117. var tm = options.theme ? 'ui' : 'fc'; // for making theme classes
  118. this.each(function() {
  119. /* Instance Initialization
  120. -----------------------------------------------------------------------------*/
  121. // element
  122. var _element = this,
  123. element = $(this).addClass('fc'),
  124. elementWidth,
  125. content = $("<div class='fc-content " + tm + "-widget-content' style='position:relative'/>").appendTo(this), // relative for ie6
  126. contentHeight;
  127. if (options.isRTL) {
  128. element.addClass('fc-rtl');
  129. }
  130. if (options.theme) {
  131. element.addClass('ui-widget');
  132. }
  133. // view managing
  134. var date = new Date(),
  135. viewName, view, // the current view
  136. viewInstances = {};
  137. if (options.year != undefined && options.year != date.getFullYear()) {
  138. date.setDate(1);
  139. date.setMonth(0);
  140. date.setFullYear(options.year);
  141. }
  142. if (options.month != undefined && options.month != date.getMonth()) {
  143. date.setDate(1);
  144. date.setMonth(options.month);
  145. }
  146. if (options.date != undefined) {
  147. date.setDate(options.date);
  148. }
  149. /* View Rendering
  150. -----------------------------------------------------------------------------*/
  151. function changeView(v) {
  152. if (v != viewName) {
  153. fixContentSize();
  154. if (view) {
  155. if (view.eventsChanged) {
  156. eventsDirtyExcept(view);
  157. view.eventsChanged = false;
  158. }
  159. view.element.hide();
  160. }
  161. if (viewInstances[v]) {
  162. (view = viewInstances[v]).element.show();
  163. if (view.shown) {
  164. view.shown();
  165. }
  166. }else{
  167. view = viewInstances[v] = $.fullCalendar.views[v](
  168. $("<div class='fc-view fc-view-" + v + "'/>").appendTo(content),
  169. options);
  170. }
  171. if (header) {
  172. // update 'active' view button
  173. header.find('div.fc-button-' + viewName).removeClass(tm + '-state-active');
  174. header.find('div.fc-button-' + v).addClass(tm + '-state-active');
  175. }
  176. view.name = viewName = v;
  177. render();
  178. unfixContentSize();
  179. }
  180. }
  181. function render(inc, forceUpdateSize) {
  182. if (_element.offsetWidth !== 0) { // visible on the screen
  183. if (!elementWidth) {
  184. elementWidth = element.width();
  185. contentHeight = calculateContentHeight();
  186. }
  187. if (inc || !view.date || +view.date != +date) { // !view.date means it hasn't been rendered yet
  188. fixContentSize();
  189. view.render(date, inc || 0, contentHeight, function(callback) {
  190. // dont refetch if new view contains the same events (or a subset)
  191. if (!eventStart || view.visStart < eventStart || view.visEnd > eventEnd) {
  192. fetchEvents(callback);
  193. }else{
  194. callback(events); // no refetching
  195. }
  196. });
  197. unfixContentSize();
  198. view.date = cloneDate(date);
  199. }
  200. else if (view.sizeDirty || forceUpdateSize) {
  201. view.updateSize(contentHeight);
  202. view.rerenderEvents();
  203. }
  204. else if (view.eventsDirty) {
  205. // ensure events are rerendered if another view messed with them
  206. // pass in 'events' b/c event might have been added/removed
  207. view.clearEvents();
  208. view.renderEvents(events);
  209. }
  210. if (header) {
  211. // update title text
  212. header.find('h2.fc-header-title').html(view.title);
  213. // enable/disable 'today' button
  214. var today = new Date();
  215. if (today >= view.start && today < view.end) {
  216. header.find('div.fc-button-today').addClass(tm + '-state-disabled');
  217. }else{
  218. header.find('div.fc-button-today').removeClass(tm + '-state-disabled');
  219. }
  220. }
  221. view.sizeDirty = false;
  222. view.eventsDirty = false;
  223. view.trigger('viewDisplay', _element);
  224. }
  225. }
  226. // marks other views' events as dirty
  227. function eventsDirtyExcept(exceptView) {
  228. $.each(viewInstances, function() {
  229. if (this != exceptView) {
  230. this.eventsDirty = true;
  231. }
  232. });
  233. }
  234. // called when any event objects have been added/removed/changed, rerenders
  235. function eventsChanged() {
  236. view.clearEvents();
  237. view.renderEvents(events);
  238. eventsDirtyExcept(view);
  239. }
  240. // marks other views' sizes as dirty
  241. function sizesDirtyExcept(exceptView) {
  242. $.each(viewInstances, function() {
  243. if (this != exceptView) {
  244. this.sizeDirty = true;
  245. }
  246. });
  247. }
  248. // called when we know the element size has changed
  249. function sizeChanged(fix) {
  250. contentHeight = calculateContentHeight();
  251. if (fix) fixContentSize();
  252. view.updateSize(contentHeight);
  253. if (fix) unfixContentSize();
  254. sizesDirtyExcept(view);
  255. view.rerenderEvents(true);
  256. }
  257. // calculate what the height of the content should be
  258. function calculateContentHeight() {
  259. if (options.contentHeight) {
  260. return options.contentHeight;
  261. }
  262. else if (options.height) {
  263. return options.height - (header ? header.height() : 0) - horizontalSides(content);
  264. }
  265. return elementWidth / options.aspectRatio;
  266. }
  267. /* Event Sources and Fetching
  268. -----------------------------------------------------------------------------*/
  269. var events = [],
  270. eventStart, eventEnd;
  271. // Fetch from ALL sources. Clear 'events' array and populate
  272. function fetchEvents(callback) {
  273. events = [];
  274. eventStart = cloneDate(view.visStart);
  275. eventEnd = cloneDate(view.visEnd);
  276. var queued = eventSources.length,
  277. sourceDone = function() {
  278. if (--queued == 0) {
  279. if (callback) {
  280. callback(events);
  281. }
  282. }
  283. }, i=0;
  284. for (; i<eventSources.length; i++) {
  285. fetchEventSource(eventSources[i], sourceDone);
  286. }
  287. }
  288. // Fetch from a particular source. Append to the 'events' array
  289. function fetchEventSource(src, callback) {
  290. var prevViewName = view.name,
  291. prevDate = cloneDate(date),
  292. reportEvents = function(a) {
  293. if (prevViewName == view.name && +prevDate == +date) { // protects from fast switching
  294. for (var i=0; i<a.length; i++) {
  295. normalizeEvent(a[i], options);
  296. a[i].source = src;
  297. }
  298. events = events.concat(a);
  299. if (callback) {
  300. callback(a);
  301. }
  302. }
  303. },
  304. reportEventsAndPop = function(a) {
  305. reportEvents(a);
  306. popLoading();
  307. };
  308. if (typeof src == 'string') {
  309. var params = {};
  310. params[options.startParam] = Math.round(eventStart.getTime() / 1000);
  311. params[options.endParam] = Math.round(eventEnd.getTime() / 1000);
  312. params[options.cacheParam] = (new Date()).getTime();
  313. pushLoading();
  314. $.getJSON(src, params, reportEventsAndPop);
  315. }
  316. else if ($.isFunction(src)) {
  317. pushLoading();
  318. src(cloneDate(eventStart), cloneDate(eventEnd), reportEventsAndPop);
  319. }
  320. else {
  321. reportEvents(src); // src is an array
  322. }
  323. }
  324. /* Loading State
  325. -----------------------------------------------------------------------------*/
  326. var loadingLevel = 0;
  327. function pushLoading() {
  328. if (!loadingLevel++) {
  329. view.trigger('loading', _element, true);
  330. }
  331. }
  332. function popLoading() {
  333. if (!--loadingLevel) {
  334. view.trigger('loading', _element, false);
  335. }
  336. }
  337. /* Public Methods
  338. -----------------------------------------------------------------------------*/
  339. var publicMethods = {
  340. render: function() {
  341. render(0, true); // true forces size to updated
  342. },
  343. changeView: changeView,
  344. getView: function() {
  345. return view;
  346. },
  347. option: function(name, value) {
  348. if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') {
  349. options[name] = value;
  350. sizeChanged();
  351. }
  352. },
  353. //
  354. // Navigation
  355. //
  356. prev: function() {
  357. render(-1);
  358. },
  359. next: function() {
  360. render(1);
  361. },
  362. prevYear: function() {
  363. addYears(date, -1);
  364. render();
  365. },
  366. nextYear: function() {
  367. addYears(date, 1);
  368. render();
  369. },
  370. today: function() {
  371. date = new Date();
  372. render();
  373. },
  374. gotoDate: function(year, month, dateNum) {
  375. if (typeof year == 'object') {
  376. date = cloneDate(year); // provided 1 argument, a Date
  377. }else{
  378. if (year != undefined) {
  379. date.setFullYear(year);
  380. }
  381. if (month != undefined) {
  382. date.setMonth(month);
  383. }
  384. if (dateNum != undefined) {
  385. date.setDate(dateNum);
  386. }
  387. }
  388. render();
  389. },
  390. incrementDate: function(years, months, days) {
  391. if (years != undefined) {
  392. addYears(date, years);
  393. }
  394. if (months != undefined) {
  395. addMonths(date, months);
  396. }
  397. if (days != undefined) {
  398. addDays(date, days);
  399. }
  400. render();
  401. },
  402. //
  403. // Event Manipulation
  404. //
  405. updateEvent: function(event) { // update an existing event
  406. var i, len = events.length, e,
  407. startDelta = event.start - event._start,
  408. endDelta = event.end ?
  409. (event.end - (event._end || view.defaultEventEnd(event))) // event._end would be null if event.end
  410. : 0; // was null and event was just resized
  411. for (i=0; i<len; i++) {
  412. e = events[i];
  413. if (e._id == event._id && e != event) {
  414. e.start = new Date(+e.start + startDelta);
  415. if (event.end) {
  416. if (e.end) {
  417. e.end = new Date(+e.end + endDelta);
  418. }else{
  419. e.end = new Date(+view.defaultEventEnd(e) + endDelta);
  420. }
  421. }else{
  422. e.end = null;
  423. }
  424. e.title = event.title;
  425. e.url = event.url;
  426. e.allDay = event.allDay;
  427. e.className = event.className;
  428. e.editable = event.editable;
  429. normalizeEvent(e, options);
  430. }
  431. }
  432. normalizeEvent(event, options);
  433. eventsChanged();
  434. },
  435. renderEvent: function(event, stick) { // render a new event
  436. normalizeEvent(event, options);
  437. if (!event.source) {
  438. if (stick) {
  439. (event.source = eventSources[0]).push(event);
  440. }
  441. events.push(event);
  442. }
  443. eventsChanged();
  444. },
  445. removeEvents: function(filter) {
  446. if (!filter) { // remove all
  447. events = [];
  448. // clear all array sources
  449. for (var i=0; i<eventSources.length; i++) {
  450. if (typeof eventSources[i] == 'object') {
  451. eventSources[i] = [];
  452. }
  453. }
  454. }else{
  455. if (!$.isFunction(filter)) { // an event ID
  456. var id = filter + '';
  457. filter = function(e) {
  458. return e._id == id;
  459. };
  460. }
  461. events = $.grep(events, filter, true);
  462. // remove events from array sources
  463. for (var i=0; i<eventSources.length; i++) {
  464. if (typeof eventSources[i] == 'object') {
  465. eventSources[i] = $.grep(eventSources[i], filter, true);
  466. }
  467. }
  468. }
  469. eventsChanged();
  470. },
  471. clientEvents: function(filter) {
  472. if ($.isFunction(filter)) {
  473. return $.grep(events, filter);
  474. }
  475. else if (filter) { // an event ID
  476. filter += '';
  477. return $.grep(events, function(e) {
  478. return e._id == filter;
  479. });
  480. }
  481. return events; // else, return all
  482. },
  483. rerenderEvents: function() {
  484. view.rerenderEvents();
  485. },
  486. //
  487. // Event Source
  488. //
  489. addEventSource: function(source) {
  490. eventSources.push(source);
  491. fetchEventSource(source, function() {
  492. eventsChanged();
  493. });
  494. },
  495. removeEventSource: function(source) {
  496. eventSources = $.grep(eventSources, function(src) {
  497. return src != source;
  498. });
  499. // remove all client events from that source
  500. events = $.grep(events, function(e) {
  501. return e.source != source;
  502. });
  503. eventsChanged();
  504. },
  505. refetchEvents: function() {
  506. fetchEvents(eventsChanged);
  507. }
  508. };
  509. $.data(this, 'fullCalendar', publicMethods);
  510. /* Header
  511. -----------------------------------------------------------------------------*/
  512. var header,
  513. sections = options.header;
  514. if (sections) {
  515. header = $("<table class='fc-header'/>")
  516. .append($("<tr/>")
  517. .append($("<td class='fc-header-left'/>").append(buildSection(sections.left)))
  518. .append($("<td class='fc-header-center'/>").append(buildSection(sections.center)))
  519. .append($("<td class='fc-header-right'/>").append(buildSection(sections.right))))
  520. .prependTo(element);
  521. }
  522. function buildSection(buttonStr) {
  523. if (buttonStr) {
  524. var tr = $("<tr/>");
  525. $.each(buttonStr.split(' '), function(i) {
  526. if (i > 0) {
  527. tr.append("<td><span class='fc-header-space'/></td>");
  528. }
  529. var prevButton;
  530. $.each(this.split(','), function(j, buttonName) {
  531. if (buttonName == 'title') {
  532. tr.append("<td><h2 class='fc-header-title'>&nbsp;</h2></td>");
  533. if (prevButton) {
  534. prevButton.addClass(tm + '-corner-right');
  535. }
  536. prevButton = null;
  537. }else{
  538. var buttonClick;
  539. if (publicMethods[buttonName]) {
  540. buttonClick = publicMethods[buttonName];
  541. }
  542. else if (views[buttonName]) {
  543. buttonClick = function() {
  544. button.removeClass(tm + '-state-hover');
  545. changeView(buttonName)
  546. };
  547. }
  548. if (buttonClick) {
  549. if (prevButton) {
  550. prevButton.addClass(tm + '-no-right');
  551. }
  552. var button,
  553. icon = options.theme ? smartProperty(options.buttonIcons, buttonName) : null,
  554. text = smartProperty(options.buttonText, buttonName);
  555. if (icon) {
  556. button = $("<div class='fc-button-" + buttonName + " ui-state-default'>" +
  557. "<a><span class='ui-icon ui-icon-" + icon + "'/></a></div>");
  558. }
  559. else if (text) {
  560. button = $("<div class='fc-button-" + buttonName + " " + tm + "-state-default'>" +
  561. "<a><span>" + text + "</span></a></div>");
  562. }
  563. if (button) {
  564. button
  565. .click(function() {
  566. if (!button.hasClass(tm + '-state-disabled')) {
  567. buttonClick();
  568. }
  569. })
  570. .mousedown(function() {
  571. button
  572. .not('.' + tm + '-state-active')
  573. .not('.' + tm + '-state-disabled')
  574. .addClass(tm + '-state-down');
  575. })
  576. .mouseup(function() {
  577. button.removeClass(tm + '-state-down');
  578. })
  579. .hover(
  580. function() {
  581. button
  582. .not('.' + tm + '-state-active')
  583. .not('.' + tm + '-state-disabled')
  584. .addClass(tm + '-state-hover');
  585. },
  586. function() {
  587. button
  588. .removeClass(tm + '-state-hover')
  589. .removeClass(tm + '-state-down');
  590. }
  591. )
  592. .appendTo($("<td/>").appendTo(tr));
  593. if (prevButton) {
  594. prevButton.addClass(tm + '-no-right');
  595. }else{
  596. button.addClass(tm + '-corner-left');
  597. }
  598. prevButton = button;
  599. }
  600. }
  601. }
  602. });
  603. if (prevButton) {
  604. prevButton.addClass(tm + '-corner-right');
  605. }
  606. });
  607. return $("<table/>").append(tr);
  608. }
  609. }
  610. /* Resizing
  611. -----------------------------------------------------------------------------*/
  612. var contentSizeFixed = false,
  613. resizeCnt = 0;
  614. function fixContentSize() {
  615. if (!contentSizeFixed) {
  616. contentSizeFixed = true;
  617. content.css({
  618. overflow: 'hidden',
  619. height: contentHeight
  620. });
  621. // TODO: previous action might have caused scrollbars
  622. // which will make the window width more narrow, possibly changing the aspect ratio
  623. }
  624. }
  625. function unfixContentSize() {
  626. if (contentSizeFixed) {
  627. content.css({
  628. overflow: 'visible',
  629. height: ''
  630. });
  631. if ($.browser.msie && ($.browser.version=='6.0' || $.browser.version=='7.0')) {
  632. // in IE6/7 the inside of the content div was invisible
  633. // bizarre hack to get this work... need both lines
  634. content[0].clientHeight;
  635. content.hide().show();
  636. }
  637. contentSizeFixed = false;
  638. }
  639. }
  640. $(window).resize(function() {
  641. if (!contentSizeFixed) {
  642. if (view.date) { // view has already been rendered
  643. var rcnt = ++resizeCnt; // add a delay
  644. setTimeout(function() {
  645. if (rcnt == resizeCnt && !contentSizeFixed) {
  646. var newWidth = element.width();
  647. if (newWidth != elementWidth) {
  648. elementWidth = newWidth;
  649. sizeChanged(true);
  650. view.trigger('windowResize', _element);
  651. }
  652. }
  653. }, 200);
  654. }else{
  655. render(); // render for first time
  656. // was probably in a 0x0 iframe that has just been resized
  657. }
  658. }
  659. });
  660. // let's begin...
  661. changeView(options.defaultView);
  662. });
  663. return this;
  664. };
  665. /* Important Event Utilities
  666. -----------------------------------------------------------------------------*/
  667. var fakeID = 0;
  668. function normalizeEvent(event, options) {
  669. event._id = event._id || (event.id == undefined ? '_fc' + fakeID++ : event.id + '');
  670. if (event.date) {
  671. if (!event.start) {
  672. event.start = event.date;
  673. }
  674. delete event.date;
  675. }
  676. event._start = cloneDate(event.start = parseDate(event.start));
  677. event.end = parseDate(event.end);
  678. if (event.end && event.end <= event.start) {
  679. event.end = null;
  680. }
  681. event._end = event.end ? cloneDate(event.end) : null;
  682. if (event.allDay == undefined) {
  683. event.allDay = options.allDayDefault;
  684. }
  685. if (event.className) {
  686. if (typeof event.className == 'string') {
  687. event.className = event.className.split(/\s+/);
  688. }
  689. }else{
  690. event.className = [];
  691. }
  692. }