6934 line
169KB

  1. /*!
  2. * FullCalendar v2.0.0-beta2
  3. * Docs & License: http://arshaw.com/fullcalendar/
  4. * (c) 2013 Adam Shaw
  5. */
  6. (function(factory) {
  7. if (typeof define === 'function' && define.amd) {
  8. define([ 'jquery', 'moment' ], factory);
  9. }
  10. else {
  11. factory(jQuery, moment);
  12. }
  13. })(function($, moment) {
  14. ;;
  15. var defaults = {
  16. lang: 'en',
  17. defaultTimedEventDuration: '02:00:00',
  18. defaultAllDayEventDuration: { days: 1 },
  19. forceEventDuration: false,
  20. nextDayThreshold: '09:00:00', // 9am
  21. // display
  22. defaultView: 'month',
  23. aspectRatio: 1.35,
  24. header: {
  25. left: 'title',
  26. center: '',
  27. right: 'today prev,next'
  28. },
  29. weekends: true,
  30. weekNumbers: false,
  31. weekNumberTitle: 'W',
  32. weekNumberCalculation: 'local',
  33. //editable: false,
  34. // event ajax
  35. lazyFetching: true,
  36. startParam: 'start',
  37. endParam: 'end',
  38. timezoneParam: 'timezone',
  39. //allDayDefault: undefined,
  40. // time formats
  41. titleFormat: {
  42. month: 'MMMM YYYY', // like "September 1986". each language will override this
  43. week: 'll', // like "Sep 4 1986"
  44. day: 'LL' // like "September 4 1986"
  45. },
  46. columnFormat: {
  47. month: 'ddd', // like "Sat"
  48. week: generateWeekColumnFormat,
  49. day: 'dddd' // like "Saturday"
  50. },
  51. timeFormat: { // for event elements
  52. 'default': generateShortTimeFormat
  53. },
  54. // locale
  55. isRTL: false,
  56. buttonText: {
  57. prev: "prev",
  58. next: "next",
  59. prevYear: "prev year",
  60. nextYear: "next year",
  61. today: 'today',
  62. month: 'month',
  63. week: 'week',
  64. day: 'day'
  65. },
  66. buttonIcons: {
  67. prev: 'left-single-arrow',
  68. next: 'right-single-arrow',
  69. prevYear: 'left-double-arrow',
  70. nextYear: 'right-double-arrow'
  71. },
  72. // jquery-ui theming
  73. theme: false,
  74. themeButtonIcons: {
  75. prev: 'circle-triangle-w',
  76. next: 'circle-triangle-e',
  77. prevYear: 'seek-prev',
  78. nextYear: 'seek-next'
  79. },
  80. //selectable: false,
  81. unselectAuto: true,
  82. dropAccept: '*',
  83. handleWindowResize: true
  84. };
  85. function generateShortTimeFormat(options, langData) {
  86. return langData.longDateFormat('LT')
  87. .replace(':mm', '(:mm)')
  88. .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs
  89. .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand
  90. }
  91. function generateWeekColumnFormat(options, langData) {
  92. var format = langData.longDateFormat('L'); // for the format like "MM/DD/YYYY"
  93. format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, ''); // strip the year off the edge, as well as other misc non-whitespace chars
  94. if (options.isRTL) {
  95. format += ' ddd'; // for RTL, add day-of-week to end
  96. }
  97. else {
  98. format = 'ddd ' + format; // for LTR, add day-of-week to beginning
  99. }
  100. return format;
  101. }
  102. var langOptionHash = {
  103. en: {
  104. columnFormat: {
  105. week: 'ddd M/D' // override for english. different from the generated default, which is MM/DD
  106. }
  107. }
  108. };
  109. // right-to-left defaults
  110. var rtlDefaults = {
  111. header: {
  112. left: 'next,prev today',
  113. center: '',
  114. right: 'title'
  115. },
  116. buttonIcons: {
  117. prev: 'right-single-arrow',
  118. next: 'left-single-arrow',
  119. prevYear: 'right-double-arrow',
  120. nextYear: 'left-double-arrow'
  121. },
  122. themeButtonIcons: {
  123. prev: 'circle-triangle-e',
  124. next: 'circle-triangle-w',
  125. nextYear: 'seek-prev',
  126. prevYear: 'seek-next'
  127. }
  128. };
  129. ;;
  130. var fc = $.fullCalendar = { version: "2.0.0-beta2" };
  131. var fcViews = fc.views = {};
  132. $.fn.fullCalendar = function(options) {
  133. var args = Array.prototype.slice.call(arguments, 1); // for a possible method call
  134. var res = this; // what this function will return (this jQuery object by default)
  135. this.each(function(i, _element) { // loop each DOM element involved
  136. var element = $(_element);
  137. var calendar = element.data('fullCalendar'); // get the existing calendar object (if any)
  138. var singleRes; // the returned value of this single method call
  139. // a method call
  140. if (typeof options === 'string') {
  141. if (calendar && $.isFunction(calendar[options])) {
  142. singleRes = calendar[options].apply(calendar, args);
  143. if (!i) {
  144. res = singleRes; // record the first method call result
  145. }
  146. if (options === 'destroy') { // for the destroy method, must remove Calendar object data
  147. element.removeData('fullCalendar');
  148. }
  149. }
  150. }
  151. // a new calendar initialization
  152. else if (!calendar) { // don't initialize twice
  153. calendar = new Calendar(element, options);
  154. element.data('fullCalendar', calendar);
  155. calendar.render();
  156. }
  157. });
  158. return res;
  159. };
  160. // function for adding/overriding defaults
  161. function setDefaults(d) {
  162. mergeOptions(defaults, d);
  163. }
  164. // Recursively combines option hash-objects.
  165. // Better than `$.extend(true, ...)` because arrays are not traversed/copied.
  166. //
  167. // called like:
  168. // mergeOptions(target, obj1, obj2, ...)
  169. //
  170. function mergeOptions(target) {
  171. function mergeIntoTarget(name, value) {
  172. if ($.isPlainObject(value) && $.isPlainObject(target[name]) && !isForcedAtomicOption(name)) {
  173. // merge into a new object to avoid destruction
  174. target[name] = mergeOptions({}, target[name], value); // combine. `value` object takes precedence
  175. }
  176. else if (value !== undefined) { // only use values that are set and not undefined
  177. target[name] = value;
  178. }
  179. }
  180. for (var i=1; i<arguments.length; i++) {
  181. $.each(arguments[i], mergeIntoTarget);
  182. }
  183. return target;
  184. }
  185. // overcome sucky view-option-hash and option-merging behavior messing with options it shouldn't
  186. function isForcedAtomicOption(name) {
  187. // Any option that ends in "Time" or "Duration" is probably a Duration,
  188. // and these will commonly be specified as plain objects, which we don't want to mess up.
  189. return /(Time|Duration)$/.test(name);
  190. }
  191. // FIX: find a different solution for view-option-hashes and have a whitelist
  192. // for options that can be recursively merged.
  193. ;;
  194. //var langOptionHash = {}; // initialized in defaults.js
  195. fc.langs = langOptionHash; // expose
  196. // Initialize jQuery UI Datepicker translations while using some of the translations
  197. // for our own purposes. Will set this as the default language for datepicker.
  198. // Called from a translation file.
  199. fc.datepickerLang = function(langCode, datepickerLangCode, options) {
  200. var langOptions = langOptionHash[langCode];
  201. // initialize FullCalendar's lang hash for this language
  202. if (!langOptions) {
  203. langOptions = langOptionHash[langCode] = {};
  204. }
  205. // merge certain Datepicker options into FullCalendar's options
  206. mergeOptions(langOptions, {
  207. isRTL: options.isRTL,
  208. weekNumberTitle: options.weekHeader,
  209. titleFormat: {
  210. month: options.showMonthAfterYear ?
  211. 'YYYY[' + options.yearSuffix + '] MMMM' :
  212. 'MMMM YYYY[' + options.yearSuffix + ']'
  213. },
  214. buttonText: {
  215. // the translations sometimes wrongly contain HTML entities
  216. prev: stripHTMLEntities(options.prevText),
  217. next: stripHTMLEntities(options.nextText),
  218. today: stripHTMLEntities(options.currentText)
  219. }
  220. });
  221. // is jQuery UI Datepicker is on the page?
  222. if ($.datepicker) {
  223. // Register the language data.
  224. // FullCalendar and MomentJS use language codes like "pt-br" but Datepicker
  225. // does it like "pt-BR" or if it doesn't have the language, maybe just "pt".
  226. // Make an alias so the language can be referenced either way.
  227. $.datepicker.regional[datepickerLangCode] =
  228. $.datepicker.regional[langCode] = // alias
  229. options;
  230. // Alias 'en' to the default language data. Do this every time.
  231. $.datepicker.regional.en = $.datepicker.regional[''];
  232. // Set as Datepicker's global defaults.
  233. $.datepicker.setDefaults(options);
  234. }
  235. };
  236. // Sets FullCalendar-specific translations. Also sets the language as the global default.
  237. // Called from a translation file.
  238. fc.lang = function(langCode, options) {
  239. var langOptions;
  240. if (options) {
  241. langOptions = langOptionHash[langCode];
  242. // initialize the hash for this language
  243. if (!langOptions) {
  244. langOptions = langOptionHash[langCode] = {};
  245. }
  246. mergeOptions(langOptions, options || {});
  247. }
  248. // set it as the default language for FullCalendar
  249. defaults.lang = langCode;
  250. };
  251. ;;
  252. function Calendar(element, instanceOptions) {
  253. var t = this;
  254. // Build options object
  255. // -----------------------------------------------------------------------------------
  256. // Precedence (lowest to highest): defaults, rtlDefaults, langOptions, instanceOptions
  257. instanceOptions = instanceOptions || {};
  258. var options = mergeOptions({}, defaults, instanceOptions);
  259. var langOptions;
  260. // determine language options
  261. if (options.lang in langOptionHash) {
  262. langOptions = langOptionHash[options.lang];
  263. }
  264. else {
  265. langOptions = langOptionHash[defaults.lang];
  266. }
  267. if (langOptions) { // if language options exist, rebuild...
  268. options = mergeOptions({}, defaults, langOptions, instanceOptions);
  269. }
  270. if (options.isRTL) { // is isRTL, rebuild...
  271. options = mergeOptions({}, defaults, rtlDefaults, langOptions || {}, instanceOptions);
  272. }
  273. // Exports
  274. // -----------------------------------------------------------------------------------
  275. t.options = options;
  276. t.render = render;
  277. t.destroy = destroy;
  278. t.refetchEvents = refetchEvents;
  279. t.reportEvents = reportEvents;
  280. t.reportEventChange = reportEventChange;
  281. t.rerenderEvents = rerenderEvents;
  282. t.changeView = changeView;
  283. t.select = select;
  284. t.unselect = unselect;
  285. t.prev = prev;
  286. t.next = next;
  287. t.prevYear = prevYear;
  288. t.nextYear = nextYear;
  289. t.today = today;
  290. t.gotoDate = gotoDate;
  291. t.incrementDate = incrementDate;
  292. t.getDate = getDate;
  293. t.getCalendar = getCalendar;
  294. t.getView = getView;
  295. t.option = option;
  296. t.trigger = trigger;
  297. // Language-data Internals
  298. // -----------------------------------------------------------------------------------
  299. // Apply overrides to the current language's data
  300. var langData = createObject( // make a cheap clone
  301. moment.langData(options.lang)
  302. );
  303. if (options.monthNames) {
  304. langData._months = options.monthNames;
  305. }
  306. if (options.monthNamesShort) {
  307. langData._monthsShort = options.monthNamesShort;
  308. }
  309. if (options.dayNames) {
  310. langData._weekdays = options.dayNames;
  311. }
  312. if (options.dayNamesShort) {
  313. langData._weekdaysShort = options.dayNamesShort;
  314. }
  315. if (options.firstDay) {
  316. var _week = createObject(langData._week); // _week: { dow: # }
  317. _week.dow = options.firstDay;
  318. langData._week = _week;
  319. }
  320. // Calendar-specific Date Utilities
  321. // -----------------------------------------------------------------------------------
  322. t.defaultAllDayEventDuration = moment.duration(options.defaultAllDayEventDuration);
  323. t.defaultTimedEventDuration = moment.duration(options.defaultTimedEventDuration);
  324. // Builds a moment using the settings of the current calendar: timezone and language.
  325. // Accepts anything the vanilla moment() constructor accepts.
  326. t.moment = function() {
  327. var mom;
  328. if (options.timezone === 'local') {
  329. mom = fc.moment.apply(null, arguments);
  330. }
  331. else if (options.timezone === 'UTC') {
  332. mom = fc.moment.utc.apply(null, arguments);
  333. }
  334. else {
  335. mom = fc.moment.parseZone.apply(null, arguments);
  336. }
  337. mom._lang = langData;
  338. return mom;
  339. };
  340. // Returns a boolean about whether or not the calendar knows how to calculate
  341. // the timezone offset of arbitrary dates in the current timezone.
  342. t.getIsAmbigTimezone = function() {
  343. return options.timezone !== 'local' && options.timezone !== 'UTC';
  344. };
  345. // Returns a copy of the given date in the current timezone of it is ambiguously zoned.
  346. // This will also give the date an unambiguous time.
  347. t.rezoneDate = function(date) {
  348. return t.moment(date.toArray());
  349. };
  350. // Returns a moment for the current date, as defined by the client's computer,
  351. // or overridden by the `now` option.
  352. t.getNow = function() {
  353. var now = options.now;
  354. if (typeof now === 'function') {
  355. now = now();
  356. }
  357. return t.moment(now);
  358. };
  359. // Calculates the week number for a moment according to the calendar's
  360. // `weekNumberCalculation` setting.
  361. t.calculateWeekNumber = function(mom) {
  362. var calc = options.weekNumberCalculation;
  363. if (typeof calc === 'function') {
  364. return calc(mom);
  365. }
  366. else if (calc === 'local') {
  367. return mom.week();
  368. }
  369. else if (calc.toUpperCase() === 'ISO') {
  370. return mom.isoWeek();
  371. }
  372. };
  373. // Get an event's normalized end date. If not present, calculate it from the defaults.
  374. t.getEventEnd = function(event) {
  375. if (event.end) {
  376. return event.end.clone();
  377. }
  378. else {
  379. return t.getDefaultEventEnd(event.allDay, event.start);
  380. }
  381. };
  382. // Given an event's allDay status and start date, return swhat its fallback end date should be.
  383. t.getDefaultEventEnd = function(allDay, start) { // TODO: rename to computeDefaultEventEnd
  384. var end = start.clone();
  385. if (allDay) {
  386. end.stripTime().add(t.defaultAllDayEventDuration);
  387. }
  388. else {
  389. end.add(t.defaultTimedEventDuration);
  390. }
  391. if (t.getIsAmbigTimezone()) {
  392. end.stripZone(); // we don't know what the tzo should be
  393. }
  394. return end;
  395. };
  396. // Date-formatting Utilities
  397. // -----------------------------------------------------------------------------------
  398. // Like the vanilla formatRange, but with calendar-specific settings applied.
  399. t.formatRange = function(m1, m2, formatStr) {
  400. // a function that returns a formatStr // TODO: in future, precompute this
  401. if (typeof formatStr === 'function') {
  402. formatStr = formatStr.call(t, options, langData);
  403. }
  404. return formatRange(m1, m2, formatStr, null, options.isRTL);
  405. };
  406. // Like the vanilla formatDate, but with calendar-specific settings applied.
  407. t.formatDate = function(mom, formatStr) {
  408. // a function that returns a formatStr // TODO: in future, precompute this
  409. if (typeof formatStr === 'function') {
  410. formatStr = formatStr.call(t, options, langData);
  411. }
  412. return formatDate(mom, formatStr);
  413. };
  414. // Imports
  415. // -----------------------------------------------------------------------------------
  416. EventManager.call(t, options);
  417. var isFetchNeeded = t.isFetchNeeded;
  418. var fetchEvents = t.fetchEvents;
  419. // Locals
  420. // -----------------------------------------------------------------------------------
  421. var _element = element[0];
  422. var header;
  423. var headerElement;
  424. var content;
  425. var tm; // for making theme classes
  426. var currentView;
  427. var elementOuterWidth;
  428. var suggestedViewHeight;
  429. var resizeUID = 0;
  430. var ignoreWindowResize = 0;
  431. var date;
  432. var events = [];
  433. var _dragElement;
  434. // Main Rendering
  435. // -----------------------------------------------------------------------------------
  436. if (options.defaultDate != null) {
  437. date = t.moment(options.defaultDate);
  438. }
  439. else {
  440. date = t.getNow();
  441. }
  442. function render(inc) {
  443. if (!content) {
  444. initialRender();
  445. }
  446. else if (elementVisible()) {
  447. // mainly for the public API
  448. calcSize();
  449. _renderView(inc);
  450. }
  451. }
  452. function initialRender() {
  453. tm = options.theme ? 'ui' : 'fc';
  454. element.addClass('fc');
  455. if (options.isRTL) {
  456. element.addClass('fc-rtl');
  457. }
  458. else {
  459. element.addClass('fc-ltr');
  460. }
  461. if (options.theme) {
  462. element.addClass('ui-widget');
  463. }
  464. content = $("<div class='fc-content' />")
  465. .prependTo(element);
  466. header = new Header(t, options);
  467. headerElement = header.render();
  468. if (headerElement) {
  469. element.prepend(headerElement);
  470. }
  471. changeView(options.defaultView);
  472. if (options.handleWindowResize) {
  473. $(window).resize(windowResize);
  474. }
  475. // needed for IE in a 0x0 iframe, b/c when it is resized, never triggers a windowResize
  476. if (!bodyVisible()) {
  477. lateRender();
  478. }
  479. }
  480. // called when we know the calendar couldn't be rendered when it was initialized,
  481. // but we think it's ready now
  482. function lateRender() {
  483. setTimeout(function() { // IE7 needs this so dimensions are calculated correctly
  484. if (!currentView.start && bodyVisible()) { // !currentView.start makes sure this never happens more than once
  485. renderView();
  486. }
  487. },0);
  488. }
  489. function destroy() {
  490. if (currentView) {
  491. trigger('viewDestroy', currentView, currentView, currentView.element);
  492. currentView.triggerEventDestroy();
  493. }
  494. $(window).unbind('resize', windowResize);
  495. header.destroy();
  496. content.remove();
  497. element.removeClass('fc fc-rtl ui-widget');
  498. }
  499. function elementVisible() {
  500. return element.is(':visible');
  501. }
  502. function bodyVisible() {
  503. return $('body').is(':visible');
  504. }
  505. // View Rendering
  506. // -----------------------------------------------------------------------------------
  507. function changeView(newViewName) {
  508. if (!currentView || newViewName != currentView.name) {
  509. _changeView(newViewName);
  510. }
  511. }
  512. function _changeView(newViewName) {
  513. ignoreWindowResize++;
  514. if (currentView) {
  515. trigger('viewDestroy', currentView, currentView, currentView.element);
  516. unselect();
  517. currentView.triggerEventDestroy(); // trigger 'eventDestroy' for each event
  518. freezeContentHeight();
  519. currentView.element.remove();
  520. header.deactivateButton(currentView.name);
  521. }
  522. header.activateButton(newViewName);
  523. currentView = new fcViews[newViewName](
  524. $("<div class='fc-view fc-view-" + newViewName + "' />")
  525. .appendTo(content),
  526. t // the calendar object
  527. );
  528. renderView();
  529. unfreezeContentHeight();
  530. ignoreWindowResize--;
  531. }
  532. function renderView(inc) {
  533. if (
  534. !currentView.start || // never rendered before
  535. inc || // explicit date window change
  536. !date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change
  537. ) {
  538. if (elementVisible()) {
  539. _renderView(inc);
  540. }
  541. }
  542. }
  543. function _renderView(inc) { // assumes elementVisible
  544. ignoreWindowResize++;
  545. if (currentView.start) { // already been rendered?
  546. trigger('viewDestroy', currentView, currentView, currentView.element);
  547. unselect();
  548. clearEvents();
  549. }
  550. freezeContentHeight();
  551. if (inc) {
  552. date = currentView.incrementDate(date, inc);
  553. }
  554. currentView.render(date.clone()); // the view's render method ONLY renders the skeleton, nothing else
  555. setSize();
  556. unfreezeContentHeight();
  557. (currentView.afterRender || noop)();
  558. updateTitle();
  559. updateTodayButton();
  560. trigger('viewRender', currentView, currentView, currentView.element);
  561. ignoreWindowResize--;
  562. getAndRenderEvents();
  563. }
  564. // Resizing
  565. // -----------------------------------------------------------------------------------
  566. function updateSize() {
  567. if (elementVisible()) {
  568. unselect();
  569. clearEvents();
  570. calcSize();
  571. setSize();
  572. renderEvents();
  573. }
  574. }
  575. function calcSize() { // assumes elementVisible
  576. if (options.contentHeight) {
  577. suggestedViewHeight = options.contentHeight;
  578. }
  579. else if (options.height) {
  580. suggestedViewHeight = options.height - (headerElement ? headerElement.height() : 0) - vsides(content);
  581. }
  582. else {
  583. suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5));
  584. }
  585. }
  586. function setSize() { // assumes elementVisible
  587. if (suggestedViewHeight === undefined) {
  588. calcSize(); // for first time
  589. // NOTE: we don't want to recalculate on every renderView because
  590. // it could result in oscillating heights due to scrollbars.
  591. }
  592. ignoreWindowResize++;
  593. currentView.setHeight(suggestedViewHeight);
  594. currentView.setWidth(content.width());
  595. ignoreWindowResize--;
  596. elementOuterWidth = element.outerWidth();
  597. }
  598. function windowResize() {
  599. if (!ignoreWindowResize) {
  600. if (currentView.start) { // view has already been rendered
  601. var uid = ++resizeUID;
  602. setTimeout(function() { // add a delay
  603. if (uid == resizeUID && !ignoreWindowResize && elementVisible()) {
  604. if (elementOuterWidth != (elementOuterWidth = element.outerWidth())) {
  605. ignoreWindowResize++; // in case the windowResize callback changes the height
  606. updateSize();
  607. currentView.trigger('windowResize', _element);
  608. ignoreWindowResize--;
  609. }
  610. }
  611. }, 200);
  612. }else{
  613. // calendar must have been initialized in a 0x0 iframe that has just been resized
  614. lateRender();
  615. }
  616. }
  617. }
  618. /* Event Fetching/Rendering
  619. -----------------------------------------------------------------------------*/
  620. // TODO: going forward, most of this stuff should be directly handled by the view
  621. function refetchEvents() { // can be called as an API method
  622. clearEvents();
  623. fetchAndRenderEvents();
  624. }
  625. function rerenderEvents(modifiedEventID) { // can be called as an API method
  626. clearEvents();
  627. renderEvents(modifiedEventID);
  628. }
  629. function renderEvents(modifiedEventID) { // TODO: remove modifiedEventID hack
  630. if (elementVisible()) {
  631. currentView.renderEvents(events, modifiedEventID); // actually render the DOM elements
  632. currentView.trigger('eventAfterAllRender');
  633. }
  634. }
  635. function clearEvents() {
  636. currentView.triggerEventDestroy(); // trigger 'eventDestroy' for each event
  637. currentView.clearEvents(); // actually remove the DOM elements
  638. currentView.clearEventData(); // for View.js, TODO: unify with clearEvents
  639. }
  640. function getAndRenderEvents() {
  641. if (!options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) {
  642. fetchAndRenderEvents();
  643. }
  644. else {
  645. renderEvents();
  646. }
  647. }
  648. function fetchAndRenderEvents() {
  649. fetchEvents(currentView.start, currentView.end);
  650. // ... will call reportEvents
  651. // ... which will call renderEvents
  652. }
  653. // called when event data arrives
  654. function reportEvents(_events) {
  655. events = _events;
  656. renderEvents();
  657. }
  658. // called when a single event's data has been changed
  659. function reportEventChange(eventID) {
  660. rerenderEvents(eventID);
  661. }
  662. /* Header Updating
  663. -----------------------------------------------------------------------------*/
  664. function updateTitle() {
  665. header.updateTitle(currentView.title);
  666. }
  667. function updateTodayButton() {
  668. var now = t.getNow();
  669. if (now.isWithin(currentView.intervalStart, currentView.intervalEnd)) {
  670. header.disableButton('today');
  671. }
  672. else {
  673. header.enableButton('today');
  674. }
  675. }
  676. /* Selection
  677. -----------------------------------------------------------------------------*/
  678. function select(start, end) {
  679. currentView.select(start, end);
  680. }
  681. function unselect() { // safe to be called before renderView
  682. if (currentView) {
  683. currentView.unselect();
  684. }
  685. }
  686. /* Date
  687. -----------------------------------------------------------------------------*/
  688. function prev() {
  689. renderView(-1);
  690. }
  691. function next() {
  692. renderView(1);
  693. }
  694. function prevYear() {
  695. date.add('years', -1);
  696. renderView();
  697. }
  698. function nextYear() {
  699. date.add('years', 1);
  700. renderView();
  701. }
  702. function today() {
  703. date = t.getNow();
  704. renderView();
  705. }
  706. function gotoDate(dateInput) {
  707. date = t.moment(dateInput);
  708. renderView();
  709. }
  710. function incrementDate() {
  711. date.add.apply(date, arguments);
  712. renderView();
  713. }
  714. function getDate() {
  715. return date.clone();
  716. }
  717. /* Height "Freezing"
  718. -----------------------------------------------------------------------------*/
  719. function freezeContentHeight() {
  720. content.css({
  721. width: '100%',
  722. height: content.height(),
  723. overflow: 'hidden'
  724. });
  725. }
  726. function unfreezeContentHeight() {
  727. content.css({
  728. width: '',
  729. height: '',
  730. overflow: ''
  731. });
  732. }
  733. /* Misc
  734. -----------------------------------------------------------------------------*/
  735. function getCalendar() {
  736. return t;
  737. }
  738. function getView() {
  739. return currentView;
  740. }
  741. function option(name, value) {
  742. if (value === undefined) {
  743. return options[name];
  744. }
  745. if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') {
  746. options[name] = value;
  747. updateSize();
  748. }
  749. }
  750. function trigger(name, thisObj) {
  751. if (options[name]) {
  752. return options[name].apply(
  753. thisObj || _element,
  754. Array.prototype.slice.call(arguments, 2)
  755. );
  756. }
  757. }
  758. /* External Dragging
  759. ------------------------------------------------------------------------*/
  760. if (options.droppable) {
  761. // TODO: unbind on destroy
  762. $(document)
  763. .bind('dragstart', function(ev, ui) {
  764. var _e = ev.target;
  765. var e = $(_e);
  766. if (!e.parents('.fc').length) { // not already inside a calendar
  767. var accept = options.dropAccept;
  768. if ($.isFunction(accept) ? accept.call(_e, e) : e.is(accept)) {
  769. _dragElement = _e;
  770. currentView.dragStart(_dragElement, ev, ui);
  771. }
  772. }
  773. })
  774. .bind('dragstop', function(ev, ui) {
  775. if (_dragElement) {
  776. currentView.dragStop(_dragElement, ev, ui);
  777. _dragElement = null;
  778. }
  779. });
  780. }
  781. }
  782. ;;
  783. function Header(calendar, options) {
  784. var t = this;
  785. // exports
  786. t.render = render;
  787. t.destroy = destroy;
  788. t.updateTitle = updateTitle;
  789. t.activateButton = activateButton;
  790. t.deactivateButton = deactivateButton;
  791. t.disableButton = disableButton;
  792. t.enableButton = enableButton;
  793. // locals
  794. var element = $([]);
  795. var tm;
  796. function render() {
  797. tm = options.theme ? 'ui' : 'fc';
  798. var sections = options.header;
  799. if (sections) {
  800. element = $("<table class='fc-header' style='width:100%'/>")
  801. .append(
  802. $("<tr/>")
  803. .append(renderSection('left'))
  804. .append(renderSection('center'))
  805. .append(renderSection('right'))
  806. );
  807. return element;
  808. }
  809. }
  810. function destroy() {
  811. element.remove();
  812. }
  813. function renderSection(position) {
  814. var e = $("<td class='fc-header-" + position + "'/>");
  815. var buttonStr = options.header[position];
  816. if (buttonStr) {
  817. $.each(buttonStr.split(' '), function(i) {
  818. if (i > 0) {
  819. e.append("<span class='fc-header-space'/>");
  820. }
  821. var prevButton;
  822. $.each(this.split(','), function(j, buttonName) {
  823. if (buttonName == 'title') {
  824. e.append("<span class='fc-header-title'><h2>&nbsp;</h2></span>");
  825. if (prevButton) {
  826. prevButton.addClass(tm + '-corner-right');
  827. }
  828. prevButton = null;
  829. }else{
  830. var buttonClick;
  831. if (calendar[buttonName]) {
  832. buttonClick = calendar[buttonName]; // calendar method
  833. }
  834. else if (fcViews[buttonName]) {
  835. buttonClick = function() {
  836. button.removeClass(tm + '-state-hover'); // forget why
  837. calendar.changeView(buttonName);
  838. };
  839. }
  840. if (buttonClick) {
  841. // smartProperty allows different text per view button (ex: "Agenda Week" vs "Basic Week")
  842. var themeIcon = smartProperty(options.themeButtonIcons, buttonName);
  843. var normalIcon = smartProperty(options.buttonIcons, buttonName);
  844. var text = smartProperty(options.buttonText, buttonName);
  845. var html;
  846. if (themeIcon && options.theme) {
  847. html = "<span class='ui-icon ui-icon-" + themeIcon + "'></span>";
  848. }
  849. else if (normalIcon && !options.theme) {
  850. html = "<span class='fc-icon fc-icon-" + normalIcon + "'></span>";
  851. }
  852. else {
  853. html = htmlEscape(text || buttonName);
  854. }
  855. var button = $(
  856. "<span class='fc-button fc-button-" + buttonName + " " + tm + "-state-default'>" +
  857. html +
  858. "</span>"
  859. )
  860. .click(function() {
  861. if (!button.hasClass(tm + '-state-disabled')) {
  862. buttonClick();
  863. }
  864. })
  865. .mousedown(function() {
  866. button
  867. .not('.' + tm + '-state-active')
  868. .not('.' + tm + '-state-disabled')
  869. .addClass(tm + '-state-down');
  870. })
  871. .mouseup(function() {
  872. button.removeClass(tm + '-state-down');
  873. })
  874. .hover(
  875. function() {
  876. button
  877. .not('.' + tm + '-state-active')
  878. .not('.' + tm + '-state-disabled')
  879. .addClass(tm + '-state-hover');
  880. },
  881. function() {
  882. button
  883. .removeClass(tm + '-state-hover')
  884. .removeClass(tm + '-state-down');
  885. }
  886. )
  887. .appendTo(e);
  888. disableTextSelection(button);
  889. if (!prevButton) {
  890. button.addClass(tm + '-corner-left');
  891. }
  892. prevButton = button;
  893. }
  894. }
  895. });
  896. if (prevButton) {
  897. prevButton.addClass(tm + '-corner-right');
  898. }
  899. });
  900. }
  901. return e;
  902. }
  903. function updateTitle(html) {
  904. element.find('h2')
  905. .html(html);
  906. }
  907. function activateButton(buttonName) {
  908. element.find('span.fc-button-' + buttonName)
  909. .addClass(tm + '-state-active');
  910. }
  911. function deactivateButton(buttonName) {
  912. element.find('span.fc-button-' + buttonName)
  913. .removeClass(tm + '-state-active');
  914. }
  915. function disableButton(buttonName) {
  916. element.find('span.fc-button-' + buttonName)
  917. .addClass(tm + '-state-disabled');
  918. }
  919. function enableButton(buttonName) {
  920. element.find('span.fc-button-' + buttonName)
  921. .removeClass(tm + '-state-disabled');
  922. }
  923. }
  924. ;;
  925. fc.sourceNormalizers = [];
  926. fc.sourceFetchers = [];
  927. var ajaxDefaults = {
  928. dataType: 'json',
  929. cache: false
  930. };
  931. var eventGUID = 1;
  932. function EventManager(options) { // assumed to be a calendar
  933. var t = this;
  934. // exports
  935. t.isFetchNeeded = isFetchNeeded;
  936. t.fetchEvents = fetchEvents;
  937. t.addEventSource = addEventSource;
  938. t.removeEventSource = removeEventSource;
  939. t.updateEvent = updateEvent;
  940. t.renderEvent = renderEvent;
  941. t.removeEvents = removeEvents;
  942. t.clientEvents = clientEvents;
  943. t.mutateEvent = mutateEvent;
  944. // imports
  945. var trigger = t.trigger;
  946. var getView = t.getView;
  947. var reportEvents = t.reportEvents;
  948. var getEventEnd = t.getEventEnd;
  949. // locals
  950. var stickySource = { events: [] };
  951. var sources = [ stickySource ];
  952. var rangeStart, rangeEnd;
  953. var currentFetchID = 0;
  954. var pendingSourceCnt = 0;
  955. var loadingLevel = 0;
  956. var cache = [];
  957. var _sources = options.eventSources || [];
  958. if (options.events) {
  959. _sources.push(options.events);
  960. }
  961. for (var i=0; i<_sources.length; i++) {
  962. _addEventSource(_sources[i]);
  963. }
  964. /* Fetching
  965. -----------------------------------------------------------------------------*/
  966. function isFetchNeeded(start, end) {
  967. return !rangeStart || // nothing has been fetched yet?
  968. // or, a part of the new range is outside of the old range? (after normalizing)
  969. start.clone().stripZone() < rangeStart.clone().stripZone() ||
  970. end.clone().stripZone() > rangeEnd.clone().stripZone();
  971. }
  972. function fetchEvents(start, end) {
  973. rangeStart = start;
  974. rangeEnd = end;
  975. cache = [];
  976. var fetchID = ++currentFetchID;
  977. var len = sources.length;
  978. pendingSourceCnt = len;
  979. for (var i=0; i<len; i++) {
  980. fetchEventSource(sources[i], fetchID);
  981. }
  982. }
  983. function fetchEventSource(source, fetchID) {
  984. _fetchEventSource(source, function(events) {
  985. if (fetchID == currentFetchID) {
  986. if (events) {
  987. for (var i=0; i<events.length; i++) {
  988. var event = buildEvent(events[i], source);
  989. if (event) {
  990. cache.push(event);
  991. }
  992. }
  993. }
  994. pendingSourceCnt--;
  995. if (!pendingSourceCnt) {
  996. reportEvents(cache);
  997. }
  998. }
  999. });
  1000. }
  1001. function _fetchEventSource(source, callback) {
  1002. var i;
  1003. var fetchers = fc.sourceFetchers;
  1004. var res;
  1005. for (i=0; i<fetchers.length; i++) {
  1006. res = fetchers[i].call(
  1007. t, // this, the Calendar object
  1008. source,
  1009. rangeStart.clone(),
  1010. rangeEnd.clone(),
  1011. options.timezone,
  1012. callback
  1013. );
  1014. if (res === true) {
  1015. // the fetcher is in charge. made its own async request
  1016. return;
  1017. }
  1018. else if (typeof res == 'object') {
  1019. // the fetcher returned a new source. process it
  1020. _fetchEventSource(res, callback);
  1021. return;
  1022. }
  1023. }
  1024. var events = source.events;
  1025. if (events) {
  1026. if ($.isFunction(events)) {
  1027. pushLoading();
  1028. events.call(
  1029. t, // this, the Calendar object
  1030. rangeStart.clone(),
  1031. rangeEnd.clone(),
  1032. options.timezone,
  1033. function(events) {
  1034. callback(events);
  1035. popLoading();
  1036. }
  1037. );
  1038. }
  1039. else if ($.isArray(events)) {
  1040. callback(events);
  1041. }
  1042. else {
  1043. callback();
  1044. }
  1045. }else{
  1046. var url = source.url;
  1047. if (url) {
  1048. var success = source.success;
  1049. var error = source.error;
  1050. var complete = source.complete;
  1051. // retrieve any outbound GET/POST $.ajax data from the options
  1052. var customData;
  1053. if ($.isFunction(source.data)) {
  1054. // supplied as a function that returns a key/value object
  1055. customData = source.data();
  1056. }
  1057. else {
  1058. // supplied as a straight key/value object
  1059. customData = source.data;
  1060. }
  1061. // use a copy of the custom data so we can modify the parameters
  1062. // and not affect the passed-in object.
  1063. var data = $.extend({}, customData || {});
  1064. var startParam = firstDefined(source.startParam, options.startParam);
  1065. var endParam = firstDefined(source.endParam, options.endParam);
  1066. var timezoneParam = firstDefined(source.timezoneParam, options.timezoneParam);
  1067. if (startParam) {
  1068. data[startParam] = rangeStart.format();
  1069. }
  1070. if (endParam) {
  1071. data[endParam] = rangeEnd.format();
  1072. }
  1073. if (options.timezone && options.timezone != 'local') {
  1074. data[timezoneParam] = options.timezone;
  1075. }
  1076. pushLoading();
  1077. $.ajax($.extend({}, ajaxDefaults, source, {
  1078. data: data,
  1079. success: function(events) {
  1080. events = events || [];
  1081. var res = applyAll(success, this, arguments);
  1082. if ($.isArray(res)) {
  1083. events = res;
  1084. }
  1085. callback(events);
  1086. },
  1087. error: function() {
  1088. applyAll(error, this, arguments);
  1089. callback();
  1090. },
  1091. complete: function() {
  1092. applyAll(complete, this, arguments);
  1093. popLoading();
  1094. }
  1095. }));
  1096. }else{
  1097. callback();
  1098. }
  1099. }
  1100. }
  1101. /* Sources
  1102. -----------------------------------------------------------------------------*/
  1103. function addEventSource(source) {
  1104. source = _addEventSource(source);
  1105. if (source) {
  1106. pendingSourceCnt++;
  1107. fetchEventSource(source, currentFetchID); // will eventually call reportEvents
  1108. }
  1109. }
  1110. function _addEventSource(source) {
  1111. if ($.isFunction(source) || $.isArray(source)) {
  1112. source = { events: source };
  1113. }
  1114. else if (typeof source == 'string') {
  1115. source = { url: source };
  1116. }
  1117. if (typeof source == 'object') {
  1118. normalizeSource(source);
  1119. sources.push(source);
  1120. return source;
  1121. }
  1122. }
  1123. function removeEventSource(source) {
  1124. sources = $.grep(sources, function(src) {
  1125. return !isSourcesEqual(src, source);
  1126. });
  1127. // remove all client events from that source
  1128. cache = $.grep(cache, function(e) {
  1129. return !isSourcesEqual(e.source, source);
  1130. });
  1131. reportEvents(cache);
  1132. }
  1133. /* Manipulation
  1134. -----------------------------------------------------------------------------*/
  1135. function updateEvent(event) {
  1136. mutateEvent(event);
  1137. propagateMiscProperties(event);
  1138. reportEvents(cache); // reports event modifications (so we can redraw)
  1139. }
  1140. var miscCopyableProps = [
  1141. 'title',
  1142. 'url',
  1143. 'allDay',
  1144. 'className',
  1145. 'editable',
  1146. 'color',
  1147. 'backgroundColor',
  1148. 'borderColor',
  1149. 'textColor'
  1150. ];
  1151. function propagateMiscProperties(event) {
  1152. var i;
  1153. var cachedEvent;
  1154. var j;
  1155. var prop;
  1156. for (i=0; i<cache.length; i++) {
  1157. cachedEvent = cache[i];
  1158. if (cachedEvent._id == event._id && cachedEvent !== event) {
  1159. for (j=0; j<miscCopyableProps.length; j++) {
  1160. prop = miscCopyableProps[j];
  1161. if (event[prop] !== undefined) {
  1162. cachedEvent[prop] = event[prop];
  1163. }
  1164. }
  1165. }
  1166. }
  1167. }
  1168. function renderEvent(eventData, stick) {
  1169. var event = buildEvent(eventData);
  1170. if (event) {
  1171. if (!event.source) {
  1172. if (stick) {
  1173. stickySource.events.push(event);
  1174. event.source = stickySource;
  1175. }
  1176. cache.push(event);
  1177. }
  1178. reportEvents(cache);
  1179. }
  1180. }
  1181. function removeEvents(filter) {
  1182. var i;
  1183. if (!filter) { // remove all
  1184. cache = [];
  1185. // clear all array sources
  1186. for (i=0; i<sources.length; i++) {
  1187. if ($.isArray(sources[i].events)) {
  1188. sources[i].events = [];
  1189. }
  1190. }
  1191. }else{
  1192. if (!$.isFunction(filter)) { // an event ID
  1193. var id = filter + '';
  1194. filter = function(e) {
  1195. return e._id == id;
  1196. };
  1197. }
  1198. cache = $.grep(cache, filter, true);
  1199. // remove events from array sources
  1200. for (i=0; i<sources.length; i++) {
  1201. if ($.isArray(sources[i].events)) {
  1202. sources[i].events = $.grep(sources[i].events, filter, true);
  1203. }
  1204. }
  1205. }
  1206. reportEvents(cache);
  1207. }
  1208. function clientEvents(filter) {
  1209. if ($.isFunction(filter)) {
  1210. return $.grep(cache, filter);
  1211. }
  1212. else if (filter) { // an event ID
  1213. filter += '';
  1214. return $.grep(cache, function(e) {
  1215. return e._id == filter;
  1216. });
  1217. }
  1218. return cache; // else, return all
  1219. }
  1220. /* Loading State
  1221. -----------------------------------------------------------------------------*/
  1222. function pushLoading() {
  1223. if (!(loadingLevel++)) {
  1224. trigger('loading', null, true, getView());
  1225. }
  1226. }
  1227. function popLoading() {
  1228. if (!(--loadingLevel)) {
  1229. trigger('loading', null, false, getView());
  1230. }
  1231. }
  1232. /* Event Normalization
  1233. -----------------------------------------------------------------------------*/
  1234. function buildEvent(data, source) { // source may be undefined!
  1235. var out = {};
  1236. var start;
  1237. var end;
  1238. var allDay;
  1239. var allDayDefault;
  1240. if (options.eventDataTransform) {
  1241. data = options.eventDataTransform(data);
  1242. }
  1243. if (source && source.eventDataTransform) {
  1244. data = source.eventDataTransform(data);
  1245. }
  1246. start = t.moment(data.start || data.date); // "date" is an alias for "start"
  1247. if (!start.isValid()) {
  1248. return;
  1249. }
  1250. end = null;
  1251. if (data.end) {
  1252. end = t.moment(data.end);
  1253. if (!end.isValid()) {
  1254. return;
  1255. }
  1256. }
  1257. allDay = data.allDay;
  1258. if (allDay === undefined) {
  1259. allDayDefault = firstDefined(
  1260. source ? source.allDayDefault : undefined,
  1261. options.allDayDefault
  1262. );
  1263. if (allDayDefault !== undefined) {
  1264. // use the default
  1265. allDay = allDayDefault;
  1266. }
  1267. else {
  1268. // all dates need to have ambig time for the event to be considered allDay
  1269. allDay = !start.hasTime() && (!end || !end.hasTime());
  1270. }
  1271. }
  1272. // normalize the date based on allDay
  1273. if (allDay) {
  1274. // neither date should have a time
  1275. if (start.hasTime()) {
  1276. start.stripTime();
  1277. }
  1278. if (end && end.hasTime()) {
  1279. end.stripTime();
  1280. }
  1281. }
  1282. else {
  1283. // force a time/zone up the dates
  1284. if (!start.hasTime()) {
  1285. start = t.rezoneDate(start);
  1286. }
  1287. if (end && !end.hasTime()) {
  1288. end = t.rezoneDate(end);
  1289. }
  1290. }
  1291. // Copy all properties over to the resulting object.
  1292. // The special-case properties will be copied over afterwards.
  1293. $.extend(out, data);
  1294. if (source) {
  1295. out.source = source;
  1296. }
  1297. out._id = data._id || (data.id === undefined ? '_fc' + eventGUID++ : data.id + '');
  1298. if (data.className) {
  1299. if (typeof data.className == 'string') {
  1300. out.className = data.className.split(/\s+/);
  1301. }
  1302. else { // assumed to be an array
  1303. out.className = data.className;
  1304. }
  1305. }
  1306. else {
  1307. out.className = [];
  1308. }
  1309. out.allDay = allDay;
  1310. out.start = start;
  1311. out.end = end;
  1312. if (options.forceEventDuration && !out.end) {
  1313. out.end = getEventEnd(out);
  1314. }
  1315. backupEventDates(out);
  1316. return out;
  1317. }
  1318. /* Event Modification Math
  1319. -----------------------------------------------------------------------------------------*/
  1320. // Modify the date(s) of an event and make this change propagate to all other events with
  1321. // the same ID (related repeating events).
  1322. //
  1323. // If `newStart`/`newEnd` are not specified, the "new" dates are assumed to be `event.start` and `event.end`.
  1324. // The "old" dates to be compare against are always `event._start` and `event._end` (set by EventManager).
  1325. //
  1326. // Returns a function that can be called to undo all the operations.
  1327. //
  1328. function mutateEvent(event, newStart, newEnd) {
  1329. var oldAllDay = event._allDay;
  1330. var oldStart = event._start;
  1331. var oldEnd = event._end;
  1332. var clearEnd = false;
  1333. var newAllDay;
  1334. var dateDelta;
  1335. var durationDelta;
  1336. // if no new dates were passed in, compare against the event's existing dates
  1337. if (!newStart && !newEnd) {
  1338. newStart = event.start;
  1339. newEnd = event.end;
  1340. }
  1341. // NOTE: throughout this function, the initial values of `newStart` and `newEnd` are
  1342. // preserved. These values may be undefined.
  1343. // detect new allDay
  1344. if (event.allDay != oldAllDay) { // if value has changed, use it
  1345. newAllDay = event.allDay;
  1346. }
  1347. else { // otherwise, see if any of the new dates are allDay
  1348. newAllDay = !(newStart || newEnd).hasTime();
  1349. }
  1350. // normalize the new dates based on allDay
  1351. if (newAllDay) {
  1352. if (newStart) {
  1353. newStart = newStart.clone().stripTime();
  1354. }
  1355. if (newEnd) {
  1356. newEnd = newEnd.clone().stripTime();
  1357. }
  1358. }
  1359. // compute dateDelta
  1360. if (newStart) {
  1361. if (newAllDay) {
  1362. dateDelta = dayishDiff(newStart, oldStart.clone().stripTime()); // treat oldStart as allDay
  1363. }
  1364. else {
  1365. dateDelta = dayishDiff(newStart, oldStart);
  1366. }
  1367. }
  1368. if (newAllDay != oldAllDay) {
  1369. // if allDay has changed, always throw away the end
  1370. clearEnd = true;
  1371. }
  1372. else if (newEnd) {
  1373. durationDelta = dayishDiff(
  1374. // new duration
  1375. newEnd || t.getDefaultEventEnd(newAllDay, newStart || oldStart),
  1376. newStart || oldStart
  1377. ).subtract(dayishDiff(
  1378. // subtract old duration
  1379. oldEnd || t.getDefaultEventEnd(oldAllDay, oldStart),
  1380. oldStart
  1381. ));
  1382. }
  1383. return mutateEvents(
  1384. clientEvents(event._id), // get events with this ID
  1385. clearEnd,
  1386. newAllDay,
  1387. dateDelta,
  1388. durationDelta
  1389. );
  1390. }
  1391. // Modifies an array of events in the following ways (operations are in order):
  1392. // - clear the event's `end`
  1393. // - convert the event to allDay
  1394. // - add `dateDelta` to the start and end
  1395. // - add `durationDelta` to the event's duration
  1396. //
  1397. // Returns a function that can be called to undo all the operations.
  1398. //
  1399. function mutateEvents(events, clearEnd, forceAllDay, dateDelta, durationDelta) {
  1400. var isAmbigTimezone = t.getIsAmbigTimezone();
  1401. var undoFunctions = [];
  1402. $.each(events, function(i, event) {
  1403. var oldAllDay = event._allDay;
  1404. var oldStart = event._start;
  1405. var oldEnd = event._end;
  1406. var newAllDay = forceAllDay != null ? forceAllDay : oldAllDay;
  1407. var newStart = oldStart.clone();
  1408. var newEnd = (!clearEnd && oldEnd) ? oldEnd.clone() : null;
  1409. // NOTE: this function is responsible for transforming `newStart` and `newEnd`,
  1410. // which were initialized to the OLD values first. `newEnd` may be null.
  1411. // normlize newStart/newEnd to be consistent with newAllDay
  1412. if (newAllDay) {
  1413. newStart.stripTime();
  1414. if (newEnd) {
  1415. newEnd.stripTime();
  1416. }
  1417. }
  1418. else {
  1419. if (!newStart.hasTime()) {
  1420. newStart = t.rezoneDate(newStart);
  1421. }
  1422. if (newEnd && !newEnd.hasTime()) {
  1423. newEnd = t.rezoneDate(newEnd);
  1424. }
  1425. }
  1426. // ensure we have an end date if necessary
  1427. if (!newEnd && (options.forceEventDuration || +durationDelta)) {
  1428. newEnd = t.getDefaultEventEnd(newAllDay, newStart);
  1429. }
  1430. // translate the dates
  1431. newStart.add(dateDelta);
  1432. if (newEnd) {
  1433. newEnd.add(dateDelta).add(durationDelta);
  1434. }
  1435. // if the dates have changed, and we know it is impossible to recompute the
  1436. // timezone offsets, strip the zone.
  1437. if (isAmbigTimezone) {
  1438. if (+dateDelta) {
  1439. newStart.stripZone();
  1440. }
  1441. if (newEnd && (+dateDelta || +durationDelta)) {
  1442. newEnd.stripZone();
  1443. }
  1444. }
  1445. event.allDay = newAllDay;
  1446. event.start = newStart;
  1447. event.end = newEnd;
  1448. backupEventDates(event);
  1449. undoFunctions.push(function() {
  1450. event.allDay = oldAllDay;
  1451. event.start = oldStart;
  1452. event.end = oldEnd;
  1453. backupEventDates(event);
  1454. });
  1455. });
  1456. return function() {
  1457. for (var i=0; i<undoFunctions.length; i++) {
  1458. undoFunctions[i]();
  1459. }
  1460. };
  1461. }
  1462. /* Utils
  1463. ------------------------------------------------------------------------------*/
  1464. function normalizeSource(source) {
  1465. if (source.className) {
  1466. // TODO: repeat code, same code for event classNames
  1467. if (typeof source.className == 'string') {
  1468. source.className = source.className.split(/\s+/);
  1469. }
  1470. }else{
  1471. source.className = [];
  1472. }
  1473. var normalizers = fc.sourceNormalizers;
  1474. for (var i=0; i<normalizers.length; i++) {
  1475. normalizers[i].call(t, source);
  1476. }
  1477. }
  1478. function isSourcesEqual(source1, source2) {
  1479. return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2);
  1480. }
  1481. function getSourcePrimitive(source) {
  1482. return ((typeof source == 'object') ? (source.events || source.url) : '') || source;
  1483. }
  1484. }
  1485. // updates the "backup" properties, which are preserved in order to compute diffs later on.
  1486. function backupEventDates(event) {
  1487. event._allDay = event.allDay;
  1488. event._start = event.start.clone();
  1489. event._end = event.end ? event.end.clone() : null;
  1490. }
  1491. ;;
  1492. fc.applyAll = applyAll;
  1493. // Create an object that has the given prototype.
  1494. // Just like Object.create
  1495. function createObject(proto) {
  1496. var f = function() {};
  1497. f.prototype = proto;
  1498. return new f();
  1499. }
  1500. // copy specifically-owned (non-protoype) properties of `b` onto `a`
  1501. function extend(a, b) {
  1502. for (var i in b) {
  1503. if (b.hasOwnProperty(i)) {
  1504. a[i] = b[i];
  1505. }
  1506. }
  1507. }
  1508. /* Date
  1509. -----------------------------------------------------------------------------*/
  1510. var dayIDs = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
  1511. // diffs the two moments into a Duration where full-days are recorded first,
  1512. // then the remaining time.
  1513. function dayishDiff(d1, d0) {
  1514. return moment.duration({
  1515. days: d1.clone().stripTime().diff(d0.clone().stripTime(), 'days'),
  1516. ms: d1.time() - d0.time()
  1517. });
  1518. }
  1519. function isNativeDate(input) {
  1520. return Object.prototype.toString.call(input) === '[object Date]' ||
  1521. input instanceof Date;
  1522. }
  1523. /* Event Element Binding
  1524. -----------------------------------------------------------------------------*/
  1525. function lazySegBind(container, segs, bindHandlers) {
  1526. container.unbind('mouseover').mouseover(function(ev) {
  1527. var parent=ev.target, e,
  1528. i, seg;
  1529. while (parent != this) {
  1530. e = parent;
  1531. parent = parent.parentNode;
  1532. }
  1533. if ((i = e._fci) !== undefined) {
  1534. e._fci = undefined;
  1535. seg = segs[i];
  1536. bindHandlers(seg.event, seg.element, seg);
  1537. $(ev.target).trigger(ev);
  1538. }
  1539. ev.stopPropagation();
  1540. });
  1541. }
  1542. /* Element Dimensions
  1543. -----------------------------------------------------------------------------*/
  1544. function setOuterWidth(element, width, includeMargins) {
  1545. for (var i=0, e; i<element.length; i++) {
  1546. e = $(element[i]);
  1547. e.width(Math.max(0, width - hsides(e, includeMargins)));
  1548. }
  1549. }
  1550. function setOuterHeight(element, height, includeMargins) {
  1551. for (var i=0, e; i<element.length; i++) {
  1552. e = $(element[i]);
  1553. e.height(Math.max(0, height - vsides(e, includeMargins)));
  1554. }
  1555. }
  1556. function hsides(element, includeMargins) {
  1557. return hpadding(element) + hborders(element) + (includeMargins ? hmargins(element) : 0);
  1558. }
  1559. function hpadding(element) {
  1560. return (parseFloat($.css(element[0], 'paddingLeft', true)) || 0) +
  1561. (parseFloat($.css(element[0], 'paddingRight', true)) || 0);
  1562. }
  1563. function hmargins(element) {
  1564. return (parseFloat($.css(element[0], 'marginLeft', true)) || 0) +
  1565. (parseFloat($.css(element[0], 'marginRight', true)) || 0);
  1566. }
  1567. function hborders(element) {
  1568. return (parseFloat($.css(element[0], 'borderLeftWidth', true)) || 0) +
  1569. (parseFloat($.css(element[0], 'borderRightWidth', true)) || 0);
  1570. }
  1571. function vsides(element, includeMargins) {
  1572. return vpadding(element) + vborders(element) + (includeMargins ? vmargins(element) : 0);
  1573. }
  1574. function vpadding(element) {
  1575. return (parseFloat($.css(element[0], 'paddingTop', true)) || 0) +
  1576. (parseFloat($.css(element[0], 'paddingBottom', true)) || 0);
  1577. }
  1578. function vmargins(element) {
  1579. return (parseFloat($.css(element[0], 'marginTop', true)) || 0) +
  1580. (parseFloat($.css(element[0], 'marginBottom', true)) || 0);
  1581. }
  1582. function vborders(element) {
  1583. return (parseFloat($.css(element[0], 'borderTopWidth', true)) || 0) +
  1584. (parseFloat($.css(element[0], 'borderBottomWidth', true)) || 0);
  1585. }
  1586. /* Misc Utils
  1587. -----------------------------------------------------------------------------*/
  1588. //TODO: arraySlice
  1589. //TODO: isFunction, grep ?
  1590. function noop() { }
  1591. function dateCompare(a, b) { // works with moments too
  1592. return a - b;
  1593. }
  1594. function arrayMax(a) {
  1595. return Math.max.apply(Math, a);
  1596. }
  1597. function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object
  1598. if (obj[name] !== undefined) {
  1599. return obj[name];
  1600. }
  1601. var parts = name.split(/(?=[A-Z])/),
  1602. i=parts.length-1, res;
  1603. for (; i>=0; i--) {
  1604. res = obj[parts[i].toLowerCase()];
  1605. if (res !== undefined) {
  1606. return res;
  1607. }
  1608. }
  1609. return obj['default'];
  1610. }
  1611. function htmlEscape(s) {
  1612. return (s + '').replace(/&/g, '&amp;')
  1613. .replace(/</g, '&lt;')
  1614. .replace(/>/g, '&gt;')
  1615. .replace(/'/g, '&#039;')
  1616. .replace(/"/g, '&quot;')
  1617. .replace(/\n/g, '<br />');
  1618. }
  1619. function stripHTMLEntities(text) {
  1620. return text.replace(/&.*?;/g, '');
  1621. }
  1622. function disableTextSelection(element) {
  1623. element
  1624. .attr('unselectable', 'on')
  1625. .css('MozUserSelect', 'none')
  1626. .bind('selectstart.ui', function() { return false; });
  1627. }
  1628. /*
  1629. function enableTextSelection(element) {
  1630. element
  1631. .attr('unselectable', 'off')
  1632. .css('MozUserSelect', '')
  1633. .unbind('selectstart.ui');
  1634. }
  1635. */
  1636. function markFirstLast(e) { // TODO: use CSS selectors instead
  1637. e.children()
  1638. .removeClass('fc-first fc-last')
  1639. .filter(':first-child')
  1640. .addClass('fc-first')
  1641. .end()
  1642. .filter(':last-child')
  1643. .addClass('fc-last');
  1644. }
  1645. function getSkinCss(event, opt) {
  1646. var source = event.source || {};
  1647. var eventColor = event.color;
  1648. var sourceColor = source.color;
  1649. var optionColor = opt('eventColor');
  1650. var backgroundColor =
  1651. event.backgroundColor ||
  1652. eventColor ||
  1653. source.backgroundColor ||
  1654. sourceColor ||
  1655. opt('eventBackgroundColor') ||
  1656. optionColor;
  1657. var borderColor =
  1658. event.borderColor ||
  1659. eventColor ||
  1660. source.borderColor ||
  1661. sourceColor ||
  1662. opt('eventBorderColor') ||
  1663. optionColor;
  1664. var textColor =
  1665. event.textColor ||
  1666. source.textColor ||
  1667. opt('eventTextColor');
  1668. var statements = [];
  1669. if (backgroundColor) {
  1670. statements.push('background-color:' + backgroundColor);
  1671. }
  1672. if (borderColor) {
  1673. statements.push('border-color:' + borderColor);
  1674. }
  1675. if (textColor) {
  1676. statements.push('color:' + textColor);
  1677. }
  1678. return statements.join(';');
  1679. }
  1680. function applyAll(functions, thisObj, args) {
  1681. if ($.isFunction(functions)) {
  1682. functions = [ functions ];
  1683. }
  1684. if (functions) {
  1685. var i;
  1686. var ret;
  1687. for (i=0; i<functions.length; i++) {
  1688. ret = functions[i].apply(thisObj, args) || ret;
  1689. }
  1690. return ret;
  1691. }
  1692. }
  1693. function firstDefined() {
  1694. for (var i=0; i<arguments.length; i++) {
  1695. if (arguments[i] !== undefined) {
  1696. return arguments[i];
  1697. }
  1698. }
  1699. }
  1700. ;;
  1701. var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/;
  1702. var ambigTimeOrZoneRegex = /^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/;
  1703. // Creating
  1704. // -------------------------------------------------------------------------------------------------
  1705. // Creates a moment in the local timezone, similar to the vanilla moment(...) constructor,
  1706. // but with extra features:
  1707. // - ambiguous times
  1708. // - enhanced formatting (TODO)
  1709. fc.moment = function() {
  1710. return makeMoment(arguments);
  1711. };
  1712. // Sames as fc.moment, but creates a moment in the UTC timezone.
  1713. fc.moment.utc = function() {
  1714. return makeMoment(arguments, true);
  1715. };
  1716. // Creates a moment and preserves the timezone offset of the ISO8601 string,
  1717. // allowing for ambigous timezones. If the string is not an ISO8601 string,
  1718. // the moment is processed in UTC-mode (a departure from moment's method).
  1719. fc.moment.parseZone = function() {
  1720. return makeMoment(arguments, true, true);
  1721. };
  1722. // when parseZone==true, if can't figure it out, fall back to parseUTC
  1723. function makeMoment(args, parseUTC, parseZone) {
  1724. var input = args[0];
  1725. var isSingleString = args.length == 1 && typeof input === 'string';
  1726. var isAmbigTime = false;
  1727. var isAmbigZone = false;
  1728. var ambigMatch;
  1729. var mom;
  1730. if (isSingleString) {
  1731. if (ambigDateOfMonthRegex.test(input)) {
  1732. // accept strings like '2014-05', but convert to the first of the month
  1733. input += '-01';
  1734. isAmbigTime = true;
  1735. isAmbigZone = true;
  1736. }
  1737. else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) {
  1738. isAmbigTime = !ambigMatch[5]; // no time part?
  1739. isAmbigZone = true;
  1740. }
  1741. }
  1742. else if ($.isArray(input)) {
  1743. // arrays have no timezone information, so assume ambiguous zone
  1744. isAmbigZone = true;
  1745. }
  1746. // instantiate a vanilla moment
  1747. if (parseUTC || parseZone || isAmbigTime) {
  1748. mom = moment.utc.apply(moment, args);
  1749. }
  1750. else {
  1751. mom = moment.apply(null, args);
  1752. }
  1753. if (moment.isMoment(input)) {
  1754. transferAmbigs(input, mom);
  1755. }
  1756. if (isAmbigTime) {
  1757. mom._ambigTime = true;
  1758. mom._ambigZone = true; // if ambiguous time, also ambiguous timezone offset
  1759. }
  1760. if (parseZone) {
  1761. if (isAmbigZone) {
  1762. mom._ambigZone = true;
  1763. }
  1764. else if (isSingleString) {
  1765. mom.zone(input); // if fails, will set it to 0, which it already was
  1766. }
  1767. else if (isNativeDate(input) || input === undefined) {
  1768. // native Date object?
  1769. // specified with no arguments?
  1770. // then consider the moment to be local
  1771. mom.local();
  1772. }
  1773. }
  1774. return new FCMoment(mom);
  1775. }
  1776. // our subclass of Moment.
  1777. // accepts an object with the internal Moment properties that should be copied over to
  1778. // this object (most likely another Moment object).
  1779. function FCMoment(config) {
  1780. extend(this, config);
  1781. }
  1782. // chain the prototype to Moment's
  1783. FCMoment.prototype = createObject(moment.fn);
  1784. // we need this because Moment's implementation will not copy of the ambig flags
  1785. FCMoment.prototype.clone = function() {
  1786. return makeMoment([ this ]);
  1787. };
  1788. // Time-of-day
  1789. // -------------------------------------------------------------------------------------------------
  1790. // GETTER
  1791. // Returns a Duration with the hours/minutes/seconds/ms values of the moment.
  1792. // If the moment has an ambiguous time, a duration of 00:00 will be returned.
  1793. //
  1794. // SETTER
  1795. // You can supply a Duration, a Moment, or a Duration-like argument.
  1796. // When setting the time, and the moment has an ambiguous time, it then becomes unambiguous.
  1797. FCMoment.prototype.time = function(time) {
  1798. if (time == null) { // getter
  1799. return moment.duration({
  1800. hours: this.hours(),
  1801. minutes: this.minutes(),
  1802. seconds: this.seconds(),
  1803. milliseconds: this.milliseconds()
  1804. });
  1805. }
  1806. else { // setter
  1807. delete this._ambigTime; // mark that the moment now has a time
  1808. if (!moment.isDuration(time) && !moment.isMoment(time)) {
  1809. time = moment.duration(time);
  1810. }
  1811. return this.hours(time.hours() + time.days() * 24) // day value will cause overflow (so 24 hours becomes 00:00:00 of next day)
  1812. .minutes(time.minutes())
  1813. .seconds(time.seconds())
  1814. .milliseconds(time.milliseconds());
  1815. }
  1816. };
  1817. // Converts the moment to UTC, stripping out its time-of-day and timezone offset,
  1818. // but preserving its YMD. A moment with a stripped time will display no time
  1819. // nor timezone offset when .format() is called.
  1820. FCMoment.prototype.stripTime = function() {
  1821. var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array
  1822. // set the internal UTC flag
  1823. moment.fn.utc.call(this); // call the original method, because we don't want to affect _ambigZone
  1824. this._ambigTime = true;
  1825. this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset
  1826. this.year(a[0])
  1827. .month(a[1])
  1828. .date(a[2])
  1829. .hours(0)
  1830. .minutes(0)
  1831. .seconds(0)
  1832. .milliseconds(0);
  1833. return this; // for chaining
  1834. };
  1835. // Returns if the moment has a non-ambiguous time (boolean)
  1836. FCMoment.prototype.hasTime = function() {
  1837. return !this._ambigTime;
  1838. };
  1839. // Timezone
  1840. // -------------------------------------------------------------------------------------------------
  1841. // Converts the moment to UTC, stripping out its timezone offset, but preserving its
  1842. // YMD and time-of-day. A moment with a stripped timezone offset will display no
  1843. // timezone offset when .format() is called.
  1844. FCMoment.prototype.stripZone = function() {
  1845. var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array
  1846. // set the internal UTC flag
  1847. moment.fn.utc.call(this); // call the original method, because we don't want to affect _ambigZone
  1848. this._ambigZone = true;
  1849. this.year(a[0])
  1850. .month(a[1])
  1851. .date(a[2])
  1852. .hours(a[3])
  1853. .minutes(a[4])
  1854. .seconds(a[5])
  1855. .milliseconds(a[6]);
  1856. return this; // for chaining
  1857. };
  1858. // Returns of the moment has a non-ambiguous timezone offset (boolean)
  1859. FCMoment.prototype.hasZone = function() {
  1860. return !this._ambigZone;
  1861. };
  1862. // this method implicitly marks a zone
  1863. FCMoment.prototype.zone = function(tzo) {
  1864. if (tzo != null) {
  1865. delete this._ambigZone;
  1866. }
  1867. return moment.fn.zone.apply(this, arguments);
  1868. };
  1869. // this method implicitly marks a zone.
  1870. // we don't need this, because .local internally calls .zone, but we don't want to depend on that.
  1871. FCMoment.prototype.local = function() {
  1872. delete this._ambigZone;
  1873. return moment.fn.local.apply(this, arguments);
  1874. };
  1875. // this method implicitly marks a zone.
  1876. // we don't need this, because .utc internally calls .zone, but we don't want to depend on that.
  1877. FCMoment.prototype.utc = function() {
  1878. delete this._ambigZone;
  1879. return moment.fn.utc.apply(this, arguments);
  1880. };
  1881. // Formatting
  1882. // -------------------------------------------------------------------------------------------------
  1883. FCMoment.prototype.format = function() {
  1884. if (arguments[0]) {
  1885. return formatDate(this, arguments[0]); // our extended formatting
  1886. }
  1887. if (this._ambigTime) {
  1888. return momentFormat(this, 'YYYY-MM-DD');
  1889. }
  1890. if (this._ambigZone) {
  1891. return momentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
  1892. }
  1893. return momentFormat(this); // default moment original formatting
  1894. };
  1895. FCMoment.prototype.toISOString = function() {
  1896. if (this._ambigTime) {
  1897. return momentFormat(this, 'YYYY-MM-DD');
  1898. }
  1899. if (this._ambigZone) {
  1900. return momentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
  1901. }
  1902. return moment.fn.toISOString.apply(this, arguments);
  1903. };
  1904. // Querying
  1905. // -------------------------------------------------------------------------------------------------
  1906. // Is the moment within the specified range? `end` is exclusive.
  1907. FCMoment.prototype.isWithin = function(start, end) {
  1908. var a = commonlyAmbiguate([ this, start, end ]);
  1909. return a[0] >= a[1] && a[0] < a[2];
  1910. };
  1911. // Make these query methods work with ambiguous moments
  1912. $.each([
  1913. 'isBefore',
  1914. 'isAfter',
  1915. 'isSame'
  1916. ], function(i, methodName) {
  1917. FCMoment.prototype[methodName] = function(input, units) {
  1918. var a = commonlyAmbiguate([ this, input ]);
  1919. return moment.fn[methodName].call(a[0], a[1], units);
  1920. };
  1921. });
  1922. // Misc Internals
  1923. // -------------------------------------------------------------------------------------------------
  1924. // transfers our internal _ambig properties from one moment to another
  1925. function transferAmbigs(src, dest) {
  1926. if (src._ambigTime) {
  1927. dest._ambigTime = true;
  1928. }
  1929. else if (dest._ambigTime) {
  1930. delete dest._ambigTime;
  1931. }
  1932. if (src._ambigZone) {
  1933. dest._ambigZone = true;
  1934. }
  1935. else if (dest._ambigZone) {
  1936. delete dest._ambigZone;
  1937. }
  1938. }
  1939. // given an array of moment-like inputs, return a parallel array w/ moments similarly ambiguated.
  1940. // for example, of one moment has ambig time, but not others, all moments will have their time stripped.
  1941. function commonlyAmbiguate(inputs) {
  1942. var outputs = [];
  1943. var anyAmbigTime = false;
  1944. var anyAmbigZone = false;
  1945. var i;
  1946. for (i=0; i<inputs.length; i++) {
  1947. outputs.push(fc.moment(inputs[i]));
  1948. anyAmbigTime = anyAmbigTime || outputs[i]._ambigTime;
  1949. anyAmbigZone = anyAmbigZone || outputs[i]._ambigZone;
  1950. }
  1951. for (i=0; i<outputs.length; i++) {
  1952. if (anyAmbigTime) {
  1953. outputs[i].stripTime();
  1954. }
  1955. else if (anyAmbigZone) {
  1956. outputs[i].stripZone();
  1957. }
  1958. }
  1959. return outputs;
  1960. }
  1961. ;;
  1962. // Single Date Formatting
  1963. // -------------------------------------------------------------------------------------------------
  1964. // call this if you want Moment's original format method to be used
  1965. function momentFormat(mom, formatStr) {
  1966. return moment.fn.format.call(mom, formatStr);
  1967. }
  1968. // Formats `date` with a Moment formatting string, but allow our non-zero areas and
  1969. // additional token.
  1970. function formatDate(date, formatStr) {
  1971. return formatDateWithChunks(date, getFormatStringChunks(formatStr));
  1972. }
  1973. function formatDateWithChunks(date, chunks) {
  1974. var s = '';
  1975. var i;
  1976. for (i=0; i<chunks.length; i++) {
  1977. s += formatDateWithChunk(date, chunks[i]);
  1978. }
  1979. return s;
  1980. }
  1981. // addition formatting tokens we want recognized
  1982. var tokenOverrides = {
  1983. t: function(date) { // "a" or "p"
  1984. return momentFormat(date, 'a').charAt(0);
  1985. },
  1986. T: function(date) { // "A" or "P"
  1987. return momentFormat(date, 'A').charAt(0);
  1988. }
  1989. };
  1990. function formatDateWithChunk(date, chunk) {
  1991. var token;
  1992. var maybeStr;
  1993. if (typeof chunk === 'string') { // a literal string
  1994. return chunk;
  1995. }
  1996. else if ((token = chunk.token)) { // a token, like "YYYY"
  1997. if (tokenOverrides[token]) {
  1998. return tokenOverrides[token](date); // use our custom token
  1999. }
  2000. return momentFormat(date, token);
  2001. }
  2002. else if (chunk.maybe) { // a grouping of other chunks that must be non-zero
  2003. maybeStr = formatDateWithChunks(date, chunk.maybe);
  2004. if (maybeStr.match(/[1-9]/)) {
  2005. return maybeStr;
  2006. }
  2007. }
  2008. return '';
  2009. }
  2010. // Date Range Formatting
  2011. // -------------------------------------------------------------------------------------------------
  2012. // TODO: make it work with timezone offset
  2013. // Using a formatting string meant for a single date, generate a range string, like
  2014. // "Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ.
  2015. // If the dates are the same as far as the format string is concerned, just return a single
  2016. // rendering of one date, without any separator.
  2017. function formatRange(date1, date2, formatStr, separator, isRTL) {
  2018. // Expand localized format strings, like "LL" -> "MMMM D YYYY"
  2019. formatStr = date1.lang().longDateFormat(formatStr) || formatStr;
  2020. // BTW, this is not important for `formatDate` because it is impossible to put custom tokens
  2021. // or non-zero areas in Moment's localized format strings.
  2022. separator = separator || ' - ';
  2023. return formatRangeWithChunks(
  2024. date1,
  2025. date2,
  2026. getFormatStringChunks(formatStr),
  2027. separator,
  2028. isRTL
  2029. );
  2030. }
  2031. fc.formatRange = formatRange; // expose
  2032. function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) {
  2033. var chunkStr; // the rendering of the chunk
  2034. var leftI;
  2035. var leftStr = '';
  2036. var rightI;
  2037. var rightStr = '';
  2038. var middleI;
  2039. var middleStr1 = '';
  2040. var middleStr2 = '';
  2041. var middleStr = '';
  2042. // Start at the leftmost side of the formatting string and continue until you hit a token
  2043. // that is not the same between dates.
  2044. for (leftI=0; leftI<chunks.length; leftI++) {
  2045. chunkStr = formatSimilarChunk(date1, date2, chunks[leftI]);
  2046. if (chunkStr === false) {
  2047. break;
  2048. }
  2049. leftStr += chunkStr;
  2050. }
  2051. // Similarly, start at the rightmost side of the formatting string and move left
  2052. for (rightI=chunks.length-1; rightI>leftI; rightI--) {
  2053. chunkStr = formatSimilarChunk(date1, date2, chunks[rightI]);
  2054. if (chunkStr === false) {
  2055. break;
  2056. }
  2057. rightStr = chunkStr + rightStr;
  2058. }
  2059. // The area in the middle is different for both of the dates.
  2060. // Collect them distinctly so we can jam them together later.
  2061. for (middleI=leftI; middleI<=rightI; middleI++) {
  2062. middleStr1 += formatDateWithChunk(date1, chunks[middleI]);
  2063. middleStr2 += formatDateWithChunk(date2, chunks[middleI]);
  2064. }
  2065. if (middleStr1 || middleStr2) {
  2066. if (isRTL) {
  2067. middleStr = middleStr2 + separator + middleStr1;
  2068. }
  2069. else {
  2070. middleStr = middleStr1 + separator + middleStr2;
  2071. }
  2072. }
  2073. return leftStr + middleStr + rightStr;
  2074. }
  2075. var similarUnitMap = {
  2076. Y: 'year',
  2077. M: 'month',
  2078. D: 'day', // day of month
  2079. d: 'day' // day of week
  2080. };
  2081. // don't go any further than day, because we don't want to break apart times like "12:30:00"
  2082. // TODO: week maybe?
  2083. // Given a formatting chunk, and given that both dates are similar in the regard the
  2084. // formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`.
  2085. function formatSimilarChunk(date1, date2, chunk) {
  2086. var token;
  2087. var unit;
  2088. if (typeof chunk === 'string') { // a literal string
  2089. return chunk;
  2090. }
  2091. else if ((token = chunk.token)) {
  2092. unit = similarUnitMap[token.charAt(0)];
  2093. // are the dates the same for this unit of measurement?
  2094. if (unit && date1.isSame(date2, unit)) {
  2095. return momentFormat(date1, token); // would be the same if we used `date2`
  2096. // BTW, don't support custom tokens
  2097. }
  2098. }
  2099. return false; // the chunk is NOT the same for the two dates
  2100. // BTW, don't support splitting on non-zero areas
  2101. }
  2102. // Chunking Utils
  2103. // -------------------------------------------------------------------------------------------------
  2104. var formatStringChunkCache = {};
  2105. function getFormatStringChunks(formatStr) {
  2106. if (formatStr in formatStringChunkCache) {
  2107. return formatStringChunkCache[formatStr];
  2108. }
  2109. return (formatStringChunkCache[formatStr] = chunkFormatString(formatStr));
  2110. }
  2111. // Break the formatting string into an array of chunks
  2112. function chunkFormatString(formatStr) {
  2113. var chunks = [];
  2114. var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|((\w)\4*o?T?)|([^\w\[\(]+)/g; // TODO: more descrimination
  2115. var match;
  2116. while ((match = chunker.exec(formatStr))) {
  2117. if (match[1]) { // a literal string instead [ ... ]
  2118. chunks.push(match[1]);
  2119. }
  2120. else if (match[2]) { // non-zero formatting inside ( ... )
  2121. chunks.push({ maybe: chunkFormatString(match[2]) });
  2122. }
  2123. else if (match[3]) { // a formatting token
  2124. chunks.push({ token: match[3] });
  2125. }
  2126. else if (match[5]) { // an unenclosed literal string
  2127. chunks.push(match[5]);
  2128. }
  2129. }
  2130. return chunks;
  2131. }
  2132. ;;
  2133. fcViews.month = MonthView;
  2134. function MonthView(element, calendar) {
  2135. var t = this;
  2136. // exports
  2137. t.incrementDate = incrementDate;
  2138. t.render = render;
  2139. // imports
  2140. BasicView.call(t, element, calendar, 'month');
  2141. function incrementDate(date, delta) {
  2142. return date.clone().stripTime().add('months', delta).startOf('month');
  2143. }
  2144. function render(date) {
  2145. t.intervalStart = date.clone().stripTime().startOf('month');
  2146. t.intervalEnd = t.intervalStart.clone().add('months', 1);
  2147. t.start = t.intervalStart.clone().startOf('week');
  2148. t.start = t.skipHiddenDays(t.start);
  2149. t.end = t.intervalEnd.clone().add('days', (7 - t.intervalEnd.weekday()) % 7);
  2150. t.end = t.skipHiddenDays(t.end, -1, true);
  2151. var rowCnt = Math.ceil( // need to ceil in case there are hidden days
  2152. t.end.diff(t.start, 'weeks', true) // returnfloat=true
  2153. );
  2154. if (t.opt('weekMode') == 'fixed') {
  2155. t.end.add('weeks', 6 - rowCnt);
  2156. rowCnt = 6;
  2157. }
  2158. t.title = calendar.formatDate(t.intervalStart, t.opt('titleFormat'));
  2159. t.renderBasic(rowCnt, t.getCellsPerWeek(), true);
  2160. }
  2161. }
  2162. ;;
  2163. fcViews.basicWeek = BasicWeekView;
  2164. function BasicWeekView(element, calendar) { // TODO: do a WeekView mixin
  2165. var t = this;
  2166. // exports
  2167. t.incrementDate = incrementDate;
  2168. t.render = render;
  2169. // imports
  2170. BasicView.call(t, element, calendar, 'basicWeek');
  2171. function incrementDate(date, delta) {
  2172. return date.clone().stripTime().add('weeks', delta).startOf('week');
  2173. }
  2174. function render(date) {
  2175. t.intervalStart = date.clone().stripTime().startOf('week');
  2176. t.intervalEnd = t.intervalStart.clone().add('weeks', 1);
  2177. t.start = t.skipHiddenDays(t.intervalStart);
  2178. t.end = t.skipHiddenDays(t.intervalEnd, -1, true);
  2179. t.title = calendar.formatRange(
  2180. t.start,
  2181. t.end.clone().subtract(1), // make inclusive by subtracting 1 ms
  2182. t.opt('titleFormat'),
  2183. ' \u2014 ' // emphasized dash
  2184. );
  2185. t.renderBasic(1, t.getCellsPerWeek(), false);
  2186. }
  2187. }
  2188. ;;
  2189. fcViews.basicDay = BasicDayView;
  2190. function BasicDayView(element, calendar) { // TODO: make a DayView mixin
  2191. var t = this;
  2192. // exports
  2193. t.incrementDate = incrementDate;
  2194. t.render = render;
  2195. // imports
  2196. BasicView.call(t, element, calendar, 'basicDay');
  2197. function incrementDate(date, delta) {
  2198. var out = date.clone().stripTime().add('days', delta);
  2199. out = t.skipHiddenDays(out, delta < 0 ? -1 : 1);
  2200. return out;
  2201. }
  2202. function render(date) {
  2203. t.start = t.intervalStart = date.clone().stripTime();
  2204. t.end = t.intervalEnd = t.start.clone().add('days', 1);
  2205. t.title = calendar.formatDate(t.start, t.opt('titleFormat'));
  2206. t.renderBasic(1, 1, false);
  2207. }
  2208. }
  2209. ;;
  2210. setDefaults({
  2211. weekMode: 'fixed'
  2212. });
  2213. function BasicView(element, calendar, viewName) {
  2214. var t = this;
  2215. // exports
  2216. t.renderBasic = renderBasic;
  2217. t.setHeight = setHeight;
  2218. t.setWidth = setWidth;
  2219. t.renderDayOverlay = renderDayOverlay;
  2220. t.defaultSelectionEnd = defaultSelectionEnd;
  2221. t.renderSelection = renderSelection;
  2222. t.clearSelection = clearSelection;
  2223. t.reportDayClick = reportDayClick; // for selection (kinda hacky)
  2224. t.dragStart = dragStart;
  2225. t.dragStop = dragStop;
  2226. t.getHoverListener = function() { return hoverListener; };
  2227. t.colLeft = colLeft;
  2228. t.colRight = colRight;
  2229. t.colContentLeft = colContentLeft;
  2230. t.colContentRight = colContentRight;
  2231. t.getIsCellAllDay = function() { return true; };
  2232. t.allDayRow = allDayRow;
  2233. t.getRowCnt = function() { return rowCnt; };
  2234. t.getColCnt = function() { return colCnt; };
  2235. t.getColWidth = function() { return colWidth; };
  2236. t.getDaySegmentContainer = function() { return daySegmentContainer; };
  2237. // imports
  2238. View.call(t, element, calendar, viewName);
  2239. OverlayManager.call(t);
  2240. SelectionManager.call(t);
  2241. BasicEventRenderer.call(t);
  2242. var opt = t.opt;
  2243. var trigger = t.trigger;
  2244. var renderOverlay = t.renderOverlay;
  2245. var clearOverlays = t.clearOverlays;
  2246. var daySelectionMousedown = t.daySelectionMousedown;
  2247. var cellToDate = t.cellToDate;
  2248. var dateToCell = t.dateToCell;
  2249. var rangeToSegments = t.rangeToSegments;
  2250. var formatDate = calendar.formatDate;
  2251. var calculateWeekNumber = calendar.calculateWeekNumber;
  2252. // locals
  2253. var table;
  2254. var head;
  2255. var headCells;
  2256. var body;
  2257. var bodyRows;
  2258. var bodyCells;
  2259. var bodyFirstCells;
  2260. var firstRowCellInners;
  2261. var firstRowCellContentInners;
  2262. var daySegmentContainer;
  2263. var viewWidth;
  2264. var viewHeight;
  2265. var colWidth;
  2266. var weekNumberWidth;
  2267. var rowCnt, colCnt;
  2268. var showNumbers;
  2269. var coordinateGrid;
  2270. var hoverListener;
  2271. var colPositions;
  2272. var colContentPositions;
  2273. var tm;
  2274. var colFormat;
  2275. var showWeekNumbers;
  2276. /* Rendering
  2277. ------------------------------------------------------------*/
  2278. disableTextSelection(element.addClass('fc-grid'));
  2279. function renderBasic(_rowCnt, _colCnt, _showNumbers) {
  2280. rowCnt = _rowCnt;
  2281. colCnt = _colCnt;
  2282. showNumbers = _showNumbers;
  2283. updateOptions();
  2284. if (!body) {
  2285. buildEventContainer();
  2286. }
  2287. buildTable();
  2288. }
  2289. function updateOptions() {
  2290. tm = opt('theme') ? 'ui' : 'fc';
  2291. colFormat = opt('columnFormat');
  2292. showWeekNumbers = opt('weekNumbers');
  2293. }
  2294. function buildEventContainer() {
  2295. daySegmentContainer =
  2296. $("<div class='fc-event-container' style='position:absolute;z-index:8;top:0;left:0'/>")
  2297. .appendTo(element);
  2298. }
  2299. function buildTable() {
  2300. var html = buildTableHTML();
  2301. if (table) {
  2302. table.remove();
  2303. }
  2304. table = $(html).appendTo(element);
  2305. head = table.find('thead');
  2306. headCells = head.find('.fc-day-header');
  2307. body = table.find('tbody');
  2308. bodyRows = body.find('tr');
  2309. bodyCells = body.find('.fc-day');
  2310. bodyFirstCells = bodyRows.find('td:first-child');
  2311. firstRowCellInners = bodyRows.eq(0).find('.fc-day > div');
  2312. firstRowCellContentInners = bodyRows.eq(0).find('.fc-day-content > div');
  2313. markFirstLast(head.add(head.find('tr'))); // marks first+last tr/th's
  2314. markFirstLast(bodyRows); // marks first+last td's
  2315. bodyRows.eq(0).addClass('fc-first');
  2316. bodyRows.filter(':last').addClass('fc-last');
  2317. bodyCells.each(function(i, _cell) {
  2318. var date = cellToDate(
  2319. Math.floor(i / colCnt),
  2320. i % colCnt
  2321. );
  2322. trigger('dayRender', t, date, $(_cell));
  2323. });
  2324. dayBind(bodyCells);
  2325. }
  2326. /* HTML Building
  2327. -----------------------------------------------------------*/
  2328. function buildTableHTML() {
  2329. var html =
  2330. "<table class='fc-border-separate' style='width:100%' cellspacing='0'>" +
  2331. buildHeadHTML() +
  2332. buildBodyHTML() +
  2333. "</table>";
  2334. return html;
  2335. }
  2336. function buildHeadHTML() {
  2337. var headerClass = tm + "-widget-header";
  2338. var html = '';
  2339. var col;
  2340. var date;
  2341. html += "<thead><tr>";
  2342. if (showWeekNumbers) {
  2343. html +=
  2344. "<th class='fc-week-number " + headerClass + "'>" +
  2345. htmlEscape(opt('weekNumberTitle')) +
  2346. "</th>";
  2347. }
  2348. for (col=0; col<colCnt; col++) {
  2349. date = cellToDate(0, col);
  2350. html +=
  2351. "<th class='fc-day-header fc-" + dayIDs[date.day()] + " " + headerClass + "'>" +
  2352. htmlEscape(formatDate(date, colFormat)) +
  2353. "</th>";
  2354. }
  2355. html += "</tr></thead>";
  2356. return html;
  2357. }
  2358. function buildBodyHTML() {
  2359. var contentClass = tm + "-widget-content";
  2360. var html = '';
  2361. var row;
  2362. var col;
  2363. var date;
  2364. html += "<tbody>";
  2365. for (row=0; row<rowCnt; row++) {
  2366. html += "<tr class='fc-week'>";
  2367. if (showWeekNumbers) {
  2368. date = cellToDate(row, 0);
  2369. html +=
  2370. "<td class='fc-week-number " + contentClass + "'>" +
  2371. "<div>" +
  2372. htmlEscape(calculateWeekNumber(date)) +
  2373. "</div>" +
  2374. "</td>";
  2375. }
  2376. for (col=0; col<colCnt; col++) {
  2377. date = cellToDate(row, col);
  2378. html += buildCellHTML(date);
  2379. }
  2380. html += "</tr>";
  2381. }
  2382. html += "</tbody>";
  2383. return html;
  2384. }
  2385. function buildCellHTML(date) { // date assumed to have stripped time
  2386. var month = t.intervalStart.month();
  2387. var today = calendar.getNow().stripTime();
  2388. var html = '';
  2389. var contentClass = tm + "-widget-content";
  2390. var classNames = [
  2391. 'fc-day',
  2392. 'fc-' + dayIDs[date.day()],
  2393. contentClass
  2394. ];
  2395. if (date.month() != month) {
  2396. classNames.push('fc-other-month');
  2397. }
  2398. if (date.isSame(today, 'day')) {
  2399. classNames.push(
  2400. 'fc-today',
  2401. tm + '-state-highlight'
  2402. );
  2403. }
  2404. else if (date < today) {
  2405. classNames.push('fc-past');
  2406. }
  2407. else {
  2408. classNames.push('fc-future');
  2409. }
  2410. html +=
  2411. "<td" +
  2412. " class='" + classNames.join(' ') + "'" +
  2413. " data-date='" + date.format() + "'" +
  2414. ">" +
  2415. "<div>";
  2416. if (showNumbers) {
  2417. html += "<div class='fc-day-number'>" + date.date() + "</div>";
  2418. }
  2419. html +=
  2420. "<div class='fc-day-content'>" +
  2421. "<div style='position:relative'>&nbsp;</div>" +
  2422. "</div>" +
  2423. "</div>" +
  2424. "</td>";
  2425. return html;
  2426. }
  2427. /* Dimensions
  2428. -----------------------------------------------------------*/
  2429. function setHeight(height) {
  2430. viewHeight = height;
  2431. var bodyHeight = Math.max(viewHeight - head.height(), 0);
  2432. var rowHeight;
  2433. var rowHeightLast;
  2434. var cell;
  2435. if (opt('weekMode') == 'variable') {
  2436. rowHeight = rowHeightLast = Math.floor(bodyHeight / (rowCnt==1 ? 2 : 6));
  2437. }else{
  2438. rowHeight = Math.floor(bodyHeight / rowCnt);
  2439. rowHeightLast = bodyHeight - rowHeight * (rowCnt-1);
  2440. }
  2441. bodyFirstCells.each(function(i, _cell) {
  2442. if (i < rowCnt) {
  2443. cell = $(_cell);
  2444. cell.find('> div').css(
  2445. 'min-height',
  2446. (i==rowCnt-1 ? rowHeightLast : rowHeight) - vsides(cell)
  2447. );
  2448. }
  2449. });
  2450. }
  2451. function setWidth(width) {
  2452. viewWidth = width;
  2453. colPositions.clear();
  2454. colContentPositions.clear();
  2455. weekNumberWidth = 0;
  2456. if (showWeekNumbers) {
  2457. weekNumberWidth = head.find('th.fc-week-number').outerWidth();
  2458. }
  2459. colWidth = Math.floor((viewWidth - weekNumberWidth) / colCnt);
  2460. setOuterWidth(headCells.slice(0, -1), colWidth);
  2461. }
  2462. /* Day clicking and binding
  2463. -----------------------------------------------------------*/
  2464. function dayBind(days) {
  2465. days.click(dayClick)
  2466. .mousedown(daySelectionMousedown);
  2467. }
  2468. function dayClick(ev) {
  2469. if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick
  2470. var date = calendar.moment($(this).data('date'));
  2471. trigger('dayClick', this, date, ev);
  2472. }
  2473. }
  2474. /* Semi-transparent Overlay Helpers
  2475. ------------------------------------------------------*/
  2476. // TODO: should be consolidated with AgendaView's methods
  2477. function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive
  2478. if (refreshCoordinateGrid) {
  2479. coordinateGrid.build();
  2480. }
  2481. var segments = rangeToSegments(overlayStart, overlayEnd);
  2482. for (var i=0; i<segments.length; i++) {
  2483. var segment = segments[i];
  2484. dayBind(
  2485. renderCellOverlay(
  2486. segment.row,
  2487. segment.leftCol,
  2488. segment.row,
  2489. segment.rightCol
  2490. )
  2491. );
  2492. }
  2493. }
  2494. function renderCellOverlay(row0, col0, row1, col1) { // row1,col1 is inclusive
  2495. var rect = coordinateGrid.rect(row0, col0, row1, col1, element);
  2496. return renderOverlay(rect, element);
  2497. }
  2498. /* Selection
  2499. -----------------------------------------------------------------------*/
  2500. function defaultSelectionEnd(start) {
  2501. return start.clone().stripTime().add('days', 1);
  2502. }
  2503. function renderSelection(start, end) { // end is exclusive
  2504. renderDayOverlay(start, end, true); // true = rebuild every time
  2505. }
  2506. function clearSelection() {
  2507. clearOverlays();
  2508. }
  2509. function reportDayClick(date, ev) {
  2510. var cell = dateToCell(date);
  2511. var _element = bodyCells[cell.row*colCnt + cell.col];
  2512. trigger('dayClick', _element, date, ev);
  2513. }
  2514. /* External Dragging
  2515. -----------------------------------------------------------------------*/
  2516. function dragStart(_dragElement, ev, ui) {
  2517. hoverListener.start(function(cell) {
  2518. clearOverlays();
  2519. if (cell) {
  2520. var d1 = cellToDate(cell);
  2521. var d2 = d1.clone().add(calendar.defaultAllDayEventDuration);
  2522. renderDayOverlay(d1, d2);
  2523. }
  2524. }, ev);
  2525. }
  2526. function dragStop(_dragElement, ev, ui) {
  2527. var cell = hoverListener.stop();
  2528. clearOverlays();
  2529. if (cell) {
  2530. trigger(
  2531. 'drop',
  2532. _dragElement,
  2533. cellToDate(cell),
  2534. ev,
  2535. ui
  2536. );
  2537. }
  2538. }
  2539. /* Utilities
  2540. --------------------------------------------------------*/
  2541. coordinateGrid = new CoordinateGrid(function(rows, cols) {
  2542. var e, n, p;
  2543. headCells.each(function(i, _e) {
  2544. e = $(_e);
  2545. n = e.offset().left;
  2546. if (i) {
  2547. p[1] = n;
  2548. }
  2549. p = [n];
  2550. cols[i] = p;
  2551. });
  2552. p[1] = n + e.outerWidth();
  2553. bodyRows.each(function(i, _e) {
  2554. if (i < rowCnt) {
  2555. e = $(_e);
  2556. n = e.offset().top;
  2557. if (i) {
  2558. p[1] = n;
  2559. }
  2560. p = [n];
  2561. rows[i] = p;
  2562. }
  2563. });
  2564. p[1] = n + e.outerHeight();
  2565. });
  2566. hoverListener = new HoverListener(coordinateGrid);
  2567. colPositions = new HorizontalPositionCache(function(col) {
  2568. return firstRowCellInners.eq(col);
  2569. });
  2570. colContentPositions = new HorizontalPositionCache(function(col) {
  2571. return firstRowCellContentInners.eq(col);
  2572. });
  2573. function colLeft(col) {
  2574. return colPositions.left(col);
  2575. }
  2576. function colRight(col) {
  2577. return colPositions.right(col);
  2578. }
  2579. function colContentLeft(col) {
  2580. return colContentPositions.left(col);
  2581. }
  2582. function colContentRight(col) {
  2583. return colContentPositions.right(col);
  2584. }
  2585. function allDayRow(i) {
  2586. return bodyRows.eq(i);
  2587. }
  2588. }
  2589. ;;
  2590. function BasicEventRenderer() {
  2591. var t = this;
  2592. // exports
  2593. t.renderEvents = renderEvents;
  2594. t.clearEvents = clearEvents;
  2595. // imports
  2596. DayEventRenderer.call(t);
  2597. function renderEvents(events, modifiedEventId) {
  2598. t.renderDayEvents(events, modifiedEventId);
  2599. }
  2600. function clearEvents() {
  2601. t.getDaySegmentContainer().empty();
  2602. }
  2603. // TODO: have this class (and AgendaEventRenderer) be responsible for creating the event container div
  2604. }
  2605. ;;
  2606. fcViews.agendaWeek = AgendaWeekView;
  2607. function AgendaWeekView(element, calendar) { // TODO: do a WeekView mixin
  2608. var t = this;
  2609. // exports
  2610. t.incrementDate = incrementDate;
  2611. t.render = render;
  2612. // imports
  2613. AgendaView.call(t, element, calendar, 'agendaWeek');
  2614. function incrementDate(date, delta) {
  2615. return date.clone().stripTime().add('weeks', delta).startOf('week');
  2616. }
  2617. function render(date) {
  2618. t.intervalStart = date.clone().stripTime().startOf('week');
  2619. t.intervalEnd = t.intervalStart.clone().add('weeks', 1);
  2620. t.start = t.skipHiddenDays(t.intervalStart);
  2621. t.end = t.skipHiddenDays(t.intervalEnd, -1, true);
  2622. t.title = calendar.formatRange(
  2623. t.start,
  2624. t.end.clone().subtract(1), // make inclusive by subtracting 1 ms
  2625. t.opt('titleFormat'),
  2626. ' \u2014 ' // emphasized dash
  2627. );
  2628. t.renderAgenda(t.getCellsPerWeek());
  2629. }
  2630. }
  2631. ;;
  2632. fcViews.agendaDay = AgendaDayView;
  2633. function AgendaDayView(element, calendar) { // TODO: make a DayView mixin
  2634. var t = this;
  2635. // exports
  2636. t.incrementDate = incrementDate;
  2637. t.render = render;
  2638. // imports
  2639. AgendaView.call(t, element, calendar, 'agendaDay');
  2640. function incrementDate(date, delta) {
  2641. var out = date.clone().stripTime().add('days', delta);
  2642. out = t.skipHiddenDays(out, delta < 0 ? -1 : 1);
  2643. return out;
  2644. }
  2645. function render(date) {
  2646. t.start = t.intervalStart = date.clone().stripTime();
  2647. t.end = t.intervalEnd = t.start.clone().add('days', 1);
  2648. t.title = calendar.formatDate(t.start, t.opt('titleFormat'));
  2649. t.renderAgenda(1);
  2650. }
  2651. }
  2652. ;;
  2653. setDefaults({
  2654. allDaySlot: true,
  2655. allDayText: 'all-day',
  2656. scrollTime: '06:00:00',
  2657. slotDuration: '00:30:00',
  2658. axisFormat: generateAgendaAxisFormat,
  2659. timeFormat: {
  2660. agenda: generateAgendaTimeFormat
  2661. },
  2662. dragOpacity: {
  2663. agenda: .5
  2664. },
  2665. minTime: '00:00:00',
  2666. maxTime: '24:00:00',
  2667. slotEventOverlap: true
  2668. });
  2669. function generateAgendaAxisFormat(options, langData) {
  2670. return langData.longDateFormat('LT')
  2671. .replace(':mm', '(:mm)')
  2672. .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs
  2673. .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
  2674. }
  2675. function generateAgendaTimeFormat(options, langData) {
  2676. return langData.longDateFormat('LT')
  2677. .replace(/\s*a$/i, ''); // remove trailing AM/PM
  2678. }
  2679. // TODO: make it work in quirks mode (event corners, all-day height)
  2680. // TODO: test liquid width, especially in IE6
  2681. function AgendaView(element, calendar, viewName) {
  2682. var t = this;
  2683. // exports
  2684. t.renderAgenda = renderAgenda;
  2685. t.setWidth = setWidth;
  2686. t.setHeight = setHeight;
  2687. t.afterRender = afterRender;
  2688. t.computeDateTop = computeDateTop;
  2689. t.getIsCellAllDay = getIsCellAllDay;
  2690. t.allDayRow = function() { return allDayRow; }; // badly named
  2691. t.getCoordinateGrid = function() { return coordinateGrid; }; // specifically for AgendaEventRenderer
  2692. t.getHoverListener = function() { return hoverListener; };
  2693. t.colLeft = colLeft;
  2694. t.colRight = colRight;
  2695. t.colContentLeft = colContentLeft;
  2696. t.colContentRight = colContentRight;
  2697. t.getDaySegmentContainer = function() { return daySegmentContainer; };
  2698. t.getSlotSegmentContainer = function() { return slotSegmentContainer; };
  2699. t.getSlotContainer = function() { return slotContainer; };
  2700. t.getRowCnt = function() { return 1; };
  2701. t.getColCnt = function() { return colCnt; };
  2702. t.getColWidth = function() { return colWidth; };
  2703. t.getSnapHeight = function() { return snapHeight; };
  2704. t.getSnapDuration = function() { return snapDuration; };
  2705. t.getSlotHeight = function() { return slotHeight; };
  2706. t.getSlotDuration = function() { return slotDuration; };
  2707. t.getMinTime = function() { return minTime; };
  2708. t.getMaxTime = function() { return maxTime; };
  2709. t.defaultSelectionEnd = defaultSelectionEnd;
  2710. t.renderDayOverlay = renderDayOverlay;
  2711. t.renderSelection = renderSelection;
  2712. t.clearSelection = clearSelection;
  2713. t.reportDayClick = reportDayClick; // selection mousedown hack
  2714. t.dragStart = dragStart;
  2715. t.dragStop = dragStop;
  2716. // imports
  2717. View.call(t, element, calendar, viewName);
  2718. OverlayManager.call(t);
  2719. SelectionManager.call(t);
  2720. AgendaEventRenderer.call(t);
  2721. var opt = t.opt;
  2722. var trigger = t.trigger;
  2723. var renderOverlay = t.renderOverlay;
  2724. var clearOverlays = t.clearOverlays;
  2725. var reportSelection = t.reportSelection;
  2726. var unselect = t.unselect;
  2727. var daySelectionMousedown = t.daySelectionMousedown;
  2728. var slotSegHtml = t.slotSegHtml;
  2729. var cellToDate = t.cellToDate;
  2730. var dateToCell = t.dateToCell;
  2731. var rangeToSegments = t.rangeToSegments;
  2732. var formatDate = calendar.formatDate;
  2733. var calculateWeekNumber = calendar.calculateWeekNumber;
  2734. // locals
  2735. var dayTable;
  2736. var dayHead;
  2737. var dayHeadCells;
  2738. var dayBody;
  2739. var dayBodyCells;
  2740. var dayBodyCellInners;
  2741. var dayBodyCellContentInners;
  2742. var dayBodyFirstCell;
  2743. var dayBodyFirstCellStretcher;
  2744. var slotLayer;
  2745. var daySegmentContainer;
  2746. var allDayTable;
  2747. var allDayRow;
  2748. var slotScroller;
  2749. var slotContainer;
  2750. var slotSegmentContainer;
  2751. var slotTable;
  2752. var selectionHelper;
  2753. var viewWidth;
  2754. var viewHeight;
  2755. var axisWidth;
  2756. var colWidth;
  2757. var gutterWidth;
  2758. var slotDuration;
  2759. var slotHeight; // TODO: what if slotHeight changes? (see issue 650)
  2760. var snapDuration;
  2761. var snapRatio; // ratio of number of "selection" slots to normal slots. (ex: 1, 2, 4)
  2762. var snapHeight; // holds the pixel hight of a "selection" slot
  2763. var colCnt;
  2764. var slotCnt;
  2765. var coordinateGrid;
  2766. var hoverListener;
  2767. var colPositions;
  2768. var colContentPositions;
  2769. var slotTopCache = {};
  2770. var tm;
  2771. var rtl;
  2772. var minTime;
  2773. var maxTime;
  2774. var colFormat;
  2775. /* Rendering
  2776. -----------------------------------------------------------------------------*/
  2777. disableTextSelection(element.addClass('fc-agenda'));
  2778. function renderAgenda(c) {
  2779. colCnt = c;
  2780. updateOptions();
  2781. if (!dayTable) { // first time rendering?
  2782. buildSkeleton(); // builds day table, slot area, events containers
  2783. }
  2784. else {
  2785. buildDayTable(); // rebuilds day table
  2786. }
  2787. }
  2788. function updateOptions() {
  2789. tm = opt('theme') ? 'ui' : 'fc';
  2790. rtl = opt('isRTL');
  2791. colFormat = opt('columnFormat');
  2792. minTime = moment.duration(opt('minTime'));
  2793. maxTime = moment.duration(opt('maxTime'));
  2794. slotDuration = moment.duration(opt('slotDuration'));
  2795. snapDuration = opt('snapDuration');
  2796. snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration;
  2797. }
  2798. /* Build DOM
  2799. -----------------------------------------------------------------------*/
  2800. function buildSkeleton() {
  2801. var s;
  2802. var headerClass = tm + "-widget-header";
  2803. var contentClass = tm + "-widget-content";
  2804. var slotTime;
  2805. var slotDate;
  2806. var minutes;
  2807. var slotNormal = slotDuration.asMinutes() % 15 === 0;
  2808. buildDayTable();
  2809. slotLayer =
  2810. $("<div style='position:absolute;z-index:2;left:0;width:100%'/>")
  2811. .appendTo(element);
  2812. if (opt('allDaySlot')) {
  2813. daySegmentContainer =
  2814. $("<div class='fc-event-container' style='position:absolute;z-index:8;top:0;left:0'/>")
  2815. .appendTo(slotLayer);
  2816. s =
  2817. "<table style='width:100%' class='fc-agenda-allday' cellspacing='0'>" +
  2818. "<tr>" +
  2819. "<th class='" + headerClass + " fc-agenda-axis'>" +
  2820. (
  2821. opt('allDayHTML') ||
  2822. htmlEscape(opt('allDayText'))
  2823. ) +
  2824. "</th>" +
  2825. "<td>" +
  2826. "<div class='fc-day-content'><div style='position:relative'/></div>" +
  2827. "</td>" +
  2828. "<th class='" + headerClass + " fc-agenda-gutter'>&nbsp;</th>" +
  2829. "</tr>" +
  2830. "</table>";
  2831. allDayTable = $(s).appendTo(slotLayer);
  2832. allDayRow = allDayTable.find('tr');
  2833. dayBind(allDayRow.find('td'));
  2834. slotLayer.append(
  2835. "<div class='fc-agenda-divider " + headerClass + "'>" +
  2836. "<div class='fc-agenda-divider-inner'/>" +
  2837. "</div>"
  2838. );
  2839. }else{
  2840. daySegmentContainer = $([]); // in jQuery 1.4, we can just do $()
  2841. }
  2842. slotScroller =
  2843. $("<div style='position:absolute;width:100%;overflow-x:hidden;overflow-y:auto'/>")
  2844. .appendTo(slotLayer);
  2845. slotContainer =
  2846. $("<div style='position:relative;width:100%;overflow:hidden'/>")
  2847. .appendTo(slotScroller);
  2848. slotSegmentContainer =
  2849. $("<div class='fc-event-container' style='position:absolute;z-index:8;top:0;left:0'/>")
  2850. .appendTo(slotContainer);
  2851. s =
  2852. "<table class='fc-agenda-slots' style='width:100%' cellspacing='0'>" +
  2853. "<tbody>";
  2854. slotTime = moment.duration(+minTime); // i wish there was .clone() for durations
  2855. slotCnt = 0;
  2856. while (slotTime < maxTime) {
  2857. slotDate = t.start.clone().time(slotTime); // will be in UTC but that's good. to avoid DST issues
  2858. minutes = slotDate.minutes();
  2859. s +=
  2860. "<tr class='fc-slot" + slotCnt + ' ' + (!minutes ? '' : 'fc-minor') + "'>" +
  2861. "<th class='fc-agenda-axis " + headerClass + "'>" +
  2862. ((!slotNormal || !minutes) ?
  2863. htmlEscape(formatDate(slotDate, opt('axisFormat'))) :
  2864. '&nbsp;'
  2865. ) +
  2866. "</th>" +
  2867. "<td class='" + contentClass + "'>" +
  2868. "<div style='position:relative'>&nbsp;</div>" +
  2869. "</td>" +
  2870. "</tr>";
  2871. slotTime.add(slotDuration);
  2872. slotCnt++;
  2873. }
  2874. s +=
  2875. "</tbody>" +
  2876. "</table>";
  2877. slotTable = $(s).appendTo(slotContainer);
  2878. slotBind(slotTable.find('td'));
  2879. }
  2880. /* Build Day Table
  2881. -----------------------------------------------------------------------*/
  2882. function buildDayTable() {
  2883. var html = buildDayTableHTML();
  2884. if (dayTable) {
  2885. dayTable.remove();
  2886. }
  2887. dayTable = $(html).appendTo(element);
  2888. dayHead = dayTable.find('thead');
  2889. dayHeadCells = dayHead.find('th').slice(1, -1); // exclude gutter
  2890. dayBody = dayTable.find('tbody');
  2891. dayBodyCells = dayBody.find('td').slice(0, -1); // exclude gutter
  2892. dayBodyCellInners = dayBodyCells.find('> div');
  2893. dayBodyCellContentInners = dayBodyCells.find('.fc-day-content > div');
  2894. dayBodyFirstCell = dayBodyCells.eq(0);
  2895. dayBodyFirstCellStretcher = dayBodyCellInners.eq(0);
  2896. markFirstLast(dayHead.add(dayHead.find('tr')));
  2897. markFirstLast(dayBody.add(dayBody.find('tr')));
  2898. // TODO: now that we rebuild the cells every time, we should call dayRender
  2899. }
  2900. function buildDayTableHTML() {
  2901. var html =
  2902. "<table style='width:100%' class='fc-agenda-days fc-border-separate' cellspacing='0'>" +
  2903. buildDayTableHeadHTML() +
  2904. buildDayTableBodyHTML() +
  2905. "</table>";
  2906. return html;
  2907. }
  2908. function buildDayTableHeadHTML() {
  2909. var headerClass = tm + "-widget-header";
  2910. var date;
  2911. var html = '';
  2912. var weekText;
  2913. var col;
  2914. html +=
  2915. "<thead>" +
  2916. "<tr>";
  2917. if (opt('weekNumbers')) {
  2918. date = cellToDate(0, 0);
  2919. weekText = calculateWeekNumber(date);
  2920. if (rtl) {
  2921. weekText += opt('weekNumberTitle');
  2922. }
  2923. else {
  2924. weekText = opt('weekNumberTitle') + weekText;
  2925. }
  2926. html +=
  2927. "<th class='fc-agenda-axis fc-week-number " + headerClass + "'>" +
  2928. htmlEscape(weekText) +
  2929. "</th>";
  2930. }
  2931. else {
  2932. html += "<th class='fc-agenda-axis " + headerClass + "'>&nbsp;</th>";
  2933. }
  2934. for (col=0; col<colCnt; col++) {
  2935. date = cellToDate(0, col);
  2936. html +=
  2937. "<th class='fc-" + dayIDs[date.day()] + " fc-col" + col + ' ' + headerClass + "'>" +
  2938. htmlEscape(formatDate(date, colFormat)) +
  2939. "</th>";
  2940. }
  2941. html +=
  2942. "<th class='fc-agenda-gutter " + headerClass + "'>&nbsp;</th>" +
  2943. "</tr>" +
  2944. "</thead>";
  2945. return html;
  2946. }
  2947. function buildDayTableBodyHTML() {
  2948. var headerClass = tm + "-widget-header"; // TODO: make these when updateOptions() called
  2949. var contentClass = tm + "-widget-content";
  2950. var date;
  2951. var today = calendar.getNow().stripTime();
  2952. var col;
  2953. var cellsHTML;
  2954. var cellHTML;
  2955. var classNames;
  2956. var html = '';
  2957. html +=
  2958. "<tbody>" +
  2959. "<tr>" +
  2960. "<th class='fc-agenda-axis " + headerClass + "'>&nbsp;</th>";
  2961. cellsHTML = '';
  2962. for (col=0; col<colCnt; col++) {
  2963. date = cellToDate(0, col);
  2964. classNames = [
  2965. 'fc-col' + col,
  2966. 'fc-' + dayIDs[date.day()],
  2967. contentClass
  2968. ];
  2969. if (date.isSame(today, 'day')) {
  2970. classNames.push(
  2971. tm + '-state-highlight',
  2972. 'fc-today'
  2973. );
  2974. }
  2975. else if (date < today) {
  2976. classNames.push('fc-past');
  2977. }
  2978. else {
  2979. classNames.push('fc-future');
  2980. }
  2981. cellHTML =
  2982. "<td class='" + classNames.join(' ') + "'>" +
  2983. "<div>" +
  2984. "<div class='fc-day-content'>" +
  2985. "<div style='position:relative'>&nbsp;</div>" +
  2986. "</div>" +
  2987. "</div>" +
  2988. "</td>";
  2989. cellsHTML += cellHTML;
  2990. }
  2991. html += cellsHTML;
  2992. html +=
  2993. "<td class='fc-agenda-gutter " + contentClass + "'>&nbsp;</td>" +
  2994. "</tr>" +
  2995. "</tbody>";
  2996. return html;
  2997. }
  2998. // TODO: data-date on the cells
  2999. /* Dimensions
  3000. -----------------------------------------------------------------------*/
  3001. function setHeight(height) {
  3002. if (height === undefined) {
  3003. height = viewHeight;
  3004. }
  3005. viewHeight = height;
  3006. slotTopCache = {};
  3007. var headHeight = dayBody.position().top;
  3008. var allDayHeight = slotScroller.position().top; // including divider
  3009. var bodyHeight = Math.min( // total body height, including borders
  3010. height - headHeight, // when scrollbars
  3011. slotTable.height() + allDayHeight + 1 // when no scrollbars. +1 for bottom border
  3012. );
  3013. dayBodyFirstCellStretcher
  3014. .height(bodyHeight - vsides(dayBodyFirstCell));
  3015. slotLayer.css('top', headHeight);
  3016. slotScroller.height(bodyHeight - allDayHeight - 1);
  3017. // the stylesheet guarantees that the first row has no border.
  3018. // this allows .height() to work well cross-browser.
  3019. var slotHeight0 = slotTable.find('tr:first').height() + 1; // +1 for bottom border
  3020. var slotHeight1 = slotTable.find('tr:eq(1)').height();
  3021. // HACK: i forget why we do this, but i think a cross-browser issue
  3022. slotHeight = (slotHeight0 + slotHeight1) / 2;
  3023. snapRatio = slotDuration / snapDuration;
  3024. snapHeight = slotHeight / snapRatio;
  3025. }
  3026. function setWidth(width) {
  3027. viewWidth = width;
  3028. colPositions.clear();
  3029. colContentPositions.clear();
  3030. var axisFirstCells = dayHead.find('th:first');
  3031. if (allDayTable) {
  3032. axisFirstCells = axisFirstCells.add(allDayTable.find('th:first'));
  3033. }
  3034. axisFirstCells = axisFirstCells.add(slotTable.find('th:first'));
  3035. axisWidth = 0;
  3036. setOuterWidth(
  3037. axisFirstCells
  3038. .width('')
  3039. .each(function(i, _cell) {
  3040. axisWidth = Math.max(axisWidth, $(_cell).outerWidth());
  3041. }),
  3042. axisWidth
  3043. );
  3044. var gutterCells = dayTable.find('.fc-agenda-gutter');
  3045. if (allDayTable) {
  3046. gutterCells = gutterCells.add(allDayTable.find('th.fc-agenda-gutter'));
  3047. }
  3048. var slotTableWidth = slotScroller[0].clientWidth; // needs to be done after axisWidth (for IE7)
  3049. gutterWidth = slotScroller.width() - slotTableWidth;
  3050. if (gutterWidth) {
  3051. setOuterWidth(gutterCells, gutterWidth);
  3052. gutterCells
  3053. .show()
  3054. .prev()
  3055. .removeClass('fc-last');
  3056. }else{
  3057. gutterCells
  3058. .hide()
  3059. .prev()
  3060. .addClass('fc-last');
  3061. }
  3062. colWidth = Math.floor((slotTableWidth - axisWidth) / colCnt);
  3063. setOuterWidth(dayHeadCells.slice(0, -1), colWidth);
  3064. }
  3065. /* Scrolling
  3066. -----------------------------------------------------------------------*/
  3067. function resetScroll() {
  3068. var top = computeTimeTop(
  3069. moment.duration(opt('scrollTime'))
  3070. ) + 1; // +1 for the border
  3071. function scroll() {
  3072. slotScroller.scrollTop(top);
  3073. }
  3074. scroll();
  3075. setTimeout(scroll, 0); // overrides any previous scroll state made by the browser
  3076. }
  3077. function afterRender() { // after the view has been freshly rendered and sized
  3078. resetScroll();
  3079. }
  3080. /* Slot/Day clicking and binding
  3081. -----------------------------------------------------------------------*/
  3082. function dayBind(cells) {
  3083. cells.click(slotClick)
  3084. .mousedown(daySelectionMousedown);
  3085. }
  3086. function slotBind(cells) {
  3087. cells.click(slotClick)
  3088. .mousedown(slotSelectionMousedown);
  3089. }
  3090. function slotClick(ev) {
  3091. if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick
  3092. var col = Math.min(colCnt-1, Math.floor((ev.pageX - dayTable.offset().left - axisWidth) / colWidth));
  3093. var date = cellToDate(0, col);
  3094. var match = this.parentNode.className.match(/fc-slot(\d+)/); // TODO: maybe use data
  3095. if (match) {
  3096. var slotIndex = parseInt(match[1]);
  3097. date.add(minTime + slotIndex * slotDuration);
  3098. date = calendar.rezoneDate(date);
  3099. trigger(
  3100. 'dayClick',
  3101. dayBodyCells[col],
  3102. date,
  3103. ev
  3104. );
  3105. }else{
  3106. trigger(
  3107. 'dayClick',
  3108. dayBodyCells[col],
  3109. date,
  3110. ev
  3111. );
  3112. }
  3113. }
  3114. }
  3115. /* Semi-transparent Overlay Helpers
  3116. -----------------------------------------------------*/
  3117. // TODO: should be consolidated with BasicView's methods
  3118. function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive
  3119. if (refreshCoordinateGrid) {
  3120. coordinateGrid.build();
  3121. }
  3122. var segments = rangeToSegments(overlayStart, overlayEnd);
  3123. for (var i=0; i<segments.length; i++) {
  3124. var segment = segments[i];
  3125. dayBind(
  3126. renderCellOverlay(
  3127. segment.row,
  3128. segment.leftCol,
  3129. segment.row,
  3130. segment.rightCol
  3131. )
  3132. );
  3133. }
  3134. }
  3135. function renderCellOverlay(row0, col0, row1, col1) { // only for all-day?
  3136. var rect = coordinateGrid.rect(row0, col0, row1, col1, slotLayer);
  3137. return renderOverlay(rect, slotLayer);
  3138. }
  3139. function renderSlotOverlay(overlayStart, overlayEnd) {
  3140. // normalize, because dayStart/dayEnd have stripped time+zone
  3141. overlayStart = overlayStart.clone().stripZone();
  3142. overlayEnd = overlayEnd.clone().stripZone();
  3143. for (var i=0; i<colCnt; i++) { // loop through the day columns
  3144. var dayStart = cellToDate(0, i);
  3145. var dayEnd = dayStart.clone().add('days', 1);
  3146. var stretchStart = dayStart < overlayStart ? overlayStart : dayStart; // the max of the two
  3147. var stretchEnd = dayEnd < overlayEnd ? dayEnd : overlayEnd; // the min of the two
  3148. if (stretchStart < stretchEnd) {
  3149. var rect = coordinateGrid.rect(0, i, 0, i, slotContainer); // only use it for horizontal coords
  3150. var top = computeDateTop(stretchStart, dayStart);
  3151. var bottom = computeDateTop(stretchEnd, dayStart);
  3152. rect.top = top;
  3153. rect.height = bottom - top;
  3154. slotBind(
  3155. renderOverlay(rect, slotContainer)
  3156. );
  3157. }
  3158. }
  3159. }
  3160. /* Coordinate Utilities
  3161. -----------------------------------------------------------------------------*/
  3162. coordinateGrid = new CoordinateGrid(function(rows, cols) {
  3163. var e, n, p;
  3164. dayHeadCells.each(function(i, _e) {
  3165. e = $(_e);
  3166. n = e.offset().left;
  3167. if (i) {
  3168. p[1] = n;
  3169. }
  3170. p = [n];
  3171. cols[i] = p;
  3172. });
  3173. p[1] = n + e.outerWidth();
  3174. if (opt('allDaySlot')) {
  3175. e = allDayRow;
  3176. n = e.offset().top;
  3177. rows[0] = [n, n+e.outerHeight()];
  3178. }
  3179. var slotTableTop = slotContainer.offset().top;
  3180. var slotScrollerTop = slotScroller.offset().top;
  3181. var slotScrollerBottom = slotScrollerTop + slotScroller.outerHeight();
  3182. function constrain(n) {
  3183. return Math.max(slotScrollerTop, Math.min(slotScrollerBottom, n));
  3184. }
  3185. for (var i=0; i<slotCnt*snapRatio; i++) { // adapt slot count to increased/decreased selection slot count
  3186. rows.push([
  3187. constrain(slotTableTop + snapHeight*i),
  3188. constrain(slotTableTop + snapHeight*(i+1))
  3189. ]);
  3190. }
  3191. });
  3192. hoverListener = new HoverListener(coordinateGrid);
  3193. colPositions = new HorizontalPositionCache(function(col) {
  3194. return dayBodyCellInners.eq(col);
  3195. });
  3196. colContentPositions = new HorizontalPositionCache(function(col) {
  3197. return dayBodyCellContentInners.eq(col);
  3198. });
  3199. function colLeft(col) {
  3200. return colPositions.left(col);
  3201. }
  3202. function colContentLeft(col) {
  3203. return colContentPositions.left(col);
  3204. }
  3205. function colRight(col) {
  3206. return colPositions.right(col);
  3207. }
  3208. function colContentRight(col) {
  3209. return colContentPositions.right(col);
  3210. }
  3211. function getIsCellAllDay(cell) { // TODO: remove because mom.hasTime() from realCellToDate() is better
  3212. return opt('allDaySlot') && !cell.row;
  3213. }
  3214. function realCellToDate(cell) { // ugh "real" ... but blame it on our abuse of the "cell" system
  3215. var date = cellToDate(0, cell.col);
  3216. var slotIndex = cell.row;
  3217. if (opt('allDaySlot')) {
  3218. slotIndex--;
  3219. }
  3220. if (slotIndex >= 0) {
  3221. date.time(moment.duration(minTime + slotIndex * slotDuration));
  3222. date = calendar.rezoneDate(date);
  3223. }
  3224. return date;
  3225. }
  3226. function computeDateTop(date, startOfDayDate) {
  3227. return computeTimeTop(
  3228. moment.duration(
  3229. date.clone().stripZone() - startOfDayDate.clone().stripTime()
  3230. )
  3231. );
  3232. }
  3233. function computeTimeTop(time) { // time is a duration
  3234. if (time < minTime) {
  3235. return 0;
  3236. }
  3237. if (time >= maxTime) {
  3238. return slotTable.height();
  3239. }
  3240. var slots = (time - minTime) / slotDuration;
  3241. var slotIndex = Math.floor(slots);
  3242. var slotPartial = slots - slotIndex;
  3243. var slotTop = slotTopCache[slotIndex];
  3244. // find the position of the corresponding <tr>
  3245. // need to use this tecnhique because not all rows are rendered at same height sometimes.
  3246. if (slotTop === undefined) {
  3247. slotTop = slotTopCache[slotIndex] =
  3248. slotTable.find('tr').eq(slotIndex).find('td div')[0].offsetTop;
  3249. // .eq() is faster than ":eq()" selector
  3250. // [0].offsetTop is faster than .position().top (do we really need this optimization?)
  3251. // a better optimization would be to cache all these divs
  3252. }
  3253. var top =
  3254. slotTop - 1 + // because first row doesn't have a top border
  3255. slotPartial * slotHeight; // part-way through the row
  3256. top = Math.max(top, 0);
  3257. return top;
  3258. }
  3259. /* Selection
  3260. ---------------------------------------------------------------------------------*/
  3261. function defaultSelectionEnd(start) {
  3262. if (start.hasTime()) {
  3263. return start.clone().add(slotDuration);
  3264. }
  3265. else {
  3266. return start.clone().add('days', 1);
  3267. }
  3268. }
  3269. function renderSelection(start, end) {
  3270. if (start.hasTime() || end.hasTime()) {
  3271. renderSlotSelection(start, end);
  3272. }
  3273. else if (opt('allDaySlot')) {
  3274. renderDayOverlay(start, end, true); // true for refreshing coordinate grid
  3275. }
  3276. }
  3277. function renderSlotSelection(startDate, endDate) {
  3278. var helperOption = opt('selectHelper');
  3279. coordinateGrid.build();
  3280. if (helperOption) {
  3281. var col = dateToCell(startDate).col;
  3282. if (col >= 0 && col < colCnt) { // only works when times are on same day
  3283. var rect = coordinateGrid.rect(0, col, 0, col, slotContainer); // only for horizontal coords
  3284. var top = computeDateTop(startDate, startDate);
  3285. var bottom = computeDateTop(endDate, startDate);
  3286. if (bottom > top) { // protect against selections that are entirely before or after visible range
  3287. rect.top = top;
  3288. rect.height = bottom - top;
  3289. rect.left += 2;
  3290. rect.width -= 5;
  3291. if ($.isFunction(helperOption)) {
  3292. var helperRes = helperOption(startDate, endDate);
  3293. if (helperRes) {
  3294. rect.position = 'absolute';
  3295. selectionHelper = $(helperRes)
  3296. .css(rect)
  3297. .appendTo(slotContainer);
  3298. }
  3299. }else{
  3300. rect.isStart = true; // conside rect a "seg" now
  3301. rect.isEnd = true; //
  3302. selectionHelper = $(slotSegHtml(
  3303. {
  3304. title: '',
  3305. start: startDate,
  3306. end: endDate,
  3307. className: ['fc-select-helper'],
  3308. editable: false
  3309. },
  3310. rect
  3311. ));
  3312. selectionHelper.css('opacity', opt('dragOpacity'));
  3313. }
  3314. if (selectionHelper) {
  3315. slotBind(selectionHelper);
  3316. slotContainer.append(selectionHelper);
  3317. setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended
  3318. setOuterHeight(selectionHelper, rect.height, true);
  3319. }
  3320. }
  3321. }
  3322. }else{
  3323. renderSlotOverlay(startDate, endDate);
  3324. }
  3325. }
  3326. function clearSelection() {
  3327. clearOverlays();
  3328. if (selectionHelper) {
  3329. selectionHelper.remove();
  3330. selectionHelper = null;
  3331. }
  3332. }
  3333. function slotSelectionMousedown(ev) {
  3334. if (ev.which == 1 && opt('selectable')) { // ev.which==1 means left mouse button
  3335. unselect(ev);
  3336. var dates;
  3337. hoverListener.start(function(cell, origCell) {
  3338. clearSelection();
  3339. if (cell && cell.col == origCell.col && !getIsCellAllDay(cell)) {
  3340. var d1 = realCellToDate(origCell);
  3341. var d2 = realCellToDate(cell);
  3342. dates = [
  3343. d1,
  3344. d1.clone().add(snapDuration), // calculate minutes depending on selection slot minutes
  3345. d2,
  3346. d2.clone().add(snapDuration)
  3347. ].sort(dateCompare);
  3348. renderSlotSelection(dates[0], dates[3]);
  3349. }else{
  3350. dates = null;
  3351. }
  3352. }, ev);
  3353. $(document).one('mouseup', function(ev) {
  3354. hoverListener.stop();
  3355. if (dates) {
  3356. if (+dates[0] == +dates[1]) {
  3357. reportDayClick(dates[0], ev);
  3358. }
  3359. reportSelection(dates[0], dates[3], ev);
  3360. }
  3361. });
  3362. }
  3363. }
  3364. function reportDayClick(date, ev) {
  3365. trigger('dayClick', dayBodyCells[dateToCell(date).col], date, ev);
  3366. }
  3367. /* External Dragging
  3368. --------------------------------------------------------------------------------*/
  3369. function dragStart(_dragElement, ev, ui) {
  3370. hoverListener.start(function(cell) {
  3371. clearOverlays();
  3372. if (cell) {
  3373. var d1 = realCellToDate(cell);
  3374. var d2 = d1.clone();
  3375. if (d1.hasTime()) {
  3376. d2.add(calendar.defaultTimedEventDuration);
  3377. renderSlotOverlay(d1, d2);
  3378. }
  3379. else {
  3380. d2.add(calendar.defaultAllDayEventDuration);
  3381. renderDayOverlay(d1, d2);
  3382. }
  3383. }
  3384. }, ev);
  3385. }
  3386. function dragStop(_dragElement, ev, ui) {
  3387. var cell = hoverListener.stop();
  3388. clearOverlays();
  3389. if (cell) {
  3390. trigger(
  3391. 'drop',
  3392. _dragElement,
  3393. realCellToDate(cell),
  3394. ev,
  3395. ui
  3396. );
  3397. }
  3398. }
  3399. }
  3400. ;;
  3401. function AgendaEventRenderer() {
  3402. var t = this;
  3403. // exports
  3404. t.renderEvents = renderEvents;
  3405. t.clearEvents = clearEvents;
  3406. t.slotSegHtml = slotSegHtml;
  3407. // imports
  3408. DayEventRenderer.call(t);
  3409. var opt = t.opt;
  3410. var trigger = t.trigger;
  3411. var isEventDraggable = t.isEventDraggable;
  3412. var isEventResizable = t.isEventResizable;
  3413. var eventElementHandlers = t.eventElementHandlers;
  3414. var setHeight = t.setHeight;
  3415. var getDaySegmentContainer = t.getDaySegmentContainer;
  3416. var getSlotSegmentContainer = t.getSlotSegmentContainer;
  3417. var getHoverListener = t.getHoverListener;
  3418. var computeDateTop = t.computeDateTop;
  3419. var getIsCellAllDay = t.getIsCellAllDay;
  3420. var colContentLeft = t.colContentLeft;
  3421. var colContentRight = t.colContentRight;
  3422. var cellToDate = t.cellToDate;
  3423. var getColCnt = t.getColCnt;
  3424. var getColWidth = t.getColWidth;
  3425. var getSnapHeight = t.getSnapHeight;
  3426. var getSnapDuration = t.getSnapDuration;
  3427. var getSlotHeight = t.getSlotHeight;
  3428. var getSlotDuration = t.getSlotDuration;
  3429. var getSlotContainer = t.getSlotContainer;
  3430. var reportEventElement = t.reportEventElement;
  3431. var showEvents = t.showEvents;
  3432. var hideEvents = t.hideEvents;
  3433. var eventDrop = t.eventDrop;
  3434. var eventResize = t.eventResize;
  3435. var renderDayOverlay = t.renderDayOverlay;
  3436. var clearOverlays = t.clearOverlays;
  3437. var renderDayEvents = t.renderDayEvents;
  3438. var getMinTime = t.getMinTime;
  3439. var getMaxTime = t.getMaxTime;
  3440. var calendar = t.calendar;
  3441. var formatDate = calendar.formatDate;
  3442. var formatRange = calendar.formatRange;
  3443. var getEventEnd = calendar.getEventEnd;
  3444. // overrides
  3445. t.draggableDayEvent = draggableDayEvent;
  3446. /* Rendering
  3447. ----------------------------------------------------------------------------*/
  3448. function renderEvents(events, modifiedEventId) {
  3449. var i, len=events.length,
  3450. dayEvents=[],
  3451. slotEvents=[];
  3452. for (i=0; i<len; i++) {
  3453. if (events[i].allDay) {
  3454. dayEvents.push(events[i]);
  3455. }else{
  3456. slotEvents.push(events[i]);
  3457. }
  3458. }
  3459. if (opt('allDaySlot')) {
  3460. renderDayEvents(dayEvents, modifiedEventId);
  3461. setHeight(); // no params means set to viewHeight
  3462. }
  3463. renderSlotSegs(compileSlotSegs(slotEvents), modifiedEventId);
  3464. }
  3465. function clearEvents() {
  3466. getDaySegmentContainer().empty();
  3467. getSlotSegmentContainer().empty();
  3468. }
  3469. function compileSlotSegs(events) {
  3470. var colCnt = getColCnt(),
  3471. minTime = getMinTime(),
  3472. maxTime = getMaxTime(),
  3473. cellDate,
  3474. i,
  3475. j, seg,
  3476. colSegs,
  3477. segs = [];
  3478. for (i=0; i<colCnt; i++) {
  3479. cellDate = cellToDate(0, i);
  3480. colSegs = sliceSegs(
  3481. events,
  3482. cellDate.clone().time(minTime),
  3483. cellDate.clone().time(maxTime)
  3484. );
  3485. colSegs = placeSlotSegs(colSegs); // returns a new order
  3486. for (j=0; j<colSegs.length; j++) {
  3487. seg = colSegs[j];
  3488. seg.col = i;
  3489. segs.push(seg);
  3490. }
  3491. }
  3492. return segs;
  3493. }
  3494. function sliceSegs(events, rangeStart, rangeEnd) {
  3495. // normalize, because all dates will be compared w/o zones
  3496. rangeStart = rangeStart.clone().stripZone();
  3497. rangeEnd = rangeEnd.clone().stripZone();
  3498. var segs = [],
  3499. i, len=events.length, event,
  3500. eventStart, eventEnd,
  3501. segStart, segEnd,
  3502. isStart, isEnd;
  3503. for (i=0; i<len; i++) {
  3504. event = events[i];
  3505. // get dates, make copies, then strip zone to normalize
  3506. eventStart = event.start.clone().stripZone();
  3507. eventEnd = getEventEnd(event).stripZone();
  3508. if (eventEnd > rangeStart && eventStart < rangeEnd) {
  3509. if (eventStart < rangeStart) {
  3510. segStart = rangeStart.clone();
  3511. isStart = false;
  3512. }
  3513. else {
  3514. segStart = eventStart;
  3515. isStart = true;
  3516. }
  3517. if (eventEnd > rangeEnd) {
  3518. segEnd = rangeEnd.clone();
  3519. isEnd = false;
  3520. }
  3521. else {
  3522. segEnd = eventEnd;
  3523. isEnd = true;
  3524. }
  3525. segs.push({
  3526. event: event,
  3527. start: segStart,
  3528. end: segEnd,
  3529. isStart: isStart,
  3530. isEnd: isEnd
  3531. });
  3532. }
  3533. }
  3534. return segs.sort(compareSlotSegs);
  3535. }
  3536. // renders events in the 'time slots' at the bottom
  3537. // TODO: when we refactor this, when user returns `false` eventRender, don't have empty space
  3538. // TODO: refactor will include using pixels to detect collisions instead of dates (handy for seg cmp)
  3539. function renderSlotSegs(segs, modifiedEventId) {
  3540. var i, segCnt=segs.length, seg,
  3541. event,
  3542. top,
  3543. bottom,
  3544. columnLeft,
  3545. columnRight,
  3546. columnWidth,
  3547. width,
  3548. left,
  3549. right,
  3550. html = '',
  3551. eventElements,
  3552. eventElement,
  3553. triggerRes,
  3554. titleElement,
  3555. height,
  3556. slotSegmentContainer = getSlotSegmentContainer(),
  3557. isRTL = opt('isRTL');
  3558. // calculate position/dimensions, create html
  3559. for (i=0; i<segCnt; i++) {
  3560. seg = segs[i];
  3561. event = seg.event;
  3562. top = computeDateTop(seg.start, seg.start);
  3563. bottom = computeDateTop(seg.end, seg.start);
  3564. columnLeft = colContentLeft(seg.col);
  3565. columnRight = colContentRight(seg.col);
  3566. columnWidth = columnRight - columnLeft;
  3567. // shave off space on right near scrollbars (2.5%)
  3568. // TODO: move this to CSS somehow
  3569. columnRight -= columnWidth * .025;
  3570. columnWidth = columnRight - columnLeft;
  3571. width = columnWidth * (seg.forwardCoord - seg.backwardCoord);
  3572. if (opt('slotEventOverlap')) {
  3573. // double the width while making sure resize handle is visible
  3574. // (assumed to be 20px wide)
  3575. width = Math.max(
  3576. (width - (20/2)) * 2,
  3577. width // narrow columns will want to make the segment smaller than
  3578. // the natural width. don't allow it
  3579. );
  3580. }
  3581. if (isRTL) {
  3582. right = columnRight - seg.backwardCoord * columnWidth;
  3583. left = right - width;
  3584. }
  3585. else {
  3586. left = columnLeft + seg.backwardCoord * columnWidth;
  3587. right = left + width;
  3588. }
  3589. // make sure horizontal coordinates are in bounds
  3590. left = Math.max(left, columnLeft);
  3591. right = Math.min(right, columnRight);
  3592. width = right - left;
  3593. seg.top = top;
  3594. seg.left = left;
  3595. seg.outerWidth = width;
  3596. seg.outerHeight = bottom - top;
  3597. html += slotSegHtml(event, seg);
  3598. }
  3599. slotSegmentContainer[0].innerHTML = html; // faster than html()
  3600. eventElements = slotSegmentContainer.children();
  3601. // retrieve elements, run through eventRender callback, bind event handlers
  3602. for (i=0; i<segCnt; i++) {
  3603. seg = segs[i];
  3604. event = seg.event;
  3605. eventElement = $(eventElements[i]); // faster than eq()
  3606. triggerRes = trigger('eventRender', event, event, eventElement);
  3607. if (triggerRes === false) {
  3608. eventElement.remove();
  3609. }else{
  3610. if (triggerRes && triggerRes !== true) {
  3611. eventElement.remove();
  3612. eventElement = $(triggerRes)
  3613. .css({
  3614. position: 'absolute',
  3615. top: seg.top,
  3616. left: seg.left
  3617. })
  3618. .appendTo(slotSegmentContainer);
  3619. }
  3620. seg.element = eventElement;
  3621. if (event._id === modifiedEventId) {
  3622. bindSlotSeg(event, eventElement, seg);
  3623. }else{
  3624. eventElement[0]._fci = i; // for lazySegBind
  3625. }
  3626. reportEventElement(event, eventElement);
  3627. }
  3628. }
  3629. lazySegBind(slotSegmentContainer, segs, bindSlotSeg);
  3630. // record event sides and title positions
  3631. for (i=0; i<segCnt; i++) {
  3632. seg = segs[i];
  3633. if ((eventElement = seg.element)) {
  3634. seg.vsides = vsides(eventElement, true);
  3635. seg.hsides = hsides(eventElement, true);
  3636. titleElement = eventElement.find('.fc-event-title');
  3637. if (titleElement.length) {
  3638. seg.contentTop = titleElement[0].offsetTop;
  3639. }
  3640. }
  3641. }
  3642. // set all positions/dimensions at once
  3643. for (i=0; i<segCnt; i++) {
  3644. seg = segs[i];
  3645. if ((eventElement = seg.element)) {
  3646. eventElement[0].style.width = Math.max(0, seg.outerWidth - seg.hsides) + 'px';
  3647. height = Math.max(0, seg.outerHeight - seg.vsides);
  3648. eventElement[0].style.height = height + 'px';
  3649. event = seg.event;
  3650. if (seg.contentTop !== undefined && height - seg.contentTop < 10) {
  3651. // not enough room for title, put it in the time (TODO: maybe make both display:inline instead)
  3652. eventElement.find('div.fc-event-time')
  3653. .text(
  3654. formatDate(event.start, opt('timeFormat')) + ' - ' + event.title
  3655. );
  3656. eventElement.find('div.fc-event-title')
  3657. .remove();
  3658. }
  3659. trigger('eventAfterRender', event, event, eventElement);
  3660. }
  3661. }
  3662. }
  3663. function slotSegHtml(event, seg) {
  3664. var html = "<";
  3665. var url = event.url;
  3666. var skinCss = getSkinCss(event, opt);
  3667. var classes = ['fc-event', 'fc-event-vert'];
  3668. if (isEventDraggable(event)) {
  3669. classes.push('fc-event-draggable');
  3670. }
  3671. if (seg.isStart) {
  3672. classes.push('fc-event-start');
  3673. }
  3674. if (seg.isEnd) {
  3675. classes.push('fc-event-end');
  3676. }
  3677. classes = classes.concat(event.className);
  3678. if (event.source) {
  3679. classes = classes.concat(event.source.className || []);
  3680. }
  3681. if (url) {
  3682. html += "a href='" + htmlEscape(event.url) + "'";
  3683. }else{
  3684. html += "div";
  3685. }
  3686. html +=
  3687. " class='" + classes.join(' ') + "'" +
  3688. " style=" +
  3689. "'" +
  3690. "position:absolute;" +
  3691. "top:" + seg.top + "px;" +
  3692. "left:" + seg.left + "px;" +
  3693. skinCss +
  3694. "'" +
  3695. ">" +
  3696. "<div class='fc-event-inner'>" +
  3697. "<div class='fc-event-time'>";
  3698. if (event.end) {
  3699. html += htmlEscape(formatRange(event.start, event.end, opt('timeFormat')));
  3700. }else{
  3701. html += htmlEscape(formatDate(event.start, opt('timeFormat')));
  3702. }
  3703. html +=
  3704. "</div>" +
  3705. "<div class='fc-event-title'>" +
  3706. htmlEscape(event.title || '') +
  3707. "</div>" +
  3708. "</div>" +
  3709. "<div class='fc-event-bg'></div>";
  3710. if (seg.isEnd && isEventResizable(event)) {
  3711. html +=
  3712. "<div class='ui-resizable-handle ui-resizable-s'>=</div>";
  3713. }
  3714. html +=
  3715. "</" + (url ? "a" : "div") + ">";
  3716. return html;
  3717. }
  3718. function bindSlotSeg(event, eventElement, seg) {
  3719. var timeElement = eventElement.find('div.fc-event-time');
  3720. if (isEventDraggable(event)) {
  3721. draggableSlotEvent(event, eventElement, timeElement);
  3722. }
  3723. if (seg.isEnd && isEventResizable(event)) {
  3724. resizableSlotEvent(event, eventElement, timeElement);
  3725. }
  3726. eventElementHandlers(event, eventElement);
  3727. }
  3728. /* Dragging
  3729. -----------------------------------------------------------------------------------*/
  3730. // when event starts out FULL-DAY
  3731. // overrides DayEventRenderer's version because it needs to account for dragging elements
  3732. // to and from the slot area.
  3733. function draggableDayEvent(event, eventElement, seg) {
  3734. var isStart = seg.isStart;
  3735. var origWidth;
  3736. var revert;
  3737. var allDay = true;
  3738. var dayDelta;
  3739. var hoverListener = getHoverListener();
  3740. var colWidth = getColWidth();
  3741. var minTime = getMinTime();
  3742. var slotDuration = getSlotDuration();
  3743. var slotHeight = getSlotHeight();
  3744. var snapDuration = getSnapDuration();
  3745. var snapHeight = getSnapHeight();
  3746. eventElement.draggable({
  3747. opacity: opt('dragOpacity', 'month'), // use whatever the month view was using
  3748. revertDuration: opt('dragRevertDuration'),
  3749. start: function(ev, ui) {
  3750. trigger('eventDragStart', eventElement, event, ev, ui);
  3751. hideEvents(event, eventElement);
  3752. origWidth = eventElement.width();
  3753. hoverListener.start(function(cell, origCell) {
  3754. clearOverlays();
  3755. if (cell) {
  3756. revert = false;
  3757. var origDate = cellToDate(0, origCell.col);
  3758. var date = cellToDate(0, cell.col);
  3759. dayDelta = date.diff(origDate, 'days');
  3760. if (!cell.row) { // on full-days
  3761. renderDayOverlay(
  3762. event.start.clone().add('days', dayDelta),
  3763. getEventEnd(event).add('days', dayDelta)
  3764. );
  3765. resetElement();
  3766. }
  3767. else { // mouse is over bottom slots
  3768. if (isStart) {
  3769. if (allDay) {
  3770. // convert event to temporary slot-event
  3771. eventElement.width(colWidth - 10); // don't use entire width
  3772. setOuterHeight(eventElement, calendar.defaultTimedEventDuration / slotDuration * slotHeight); // the default height
  3773. eventElement.draggable('option', 'grid', [ colWidth, 1 ]);
  3774. allDay = false;
  3775. }
  3776. }
  3777. else {
  3778. revert = true;
  3779. }
  3780. }
  3781. revert = revert || (allDay && !dayDelta);
  3782. }
  3783. else {
  3784. resetElement();
  3785. revert = true;
  3786. }
  3787. eventElement.draggable('option', 'revert', revert);
  3788. }, ev, 'drag');
  3789. },
  3790. stop: function(ev, ui) {
  3791. hoverListener.stop();
  3792. clearOverlays();
  3793. trigger('eventDragStop', eventElement, event, ev, ui);
  3794. if (revert) { // hasn't moved or is out of bounds (draggable has already reverted)
  3795. resetElement();
  3796. eventElement.css('filter', ''); // clear IE opacity side-effects
  3797. showEvents(event, eventElement);
  3798. }
  3799. else { // changed!
  3800. var eventStart = event.start.clone().add('days', dayDelta); // already assumed to have a stripped time
  3801. var snapTime;
  3802. var snapIndex;
  3803. if (!allDay) {
  3804. snapIndex = Math.round((eventElement.offset().top - getSlotContainer().offset().top) / snapHeight); // why not use ui.offset.top?
  3805. snapTime = moment.duration(minTime + snapIndex * snapDuration);
  3806. eventStart = calendar.rezoneDate(eventStart.clone().time(snapTime));
  3807. }
  3808. eventDrop(
  3809. this, // el
  3810. event,
  3811. eventStart,
  3812. ev,
  3813. ui
  3814. );
  3815. }
  3816. }
  3817. });
  3818. function resetElement() {
  3819. if (!allDay) {
  3820. eventElement
  3821. .width(origWidth)
  3822. .height('')
  3823. .draggable('option', 'grid', null);
  3824. allDay = true;
  3825. }
  3826. }
  3827. }
  3828. // when event starts out IN TIMESLOTS
  3829. function draggableSlotEvent(event, eventElement, timeElement) {
  3830. var coordinateGrid = t.getCoordinateGrid();
  3831. var colCnt = getColCnt();
  3832. var colWidth = getColWidth();
  3833. var snapHeight = getSnapHeight();
  3834. var snapDuration = getSnapDuration();
  3835. // states
  3836. var origPosition; // original position of the element, not the mouse
  3837. var origCell;
  3838. var isInBounds, prevIsInBounds;
  3839. var isAllDay, prevIsAllDay;
  3840. var colDelta, prevColDelta;
  3841. var dayDelta; // derived from colDelta
  3842. var snapDelta, prevSnapDelta; // the number of snaps away from the original position
  3843. // newly computed
  3844. var eventStart, eventEnd;
  3845. eventElement.draggable({
  3846. scroll: false,
  3847. grid: [ colWidth, snapHeight ],
  3848. axis: colCnt==1 ? 'y' : false,
  3849. opacity: opt('dragOpacity'),
  3850. revertDuration: opt('dragRevertDuration'),
  3851. start: function(ev, ui) {
  3852. trigger('eventDragStart', eventElement, event, ev, ui);
  3853. hideEvents(event, eventElement);
  3854. coordinateGrid.build();
  3855. // initialize states
  3856. origPosition = eventElement.position();
  3857. origCell = coordinateGrid.cell(ev.pageX, ev.pageY);
  3858. isInBounds = prevIsInBounds = true;
  3859. isAllDay = prevIsAllDay = getIsCellAllDay(origCell);
  3860. colDelta = prevColDelta = 0;
  3861. dayDelta = 0;
  3862. snapDelta = prevSnapDelta = 0;
  3863. eventStart = null;
  3864. eventEnd = null;
  3865. },
  3866. drag: function(ev, ui) {
  3867. // NOTE: this `cell` value is only useful for determining in-bounds and all-day.
  3868. // Bad for anything else due to the discrepancy between the mouse position and the
  3869. // element position while snapping. (problem revealed in PR #55)
  3870. //
  3871. // PS- the problem exists for draggableDayEvent() when dragging an all-day event to a slot event.
  3872. // We should overhaul the dragging system and stop relying on jQuery UI.
  3873. var cell = coordinateGrid.cell(ev.pageX, ev.pageY);
  3874. // update states
  3875. isInBounds = !!cell;
  3876. if (isInBounds) {
  3877. isAllDay = getIsCellAllDay(cell);
  3878. // calculate column delta
  3879. colDelta = Math.round((ui.position.left - origPosition.left) / colWidth);
  3880. if (colDelta != prevColDelta) {
  3881. // calculate the day delta based off of the original clicked column and the column delta
  3882. var origDate = cellToDate(0, origCell.col);
  3883. var col = origCell.col + colDelta;
  3884. col = Math.max(0, col);
  3885. col = Math.min(colCnt-1, col);
  3886. var date = cellToDate(0, col);
  3887. dayDelta = date.diff(origDate, 'days');
  3888. }
  3889. // calculate minute delta (only if over slots)
  3890. if (!isAllDay) {
  3891. snapDelta = Math.round((ui.position.top - origPosition.top) / snapHeight);
  3892. }
  3893. }
  3894. // any state changes?
  3895. if (
  3896. isInBounds != prevIsInBounds ||
  3897. isAllDay != prevIsAllDay ||
  3898. colDelta != prevColDelta ||
  3899. snapDelta != prevSnapDelta
  3900. ) {
  3901. // compute new dates
  3902. if (isAllDay) {
  3903. eventStart = event.start.clone().stripTime().add('days', dayDelta);
  3904. eventEnd = eventStart.clone().add(calendar.defaultAllDayEventDuration);
  3905. }
  3906. else {
  3907. eventStart = event.start.clone().add(snapDelta * snapDuration).add('days', dayDelta);
  3908. eventEnd = getEventEnd(event).add(snapDelta * snapDuration).add('days', dayDelta);
  3909. }
  3910. updateUI();
  3911. // update previous states for next time
  3912. prevIsInBounds = isInBounds;
  3913. prevIsAllDay = isAllDay;
  3914. prevColDelta = colDelta;
  3915. prevSnapDelta = snapDelta;
  3916. }
  3917. // if out-of-bounds, revert when done, and vice versa.
  3918. eventElement.draggable('option', 'revert', !isInBounds);
  3919. },
  3920. stop: function(ev, ui) {
  3921. clearOverlays();
  3922. trigger('eventDragStop', eventElement, event, ev, ui);
  3923. if (isInBounds && (isAllDay || dayDelta || snapDelta)) { // changed!
  3924. eventDrop(
  3925. this, // el
  3926. event,
  3927. eventStart,
  3928. ev,
  3929. ui
  3930. );
  3931. }
  3932. else { // either no change or out-of-bounds (draggable has already reverted)
  3933. // reset states for next time, and for updateUI()
  3934. isInBounds = true;
  3935. isAllDay = false;
  3936. colDelta = 0;
  3937. dayDelta = 0;
  3938. snapDelta = 0;
  3939. updateUI();
  3940. eventElement.css('filter', ''); // clear IE opacity side-effects
  3941. // sometimes fast drags make event revert to wrong position, so reset.
  3942. // also, if we dragged the element out of the area because of snapping,
  3943. // but the *mouse* is still in bounds, we need to reset the position.
  3944. eventElement.css(origPosition);
  3945. showEvents(event, eventElement);
  3946. }
  3947. }
  3948. });
  3949. function updateUI() {
  3950. clearOverlays();
  3951. if (isInBounds) {
  3952. if (isAllDay) {
  3953. timeElement.hide();
  3954. eventElement.draggable('option', 'grid', null); // disable grid snapping
  3955. renderDayOverlay(eventStart, eventEnd);
  3956. }
  3957. else {
  3958. updateTimeText();
  3959. timeElement.css('display', ''); // show() was causing display=inline
  3960. eventElement.draggable('option', 'grid', [colWidth, snapHeight]); // re-enable grid snapping
  3961. }
  3962. }
  3963. }
  3964. function updateTimeText() {
  3965. var text;
  3966. if (eventStart) { // must of had a state change
  3967. if (event.end) {
  3968. text = formatRange(eventStart, eventEnd, opt('timeFormat'));
  3969. }
  3970. else {
  3971. text = formatDate(eventStart, opt('timeFormat'));
  3972. }
  3973. timeElement.text(text);
  3974. }
  3975. }
  3976. }
  3977. /* Resizing
  3978. --------------------------------------------------------------------------------------*/
  3979. function resizableSlotEvent(event, eventElement, timeElement) {
  3980. var snapDelta, prevSnapDelta;
  3981. var snapHeight = getSnapHeight();
  3982. var snapDuration = getSnapDuration();
  3983. var eventEnd;
  3984. eventElement.resizable({
  3985. handles: {
  3986. s: '.ui-resizable-handle'
  3987. },
  3988. grid: snapHeight,
  3989. start: function(ev, ui) {
  3990. snapDelta = prevSnapDelta = 0;
  3991. hideEvents(event, eventElement);
  3992. trigger('eventResizeStart', this, event, ev, ui);
  3993. },
  3994. resize: function(ev, ui) {
  3995. // don't rely on ui.size.height, doesn't take grid into account
  3996. snapDelta = Math.round((Math.max(snapHeight, eventElement.height()) - ui.originalSize.height) / snapHeight);
  3997. if (snapDelta != prevSnapDelta) {
  3998. eventEnd = getEventEnd(event).add(snapDuration * snapDelta);
  3999. var text;
  4000. if (snapDelta || event.end) {
  4001. text = formatRange(
  4002. event.start,
  4003. eventEnd,
  4004. opt('timeFormat')
  4005. );
  4006. }
  4007. else {
  4008. text = formatDate(event.start, opt('timeFormat'));
  4009. }
  4010. timeElement.text(text);
  4011. prevSnapDelta = snapDelta;
  4012. }
  4013. },
  4014. stop: function(ev, ui) {
  4015. trigger('eventResizeStop', this, event, ev, ui);
  4016. if (snapDelta) {
  4017. eventResize(
  4018. this,
  4019. event,
  4020. eventEnd,
  4021. ev,
  4022. ui
  4023. );
  4024. }
  4025. else {
  4026. showEvents(event, eventElement);
  4027. // BUG: if event was really short, need to put title back in span
  4028. }
  4029. }
  4030. });
  4031. }
  4032. }
  4033. /* Agenda Event Segment Utilities
  4034. -----------------------------------------------------------------------------*/
  4035. // Sets the seg.backwardCoord and seg.forwardCoord on each segment and returns a new
  4036. // list in the order they should be placed into the DOM (an implicit z-index).
  4037. function placeSlotSegs(segs) {
  4038. var levels = buildSlotSegLevels(segs);
  4039. var level0 = levels[0];
  4040. var i;
  4041. computeForwardSlotSegs(levels);
  4042. if (level0) {
  4043. for (i=0; i<level0.length; i++) {
  4044. computeSlotSegPressures(level0[i]);
  4045. }
  4046. for (i=0; i<level0.length; i++) {
  4047. computeSlotSegCoords(level0[i], 0, 0);
  4048. }
  4049. }
  4050. return flattenSlotSegLevels(levels);
  4051. }
  4052. // Builds an array of segments "levels". The first level will be the leftmost tier of segments
  4053. // if the calendar is left-to-right, or the rightmost if the calendar is right-to-left.
  4054. function buildSlotSegLevels(segs) {
  4055. var levels = [];
  4056. var i, seg;
  4057. var j;
  4058. for (i=0; i<segs.length; i++) {
  4059. seg = segs[i];
  4060. // go through all the levels and stop on the first level where there are no collisions
  4061. for (j=0; j<levels.length; j++) {
  4062. if (!computeSlotSegCollisions(seg, levels[j]).length) {
  4063. break;
  4064. }
  4065. }
  4066. (levels[j] || (levels[j] = [])).push(seg);
  4067. }
  4068. return levels;
  4069. }
  4070. // For every segment, figure out the other segments that are in subsequent
  4071. // levels that also occupy the same vertical space. Accumulate in seg.forwardSegs
  4072. function computeForwardSlotSegs(levels) {
  4073. var i, level;
  4074. var j, seg;
  4075. var k;
  4076. for (i=0; i<levels.length; i++) {
  4077. level = levels[i];
  4078. for (j=0; j<level.length; j++) {
  4079. seg = level[j];
  4080. seg.forwardSegs = [];
  4081. for (k=i+1; k<levels.length; k++) {
  4082. computeSlotSegCollisions(seg, levels[k], seg.forwardSegs);
  4083. }
  4084. }
  4085. }
  4086. }
  4087. // Figure out which path forward (via seg.forwardSegs) results in the longest path until
  4088. // the furthest edge is reached. The number of segments in this path will be seg.forwardPressure
  4089. function computeSlotSegPressures(seg) {
  4090. var forwardSegs = seg.forwardSegs;
  4091. var forwardPressure = 0;
  4092. var i, forwardSeg;
  4093. if (seg.forwardPressure === undefined) { // not already computed
  4094. for (i=0; i<forwardSegs.length; i++) {
  4095. forwardSeg = forwardSegs[i];
  4096. // figure out the child's maximum forward path
  4097. computeSlotSegPressures(forwardSeg);
  4098. // either use the existing maximum, or use the child's forward pressure
  4099. // plus one (for the forwardSeg itself)
  4100. forwardPressure = Math.max(
  4101. forwardPressure,
  4102. 1 + forwardSeg.forwardPressure
  4103. );
  4104. }
  4105. seg.forwardPressure = forwardPressure;
  4106. }
  4107. }
  4108. // Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
  4109. // from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
  4110. // seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
  4111. //
  4112. // The segment might be part of a "series", which means consecutive segments with the same pressure
  4113. // who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
  4114. // segments behind this one in the current series, and `seriesBackwardCoord` is the starting
  4115. // coordinate of the first segment in the series.
  4116. function computeSlotSegCoords(seg, seriesBackwardPressure, seriesBackwardCoord) {
  4117. var forwardSegs = seg.forwardSegs;
  4118. var i;
  4119. if (seg.forwardCoord === undefined) { // not already computed
  4120. if (!forwardSegs.length) {
  4121. // if there are no forward segments, this segment should butt up against the edge
  4122. seg.forwardCoord = 1;
  4123. }
  4124. else {
  4125. // sort highest pressure first
  4126. forwardSegs.sort(compareForwardSlotSegs);
  4127. // this segment's forwardCoord will be calculated from the backwardCoord of the
  4128. // highest-pressure forward segment.
  4129. computeSlotSegCoords(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord);
  4130. seg.forwardCoord = forwardSegs[0].backwardCoord;
  4131. }
  4132. // calculate the backwardCoord from the forwardCoord. consider the series
  4133. seg.backwardCoord = seg.forwardCoord -
  4134. (seg.forwardCoord - seriesBackwardCoord) / // available width for series
  4135. (seriesBackwardPressure + 1); // # of segments in the series
  4136. // use this segment's coordinates to computed the coordinates of the less-pressurized
  4137. // forward segments
  4138. for (i=0; i<forwardSegs.length; i++) {
  4139. computeSlotSegCoords(forwardSegs[i], 0, seg.forwardCoord);
  4140. }
  4141. }
  4142. }
  4143. // Outputs a flat array of segments, from lowest to highest level
  4144. function flattenSlotSegLevels(levels) {
  4145. var segs = [];
  4146. var i, level;
  4147. var j;
  4148. for (i=0; i<levels.length; i++) {
  4149. level = levels[i];
  4150. for (j=0; j<level.length; j++) {
  4151. segs.push(level[j]);
  4152. }
  4153. }
  4154. return segs;
  4155. }
  4156. // Find all the segments in `otherSegs` that vertically collide with `seg`.
  4157. // Append into an optionally-supplied `results` array and return.
  4158. function computeSlotSegCollisions(seg, otherSegs, results) {
  4159. results = results || [];
  4160. for (var i=0; i<otherSegs.length; i++) {
  4161. if (isSlotSegCollision(seg, otherSegs[i])) {
  4162. results.push(otherSegs[i]);
  4163. }
  4164. }
  4165. return results;
  4166. }
  4167. // Do these segments occupy the same vertical space?
  4168. function isSlotSegCollision(seg1, seg2) {
  4169. return seg1.end > seg2.start && seg1.start < seg2.end;
  4170. }
  4171. // A cmp function for determining which forward segment to rely on more when computing coordinates.
  4172. function compareForwardSlotSegs(seg1, seg2) {
  4173. // put higher-pressure first
  4174. return seg2.forwardPressure - seg1.forwardPressure ||
  4175. // put segments that are closer to initial edge first (and favor ones with no coords yet)
  4176. (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) ||
  4177. // do normal sorting...
  4178. compareSlotSegs(seg1, seg2);
  4179. }
  4180. // A cmp function for determining which segment should be closer to the initial edge
  4181. // (the left edge on a left-to-right calendar).
  4182. function compareSlotSegs(seg1, seg2) {
  4183. return seg1.start - seg2.start || // earlier start time goes first
  4184. (seg2.end - seg2.start) - (seg1.end - seg1.start) || // tie? longer-duration goes first
  4185. (seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title
  4186. }
  4187. ;;
  4188. function View(element, calendar, viewName) {
  4189. var t = this;
  4190. // exports
  4191. t.element = element;
  4192. t.calendar = calendar;
  4193. t.name = viewName;
  4194. t.opt = opt;
  4195. t.trigger = trigger;
  4196. t.isEventDraggable = isEventDraggable;
  4197. t.isEventResizable = isEventResizable;
  4198. t.clearEventData = clearEventData;
  4199. t.reportEventElement = reportEventElement;
  4200. t.triggerEventDestroy = triggerEventDestroy;
  4201. t.eventElementHandlers = eventElementHandlers;
  4202. t.showEvents = showEvents;
  4203. t.hideEvents = hideEvents;
  4204. t.eventDrop = eventDrop;
  4205. t.eventResize = eventResize;
  4206. // t.start, t.end // moments with ambiguous-time
  4207. // t.intervalStart, t.intervalEnd // moments with ambiguous-time
  4208. // imports
  4209. var reportEventChange = calendar.reportEventChange;
  4210. // locals
  4211. var eventElementsByID = {}; // eventID mapped to array of jQuery elements
  4212. var eventElementCouples = []; // array of objects, { event, element } // TODO: unify with segment system
  4213. var options = calendar.options;
  4214. var nextDayThreshold = moment.duration(options.nextDayThreshold);
  4215. function opt(name, viewNameOverride) {
  4216. var v = options[name];
  4217. if ($.isPlainObject(v) && !isForcedAtomicOption(name)) {
  4218. return smartProperty(v, viewNameOverride || viewName);
  4219. }
  4220. return v;
  4221. }
  4222. function trigger(name, thisObj) {
  4223. return calendar.trigger.apply(
  4224. calendar,
  4225. [name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t])
  4226. );
  4227. }
  4228. /* Event Editable Boolean Calculations
  4229. ------------------------------------------------------------------------------*/
  4230. function isEventDraggable(event) {
  4231. var source = event.source || {};
  4232. return firstDefined(
  4233. event.startEditable,
  4234. source.startEditable,
  4235. opt('eventStartEditable'),
  4236. event.editable,
  4237. source.editable,
  4238. opt('editable')
  4239. );
  4240. }
  4241. function isEventResizable(event) { // but also need to make sure the seg.isEnd == true
  4242. var source = event.source || {};
  4243. return firstDefined(
  4244. event.durationEditable,
  4245. source.durationEditable,
  4246. opt('eventDurationEditable'),
  4247. event.editable,
  4248. source.editable,
  4249. opt('editable')
  4250. );
  4251. }
  4252. /* Event Data
  4253. ------------------------------------------------------------------------------*/
  4254. function clearEventData() {
  4255. eventElementsByID = {};
  4256. eventElementCouples = [];
  4257. }
  4258. /* Event Elements
  4259. ------------------------------------------------------------------------------*/
  4260. // report when view creates an element for an event
  4261. function reportEventElement(event, element) {
  4262. eventElementCouples.push({ event: event, element: element });
  4263. if (eventElementsByID[event._id]) {
  4264. eventElementsByID[event._id].push(element);
  4265. }else{
  4266. eventElementsByID[event._id] = [element];
  4267. }
  4268. }
  4269. function triggerEventDestroy() {
  4270. $.each(eventElementCouples, function(i, couple) {
  4271. t.trigger('eventDestroy', couple.event, couple.event, couple.element);
  4272. });
  4273. }
  4274. // attaches eventClick, eventMouseover, eventMouseout
  4275. function eventElementHandlers(event, eventElement) {
  4276. eventElement
  4277. .click(function(ev) {
  4278. if (!eventElement.hasClass('ui-draggable-dragging') &&
  4279. !eventElement.hasClass('ui-resizable-resizing')) {
  4280. return trigger('eventClick', this, event, ev);
  4281. }
  4282. })
  4283. .hover(
  4284. function(ev) {
  4285. trigger('eventMouseover', this, event, ev);
  4286. },
  4287. function(ev) {
  4288. trigger('eventMouseout', this, event, ev);
  4289. }
  4290. );
  4291. // TODO: don't fire eventMouseover/eventMouseout *while* dragging is occuring (on subject element)
  4292. // TODO: same for resizing
  4293. }
  4294. function showEvents(event, exceptElement) {
  4295. eachEventElement(event, exceptElement, 'show');
  4296. }
  4297. function hideEvents(event, exceptElement) {
  4298. eachEventElement(event, exceptElement, 'hide');
  4299. }
  4300. function eachEventElement(event, exceptElement, funcName) {
  4301. // NOTE: there may be multiple events per ID (repeating events)
  4302. // and multiple segments per event
  4303. var elements = eventElementsByID[event._id],
  4304. i, len = elements.length;
  4305. for (i=0; i<len; i++) {
  4306. if (!exceptElement || elements[i][0] != exceptElement[0]) {
  4307. elements[i][funcName]();
  4308. }
  4309. }
  4310. }
  4311. /* Event Modification Reporting
  4312. ---------------------------------------------------------------------------------*/
  4313. function eventDrop(el, event, newStart, ev, ui) {
  4314. var undoMutation = calendar.mutateEvent(event, newStart, null);
  4315. trigger(
  4316. 'eventDrop',
  4317. el,
  4318. event,
  4319. function() {
  4320. undoMutation();
  4321. reportEventChange(event._id);
  4322. },
  4323. ev,
  4324. ui
  4325. );
  4326. reportEventChange(event._id);
  4327. }
  4328. function eventResize(el, event, newEnd, ev, ui) {
  4329. var undoMutation = calendar.mutateEvent(event, null, newEnd);
  4330. trigger(
  4331. 'eventResize',
  4332. el,
  4333. event,
  4334. function() {
  4335. undoMutation();
  4336. reportEventChange(event._id);
  4337. },
  4338. ev,
  4339. ui
  4340. );
  4341. reportEventChange(event._id);
  4342. }
  4343. // ====================================================================================================
  4344. // Utilities for day "cells"
  4345. // ====================================================================================================
  4346. // The "basic" views are completely made up of day cells.
  4347. // The "agenda" views have day cells at the top "all day" slot.
  4348. // This was the obvious common place to put these utilities, but they should be abstracted out into
  4349. // a more meaningful class (like DayEventRenderer).
  4350. // ====================================================================================================
  4351. // For determining how a given "cell" translates into a "date":
  4352. //
  4353. // 1. Convert the "cell" (row and column) into a "cell offset" (the # of the cell, cronologically from the first).
  4354. // Keep in mind that column indices are inverted with isRTL. This is taken into account.
  4355. //
  4356. // 2. Convert the "cell offset" to a "day offset" (the # of days since the first visible day in the view).
  4357. //
  4358. // 3. Convert the "day offset" into a "date" (a Moment).
  4359. //
  4360. // The reverse transformation happens when transforming a date into a cell.
  4361. // exports
  4362. t.isHiddenDay = isHiddenDay;
  4363. t.skipHiddenDays = skipHiddenDays;
  4364. t.getCellsPerWeek = getCellsPerWeek;
  4365. t.dateToCell = dateToCell;
  4366. t.dateToDayOffset = dateToDayOffset;
  4367. t.dayOffsetToCellOffset = dayOffsetToCellOffset;
  4368. t.cellOffsetToCell = cellOffsetToCell;
  4369. t.cellToDate = cellToDate;
  4370. t.cellToCellOffset = cellToCellOffset;
  4371. t.cellOffsetToDayOffset = cellOffsetToDayOffset;
  4372. t.dayOffsetToDate = dayOffsetToDate;
  4373. t.rangeToSegments = rangeToSegments;
  4374. // internals
  4375. var hiddenDays = opt('hiddenDays') || []; // array of day-of-week indices that are hidden
  4376. var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
  4377. var cellsPerWeek;
  4378. var dayToCellMap = []; // hash from dayIndex -> cellIndex, for one week
  4379. var cellToDayMap = []; // hash from cellIndex -> dayIndex, for one week
  4380. var isRTL = opt('isRTL');
  4381. // initialize important internal variables
  4382. (function() {
  4383. if (opt('weekends') === false) {
  4384. hiddenDays.push(0, 6); // 0=sunday, 6=saturday
  4385. }
  4386. // Loop through a hypothetical week and determine which
  4387. // days-of-week are hidden. Record in both hashes (one is the reverse of the other).
  4388. for (var dayIndex=0, cellIndex=0; dayIndex<7; dayIndex++) {
  4389. dayToCellMap[dayIndex] = cellIndex;
  4390. isHiddenDayHash[dayIndex] = $.inArray(dayIndex, hiddenDays) != -1;
  4391. if (!isHiddenDayHash[dayIndex]) {
  4392. cellToDayMap[cellIndex] = dayIndex;
  4393. cellIndex++;
  4394. }
  4395. }
  4396. cellsPerWeek = cellIndex;
  4397. if (!cellsPerWeek) {
  4398. throw 'invalid hiddenDays'; // all days were hidden? bad.
  4399. }
  4400. })();
  4401. // Is the current day hidden?
  4402. // `day` is a day-of-week index (0-6), or a Moment
  4403. function isHiddenDay(day) {
  4404. if (moment.isMoment(day)) {
  4405. day = day.day();
  4406. }
  4407. return isHiddenDayHash[day];
  4408. }
  4409. function getCellsPerWeek() {
  4410. return cellsPerWeek;
  4411. }
  4412. // Incrementing the current day until it is no longer a hidden day, returning a copy.
  4413. // If the initial value of `date` is not a hidden day, don't do anything.
  4414. // Pass `isExclusive` as `true` if you are dealing with an end date.
  4415. // `inc` defaults to `1` (increment one day forward each time)
  4416. function skipHiddenDays(date, inc, isExclusive) {
  4417. var out = date.clone();
  4418. inc = inc || 1;
  4419. while (
  4420. isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7]
  4421. ) {
  4422. out.add('days', inc);
  4423. }
  4424. return out;
  4425. }
  4426. //
  4427. // TRANSFORMATIONS: cell -> cell offset -> day offset -> date
  4428. //
  4429. // cell -> date (combines all transformations)
  4430. // Possible arguments:
  4431. // - row, col
  4432. // - { row:#, col: # }
  4433. function cellToDate() {
  4434. var cellOffset = cellToCellOffset.apply(null, arguments);
  4435. var dayOffset = cellOffsetToDayOffset(cellOffset);
  4436. var date = dayOffsetToDate(dayOffset);
  4437. return date;
  4438. }
  4439. // cell -> cell offset
  4440. // Possible arguments:
  4441. // - row, col
  4442. // - { row:#, col:# }
  4443. function cellToCellOffset(row, col) {
  4444. var colCnt = t.getColCnt();
  4445. // rtl variables. wish we could pre-populate these. but where?
  4446. var dis = isRTL ? -1 : 1;
  4447. var dit = isRTL ? colCnt - 1 : 0;
  4448. if (typeof row == 'object') {
  4449. col = row.col;
  4450. row = row.row;
  4451. }
  4452. var cellOffset = row * colCnt + (col * dis + dit); // column, adjusted for RTL (dis & dit)
  4453. return cellOffset;
  4454. }
  4455. // cell offset -> day offset
  4456. function cellOffsetToDayOffset(cellOffset) {
  4457. var day0 = t.start.day(); // first date's day of week
  4458. cellOffset += dayToCellMap[day0]; // normlize cellOffset to beginning-of-week
  4459. return Math.floor(cellOffset / cellsPerWeek) * 7 + // # of days from full weeks
  4460. cellToDayMap[ // # of days from partial last week
  4461. (cellOffset % cellsPerWeek + cellsPerWeek) % cellsPerWeek // crazy math to handle negative cellOffsets
  4462. ] -
  4463. day0; // adjustment for beginning-of-week normalization
  4464. }
  4465. // day offset -> date
  4466. function dayOffsetToDate(dayOffset) {
  4467. return t.start.clone().add('days', dayOffset);
  4468. }
  4469. //
  4470. // TRANSFORMATIONS: date -> day offset -> cell offset -> cell
  4471. //
  4472. // date -> cell (combines all transformations)
  4473. function dateToCell(date) {
  4474. var dayOffset = dateToDayOffset(date);
  4475. var cellOffset = dayOffsetToCellOffset(dayOffset);
  4476. var cell = cellOffsetToCell(cellOffset);
  4477. return cell;
  4478. }
  4479. // date -> day offset
  4480. function dateToDayOffset(date) {
  4481. return date.clone().stripTime().diff(t.start, 'days');
  4482. }
  4483. // day offset -> cell offset
  4484. function dayOffsetToCellOffset(dayOffset) {
  4485. var day0 = t.start.day(); // first date's day of week
  4486. dayOffset += day0; // normalize dayOffset to beginning-of-week
  4487. return Math.floor(dayOffset / 7) * cellsPerWeek + // # of cells from full weeks
  4488. dayToCellMap[ // # of cells from partial last week
  4489. (dayOffset % 7 + 7) % 7 // crazy math to handle negative dayOffsets
  4490. ] -
  4491. dayToCellMap[day0]; // adjustment for beginning-of-week normalization
  4492. }
  4493. // cell offset -> cell (object with row & col keys)
  4494. function cellOffsetToCell(cellOffset) {
  4495. var colCnt = t.getColCnt();
  4496. // rtl variables. wish we could pre-populate these. but where?
  4497. var dis = isRTL ? -1 : 1;
  4498. var dit = isRTL ? colCnt - 1 : 0;
  4499. var row = Math.floor(cellOffset / colCnt);
  4500. var col = ((cellOffset % colCnt + colCnt) % colCnt) * dis + dit; // column, adjusted for RTL (dis & dit)
  4501. return {
  4502. row: row,
  4503. col: col
  4504. };
  4505. }
  4506. //
  4507. // Converts a date range into an array of segment objects.
  4508. // "Segments" are horizontal stretches of time, sliced up by row.
  4509. // A segment object has the following properties:
  4510. // - row
  4511. // - cols
  4512. // - isStart
  4513. // - isEnd
  4514. //
  4515. function rangeToSegments(start, end) {
  4516. var rowCnt = t.getRowCnt();
  4517. var colCnt = t.getColCnt();
  4518. var segments = []; // array of segments to return
  4519. // day offset for given date range
  4520. var rangeDayOffsetStart = dateToDayOffset(start);
  4521. var rangeDayOffsetEnd = dateToDayOffset(end); // an exclusive value
  4522. var endTimeMS = +end.time();
  4523. if (endTimeMS && endTimeMS >= nextDayThreshold) {
  4524. rangeDayOffsetEnd++;
  4525. }
  4526. rangeDayOffsetEnd = Math.max(rangeDayOffsetEnd, rangeDayOffsetStart + 1);
  4527. // first and last cell offset for the given date range
  4528. // "last" implies inclusivity
  4529. var rangeCellOffsetFirst = dayOffsetToCellOffset(rangeDayOffsetStart);
  4530. var rangeCellOffsetLast = dayOffsetToCellOffset(rangeDayOffsetEnd) - 1;
  4531. // loop through all the rows in the view
  4532. for (var row=0; row<rowCnt; row++) {
  4533. // first and last cell offset for the row
  4534. var rowCellOffsetFirst = row * colCnt;
  4535. var rowCellOffsetLast = rowCellOffsetFirst + colCnt - 1;
  4536. // get the segment's cell offsets by constraining the range's cell offsets to the bounds of the row
  4537. var segmentCellOffsetFirst = Math.max(rangeCellOffsetFirst, rowCellOffsetFirst);
  4538. var segmentCellOffsetLast = Math.min(rangeCellOffsetLast, rowCellOffsetLast);
  4539. // make sure segment's offsets are valid and in view
  4540. if (segmentCellOffsetFirst <= segmentCellOffsetLast) {
  4541. // translate to cells
  4542. var segmentCellFirst = cellOffsetToCell(segmentCellOffsetFirst);
  4543. var segmentCellLast = cellOffsetToCell(segmentCellOffsetLast);
  4544. // view might be RTL, so order by leftmost column
  4545. var cols = [ segmentCellFirst.col, segmentCellLast.col ].sort();
  4546. // Determine if segment's first/last cell is the beginning/end of the date range.
  4547. // We need to compare "day offset" because "cell offsets" are often ambiguous and
  4548. // can translate to multiple days, and an edge case reveals itself when we the
  4549. // range's first cell is hidden (we don't want isStart to be true).
  4550. var isStart = cellOffsetToDayOffset(segmentCellOffsetFirst) == rangeDayOffsetStart;
  4551. var isEnd = cellOffsetToDayOffset(segmentCellOffsetLast) + 1 == rangeDayOffsetEnd; // +1 for comparing exclusively
  4552. segments.push({
  4553. row: row,
  4554. leftCol: cols[0],
  4555. rightCol: cols[1],
  4556. isStart: isStart,
  4557. isEnd: isEnd
  4558. });
  4559. }
  4560. }
  4561. return segments;
  4562. }
  4563. }
  4564. ;;
  4565. function DayEventRenderer() {
  4566. var t = this;
  4567. // exports
  4568. t.renderDayEvents = renderDayEvents;
  4569. t.draggableDayEvent = draggableDayEvent; // made public so that subclasses can override
  4570. t.resizableDayEvent = resizableDayEvent; // "
  4571. // imports
  4572. var opt = t.opt;
  4573. var trigger = t.trigger;
  4574. var isEventDraggable = t.isEventDraggable;
  4575. var isEventResizable = t.isEventResizable;
  4576. var reportEventElement = t.reportEventElement;
  4577. var eventElementHandlers = t.eventElementHandlers;
  4578. var showEvents = t.showEvents;
  4579. var hideEvents = t.hideEvents;
  4580. var eventDrop = t.eventDrop;
  4581. var eventResize = t.eventResize;
  4582. var getRowCnt = t.getRowCnt;
  4583. var getColCnt = t.getColCnt;
  4584. var allDayRow = t.allDayRow; // TODO: rename
  4585. var colLeft = t.colLeft;
  4586. var colRight = t.colRight;
  4587. var colContentLeft = t.colContentLeft;
  4588. var colContentRight = t.colContentRight;
  4589. var getDaySegmentContainer = t.getDaySegmentContainer;
  4590. var renderDayOverlay = t.renderDayOverlay;
  4591. var clearOverlays = t.clearOverlays;
  4592. var clearSelection = t.clearSelection;
  4593. var getHoverListener = t.getHoverListener;
  4594. var rangeToSegments = t.rangeToSegments;
  4595. var cellToDate = t.cellToDate;
  4596. var cellToCellOffset = t.cellToCellOffset;
  4597. var cellOffsetToDayOffset = t.cellOffsetToDayOffset;
  4598. var dateToDayOffset = t.dateToDayOffset;
  4599. var dayOffsetToCellOffset = t.dayOffsetToCellOffset;
  4600. var calendar = t.calendar;
  4601. var getEventEnd = calendar.getEventEnd;
  4602. var formatDate = calendar.formatDate;
  4603. // Render `events` onto the calendar, attach mouse event handlers, and call the `eventAfterRender` callback for each.
  4604. // Mouse event will be lazily applied, except if the event has an ID of `modifiedEventId`.
  4605. // Can only be called when the event container is empty (because it wipes out all innerHTML).
  4606. function renderDayEvents(events, modifiedEventId) {
  4607. // do the actual rendering. Receive the intermediate "segment" data structures.
  4608. var segments = _renderDayEvents(
  4609. events,
  4610. false, // don't append event elements
  4611. true // set the heights of the rows
  4612. );
  4613. // report the elements to the View, for general drag/resize utilities
  4614. segmentElementEach(segments, function(segment, element) {
  4615. reportEventElement(segment.event, element);
  4616. });
  4617. // attach mouse handlers
  4618. attachHandlers(segments, modifiedEventId);
  4619. // call `eventAfterRender` callback for each event
  4620. segmentElementEach(segments, function(segment, element) {
  4621. trigger('eventAfterRender', segment.event, segment.event, element);
  4622. });
  4623. }
  4624. // Render an event on the calendar, but don't report them anywhere, and don't attach mouse handlers.
  4625. // Append this event element to the event container, which might already be populated with events.
  4626. // If an event's segment will have row equal to `adjustRow`, then explicitly set its top coordinate to `adjustTop`.
  4627. // This hack is used to maintain continuity when user is manually resizing an event.
  4628. // Returns an array of DOM elements for the event.
  4629. function renderTempDayEvent(event, adjustRow, adjustTop) {
  4630. // actually render the event. `true` for appending element to container.
  4631. // Recieve the intermediate "segment" data structures.
  4632. var segments = _renderDayEvents(
  4633. [ event ],
  4634. true, // append event elements
  4635. false // don't set the heights of the rows
  4636. );
  4637. var elements = [];
  4638. // Adjust certain elements' top coordinates
  4639. segmentElementEach(segments, function(segment, element) {
  4640. if (segment.row === adjustRow) {
  4641. element.css('top', adjustTop);
  4642. }
  4643. elements.push(element[0]); // accumulate DOM nodes
  4644. });
  4645. return elements;
  4646. }
  4647. // Render events onto the calendar. Only responsible for the VISUAL aspect.
  4648. // Not responsible for attaching handlers or calling callbacks.
  4649. // Set `doAppend` to `true` for rendering elements without clearing the existing container.
  4650. // Set `doRowHeights` to allow setting the height of each row, to compensate for vertical event overflow.
  4651. function _renderDayEvents(events, doAppend, doRowHeights) {
  4652. // where the DOM nodes will eventually end up
  4653. var finalContainer = getDaySegmentContainer();
  4654. // the container where the initial HTML will be rendered.
  4655. // If `doAppend`==true, uses a temporary container.
  4656. var renderContainer = doAppend ? $("<div/>") : finalContainer;
  4657. var segments = buildSegments(events);
  4658. var html;
  4659. var elements;
  4660. // calculate the desired `left` and `width` properties on each segment object
  4661. calculateHorizontals(segments);
  4662. // build the HTML string. relies on `left` property
  4663. html = buildHTML(segments);
  4664. // render the HTML. innerHTML is considerably faster than jQuery's .html()
  4665. renderContainer[0].innerHTML = html;
  4666. // retrieve the individual elements
  4667. elements = renderContainer.children();
  4668. // if we were appending, and thus using a temporary container,
  4669. // re-attach elements to the real container.
  4670. if (doAppend) {
  4671. finalContainer.append(elements);
  4672. }
  4673. // assigns each element to `segment.event`, after filtering them through user callbacks
  4674. resolveElements(segments, elements);
  4675. // Calculate the left and right padding+margin for each element.
  4676. // We need this for setting each element's desired outer width, because of the W3C box model.
  4677. // It's important we do this in a separate pass from acually setting the width on the DOM elements
  4678. // because alternating reading/writing dimensions causes reflow for every iteration.
  4679. segmentElementEach(segments, function(segment, element) {
  4680. segment.hsides = hsides(element, true); // include margins = `true`
  4681. });
  4682. // Set the width of each element
  4683. segmentElementEach(segments, function(segment, element) {
  4684. element.width(
  4685. Math.max(0, segment.outerWidth - segment.hsides)
  4686. );
  4687. });
  4688. // Grab each element's outerHeight (setVerticals uses this).
  4689. // To get an accurate reading, it's important to have each element's width explicitly set already.
  4690. segmentElementEach(segments, function(segment, element) {
  4691. segment.outerHeight = element.outerHeight(true); // include margins = `true`
  4692. });
  4693. // Set the top coordinate on each element (requires segment.outerHeight)
  4694. setVerticals(segments, doRowHeights);
  4695. return segments;
  4696. }
  4697. // Generate an array of "segments" for all events.
  4698. function buildSegments(events) {
  4699. var segments = [];
  4700. for (var i=0; i<events.length; i++) {
  4701. var eventSegments = buildSegmentsForEvent(events[i]);
  4702. segments.push.apply(segments, eventSegments); // append an array to an array
  4703. }
  4704. return segments;
  4705. }
  4706. // Generate an array of segments for a single event.
  4707. // A "segment" is the same data structure that View.rangeToSegments produces,
  4708. // with the addition of the `event` property being set to reference the original event.
  4709. function buildSegmentsForEvent(event) {
  4710. var segments = rangeToSegments(event.start, getEventEnd(event));
  4711. for (var i=0; i<segments.length; i++) {
  4712. segments[i].event = event;
  4713. }
  4714. return segments;
  4715. }
  4716. // Sets the `left` and `outerWidth` property of each segment.
  4717. // These values are the desired dimensions for the eventual DOM elements.
  4718. function calculateHorizontals(segments) {
  4719. var isRTL = opt('isRTL');
  4720. for (var i=0; i<segments.length; i++) {
  4721. var segment = segments[i];
  4722. // Determine functions used for calulating the elements left/right coordinates,
  4723. // depending on whether the view is RTL or not.
  4724. // NOTE:
  4725. // colLeft/colRight returns the coordinate butting up the edge of the cell.
  4726. // colContentLeft/colContentRight is indented a little bit from the edge.
  4727. var leftFunc = (isRTL ? segment.isEnd : segment.isStart) ? colContentLeft : colLeft;
  4728. var rightFunc = (isRTL ? segment.isStart : segment.isEnd) ? colContentRight : colRight;
  4729. var left = leftFunc(segment.leftCol);
  4730. var right = rightFunc(segment.rightCol);
  4731. segment.left = left;
  4732. segment.outerWidth = right - left;
  4733. }
  4734. }
  4735. // Build a concatenated HTML string for an array of segments
  4736. function buildHTML(segments) {
  4737. var html = '';
  4738. for (var i=0; i<segments.length; i++) {
  4739. html += buildHTMLForSegment(segments[i]);
  4740. }
  4741. return html;
  4742. }
  4743. // Build an HTML string for a single segment.
  4744. // Relies on the following properties:
  4745. // - `segment.event` (from `buildSegmentsForEvent`)
  4746. // - `segment.left` (from `calculateHorizontals`)
  4747. function buildHTMLForSegment(segment) {
  4748. var html = '';
  4749. var isRTL = opt('isRTL');
  4750. var event = segment.event;
  4751. var url = event.url;
  4752. // generate the list of CSS classNames
  4753. var classNames = [ 'fc-event', 'fc-event-hori' ];
  4754. if (isEventDraggable(event)) {
  4755. classNames.push('fc-event-draggable');
  4756. }
  4757. if (segment.isStart) {
  4758. classNames.push('fc-event-start');
  4759. }
  4760. if (segment.isEnd) {
  4761. classNames.push('fc-event-end');
  4762. }
  4763. // use the event's configured classNames
  4764. // guaranteed to be an array via `buildEvent`
  4765. classNames = classNames.concat(event.className);
  4766. if (event.source) {
  4767. // use the event's source's classNames, if specified
  4768. classNames = classNames.concat(event.source.className || []);
  4769. }
  4770. // generate a semicolon delimited CSS string for any of the "skin" properties
  4771. // of the event object (`backgroundColor`, `borderColor` and such)
  4772. var skinCss = getSkinCss(event, opt);
  4773. if (url) {
  4774. html += "<a href='" + htmlEscape(url) + "'";
  4775. }else{
  4776. html += "<div";
  4777. }
  4778. html +=
  4779. " class='" + classNames.join(' ') + "'" +
  4780. " style=" +
  4781. "'" +
  4782. "position:absolute;" +
  4783. "left:" + segment.left + "px;" +
  4784. skinCss +
  4785. "'" +
  4786. ">" +
  4787. "<div class='fc-event-inner'>";
  4788. if (!event.allDay && segment.isStart) {
  4789. html +=
  4790. "<span class='fc-event-time'>" +
  4791. htmlEscape(
  4792. formatDate(event.start, opt('timeFormat'))
  4793. ) +
  4794. "</span>";
  4795. }
  4796. html +=
  4797. "<span class='fc-event-title'>" +
  4798. htmlEscape(event.title || '') +
  4799. "</span>" +
  4800. "</div>";
  4801. if (event.allDay && segment.isEnd && isEventResizable(event)) {
  4802. html +=
  4803. "<div class='ui-resizable-handle ui-resizable-" + (isRTL ? 'w' : 'e') + "'>" +
  4804. "&nbsp;&nbsp;&nbsp;" + // makes hit area a lot better for IE6/7
  4805. "</div>";
  4806. }
  4807. html += "</" + (url ? "a" : "div") + ">";
  4808. // TODO:
  4809. // When these elements are initially rendered, they will be briefly visibile on the screen,
  4810. // even though their widths/heights are not set.
  4811. // SOLUTION: initially set them as visibility:hidden ?
  4812. return html;
  4813. }
  4814. // Associate each segment (an object) with an element (a jQuery object),
  4815. // by setting each `segment.element`.
  4816. // Run each element through the `eventRender` filter, which allows developers to
  4817. // modify an existing element, supply a new one, or cancel rendering.
  4818. function resolveElements(segments, elements) {
  4819. for (var i=0; i<segments.length; i++) {
  4820. var segment = segments[i];
  4821. var event = segment.event;
  4822. var element = elements.eq(i);
  4823. // call the trigger with the original element
  4824. var triggerRes = trigger('eventRender', event, event, element);
  4825. if (triggerRes === false) {
  4826. // if `false`, remove the event from the DOM and don't assign it to `segment.event`
  4827. element.remove();
  4828. }
  4829. else {
  4830. if (triggerRes && triggerRes !== true) {
  4831. // the trigger returned a new element, but not `true` (which means keep the existing element)
  4832. // re-assign the important CSS dimension properties that were already assigned in `buildHTMLForSegment`
  4833. triggerRes = $(triggerRes)
  4834. .css({
  4835. position: 'absolute',
  4836. left: segment.left
  4837. });
  4838. element.replaceWith(triggerRes);
  4839. element = triggerRes;
  4840. }
  4841. segment.element = element;
  4842. }
  4843. }
  4844. }
  4845. /* Top-coordinate Methods
  4846. -------------------------------------------------------------------------------------------------*/
  4847. // Sets the "top" CSS property for each element.
  4848. // If `doRowHeights` is `true`, also sets each row's first cell to an explicit height,
  4849. // so that if elements vertically overflow, the cell expands vertically to compensate.
  4850. function setVerticals(segments, doRowHeights) {
  4851. var rowContentHeights = calculateVerticals(segments); // also sets segment.top
  4852. var rowContentElements = getRowContentElements(); // returns 1 inner div per row
  4853. var rowContentTops = [];
  4854. var i;
  4855. // Set each row's height by setting height of first inner div
  4856. if (doRowHeights) {
  4857. for (i=0; i<rowContentElements.length; i++) {
  4858. rowContentElements[i].height(rowContentHeights[i]);
  4859. }
  4860. }
  4861. // Get each row's top, relative to the views's origin.
  4862. // Important to do this after setting each row's height.
  4863. for (i=0; i<rowContentElements.length; i++) {
  4864. rowContentTops.push(
  4865. rowContentElements[i].position().top
  4866. );
  4867. }
  4868. // Set each segment element's CSS "top" property.
  4869. // Each segment object has a "top" property, which is relative to the row's top, but...
  4870. segmentElementEach(segments, function(segment, element) {
  4871. element.css(
  4872. 'top',
  4873. rowContentTops[segment.row] + segment.top // ...now, relative to views's origin
  4874. );
  4875. });
  4876. }
  4877. // Calculate the "top" coordinate for each segment, relative to the "top" of the row.
  4878. // Also, return an array that contains the "content" height for each row
  4879. // (the height displaced by the vertically stacked events in the row).
  4880. // Requires segments to have their `outerHeight` property already set.
  4881. function calculateVerticals(segments) {
  4882. var rowCnt = getRowCnt();
  4883. var colCnt = getColCnt();
  4884. var rowContentHeights = []; // content height for each row
  4885. var segmentRows = buildSegmentRows(segments); // an array of segment arrays, one for each row
  4886. var colI;
  4887. for (var rowI=0; rowI<rowCnt; rowI++) {
  4888. var segmentRow = segmentRows[rowI];
  4889. // an array of running total heights for each column.
  4890. // initialize with all zeros.
  4891. var colHeights = [];
  4892. for (colI=0; colI<colCnt; colI++) {
  4893. colHeights.push(0);
  4894. }
  4895. // loop through every segment
  4896. for (var segmentI=0; segmentI<segmentRow.length; segmentI++) {
  4897. var segment = segmentRow[segmentI];
  4898. // find the segment's top coordinate by looking at the max height
  4899. // of all the columns the segment will be in.
  4900. segment.top = arrayMax(
  4901. colHeights.slice(
  4902. segment.leftCol,
  4903. segment.rightCol + 1 // make exclusive for slice
  4904. )
  4905. );
  4906. // adjust the columns to account for the segment's height
  4907. for (colI=segment.leftCol; colI<=segment.rightCol; colI++) {
  4908. colHeights[colI] = segment.top + segment.outerHeight;
  4909. }
  4910. }
  4911. // the tallest column in the row should be the "content height"
  4912. rowContentHeights.push(arrayMax(colHeights));
  4913. }
  4914. return rowContentHeights;
  4915. }
  4916. // Build an array of segment arrays, each representing the segments that will
  4917. // be in a row of the grid, sorted by which event should be closest to the top.
  4918. function buildSegmentRows(segments) {
  4919. var rowCnt = getRowCnt();
  4920. var segmentRows = [];
  4921. var segmentI;
  4922. var segment;
  4923. var rowI;
  4924. // group segments by row
  4925. for (segmentI=0; segmentI<segments.length; segmentI++) {
  4926. segment = segments[segmentI];
  4927. rowI = segment.row;
  4928. if (segment.element) { // was rendered?
  4929. if (segmentRows[rowI]) {
  4930. // already other segments. append to array
  4931. segmentRows[rowI].push(segment);
  4932. }
  4933. else {
  4934. // first segment in row. create new array
  4935. segmentRows[rowI] = [ segment ];
  4936. }
  4937. }
  4938. }
  4939. // sort each row
  4940. for (rowI=0; rowI<rowCnt; rowI++) {
  4941. segmentRows[rowI] = sortSegmentRow(
  4942. segmentRows[rowI] || [] // guarantee an array, even if no segments
  4943. );
  4944. }
  4945. return segmentRows;
  4946. }
  4947. // Sort an array of segments according to which segment should appear closest to the top
  4948. function sortSegmentRow(segments) {
  4949. var sortedSegments = [];
  4950. // build the subrow array
  4951. var subrows = buildSegmentSubrows(segments);
  4952. // flatten it
  4953. for (var i=0; i<subrows.length; i++) {
  4954. sortedSegments.push.apply(sortedSegments, subrows[i]); // append an array to an array
  4955. }
  4956. return sortedSegments;
  4957. }
  4958. // Take an array of segments, which are all assumed to be in the same row,
  4959. // and sort into subrows.
  4960. function buildSegmentSubrows(segments) {
  4961. // Give preference to elements with certain criteria, so they have
  4962. // a chance to be closer to the top.
  4963. segments.sort(compareDaySegments);
  4964. var subrows = [];
  4965. for (var i=0; i<segments.length; i++) {
  4966. var segment = segments[i];
  4967. // loop through subrows, starting with the topmost, until the segment
  4968. // doesn't collide with other segments.
  4969. for (var j=0; j<subrows.length; j++) {
  4970. if (!isDaySegmentCollision(segment, subrows[j])) {
  4971. break;
  4972. }
  4973. }
  4974. // `j` now holds the desired subrow index
  4975. if (subrows[j]) {
  4976. subrows[j].push(segment);
  4977. }
  4978. else {
  4979. subrows[j] = [ segment ];
  4980. }
  4981. }
  4982. return subrows;
  4983. }
  4984. // Return an array of jQuery objects for the placeholder content containers of each row.
  4985. // The content containers don't actually contain anything, but their dimensions should match
  4986. // the events that are overlaid on top.
  4987. function getRowContentElements() {
  4988. var i;
  4989. var rowCnt = getRowCnt();
  4990. var rowDivs = [];
  4991. for (i=0; i<rowCnt; i++) {
  4992. rowDivs[i] = allDayRow(i)
  4993. .find('div.fc-day-content > div');
  4994. }
  4995. return rowDivs;
  4996. }
  4997. /* Mouse Handlers
  4998. ---------------------------------------------------------------------------------------------------*/
  4999. // TODO: better documentation!
  5000. function attachHandlers(segments, modifiedEventId) {
  5001. var segmentContainer = getDaySegmentContainer();
  5002. segmentElementEach(segments, function(segment, element, i) {
  5003. var event = segment.event;
  5004. if (event._id === modifiedEventId) {
  5005. bindDaySeg(event, element, segment);
  5006. }else{
  5007. element[0]._fci = i; // for lazySegBind
  5008. }
  5009. });
  5010. lazySegBind(segmentContainer, segments, bindDaySeg);
  5011. }
  5012. function bindDaySeg(event, eventElement, segment) {
  5013. if (isEventDraggable(event)) {
  5014. t.draggableDayEvent(event, eventElement, segment); // use `t` so subclasses can override
  5015. }
  5016. if (
  5017. event.allDay &&
  5018. segment.isEnd && // only allow resizing on the final segment for an event
  5019. isEventResizable(event)
  5020. ) {
  5021. t.resizableDayEvent(event, eventElement, segment); // use `t` so subclasses can override
  5022. }
  5023. // attach all other handlers.
  5024. // needs to be after, because resizableDayEvent might stopImmediatePropagation on click
  5025. eventElementHandlers(event, eventElement);
  5026. }
  5027. function draggableDayEvent(event, eventElement) {
  5028. var hoverListener = getHoverListener();
  5029. var dayDelta;
  5030. var eventStart;
  5031. eventElement.draggable({
  5032. delay: 50,
  5033. opacity: opt('dragOpacity'),
  5034. revertDuration: opt('dragRevertDuration'),
  5035. start: function(ev, ui) {
  5036. trigger('eventDragStart', eventElement, event, ev, ui);
  5037. hideEvents(event, eventElement);
  5038. hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
  5039. eventElement.draggable('option', 'revert', !cell || !rowDelta && !colDelta);
  5040. clearOverlays();
  5041. if (cell) {
  5042. var origCellDate = cellToDate(origCell);
  5043. var cellDate = cellToDate(cell);
  5044. dayDelta = cellDate.diff(origCellDate, 'days');
  5045. eventStart = event.start.clone().add('days', dayDelta);
  5046. renderDayOverlay(
  5047. eventStart,
  5048. getEventEnd(event).add('days', dayDelta)
  5049. );
  5050. }
  5051. else {
  5052. dayDelta = 0;
  5053. }
  5054. }, ev, 'drag');
  5055. },
  5056. stop: function(ev, ui) {
  5057. hoverListener.stop();
  5058. clearOverlays();
  5059. trigger('eventDragStop', eventElement, event, ev, ui);
  5060. if (dayDelta) {
  5061. eventDrop(
  5062. this, // el
  5063. event,
  5064. eventStart,
  5065. ev,
  5066. ui
  5067. );
  5068. }
  5069. else {
  5070. eventElement.css('filter', ''); // clear IE opacity side-effects
  5071. showEvents(event, eventElement);
  5072. }
  5073. }
  5074. });
  5075. }
  5076. function resizableDayEvent(event, element, segment) {
  5077. var isRTL = opt('isRTL');
  5078. var direction = isRTL ? 'w' : 'e';
  5079. var handle = element.find('.ui-resizable-' + direction); // TODO: stop using this class because we aren't using jqui for this
  5080. var isResizing = false;
  5081. // TODO: look into using jquery-ui mouse widget for this stuff
  5082. disableTextSelection(element); // prevent native <a> selection for IE
  5083. element
  5084. .mousedown(function(ev) { // prevent native <a> selection for others
  5085. ev.preventDefault();
  5086. })
  5087. .click(function(ev) {
  5088. if (isResizing) {
  5089. ev.preventDefault(); // prevent link from being visited (only method that worked in IE6)
  5090. ev.stopImmediatePropagation(); // prevent fullcalendar eventClick handler from being called
  5091. // (eventElementHandlers needs to be bound after resizableDayEvent)
  5092. }
  5093. });
  5094. handle.mousedown(function(ev) {
  5095. if (ev.which != 1) {
  5096. return; // needs to be left mouse button
  5097. }
  5098. isResizing = true;
  5099. var hoverListener = getHoverListener();
  5100. var elementTop = element.css('top');
  5101. var dayDelta;
  5102. var eventEnd;
  5103. var helpers;
  5104. var eventCopy = $.extend({}, event);
  5105. var minCellOffset = dayOffsetToCellOffset(dateToDayOffset(event.start));
  5106. clearSelection();
  5107. $('body')
  5108. .css('cursor', direction + '-resize')
  5109. .one('mouseup', mouseup);
  5110. trigger('eventResizeStart', this, event, ev);
  5111. hoverListener.start(function(cell, origCell) {
  5112. if (cell) {
  5113. var origCellOffset = cellToCellOffset(origCell);
  5114. var cellOffset = cellToCellOffset(cell);
  5115. // don't let resizing move earlier than start date cell
  5116. cellOffset = Math.max(cellOffset, minCellOffset);
  5117. dayDelta =
  5118. cellOffsetToDayOffset(cellOffset) -
  5119. cellOffsetToDayOffset(origCellOffset);
  5120. eventEnd = getEventEnd(event).add('days', dayDelta); // assumed to already have a stripped time
  5121. if (dayDelta) {
  5122. eventCopy.end = eventEnd;
  5123. var oldHelpers = helpers;
  5124. helpers = renderTempDayEvent(eventCopy, segment.row, elementTop);
  5125. helpers = $(helpers); // turn array into a jQuery object
  5126. helpers.find('*').css('cursor', direction + '-resize');
  5127. if (oldHelpers) {
  5128. oldHelpers.remove();
  5129. }
  5130. hideEvents(event);
  5131. }
  5132. else {
  5133. if (helpers) {
  5134. showEvents(event);
  5135. helpers.remove();
  5136. helpers = null;
  5137. }
  5138. }
  5139. clearOverlays();
  5140. renderDayOverlay( // coordinate grid already rebuilt with hoverListener.start()
  5141. event.start,
  5142. eventEnd
  5143. // TODO: instead of calling renderDayOverlay() with dates,
  5144. // call _renderDayOverlay (or whatever) with cell offsets.
  5145. );
  5146. }
  5147. }, ev);
  5148. function mouseup(ev) {
  5149. trigger('eventResizeStop', this, event, ev);
  5150. $('body').css('cursor', '');
  5151. hoverListener.stop();
  5152. clearOverlays();
  5153. if (dayDelta) {
  5154. eventResize(
  5155. this, // el
  5156. event,
  5157. eventEnd,
  5158. ev
  5159. );
  5160. // event redraw will clear helpers
  5161. }
  5162. // otherwise, the drag handler already restored the old events
  5163. setTimeout(function() { // make this happen after the element's click event
  5164. isResizing = false;
  5165. },0);
  5166. }
  5167. });
  5168. }
  5169. }
  5170. /* Generalized Segment Utilities
  5171. -------------------------------------------------------------------------------------------------*/
  5172. function isDaySegmentCollision(segment, otherSegments) {
  5173. for (var i=0; i<otherSegments.length; i++) {
  5174. var otherSegment = otherSegments[i];
  5175. if (
  5176. otherSegment.leftCol <= segment.rightCol &&
  5177. otherSegment.rightCol >= segment.leftCol
  5178. ) {
  5179. return true;
  5180. }
  5181. }
  5182. return false;
  5183. }
  5184. function segmentElementEach(segments, callback) { // TODO: use in AgendaView?
  5185. for (var i=0; i<segments.length; i++) {
  5186. var segment = segments[i];
  5187. var element = segment.element;
  5188. if (element) {
  5189. callback(segment, element, i);
  5190. }
  5191. }
  5192. }
  5193. // A cmp function for determining which segments should appear higher up
  5194. function compareDaySegments(a, b) {
  5195. return (b.rightCol - b.leftCol) - (a.rightCol - a.leftCol) || // put wider events first
  5196. b.event.allDay - a.event.allDay || // if tie, put all-day events first (booleans cast to 0/1)
  5197. a.event.start - b.event.start || // if a tie, sort by event start date
  5198. (a.event.title || '').localeCompare(b.event.title); // if a tie, sort by event title
  5199. }
  5200. ;;
  5201. //BUG: unselect needs to be triggered when events are dragged+dropped
  5202. function SelectionManager() {
  5203. var t = this;
  5204. // exports
  5205. t.select = select;
  5206. t.unselect = unselect;
  5207. t.reportSelection = reportSelection;
  5208. t.daySelectionMousedown = daySelectionMousedown;
  5209. // imports
  5210. var calendar = t.calendar;
  5211. var opt = t.opt;
  5212. var trigger = t.trigger;
  5213. var defaultSelectionEnd = t.defaultSelectionEnd;
  5214. var renderSelection = t.renderSelection;
  5215. var clearSelection = t.clearSelection;
  5216. // locals
  5217. var selected = false;
  5218. // unselectAuto
  5219. if (opt('selectable') && opt('unselectAuto')) {
  5220. // TODO: unbind on destroy
  5221. $(document).mousedown(function(ev) {
  5222. var ignore = opt('unselectCancel');
  5223. if (ignore) {
  5224. if ($(ev.target).parents(ignore).length) { // could be optimized to stop after first match
  5225. return;
  5226. }
  5227. }
  5228. unselect(ev);
  5229. });
  5230. }
  5231. function select(start, end) {
  5232. unselect();
  5233. start = calendar.moment(start);
  5234. if (end) {
  5235. end = calendar.moment(end);
  5236. }
  5237. else {
  5238. end = defaultSelectionEnd(start);
  5239. }
  5240. renderSelection(start, end);
  5241. reportSelection(start, end);
  5242. }
  5243. function unselect(ev) {
  5244. if (selected) {
  5245. selected = false;
  5246. clearSelection();
  5247. trigger('unselect', null, ev);
  5248. }
  5249. }
  5250. function reportSelection(start, end, ev) {
  5251. selected = true;
  5252. trigger('select', null, start, end, ev);
  5253. }
  5254. function daySelectionMousedown(ev) { // not really a generic manager method, oh well
  5255. var cellToDate = t.cellToDate;
  5256. var getIsCellAllDay = t.getIsCellAllDay;
  5257. var hoverListener = t.getHoverListener();
  5258. var reportDayClick = t.reportDayClick; // this is hacky and sort of weird
  5259. if (ev.which == 1 && opt('selectable')) { // which==1 means left mouse button
  5260. unselect(ev);
  5261. var dates;
  5262. hoverListener.start(function(cell, origCell) { // TODO: maybe put cellToDate/getIsCellAllDay info in cell
  5263. clearSelection();
  5264. if (cell && getIsCellAllDay(cell)) {
  5265. dates = [ cellToDate(origCell), cellToDate(cell) ].sort(dateCompare);
  5266. renderSelection(
  5267. dates[0],
  5268. dates[1].clone().add('days', 1) // make exclusive
  5269. );
  5270. }else{
  5271. dates = null;
  5272. }
  5273. }, ev);
  5274. $(document).one('mouseup', function(ev) {
  5275. hoverListener.stop();
  5276. if (dates) {
  5277. if (+dates[0] == +dates[1]) {
  5278. reportDayClick(dates[0], ev);
  5279. }
  5280. reportSelection(
  5281. dates[0],
  5282. dates[1].clone().add('days', 1), // make exclusive
  5283. ev
  5284. );
  5285. }
  5286. });
  5287. }
  5288. }
  5289. }
  5290. ;;
  5291. function OverlayManager() {
  5292. var t = this;
  5293. // exports
  5294. t.renderOverlay = renderOverlay;
  5295. t.clearOverlays = clearOverlays;
  5296. // locals
  5297. var usedOverlays = [];
  5298. var unusedOverlays = [];
  5299. function renderOverlay(rect, parent) {
  5300. var e = unusedOverlays.shift();
  5301. if (!e) {
  5302. e = $("<div class='fc-cell-overlay' style='position:absolute;z-index:3'/>");
  5303. }
  5304. if (e[0].parentNode != parent[0]) {
  5305. e.appendTo(parent);
  5306. }
  5307. usedOverlays.push(e.css(rect).show());
  5308. return e;
  5309. }
  5310. function clearOverlays() {
  5311. var e;
  5312. while ((e = usedOverlays.shift())) {
  5313. unusedOverlays.push(e.hide().unbind());
  5314. }
  5315. }
  5316. }
  5317. ;;
  5318. function CoordinateGrid(buildFunc) {
  5319. var t = this;
  5320. var rows;
  5321. var cols;
  5322. t.build = function() {
  5323. rows = [];
  5324. cols = [];
  5325. buildFunc(rows, cols);
  5326. };
  5327. t.cell = function(x, y) {
  5328. var rowCnt = rows.length;
  5329. var colCnt = cols.length;
  5330. var i, r=-1, c=-1;
  5331. for (i=0; i<rowCnt; i++) {
  5332. if (y >= rows[i][0] && y < rows[i][1]) {
  5333. r = i;
  5334. break;
  5335. }
  5336. }
  5337. for (i=0; i<colCnt; i++) {
  5338. if (x >= cols[i][0] && x < cols[i][1]) {
  5339. c = i;
  5340. break;
  5341. }
  5342. }
  5343. return (r>=0 && c>=0) ? { row: r, col: c } : null;
  5344. };
  5345. t.rect = function(row0, col0, row1, col1, originElement) { // row1,col1 is inclusive
  5346. var origin = originElement.offset();
  5347. return {
  5348. top: rows[row0][0] - origin.top,
  5349. left: cols[col0][0] - origin.left,
  5350. width: cols[col1][1] - cols[col0][0],
  5351. height: rows[row1][1] - rows[row0][0]
  5352. };
  5353. };
  5354. }
  5355. ;;
  5356. function HoverListener(coordinateGrid) {
  5357. var t = this;
  5358. var bindType;
  5359. var change;
  5360. var firstCell;
  5361. var cell;
  5362. t.start = function(_change, ev, _bindType) {
  5363. change = _change;
  5364. firstCell = cell = null;
  5365. coordinateGrid.build();
  5366. mouse(ev);
  5367. bindType = _bindType || 'mousemove';
  5368. $(document).bind(bindType, mouse);
  5369. };
  5370. function mouse(ev) {
  5371. _fixUIEvent(ev); // see below
  5372. var newCell = coordinateGrid.cell(ev.pageX, ev.pageY);
  5373. if (
  5374. Boolean(newCell) !== Boolean(cell) ||
  5375. newCell && (newCell.row != cell.row || newCell.col != cell.col)
  5376. ) {
  5377. if (newCell) {
  5378. if (!firstCell) {
  5379. firstCell = newCell;
  5380. }
  5381. change(newCell, firstCell, newCell.row-firstCell.row, newCell.col-firstCell.col);
  5382. }else{
  5383. change(newCell, firstCell);
  5384. }
  5385. cell = newCell;
  5386. }
  5387. }
  5388. t.stop = function() {
  5389. $(document).unbind(bindType, mouse);
  5390. return cell;
  5391. };
  5392. }
  5393. // this fix was only necessary for jQuery UI 1.8.16 (and jQuery 1.7 or 1.7.1)
  5394. // upgrading to jQuery UI 1.8.17 (and using either jQuery 1.7 or 1.7.1) fixed the problem
  5395. // but keep this in here for 1.8.16 users
  5396. // and maybe remove it down the line
  5397. function _fixUIEvent(event) { // for issue 1168
  5398. if (event.pageX === undefined) {
  5399. event.pageX = event.originalEvent.pageX;
  5400. event.pageY = event.originalEvent.pageY;
  5401. }
  5402. }
  5403. ;;
  5404. function HorizontalPositionCache(getElement) {
  5405. var t = this,
  5406. elements = {},
  5407. lefts = {},
  5408. rights = {};
  5409. function e(i) {
  5410. return (elements[i] = (elements[i] || getElement(i)));
  5411. }
  5412. t.left = function(i) {
  5413. return (lefts[i] = (lefts[i] === undefined ? e(i).position().left : lefts[i]));
  5414. };
  5415. t.right = function(i) {
  5416. return (rights[i] = (rights[i] === undefined ? t.left(i) + e(i).width() : rights[i]));
  5417. };
  5418. t.clear = function() {
  5419. elements = {};
  5420. lefts = {};
  5421. rights = {};
  5422. };
  5423. }
  5424. ;;
  5425. });