Guest User

fullcalendar.js

a guest
Nov 19th, 2014
82
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. /*!
  2.  * FullCalendar v2.2.0
  3.  * Docs & License: http://arshaw.com/fullcalendar/
  4.  * (c) 2013 Adam Shaw
  5.  */
  6.  
  7.  
  8. (function(factory) {
  9.     if (typeof define === 'function' && define.amd) {
  10.         define([ 'jquery', 'moment' ], factory);
  11.     }
  12.     else {
  13.         factory(jQuery, moment);
  14.     }
  15. })(function($, moment) {
  16.  
  17. ;;
  18.  
  19. var defaults = {
  20.  
  21.     lang: 'en',
  22.  
  23.     defaultTimedEventDuration: '02:00:00',
  24.     defaultAllDayEventDuration: { days: 1 },
  25.     forceEventDuration: false,
  26.     nextDayThreshold: '09:00:00', // 9am
  27.  
  28.     // display
  29.     defaultView: 'month',
  30.     aspectRatio: 1.35,
  31.     header: {
  32.         left: 'title',
  33.         center: '',
  34.         right: 'today prev,next'
  35.     },
  36.     weekends: true,
  37.     weekNumbers: false,
  38.  
  39.     weekNumberTitle: 'W',
  40.     weekNumberCalculation: 'local',
  41.    
  42.     //editable: false,
  43.    
  44.     // event ajax
  45.     lazyFetching: true,
  46.     startParam: 'start',
  47.     endParam: 'end',
  48.     timezoneParam: 'timezone',
  49.  
  50.     timezone: false,
  51.  
  52.     //allDayDefault: undefined,
  53.    
  54.     // time formats
  55.     titleFormat: {
  56.         month: 'MMMM YYYY', // like "September 1986". each language will override this
  57.         week: 'll', // like "Sep 4 1986"
  58.         day: 'LL' // like "September 4 1986"
  59.     },
  60.     columnFormat: {
  61.         month: 'ddd', // like "Sat"
  62.         week: generateWeekColumnFormat,
  63.         day: 'dddd' // like "Saturday"
  64.     },
  65.     timeFormat: { // for event elements
  66.         'default': generateShortTimeFormat
  67.     },
  68.  
  69.     displayEventEnd: {
  70.         month: false,
  71.         basicWeek: false,
  72.         'default': true
  73.     },
  74.    
  75.     // locale
  76.     isRTL: false,
  77.     defaultButtonText: {
  78.         prev: "prev",
  79.         next: "next",
  80.         prevYear: "prev year",
  81.         nextYear: "next year",
  82.         today: 'today',
  83.         month: 'month',
  84.         week: 'week',
  85.         day: 'day'
  86.     },
  87.  
  88.     buttonIcons: {
  89.         prev: 'left-single-arrow',
  90.         next: 'right-single-arrow',
  91.         prevYear: 'left-double-arrow',
  92.         nextYear: 'right-double-arrow'
  93.     },
  94.    
  95.     // jquery-ui theming
  96.     theme: false,
  97.     themeButtonIcons: {
  98.         prev: 'circle-triangle-w',
  99.         next: 'circle-triangle-e',
  100.         prevYear: 'seek-prev',
  101.         nextYear: 'seek-next'
  102.     },
  103.  
  104.     dragOpacity: .75,
  105.     dragRevertDuration: 500,
  106.     dragScroll: true,
  107.    
  108.     //selectable: false,
  109.     unselectAuto: true,
  110.    
  111.     dropAccept: '*',
  112.  
  113.     eventLimit: false,
  114.     eventLimitText: 'more',
  115.     eventLimitClick: 'popover',
  116.     dayPopoverFormat: 'LL',
  117.    
  118.     handleWindowResize: true,
  119.     windowResizeDelay: 200 // milliseconds before a rerender happens
  120.    
  121. };
  122.  
  123.  
  124. function generateShortTimeFormat(options, langData) {
  125.     return langData.longDateFormat('LT')
  126.         .replace(':mm', '(:mm)')
  127.         .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs
  128.         .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand
  129. }
  130.  
  131.  
  132. function generateWeekColumnFormat(options, langData) {
  133.     var format = langData.longDateFormat('L'); // for the format like "MM/DD/YYYY"
  134.     format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, ''); // strip the year off the edge, as well as other misc non-whitespace chars
  135.     if (options.isRTL) {
  136.         format += ' ddd'; // for RTL, add day-of-week to end
  137.     }
  138.     else {
  139.         format = 'ddd ' + format; // for LTR, add day-of-week to beginning
  140.     }
  141.     return format;
  142. }
  143.  
  144.  
  145. var langOptionHash = {
  146.     en: {
  147.         columnFormat: {
  148.             week: 'ddd M/D' // override for english. different from the generated default, which is MM/DD
  149.         },
  150.         dayPopoverFormat: 'dddd, MMMM D'
  151.     }
  152. };
  153.  
  154.  
  155. // right-to-left defaults
  156. var rtlDefaults = {
  157.     header: {
  158.         left: 'next,prev today',
  159.         center: '',
  160.         right: 'title'
  161.     },
  162.     buttonIcons: {
  163.         prev: 'right-single-arrow',
  164.         next: 'left-single-arrow',
  165.         prevYear: 'right-double-arrow',
  166.         nextYear: 'left-double-arrow'
  167.     },
  168.     themeButtonIcons: {
  169.         prev: 'circle-triangle-e',
  170.         next: 'circle-triangle-w',
  171.         nextYear: 'seek-prev',
  172.         prevYear: 'seek-next'
  173.     }
  174. };
  175.  
  176. ;;
  177.  
  178. var fc = $.fullCalendar = { version: "2.2.0" };
  179. var fcViews = fc.views = {};
  180.  
  181.  
  182. $.fn.fullCalendar = function(options) {
  183.     var args = Array.prototype.slice.call(arguments, 1); // for a possible method call
  184.     var res = this; // what this function will return (this jQuery object by default)
  185.  
  186.     this.each(function(i, _element) { // loop each DOM element involved
  187.         var element = $(_element);
  188.         var calendar = element.data('fullCalendar'); // get the existing calendar object (if any)
  189.         var singleRes; // the returned value of this single method call
  190.  
  191.         // a method call
  192.         if (typeof options === 'string') {
  193.             if (calendar && $.isFunction(calendar[options])) {
  194.                 singleRes = calendar[options].apply(calendar, args);
  195.                 if (!i) {
  196.                     res = singleRes; // record the first method call result
  197.                 }
  198.                 if (options === 'destroy') { // for the destroy method, must remove Calendar object data
  199.                     element.removeData('fullCalendar');
  200.                 }
  201.             }
  202.         }
  203.         // a new calendar initialization
  204.         else if (!calendar) { // don't initialize twice
  205.             calendar = new Calendar(element, options);
  206.             element.data('fullCalendar', calendar);
  207.             calendar.render();
  208.         }
  209.     });
  210.    
  211.     return res;
  212. };
  213.  
  214.  
  215. // function for adding/overriding defaults
  216. function setDefaults(d) {
  217.     mergeOptions(defaults, d);
  218. }
  219.  
  220.  
  221. // Recursively combines option hash-objects.
  222. // Better than `$.extend(true, ...)` because arrays are not traversed/copied.
  223. //
  224. // called like:
  225. //     mergeOptions(target, obj1, obj2, ...)
  226. //
  227. function mergeOptions(target) {
  228.  
  229.     function mergeIntoTarget(name, value) {
  230.         if ($.isPlainObject(value) && $.isPlainObject(target[name]) && !isForcedAtomicOption(name)) {
  231.             // merge into a new object to avoid destruction
  232.             target[name] = mergeOptions({}, target[name], value); // combine. `value` object takes precedence
  233.         }
  234.         else if (value !== undefined) { // only use values that are set and not undefined
  235.             target[name] = value;
  236.         }
  237.     }
  238.  
  239.     for (var i=1; i<arguments.length; i++) {
  240.         $.each(arguments[i], mergeIntoTarget);
  241.     }
  242.  
  243.     return target;
  244. }
  245.  
  246.  
  247. // overcome sucky view-option-hash and option-merging behavior messing with options it shouldn't
  248. function isForcedAtomicOption(name) {
  249.     // Any option that ends in "Time" or "Duration" is probably a Duration,
  250.     // and these will commonly be specified as plain objects, which we don't want to mess up.
  251.     return /(Time|Duration)$/.test(name);
  252. }
  253. // FIX: find a different solution for view-option-hashes and have a whitelist
  254. // for options that can be recursively merged.
  255.  
  256. ;;
  257.  
  258. //var langOptionHash = {}; // initialized in defaults.js
  259. fc.langs = langOptionHash; // expose
  260.  
  261.  
  262. // Initialize jQuery UI Datepicker translations while using some of the translations
  263. // for our own purposes. Will set this as the default language for datepicker.
  264. // Called from a translation file.
  265. fc.datepickerLang = function(langCode, datepickerLangCode, options) {
  266.     var langOptions = langOptionHash[langCode];
  267.  
  268.     // initialize FullCalendar's lang hash for this language
  269.     if (!langOptions) {
  270.         langOptions = langOptionHash[langCode] = {};
  271.     }
  272.  
  273.     // merge certain Datepicker options into FullCalendar's options
  274.     mergeOptions(langOptions, {
  275.         isRTL: options.isRTL,
  276.         weekNumberTitle: options.weekHeader,
  277.         titleFormat: {
  278.             month: options.showMonthAfterYear ?
  279.                 'YYYY[' + options.yearSuffix + '] MMMM' :
  280.                 'MMMM YYYY[' + options.yearSuffix + ']'
  281.         },
  282.         defaultButtonText: {
  283.             // the translations sometimes wrongly contain HTML entities
  284.             prev: stripHtmlEntities(options.prevText),
  285.             next: stripHtmlEntities(options.nextText),
  286.             today: stripHtmlEntities(options.currentText)
  287.         }
  288.     });
  289.  
  290.     // is jQuery UI Datepicker is on the page?
  291.     if ($.datepicker) {
  292.  
  293.         // Register the language data.
  294.         // FullCalendar and MomentJS use language codes like "pt-br" but Datepicker
  295.         // does it like "pt-BR" or if it doesn't have the language, maybe just "pt".
  296.         // Make an alias so the language can be referenced either way.
  297.         $.datepicker.regional[datepickerLangCode] =
  298.             $.datepicker.regional[langCode] = // alias
  299.                 options;
  300.  
  301.         // Alias 'en' to the default language data. Do this every time.
  302.         $.datepicker.regional.en = $.datepicker.regional[''];
  303.  
  304.         // Set as Datepicker's global defaults.
  305.         $.datepicker.setDefaults(options);
  306.     }
  307. };
  308.  
  309.  
  310. // Sets FullCalendar-specific translations. Also sets the language as the global default.
  311. // Called from a translation file.
  312. fc.lang = function(langCode, options) {
  313.     var langOptions;
  314.  
  315.     if (options) {
  316.         langOptions = langOptionHash[langCode];
  317.  
  318.         // initialize the hash for this language
  319.         if (!langOptions) {
  320.             langOptions = langOptionHash[langCode] = {};
  321.         }
  322.  
  323.         mergeOptions(langOptions, options || {});
  324.     }
  325.  
  326.     // set it as the default language for FullCalendar
  327.     defaults.lang = langCode;
  328. };
  329. ;;
  330.  
  331.  
  332. function Calendar(element, instanceOptions) {
  333.     var t = this;
  334.  
  335.  
  336.  
  337.     // Build options object
  338.     // -----------------------------------------------------------------------------------
  339.     // Precedence (lowest to highest): defaults, rtlDefaults, langOptions, instanceOptions
  340.  
  341.     instanceOptions = instanceOptions || {};
  342.  
  343.     var options = mergeOptions({}, defaults, instanceOptions);
  344.     var langOptions;
  345.  
  346.     // determine language options
  347.     if (options.lang in langOptionHash) {
  348.         langOptions = langOptionHash[options.lang];
  349.     }
  350.     else {
  351.         langOptions = langOptionHash[defaults.lang];
  352.     }
  353.  
  354.     if (langOptions) { // if language options exist, rebuild...
  355.         options = mergeOptions({}, defaults, langOptions, instanceOptions);
  356.     }
  357.  
  358.     if (options.isRTL) { // is isRTL, rebuild...
  359.         options = mergeOptions({}, defaults, rtlDefaults, langOptions || {}, instanceOptions);
  360.     }
  361.  
  362.  
  363.    
  364.     // Exports
  365.     // -----------------------------------------------------------------------------------
  366.  
  367.     t.options = options;
  368.     t.render = render;
  369.     t.destroy = destroy;
  370.     t.refetchEvents = refetchEvents;
  371.     t.reportEvents = reportEvents;
  372.     t.reportEventChange = reportEventChange;
  373.     t.rerenderEvents = renderEvents; // `renderEvents` serves as a rerender. an API method
  374.     t.changeView = changeView;
  375.     t.select = select;
  376.     t.unselect = unselect;
  377.     t.prev = prev;
  378.     t.next = next;
  379.     t.prevYear = prevYear;
  380.     t.nextYear = nextYear;
  381.     t.today = today;
  382.     t.gotoDate = gotoDate;
  383.     t.incrementDate = incrementDate;
  384.     t.zoomTo = zoomTo;
  385.     t.getDate = getDate;
  386.     t.getCalendar = getCalendar;
  387.     t.getView = getView;
  388.     t.option = option;
  389.     t.trigger = trigger;
  390.  
  391.  
  392.  
  393.     // Language-data Internals
  394.     // -----------------------------------------------------------------------------------
  395.     // Apply overrides to the current language's data
  396.  
  397.  
  398.     // Returns moment's internal locale data. If doesn't exist, returns English.
  399.     // Works with moment-pre-2.8
  400.     function getLocaleData(langCode) {
  401.         var f = moment.localeData || moment.langData;
  402.         return f.call(moment, langCode) ||
  403.             f.call(moment, 'en'); // the newer localData could return null, so fall back to en
  404.     }
  405.  
  406.  
  407.     var localeData = createObject(getLocaleData(options.lang)); // make a cheap copy
  408.  
  409.     if (options.monthNames) {
  410.         localeData._months = options.monthNames;
  411.     }
  412.     if (options.monthNamesShort) {
  413.         localeData._monthsShort = options.monthNamesShort;
  414.     }
  415.     if (options.dayNames) {
  416.         localeData._weekdays = options.dayNames;
  417.     }
  418.     if (options.dayNamesShort) {
  419.         localeData._weekdaysShort = options.dayNamesShort;
  420.     }
  421.     if (options.firstDay != null) {
  422.         var _week = createObject(localeData._week); // _week: { dow: # }
  423.         _week.dow = options.firstDay;
  424.         localeData._week = _week;
  425.     }
  426.  
  427.  
  428.  
  429.     // Calendar-specific Date Utilities
  430.     // -----------------------------------------------------------------------------------
  431.  
  432.  
  433.     t.defaultAllDayEventDuration = moment.duration(options.defaultAllDayEventDuration);
  434.     t.defaultTimedEventDuration = moment.duration(options.defaultTimedEventDuration);
  435.  
  436.  
  437.     // Builds a moment using the settings of the current calendar: timezone and language.
  438.     // Accepts anything the vanilla moment() constructor accepts.
  439.     t.moment = function() {
  440.         var mom;
  441.  
  442.         if (options.timezone === 'local') {
  443.             mom = fc.moment.apply(null, arguments);
  444.  
  445.             // Force the moment to be local, because fc.moment doesn't guarantee it.
  446.             if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone
  447.                 mom.local();
  448.             }
  449.         }
  450.         else if (options.timezone === 'UTC') {
  451.             mom = fc.moment.utc.apply(null, arguments); // process as UTC
  452.         }
  453.         else {
  454.             mom = fc.moment.parseZone.apply(null, arguments); // let the input decide the zone
  455.         }
  456.  
  457.         if ('_locale' in mom) { // moment 2.8 and above
  458.             mom._locale = localeData;
  459.         }
  460.         else { // pre-moment-2.8
  461.             mom._lang = localeData;
  462.         }
  463.  
  464.         return mom;
  465.     };
  466.  
  467.  
  468.     // Returns a boolean about whether or not the calendar knows how to calculate
  469.     // the timezone offset of arbitrary dates in the current timezone.
  470.     t.getIsAmbigTimezone = function() {
  471.         return options.timezone !== 'local' && options.timezone !== 'UTC';
  472.     };
  473.  
  474.  
  475.     // Returns a copy of the given date in the current timezone of it is ambiguously zoned.
  476.     // This will also give the date an unambiguous time.
  477.     t.rezoneDate = function(date) {
  478.         return t.moment(date.toArray());
  479.     };
  480.  
  481.  
  482.     // Returns a moment for the current date, as defined by the client's computer,
  483.     // or overridden by the `now` option.
  484.     t.getNow = function() {
  485.         var now = options.now;
  486.         if (typeof now === 'function') {
  487.             now = now();
  488.         }
  489.         return t.moment(now);
  490.     };
  491.  
  492.  
  493.     // Calculates the week number for a moment according to the calendar's
  494.     // `weekNumberCalculation` setting.
  495.     t.calculateWeekNumber = function(mom) {
  496.         var calc = options.weekNumberCalculation;
  497.  
  498.         if (typeof calc === 'function') {
  499.             return calc(mom);
  500.         }
  501.         else if (calc === 'local') {
  502.             return mom.week();
  503.         }
  504.         else if (calc.toUpperCase() === 'ISO') {
  505.             return mom.isoWeek();
  506.         }
  507.     };
  508.  
  509.  
  510.     // Get an event's normalized end date. If not present, calculate it from the defaults.
  511.     t.getEventEnd = function(event) {
  512.         if (event.end) {
  513.             return event.end.clone();
  514.         }
  515.         else {
  516.             return t.getDefaultEventEnd(event.allDay, event.start);
  517.         }
  518.     };
  519.  
  520.  
  521.     // Given an event's allDay status and start date, return swhat its fallback end date should be.
  522.     t.getDefaultEventEnd = function(allDay, start) { // TODO: rename to computeDefaultEventEnd
  523.         var end = start.clone();
  524.  
  525.         if (allDay) {
  526.             end.stripTime().add(t.defaultAllDayEventDuration);
  527.         }
  528.         else {
  529.             end.add(t.defaultTimedEventDuration);
  530.         }
  531.  
  532.         if (t.getIsAmbigTimezone()) {
  533.             end.stripZone(); // we don't know what the tzo should be
  534.         }
  535.  
  536.         return end;
  537.     };
  538.  
  539.  
  540.  
  541.     // Date-formatting Utilities
  542.     // -----------------------------------------------------------------------------------
  543.  
  544.  
  545.     // Like the vanilla formatRange, but with calendar-specific settings applied.
  546.     t.formatRange = function(m1, m2, formatStr) {
  547.  
  548.         // a function that returns a formatStr // TODO: in future, precompute this
  549.         if (typeof formatStr === 'function') {
  550.             formatStr = formatStr.call(t, options, localeData);
  551.         }
  552.  
  553.         return formatRange(m1, m2, formatStr, null, options.isRTL);
  554.     };
  555.  
  556.  
  557.     // Like the vanilla formatDate, but with calendar-specific settings applied.
  558.     t.formatDate = function(mom, formatStr) {
  559.  
  560.         // a function that returns a formatStr // TODO: in future, precompute this
  561.         if (typeof formatStr === 'function') {
  562.             formatStr = formatStr.call(t, options, localeData);
  563.         }
  564.  
  565.         return formatDate(mom, formatStr);
  566.     };
  567.  
  568.  
  569.    
  570.     // Imports
  571.     // -----------------------------------------------------------------------------------
  572.  
  573.  
  574.     EventManager.call(t, options);
  575.     var isFetchNeeded = t.isFetchNeeded;
  576.     var fetchEvents = t.fetchEvents;
  577.  
  578.  
  579.  
  580.     // Locals
  581.     // -----------------------------------------------------------------------------------
  582.  
  583.  
  584.     var _element = element[0];
  585.     var header;
  586.     var headerElement;
  587.     var content;
  588.     var tm; // for making theme classes
  589.     var currentView;
  590.     var suggestedViewHeight;
  591.     var windowResizeProxy; // wraps the windowResize function
  592.     var ignoreWindowResize = 0;
  593.     var date;
  594.     var events = [];
  595.    
  596.    
  597.    
  598.     // Main Rendering
  599.     // -----------------------------------------------------------------------------------
  600.  
  601.  
  602.     if (options.defaultDate != null) {
  603.         date = t.moment(options.defaultDate);
  604.     }
  605.     else {
  606.         date = t.getNow();
  607.     }
  608.    
  609.    
  610.     function render(inc) {
  611.         if (!content) {
  612.             initialRender();
  613.         }
  614.         else if (elementVisible()) {
  615.             // mainly for the public API
  616.             calcSize();
  617.             renderView(inc);
  618.         }
  619.     }
  620.    
  621.    
  622.     function initialRender() {
  623.         tm = options.theme ? 'ui' : 'fc';
  624.         element.addClass('fc');
  625.  
  626.         if (options.isRTL) {
  627.             element.addClass('fc-rtl');
  628.         }
  629.         else {
  630.             element.addClass('fc-ltr');
  631.         }
  632.  
  633.         if (options.theme) {
  634.             element.addClass('ui-widget');
  635.         }
  636.         else {
  637.             element.addClass('fc-unthemed');
  638.         }
  639.  
  640.         content = $("<div class='fc-view-container'/>").prependTo(element);
  641.  
  642.         header = new Header(t, options);
  643.         headerElement = header.render();
  644.         if (headerElement) {
  645.             element.prepend(headerElement);
  646.         }
  647.  
  648.         changeView(options.defaultView);
  649.  
  650.         if (options.handleWindowResize) {
  651.             windowResizeProxy = debounce(windowResize, options.windowResizeDelay); // prevents rapid calls
  652.             $(window).resize(windowResizeProxy);
  653.         }
  654.     }
  655.    
  656.    
  657.     function destroy() {
  658.  
  659.         if (currentView) {
  660.             currentView.destroy();
  661.         }
  662.  
  663.         header.destroy();
  664.         content.remove();
  665.         element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');
  666.  
  667.         $(window).unbind('resize', windowResizeProxy);
  668.     }
  669.    
  670.    
  671.     function elementVisible() {
  672.         return element.is(':visible');
  673.     }
  674.    
  675.    
  676.  
  677.     // View Rendering
  678.     // -----------------------------------------------------------------------------------
  679.  
  680.  
  681.     function changeView(viewName) {
  682.         renderView(0, viewName);
  683.     }
  684.  
  685.  
  686.     // Renders a view because of a date change, view-type change, or for the first time
  687.     function renderView(delta, viewName) {
  688.         ignoreWindowResize++;
  689.  
  690.         // if viewName is changing, destroy the old view
  691.         if (currentView && viewName && currentView.name !== viewName) {
  692.             header.deactivateButton(currentView.name);
  693.             freezeContentHeight(); // prevent a scroll jump when view element is removed
  694.             if (currentView.start) { // rendered before?
  695.                 currentView.destroy();
  696.             }
  697.             currentView.el.remove();
  698.             currentView = null;
  699.         }
  700.  
  701.         // if viewName changed, or the view was never created, create a fresh view
  702.         if (!currentView && viewName) {
  703.             currentView = new fcViews[viewName](t);
  704.             currentView.el =  $("<div class='fc-view fc-" + viewName + "-view' />").appendTo(content);
  705.             header.activateButton(viewName);
  706.         }
  707.  
  708.         if (currentView) {
  709.  
  710.             // let the view determine what the delta means
  711.             if (delta) {
  712.                 date = currentView.incrementDate(date, delta);
  713.             }
  714.  
  715.             // render or rerender the view
  716.             if (
  717.                 !currentView.start || // never rendered before
  718.                 delta || // explicit date window change
  719.                 !date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change
  720.             ) {
  721.                 if (elementVisible()) {
  722.  
  723.                     freezeContentHeight();
  724.                     if (currentView.start) { // rendered before?
  725.                         currentView.destroy();
  726.                     }
  727.                     currentView.render(date);
  728.                     unfreezeContentHeight();
  729.  
  730.                     // need to do this after View::render, so dates are calculated
  731.                     updateTitle();
  732.                     updateTodayButton();
  733.  
  734.                     getAndRenderEvents();
  735.                 }
  736.             }
  737.         }
  738.  
  739.         unfreezeContentHeight(); // undo any lone freezeContentHeight calls
  740.         ignoreWindowResize--;
  741.     }
  742.    
  743.    
  744.  
  745.     // Resizing
  746.     // -----------------------------------------------------------------------------------
  747.  
  748.  
  749.     t.getSuggestedViewHeight = function() {
  750.         if (suggestedViewHeight === undefined) {
  751.             calcSize();
  752.         }
  753.         return suggestedViewHeight;
  754.     };
  755.  
  756.  
  757.     t.isHeightAuto = function() {
  758.         return options.contentHeight === 'auto' || options.height === 'auto';
  759.     };
  760.    
  761.    
  762.     function updateSize(shouldRecalc) {
  763.         if (elementVisible()) {
  764.  
  765.             if (shouldRecalc) {
  766.                 _calcSize();
  767.             }
  768.  
  769.             ignoreWindowResize++;
  770.             currentView.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto()
  771.             ignoreWindowResize--;
  772.  
  773.             return true; // signal success
  774.         }
  775.     }
  776.  
  777.  
  778.     function calcSize() {
  779.         if (elementVisible()) {
  780.             _calcSize();
  781.         }
  782.     }
  783.    
  784.    
  785.     function _calcSize() { // assumes elementVisible
  786.         if (typeof options.contentHeight === 'number') { // exists and not 'auto'
  787.             suggestedViewHeight = options.contentHeight;
  788.         }
  789.         else if (typeof options.height === 'number') { // exists and not 'auto'
  790.             suggestedViewHeight = options.height - (headerElement ? headerElement.outerHeight(true) : 0);
  791.         }
  792.         else {
  793.             suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5));
  794.         }
  795.     }
  796.    
  797.    
  798.     function windowResize(ev) {
  799.         if (
  800.             !ignoreWindowResize &&
  801.             ev.target === window && // so we don't process jqui "resize" events that have bubbled up
  802.             currentView.start // view has already been rendered
  803.         ) {
  804.             if (updateSize(true)) {
  805.                 currentView.trigger('windowResize', _element);
  806.             }
  807.         }
  808.     }
  809.    
  810.    
  811.    
  812.     /* Event Fetching/Rendering
  813.     -----------------------------------------------------------------------------*/
  814.     // TODO: going forward, most of this stuff should be directly handled by the view
  815.  
  816.  
  817.     function refetchEvents() { // can be called as an API method
  818.         destroyEvents(); // so that events are cleared before user starts waiting for AJAX
  819.         fetchAndRenderEvents();
  820.     }
  821.  
  822.  
  823.     function renderEvents() { // destroys old events if previously rendered
  824.         if (elementVisible()) {
  825.             freezeContentHeight();
  826.             currentView.destroyEvents(); // no performance cost if never rendered
  827.             currentView.renderEvents(events);
  828.             unfreezeContentHeight();
  829.         }
  830.     }
  831.  
  832.  
  833.     function destroyEvents() {
  834.         freezeContentHeight();
  835.         currentView.destroyEvents();
  836.         unfreezeContentHeight();
  837.     }
  838.    
  839.  
  840.     function getAndRenderEvents() {
  841.         if (!options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) {
  842.             fetchAndRenderEvents();
  843.         }
  844.         else {
  845.             renderEvents();
  846.         }
  847.     }
  848.  
  849.  
  850.     function fetchAndRenderEvents() {
  851.         fetchEvents(currentView.start, currentView.end);
  852.             // ... will call reportEvents
  853.             // ... which will call renderEvents
  854.     }
  855.  
  856.    
  857.     // called when event data arrives
  858.     function reportEvents(_events) {
  859.         events = _events;
  860.         renderEvents();
  861.     }
  862.  
  863.  
  864.     // called when a single event's data has been changed
  865.     function reportEventChange() {
  866.         renderEvents();
  867.     }
  868.  
  869.  
  870.  
  871.     /* Header Updating
  872.     -----------------------------------------------------------------------------*/
  873.  
  874.  
  875.     function updateTitle() {
  876.         header.updateTitle(currentView.title);
  877.     }
  878.  
  879.  
  880.     function updateTodayButton() {
  881.         var now = t.getNow();
  882.         if (now.isWithin(currentView.intervalStart, currentView.intervalEnd)) {
  883.             header.disableButton('today');
  884.         }
  885.         else {
  886.             header.enableButton('today');
  887.         }
  888.     }
  889.    
  890.  
  891.  
  892.     /* Selection
  893.     -----------------------------------------------------------------------------*/
  894.    
  895.  
  896.     function select(start, end) {
  897.  
  898.         start = t.moment(start);
  899.         if (end) {
  900.             end = t.moment(end);
  901.         }
  902.         else if (start.hasTime()) {
  903.             end = start.clone().add(t.defaultTimedEventDuration);
  904.         }
  905.         else {
  906.             end = start.clone().add(t.defaultAllDayEventDuration);
  907.         }
  908.  
  909.         currentView.select(start, end);
  910.     }
  911.    
  912.  
  913.     function unselect() { // safe to be called before renderView
  914.         if (currentView) {
  915.             currentView.unselect();
  916.         }
  917.     }
  918.    
  919.    
  920.    
  921.     /* Date
  922.     -----------------------------------------------------------------------------*/
  923.    
  924.    
  925.     function prev() {
  926.         renderView(-1);
  927.     }
  928.    
  929.    
  930.     function next() {
  931.         renderView(1);
  932.     }
  933.    
  934.    
  935.     function prevYear() {
  936.         date.add(-1, 'years');
  937.         renderView();
  938.     }
  939.    
  940.    
  941.     function nextYear() {
  942.         date.add(1, 'years');
  943.         renderView();
  944.     }
  945.    
  946.    
  947.     function today() {
  948.         date = t.getNow();
  949.         renderView();
  950.     }
  951.    
  952.    
  953.     function gotoDate(dateInput) {
  954.         date = t.moment(dateInput);
  955.         renderView();
  956.     }
  957.    
  958.    
  959.     function incrementDate(delta) {
  960.         date.add(moment.duration(delta));
  961.         renderView();
  962.     }
  963.  
  964.  
  965.     // Forces navigation to a view for the given date.
  966.     // `viewName` can be a specific view name or a generic one like "week" or "day".
  967.     function zoomTo(newDate, viewName) {
  968.         var viewStr;
  969.         var match;
  970.  
  971.         if (!viewName || fcViews[viewName] === undefined) { // a general view name, or "auto"
  972.             viewName = viewName || 'day';
  973.             viewStr = header.getViewsWithButtons().join(' '); // space-separated string of all the views in the header
  974.  
  975.             // try to match a general view name, like "week", against a specific one, like "agendaWeek"
  976.             match = viewStr.match(new RegExp('\\w+' + capitaliseFirstLetter(viewName)));
  977.  
  978.             // fall back to the day view being used in the header
  979.             if (!match) {
  980.                 match = viewStr.match(/\w+Day/);
  981.             }
  982.  
  983.             viewName = match ? match[0] : 'agendaDay'; // fall back to agendaDay
  984.         }
  985.  
  986.         date = newDate;
  987.         changeView(viewName);
  988.     }
  989.    
  990.    
  991.     function getDate() {
  992.         return date.clone();
  993.     }
  994.  
  995.  
  996.  
  997.     /* Height "Freezing"
  998.     -----------------------------------------------------------------------------*/
  999.  
  1000.  
  1001.     function freezeContentHeight() {
  1002.         content.css({
  1003.             width: '100%',
  1004.             height: content.height(),
  1005.             overflow: 'hidden'
  1006.         });
  1007.     }
  1008.  
  1009.  
  1010.     function unfreezeContentHeight() {
  1011.         content.css({
  1012.             width: '',
  1013.             height: '',
  1014.             overflow: ''
  1015.         });
  1016.     }
  1017.    
  1018.    
  1019.    
  1020.     /* Misc
  1021.     -----------------------------------------------------------------------------*/
  1022.    
  1023.  
  1024.     function getCalendar() {
  1025.         return t;
  1026.     }
  1027.  
  1028.    
  1029.     function getView() {
  1030.         return currentView;
  1031.     }
  1032.    
  1033.    
  1034.     function option(name, value) {
  1035.         if (value === undefined) {
  1036.             return options[name];
  1037.         }
  1038.         if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') {
  1039.             options[name] = value;
  1040.             updateSize(true); // true = allow recalculation of height
  1041.         }
  1042.     }
  1043.    
  1044.    
  1045.     function trigger(name, thisObj) {
  1046.         if (options[name]) {
  1047.             return options[name].apply(
  1048.                 thisObj || _element,
  1049.                 Array.prototype.slice.call(arguments, 2)
  1050.             );
  1051.         }
  1052.     }
  1053.  
  1054. }
  1055.  
  1056. ;;
  1057.  
  1058. /* Top toolbar area with buttons and title
  1059. ----------------------------------------------------------------------------------------------------------------------*/
  1060. // TODO: rename all header-related things to "toolbar"
  1061.  
  1062. function Header(calendar, options) {
  1063.     var t = this;
  1064.    
  1065.     // exports
  1066.     t.render = render;
  1067.     t.destroy = destroy;
  1068.     t.updateTitle = updateTitle;
  1069.     t.activateButton = activateButton;
  1070.     t.deactivateButton = deactivateButton;
  1071.     t.disableButton = disableButton;
  1072.     t.enableButton = enableButton;
  1073.     t.getViewsWithButtons = getViewsWithButtons;
  1074.    
  1075.     // locals
  1076.     var el = $();
  1077.     var viewsWithButtons = [];
  1078.     var tm;
  1079.  
  1080.  
  1081.     function render() {
  1082.         var sections = options.header;
  1083.  
  1084.         tm = options.theme ? 'ui' : 'fc';
  1085.  
  1086.         if (sections) {
  1087.             el = $("<div class='fc-toolbar'/>")
  1088.                 .append(renderSection('left'))
  1089.                 .append(renderSection('right'))
  1090.                 .append(renderSection('center'))
  1091.                 .append('<div class="fc-clear"/>');
  1092.  
  1093.             return el;
  1094.         }
  1095.     }
  1096.    
  1097.    
  1098.     function destroy() {
  1099.         el.remove();
  1100.     }
  1101.    
  1102.    
  1103.     function renderSection(position) {
  1104.         var sectionEl = $('<div class="fc-' + position + '"/>');
  1105.         var buttonStr = options.header[position];
  1106.  
  1107.         if (buttonStr) {
  1108.             $.each(buttonStr.split(' '), function(i) {
  1109.                 var groupChildren = $();
  1110.                 var isOnlyButtons = true;
  1111.                 var groupEl;
  1112.  
  1113.                 $.each(this.split(','), function(j, buttonName) {
  1114.                     var buttonClick;
  1115.                     var themeIcon;
  1116.                     var normalIcon;
  1117.                     var defaultText;
  1118.                     var customText;
  1119.                     var innerHtml;
  1120.                     var classes;
  1121.                     var button;
  1122.  
  1123.                     if (buttonName == 'title') {
  1124.                         groupChildren = groupChildren.add($('<h2>&nbsp;</h2>')); // we always want it to take up height
  1125.                         isOnlyButtons = false;
  1126.                     }
  1127.                     else {
  1128.                         if (calendar[buttonName]) { // a calendar method
  1129.                             buttonClick = function() {
  1130.                                 calendar[buttonName]();
  1131.                             };
  1132.                         }
  1133.                         else if (fcViews[buttonName]) { // a view name
  1134.                             buttonClick = function() {
  1135.                                 calendar.changeView(buttonName);
  1136.                             };
  1137.                             viewsWithButtons.push(buttonName);
  1138.                         }
  1139.                         if (buttonClick) {
  1140.  
  1141.                             // smartProperty allows different text per view button (ex: "Agenda Week" vs "Basic Week")
  1142.                             themeIcon = smartProperty(options.themeButtonIcons, buttonName);
  1143.                             normalIcon = smartProperty(options.buttonIcons, buttonName);
  1144.                             defaultText = smartProperty(options.defaultButtonText, buttonName);
  1145.                             customText = smartProperty(options.buttonText, buttonName);
  1146.  
  1147.                             if (customText) {
  1148.                                 innerHtml = htmlEscape(customText);
  1149.                             }
  1150.                             else if (themeIcon && options.theme) {
  1151.                                 innerHtml = "<span class='ui-icon ui-icon-" + themeIcon + "'></span>";
  1152.                             }
  1153.                             else if (normalIcon && !options.theme) {
  1154.                                 innerHtml = "<span class='fc-icon fc-icon-" + normalIcon + "'></span>";
  1155.                             }
  1156.                             else {
  1157.                                 innerHtml = htmlEscape(defaultText || buttonName);
  1158.                             }
  1159.  
  1160.                             classes = [
  1161.                                 'fc-' + buttonName + '-button',
  1162.                                 tm + '-button',
  1163.                                 tm + '-state-default'
  1164.                             ];
  1165.  
  1166.                             button = $( // type="button" so that it doesn't submit a form
  1167.                                 '<button type="button" class="' + classes.join(' ') + '">' +
  1168.                                     innerHtml +
  1169.                                 '</button>'
  1170.                                 )
  1171.                                 .click(function() {
  1172.                                     // don't process clicks for disabled buttons
  1173.                                     if (!button.hasClass(tm + '-state-disabled')) {
  1174.  
  1175.                                         buttonClick();
  1176.  
  1177.                                         // after the click action, if the button becomes the "active" tab, or disabled,
  1178.                                         // it should never have a hover class, so remove it now.
  1179.                                         if (
  1180.                                             button.hasClass(tm + '-state-active') ||
  1181.                                             button.hasClass(tm + '-state-disabled')
  1182.                                         ) {
  1183.                                             button.removeClass(tm + '-state-hover');
  1184.                                         }
  1185.                                     }
  1186.                                 })
  1187.                                 .mousedown(function() {
  1188.                                     // the *down* effect (mouse pressed in).
  1189.                                     // only on buttons that are not the "active" tab, or disabled
  1190.                                     button
  1191.                                         .not('.' + tm + '-state-active')
  1192.                                         .not('.' + tm + '-state-disabled')
  1193.                                         .addClass(tm + '-state-down');
  1194.                                 })
  1195.                                 .mouseup(function() {
  1196.                                     // undo the *down* effect
  1197.                                     button.removeClass(tm + '-state-down');
  1198.                                 })
  1199.                                 .hover(
  1200.                                     function() {
  1201.                                         // the *hover* effect.
  1202.                                         // only on buttons that are not the "active" tab, or disabled
  1203.                                         button
  1204.                                             .not('.' + tm + '-state-active')
  1205.                                             .not('.' + tm + '-state-disabled')
  1206.                                             .addClass(tm + '-state-hover');
  1207.                                     },
  1208.                                     function() {
  1209.                                         // undo the *hover* effect
  1210.                                         button
  1211.                                             .removeClass(tm + '-state-hover')
  1212.                                             .removeClass(tm + '-state-down'); // if mouseleave happens before mouseup
  1213.                                     }
  1214.                                 );
  1215.  
  1216.                             groupChildren = groupChildren.add(button);
  1217.                         }
  1218.                     }
  1219.                 });
  1220.  
  1221.                 if (isOnlyButtons) {
  1222.                     groupChildren
  1223.                         .first().addClass(tm + '-corner-left').end()
  1224.                         .last().addClass(tm + '-corner-right').end();
  1225.                 }
  1226.  
  1227.                 if (groupChildren.length > 1) {
  1228.                     groupEl = $('<div/>');
  1229.                     if (isOnlyButtons) {
  1230.                         groupEl.addClass('fc-button-group');
  1231.                     }
  1232.                     groupEl.append(groupChildren);
  1233.                     sectionEl.append(groupEl);
  1234.                 }
  1235.                 else {
  1236.                     sectionEl.append(groupChildren); // 1 or 0 children
  1237.                 }
  1238.             });
  1239.         }
  1240.  
  1241.         return sectionEl;
  1242.     }
  1243.    
  1244.    
  1245.     function updateTitle(text) {
  1246.         el.find('h2').text(text);
  1247.     }
  1248.    
  1249.    
  1250.     function activateButton(buttonName) {
  1251.         el.find('.fc-' + buttonName + '-button')
  1252.             .addClass(tm + '-state-active');
  1253.     }
  1254.    
  1255.    
  1256.     function deactivateButton(buttonName) {
  1257.         el.find('.fc-' + buttonName + '-button')
  1258.             .removeClass(tm + '-state-active');
  1259.     }
  1260.    
  1261.    
  1262.     function disableButton(buttonName) {
  1263.         el.find('.fc-' + buttonName + '-button')
  1264.             .attr('disabled', 'disabled')
  1265.             .addClass(tm + '-state-disabled');
  1266.     }
  1267.    
  1268.    
  1269.     function enableButton(buttonName) {
  1270.         el.find('.fc-' + buttonName + '-button')
  1271.             .removeAttr('disabled')
  1272.             .removeClass(tm + '-state-disabled');
  1273.     }
  1274.  
  1275.  
  1276.     function getViewsWithButtons() {
  1277.         return viewsWithButtons;
  1278.     }
  1279.  
  1280. }
  1281.  
  1282. ;;
  1283.  
  1284. fc.sourceNormalizers = [];
  1285. fc.sourceFetchers = [];
  1286.  
  1287. var ajaxDefaults = {
  1288.     dataType: 'json',
  1289.     cache: false
  1290. };
  1291.  
  1292. var eventGUID = 1;
  1293.  
  1294.  
  1295. function EventManager(options) { // assumed to be a calendar
  1296.     var t = this;
  1297.    
  1298.    
  1299.     // exports
  1300.     t.isFetchNeeded = isFetchNeeded;
  1301.     t.fetchEvents = fetchEvents;
  1302.     t.addEventSource = addEventSource;
  1303.     t.removeEventSource = removeEventSource;
  1304.     t.updateEvent = updateEvent;
  1305.     t.renderEvent = renderEvent;
  1306.     t.removeEvents = removeEvents;
  1307.     t.clientEvents = clientEvents;
  1308.     t.mutateEvent = mutateEvent;
  1309.    
  1310.    
  1311.     // imports
  1312.     var trigger = t.trigger;
  1313.     var getView = t.getView;
  1314.     var reportEvents = t.reportEvents;
  1315.     var getEventEnd = t.getEventEnd;
  1316.    
  1317.    
  1318.     // locals
  1319.     var stickySource = { events: [] };
  1320.     var sources = [ stickySource ];
  1321.     var rangeStart, rangeEnd;
  1322.     var currentFetchID = 0;
  1323.     var pendingSourceCnt = 0;
  1324.     var loadingLevel = 0;
  1325.     var cache = []; // holds events that have already been expanded
  1326.  
  1327.  
  1328.     $.each(
  1329.         (options.events ? [ options.events ] : []).concat(options.eventSources || []),
  1330.         function(i, sourceInput) {
  1331.             var source = buildEventSource(sourceInput);
  1332.             if (source) {
  1333.                 sources.push(source);
  1334.             }
  1335.         }
  1336.     );
  1337.    
  1338.    
  1339.    
  1340.     /* Fetching
  1341.     -----------------------------------------------------------------------------*/
  1342.    
  1343.    
  1344.     function isFetchNeeded(start, end) {
  1345.         return !rangeStart || // nothing has been fetched yet?
  1346.             // or, a part of the new range is outside of the old range? (after normalizing)
  1347.             start.clone().stripZone() < rangeStart.clone().stripZone() ||
  1348.             end.clone().stripZone() > rangeEnd.clone().stripZone();
  1349.     }
  1350.    
  1351.    
  1352.     function fetchEvents(start, end) {
  1353.         rangeStart = start;
  1354.         rangeEnd = end;
  1355.         cache = [];
  1356.         var fetchID = ++currentFetchID;
  1357.         var len = sources.length;
  1358.         pendingSourceCnt = len;
  1359.         for (var i=0; i<len; i++) {
  1360.             fetchEventSource(sources[i], fetchID);
  1361.         }
  1362.     }
  1363.    
  1364.    
  1365.     function fetchEventSource(source, fetchID) {
  1366.         _fetchEventSource(source, function(eventInputs) {
  1367.             var isArraySource = $.isArray(source.events);
  1368.             var i, eventInput;
  1369.             var abstractEvent;
  1370.  
  1371.             if (fetchID == currentFetchID) {
  1372.  
  1373.                 if (eventInputs) {
  1374.                     for (i = 0; i < eventInputs.length; i++) {
  1375.                         eventInput = eventInputs[i];
  1376.  
  1377.                         if (isArraySource) { // array sources have already been convert to Event Objects
  1378.                             abstractEvent = eventInput;
  1379.                         }
  1380.                         else {
  1381.                             abstractEvent = buildEventFromInput(eventInput, source);
  1382.                         }
  1383.  
  1384.                         if (abstractEvent) { // not false (an invalid event)
  1385.                             cache.push.apply(
  1386.                                 cache,
  1387.                                 expandEvent(abstractEvent) // add individual expanded events to the cache
  1388.                             );
  1389.                         }
  1390.                     }
  1391.                 }
  1392.  
  1393.                 pendingSourceCnt--;
  1394.                 if (!pendingSourceCnt) {
  1395.                     reportEvents(cache);
  1396.                 }
  1397.             }
  1398.         });
  1399.     }
  1400.    
  1401.    
  1402.     function _fetchEventSource(source, callback) {
  1403.         var i;
  1404.         var fetchers = fc.sourceFetchers;
  1405.         var res;
  1406.  
  1407.         for (i=0; i<fetchers.length; i++) {
  1408.             res = fetchers[i].call(
  1409.                 t, // this, the Calendar object
  1410.                 source,
  1411.                 rangeStart.clone(),
  1412.                 rangeEnd.clone(),
  1413.                 options.timezone,
  1414.                 callback
  1415.             );
  1416.  
  1417.             if (res === true) {
  1418.                 // the fetcher is in charge. made its own async request
  1419.                 return;
  1420.             }
  1421.             else if (typeof res == 'object') {
  1422.                 // the fetcher returned a new source. process it
  1423.                 _fetchEventSource(res, callback);
  1424.                 return;
  1425.             }
  1426.         }
  1427.  
  1428.         var events = source.events;
  1429.         if (events) {
  1430.             if ($.isFunction(events)) {
  1431.                 pushLoading();
  1432.                 events.call(
  1433.                     t, // this, the Calendar object
  1434.                     rangeStart.clone(),
  1435.                     rangeEnd.clone(),
  1436.                     options.timezone,
  1437.                     function(events) {
  1438.                         callback(events);
  1439.                         popLoading();
  1440.                     }
  1441.                 );
  1442.             }
  1443.             else if ($.isArray(events)) {
  1444.                 callback(events);
  1445.             }
  1446.             else {
  1447.                 callback();
  1448.             }
  1449.         }else{
  1450.             var url = source.url;
  1451.             if (url) {
  1452.                 var success = source.success;
  1453.                 var error = source.error;
  1454.                 var complete = source.complete;
  1455.  
  1456.                 // retrieve any outbound GET/POST $.ajax data from the options
  1457.                 var customData;
  1458.                 if ($.isFunction(source.data)) {
  1459.                     // supplied as a function that returns a key/value object
  1460.                     customData = source.data();
  1461.                 }
  1462.                 else {
  1463.                     // supplied as a straight key/value object
  1464.                     customData = source.data;
  1465.                 }
  1466.  
  1467.                 // use a copy of the custom data so we can modify the parameters
  1468.                 // and not affect the passed-in object.
  1469.                 var data = $.extend({}, customData || {});
  1470.  
  1471.                 var startParam = firstDefined(source.startParam, options.startParam);
  1472.                 var endParam = firstDefined(source.endParam, options.endParam);
  1473.                 var timezoneParam = firstDefined(source.timezoneParam, options.timezoneParam);
  1474.  
  1475.                 if (startParam) {
  1476.                     data[startParam] = rangeStart.format();
  1477.                 }
  1478.                 if (endParam) {
  1479.                     data[endParam] = rangeEnd.format();
  1480.                 }
  1481.                 if (options.timezone && options.timezone != 'local') {
  1482.                     data[timezoneParam] = options.timezone;
  1483.                 }
  1484.  
  1485.                 pushLoading();
  1486.                 $.ajax($.extend({}, ajaxDefaults, source, {
  1487.                     data: data,
  1488.                     success: function(events) {
  1489.                         events = events || [];
  1490.                         var res = applyAll(success, this, arguments);
  1491.                         if ($.isArray(res)) {
  1492.                             events = res;
  1493.                         }
  1494.                         callback(events);
  1495.                     },
  1496.                     error: function() {
  1497.                         applyAll(error, this, arguments);
  1498.                         callback();
  1499.                     },
  1500.                     complete: function() {
  1501.                         applyAll(complete, this, arguments);
  1502.                         popLoading();
  1503.                     }
  1504.                 }));
  1505.             }else{
  1506.                 callback();
  1507.             }
  1508.         }
  1509.     }
  1510.    
  1511.    
  1512.    
  1513.     /* Sources
  1514.     -----------------------------------------------------------------------------*/
  1515.    
  1516.  
  1517.     function addEventSource(sourceInput) {
  1518.         var source = buildEventSource(sourceInput);
  1519.         if (source) {
  1520.             sources.push(source);
  1521.             pendingSourceCnt++;
  1522.             fetchEventSource(source, currentFetchID); // will eventually call reportEvents
  1523.         }
  1524.     }
  1525.  
  1526.  
  1527.     function buildEventSource(sourceInput) { // will return undefined if invalid source
  1528.         var normalizers = fc.sourceNormalizers;
  1529.         var source;
  1530.         var i;
  1531.  
  1532.         if ($.isFunction(sourceInput) || $.isArray(sourceInput)) {
  1533.             source = { events: sourceInput };
  1534.         }
  1535.         else if (typeof sourceInput === 'string') {
  1536.             source = { url: sourceInput };
  1537.         }
  1538.         else if (typeof sourceInput === 'object') {
  1539.             source = $.extend({}, sourceInput); // shallow copy
  1540.         }
  1541.  
  1542.         if (source) {
  1543.  
  1544.             // TODO: repeat code, same code for event classNames
  1545.             if (source.className) {
  1546.                 if (typeof source.className === 'string') {
  1547.                     source.className = source.className.split(/\s+/);
  1548.                 }
  1549.                 // otherwise, assumed to be an array
  1550.             }
  1551.             else {
  1552.                 source.className = [];
  1553.             }
  1554.  
  1555.             // for array sources, we convert to standard Event Objects up front
  1556.             if ($.isArray(source.events)) {
  1557.                 source.origArray = source.events; // for removeEventSource
  1558.                 source.events = $.map(source.events, function(eventInput) {
  1559.                     return buildEventFromInput(eventInput, source);
  1560.                 });
  1561.             }
  1562.  
  1563.             for (i=0; i<normalizers.length; i++) {
  1564.                 normalizers[i].call(t, source);
  1565.             }
  1566.  
  1567.             return source;
  1568.         }
  1569.     }
  1570.  
  1571.  
  1572.     function removeEventSource(source) {
  1573.         sources = $.grep(sources, function(src) {
  1574.             return !isSourcesEqual(src, source);
  1575.         });
  1576.         // remove all client events from that source
  1577.         cache = $.grep(cache, function(e) {
  1578.             return !isSourcesEqual(e.source, source);
  1579.         });
  1580.         reportEvents(cache);
  1581.     }
  1582.  
  1583.  
  1584.     function isSourcesEqual(source1, source2) {
  1585.         return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2);
  1586.     }
  1587.  
  1588.  
  1589.     function getSourcePrimitive(source) {
  1590.         return (
  1591.             (typeof source === 'object') ? // a normalized event source?
  1592.                 (source.origArray || source.url || source.events) : // get the primitive
  1593.                 null
  1594.         ) ||
  1595.         source; // the given argument *is* the primitive
  1596.     }
  1597.    
  1598.    
  1599.    
  1600.     /* Manipulation
  1601.     -----------------------------------------------------------------------------*/
  1602.  
  1603.  
  1604.     function updateEvent(event) {
  1605.  
  1606.         event.start = t.moment(event.start);
  1607.         if (event.end) {
  1608.             event.end = t.moment(event.end);
  1609.         }
  1610.  
  1611.         mutateEvent(event);
  1612.         propagateMiscProperties(event);
  1613.         reportEvents(cache); // reports event modifications (so we can redraw)
  1614.     }
  1615.  
  1616.  
  1617.     var miscCopyableProps = [
  1618.         'title',
  1619.         'url',
  1620.         'allDay',
  1621.         'className',
  1622.         'editable',
  1623.         'color',
  1624.         'backgroundColor',
  1625.         'borderColor',
  1626.         'textColor'
  1627.     ];
  1628.  
  1629.     function propagateMiscProperties(event) {
  1630.         var i;
  1631.         var cachedEvent;
  1632.         var j;
  1633.         var prop;
  1634.  
  1635.         for (i=0; i<cache.length; i++) {
  1636.             cachedEvent = cache[i];
  1637.             if (cachedEvent._id == event._id && cachedEvent !== event) {
  1638.                 for (j=0; j<miscCopyableProps.length; j++) {
  1639.                     prop = miscCopyableProps[j];
  1640.                     if (event[prop] !== undefined) {
  1641.                         cachedEvent[prop] = event[prop];
  1642.                     }
  1643.                 }
  1644.             }
  1645.         }
  1646.     }
  1647.  
  1648.    
  1649.     // returns the expanded events that were created
  1650.     function renderEvent(eventInput, stick) {
  1651.         var abstractEvent = buildEventFromInput(eventInput);
  1652.         var events;
  1653.         var i, event;
  1654.  
  1655.         if (abstractEvent) { // not false (a valid input)
  1656.             events = expandEvent(abstractEvent);
  1657.  
  1658.             for (i = 0; i < events.length; i++) {
  1659.                 event = events[i];
  1660.  
  1661.                 if (!event.source) {
  1662.                     if (stick) {
  1663.                         stickySource.events.push(event);
  1664.                         event.source = stickySource;
  1665.                     }
  1666.                     cache.push(event);
  1667.                 }
  1668.             }
  1669.  
  1670.             reportEvents(cache);
  1671.  
  1672.             return events;
  1673.         }
  1674.  
  1675.         return [];
  1676.     }
  1677.    
  1678.    
  1679.     function removeEvents(filter) {
  1680.         var eventID;
  1681.         var i;
  1682.  
  1683.         if (filter == null) { // null or undefined. remove all events
  1684.             filter = function() { return true; }; // will always match
  1685.         }
  1686.         else if (!$.isFunction(filter)) { // an event ID
  1687.             eventID = filter + '';
  1688.             filter = function(event) {
  1689.                 return event._id == eventID;
  1690.             };
  1691.         }
  1692.  
  1693.         // Purge event(s) from our local cache
  1694.         cache = $.grep(cache, filter, true); // inverse=true
  1695.  
  1696.         // Remove events from array sources.
  1697.         // This works because they have been converted to official Event Objects up front.
  1698.         // (and as a result, event._id has been calculated).
  1699.         for (i=0; i<sources.length; i++) {
  1700.             if ($.isArray(sources[i].events)) {
  1701.                 sources[i].events = $.grep(sources[i].events, filter, true);
  1702.             }
  1703.         }
  1704.  
  1705.         reportEvents(cache);
  1706.     }
  1707.    
  1708.    
  1709.     function clientEvents(filter) {
  1710.         if ($.isFunction(filter)) {
  1711.             return $.grep(cache, filter);
  1712.         }
  1713.         else if (filter != null) { // not null, not undefined. an event ID
  1714.             filter += '';
  1715.             return $.grep(cache, function(e) {
  1716.                 return e._id == filter;
  1717.             });
  1718.         }
  1719.         return cache; // else, return all
  1720.     }
  1721.    
  1722.    
  1723.    
  1724.     /* Loading State
  1725.     -----------------------------------------------------------------------------*/
  1726.    
  1727.    
  1728.     function pushLoading() {
  1729.         if (!(loadingLevel++)) {
  1730.             trigger('loading', null, true, getView());
  1731.         }
  1732.     }
  1733.    
  1734.    
  1735.     function popLoading() {
  1736.         if (!(--loadingLevel)) {
  1737.             trigger('loading', null, false, getView());
  1738.         }
  1739.     }
  1740.    
  1741.    
  1742.    
  1743.     /* Event Normalization
  1744.     -----------------------------------------------------------------------------*/
  1745.  
  1746.  
  1747.     // Given a raw object with key/value properties, returns an "abstract" Event object.
  1748.     // An "abstract" event is an event that, if recurring, will not have been expanded yet.
  1749.     // Will return `false` when input is invalid.
  1750.     // `source` is optional
  1751.     function buildEventFromInput(input, source) {
  1752.         var out = {};
  1753.         var start, end;
  1754.         var allDay;
  1755.         var allDayDefault;
  1756.  
  1757.         if (options.eventDataTransform) {
  1758.             input = options.eventDataTransform(input);
  1759.         }
  1760.         if (source && source.eventDataTransform) {
  1761.             input = source.eventDataTransform(input);
  1762.         }
  1763.  
  1764.         // Copy all properties over to the resulting object.
  1765.         // The special-case properties will be copied over afterwards.
  1766.         $.extend(out, input);
  1767.  
  1768.         if (source) {
  1769.             out.source = source;
  1770.         }
  1771.  
  1772.         out._id = input._id || (input.id === undefined ? '_fc' + eventGUID++ : input.id + '');
  1773.  
  1774.         if (input.className) {
  1775.             if (typeof input.className == 'string') {
  1776.                 out.className = input.className.split(/\s+/);
  1777.             }
  1778.             else { // assumed to be an array
  1779.                 out.className = input.className;
  1780.             }
  1781.         }
  1782.         else {
  1783.             out.className = [];
  1784.         }
  1785.  
  1786.         start = input.start || input.date; // "date" is an alias for "start"
  1787.         end = input.end;
  1788.  
  1789.         // parse as a time (Duration) if applicable
  1790.         if (isTimeString(start)) {
  1791.             start = moment.duration(start);
  1792.         }
  1793.         if (isTimeString(end)) {
  1794.             end = moment.duration(end);
  1795.         }
  1796.  
  1797.         if (input.dow || moment.isDuration(start) || moment.isDuration(end)) {
  1798.  
  1799.             // the event is "abstract" (recurring) so don't calculate exact start/end dates just yet
  1800.             out.start = start ? moment.duration(start) : null; // will be a Duration or null
  1801.             out.end = end ? moment.duration(end) : null; // will be a Duration or null
  1802.             out._recurring = true; // our internal marker
  1803.         }
  1804.         else {
  1805.  
  1806.             if (start) {
  1807.                 start = t.moment(start);
  1808.                 if (!start.isValid()) {
  1809.                     return false;
  1810.                 }
  1811.             }
  1812.  
  1813.             if (end) {
  1814.                 end = t.moment(end);
  1815.                 if (!end.isValid()) {
  1816.                     return false;
  1817.                 }
  1818.             }
  1819.  
  1820.             allDay = input.allDay;
  1821.             if (allDay === undefined) {
  1822.                 allDayDefault = firstDefined(
  1823.                     source ? source.allDayDefault : undefined,
  1824.                     options.allDayDefault
  1825.                 );
  1826.                 if (allDayDefault !== undefined) {
  1827.                     // use the default
  1828.                     allDay = allDayDefault;
  1829.                 }
  1830.                 else {
  1831.                     // if a single date has a time, the event should not be all-day
  1832.                     allDay = !start.hasTime() && (!end || !end.hasTime());
  1833.                 }
  1834.             }
  1835.  
  1836.             assignDatesToEvent(start, end, allDay, out);
  1837.         }
  1838.  
  1839.         return out;
  1840.     }
  1841.  
  1842.  
  1843.     // Normalizes and assigns the given dates to the given partially-formed event object.
  1844.     // Requires an explicit `allDay` boolean parameter.
  1845.     // NOTE: mutates the given start/end moments. does not make an internal copy
  1846.     function assignDatesToEvent(start, end, allDay, event) {
  1847.  
  1848.         // normalize the date based on allDay
  1849.         if (allDay) {
  1850.             // neither date should have a time
  1851.             if (start.hasTime()) {
  1852.                 start.stripTime();
  1853.             }
  1854.             if (end && end.hasTime()) {
  1855.                 end.stripTime();
  1856.             }
  1857.         }
  1858.         else {
  1859.             // force a time/zone up the dates
  1860.             if (!start.hasTime()) {
  1861.                 start = t.rezoneDate(start);
  1862.             }
  1863.             if (end && !end.hasTime()) {
  1864.                 end = t.rezoneDate(end);
  1865.             }
  1866.         }
  1867.  
  1868.         event.allDay = allDay;
  1869.         event.start = start;
  1870.         event.end = end || null; // ensure null if falsy
  1871.  
  1872.         if (options.forceEventDuration && !event.end) {
  1873.             event.end = getEventEnd(event);
  1874.         }
  1875.  
  1876.         backupEventDates(event);
  1877.     }
  1878.  
  1879.  
  1880.     // If the given event is a recurring event, break it down into an array of individual instances.
  1881.     // If not a recurring event, return an array with the single original event.
  1882.     // If given a falsy input (probably because of a failed buildEventFromInput call), returns an empty array.
  1883.     function expandEvent(abstractEvent) {
  1884.         var events = [];
  1885.         var view;
  1886.         var _rangeStart = rangeStart;
  1887.         var _rangeEnd = rangeEnd;
  1888.         var dowHash;
  1889.         var dow;
  1890.         var i;
  1891.         var date;
  1892.         var startTime, endTime;
  1893.         var start, end;
  1894.         var event;
  1895.  
  1896.         // hack for when fetchEvents hasn't been called yet (calculating businessHours for example)
  1897.         if (!_rangeStart || !_rangeEnd) {
  1898.             view = t.getView();
  1899.             _rangeStart = view.start;
  1900.             _rangeEnd = view.end;
  1901.         }
  1902.  
  1903.         if (abstractEvent) {
  1904.             if (abstractEvent._recurring) {
  1905.  
  1906.                 // make a boolean hash as to whether the event occurs on each day-of-week
  1907.                 if ((dow = abstractEvent.dow)) {
  1908.                     dowHash = {};
  1909.                     for (i = 0; i < dow.length; i++) {
  1910.                         dowHash[dow[i]] = true;
  1911.                     }
  1912.                 }
  1913.  
  1914.                 // iterate through every day in the current range
  1915.                 date = _rangeStart.clone().stripTime(); // holds the date of the current day
  1916.                 while (date.isBefore(_rangeEnd)) {
  1917.  
  1918.                     if (!dowHash || dowHash[date.day()]) { // if everyday, or this particular day-of-week
  1919.  
  1920.                         startTime = abstractEvent.start; // the stored start and end properties are times (Durations)
  1921.                         endTime = abstractEvent.end; // "
  1922.                         start = date.clone();
  1923.                         end = null;
  1924.  
  1925.                         if (startTime) {
  1926.                             start = start.time(startTime);
  1927.                         }
  1928.                         if (endTime) {
  1929.                             end = date.clone().time(endTime);
  1930.                         }
  1931.  
  1932.                         event = $.extend({}, abstractEvent); // make a copy of the original
  1933.                         assignDatesToEvent(
  1934.                             start, end,
  1935.                             !startTime && !endTime, // allDay?
  1936.                             event
  1937.                         );
  1938.                         events.push(event);
  1939.                     }
  1940.  
  1941.                     date.add(1, 'days');
  1942.                 }
  1943.             }
  1944.             else {
  1945.                 events.push(abstractEvent); // return the original event. will be a one-item array
  1946.             }
  1947.         }
  1948.  
  1949.         return events;
  1950.     }
  1951.  
  1952.  
  1953.  
  1954.     /* Event Modification Math
  1955.     -----------------------------------------------------------------------------------------*/
  1956.  
  1957.  
  1958.     // Modify the date(s) of an event and make this change propagate to all other events with
  1959.     // the same ID (related repeating events).
  1960.     //
  1961.     // If `newStart`/`newEnd` are not specified, the "new" dates are assumed to be `event.start` and `event.end`.
  1962.     // The "old" dates to be compare against are always `event._start` and `event._end` (set by EventManager).
  1963.     //
  1964.     // Returns an object with delta information and a function to undo all operations.
  1965.     //
  1966.     function mutateEvent(event, newStart, newEnd) {
  1967.         var oldAllDay = event._allDay;
  1968.         var oldStart = event._start;
  1969.         var oldEnd = event._end;
  1970.         var clearEnd = false;
  1971.         var newAllDay;
  1972.         var dateDelta;
  1973.         var durationDelta;
  1974.         var undoFunc;
  1975.  
  1976.         // if no new dates were passed in, compare against the event's existing dates
  1977.         if (!newStart && !newEnd) {
  1978.             newStart = event.start;
  1979.             newEnd = event.end;
  1980.         }
  1981.  
  1982.         // NOTE: throughout this function, the initial values of `newStart` and `newEnd` are
  1983.         // preserved. These values may be undefined.
  1984.  
  1985.         // detect new allDay
  1986.         if (event.allDay != oldAllDay) { // if value has changed, use it
  1987.             newAllDay = event.allDay;
  1988.         }
  1989.         else { // otherwise, see if any of the new dates are allDay
  1990.             newAllDay = !(newStart || newEnd).hasTime();
  1991.         }
  1992.  
  1993.         // normalize the new dates based on allDay
  1994.         if (newAllDay) {
  1995.             if (newStart) {
  1996.                 newStart = newStart.clone().stripTime();
  1997.             }
  1998.             if (newEnd) {
  1999.                 newEnd = newEnd.clone().stripTime();
  2000.             }
  2001.         }
  2002.  
  2003.         // compute dateDelta
  2004.         if (newStart) {
  2005.             if (newAllDay) {
  2006.                 dateDelta = dayishDiff(newStart, oldStart.clone().stripTime()); // treat oldStart as allDay
  2007.             }
  2008.             else {
  2009.                 dateDelta = dayishDiff(newStart, oldStart);
  2010.             }
  2011.         }
  2012.  
  2013.         if (newAllDay != oldAllDay) {
  2014.             // if allDay has changed, always throw away the end
  2015.             clearEnd = true;
  2016.         }
  2017.         else if (newEnd) {
  2018.             durationDelta = dayishDiff(
  2019.                 // new duration
  2020.                 newEnd || t.getDefaultEventEnd(newAllDay, newStart || oldStart),
  2021.                 newStart || oldStart
  2022.             ).subtract(dayishDiff(
  2023.                 // subtract old duration
  2024.                 oldEnd || t.getDefaultEventEnd(oldAllDay, oldStart),
  2025.                 oldStart
  2026.             ));
  2027.         }
  2028.  
  2029.         undoFunc = mutateEvents(
  2030.             clientEvents(event._id), // get events with this ID
  2031.             clearEnd,
  2032.             newAllDay,
  2033.             dateDelta,
  2034.             durationDelta
  2035.         );
  2036.  
  2037.         return {
  2038.             dateDelta: dateDelta,
  2039.             durationDelta: durationDelta,
  2040.             undo: undoFunc
  2041.         };
  2042.     }
  2043.  
  2044.  
  2045.     // Modifies an array of events in the following ways (operations are in order):
  2046.     // - clear the event's `end`
  2047.     // - convert the event to allDay
  2048.     // - add `dateDelta` to the start and end
  2049.     // - add `durationDelta` to the event's duration
  2050.     //
  2051.     // Returns a function that can be called to undo all the operations.
  2052.     //
  2053.     function mutateEvents(events, clearEnd, forceAllDay, dateDelta, durationDelta) {
  2054.         var isAmbigTimezone = t.getIsAmbigTimezone();
  2055.         var undoFunctions = [];
  2056.  
  2057.         $.each(events, function(i, event) {
  2058.             var oldAllDay = event._allDay;
  2059.             var oldStart = event._start;
  2060.             var oldEnd = event._end;
  2061.             var newAllDay = forceAllDay != null ? forceAllDay : oldAllDay;
  2062.             var newStart = oldStart.clone();
  2063.             var newEnd = (!clearEnd && oldEnd) ? oldEnd.clone() : null;
  2064.  
  2065.             // NOTE: this function is responsible for transforming `newStart` and `newEnd`,
  2066.             // which were initialized to the OLD values first. `newEnd` may be null.
  2067.  
  2068.             // normlize newStart/newEnd to be consistent with newAllDay
  2069.             if (newAllDay) {
  2070.                 newStart.stripTime();
  2071.                 if (newEnd) {
  2072.                     newEnd.stripTime();
  2073.                 }
  2074.             }
  2075.             else {
  2076.                 if (!newStart.hasTime()) {
  2077.                     newStart = t.rezoneDate(newStart);
  2078.                 }
  2079.                 if (newEnd && !newEnd.hasTime()) {
  2080.                     newEnd = t.rezoneDate(newEnd);
  2081.                 }
  2082.             }
  2083.  
  2084.             // ensure we have an end date if necessary
  2085.             if (!newEnd && (options.forceEventDuration || +durationDelta)) {
  2086.                 newEnd = t.getDefaultEventEnd(newAllDay, newStart);
  2087.             }
  2088.  
  2089.             // translate the dates
  2090.             newStart.add(dateDelta);
  2091.             if (newEnd) {
  2092.                 newEnd.add(dateDelta).add(durationDelta);
  2093.             }
  2094.  
  2095.             // if the dates have changed, and we know it is impossible to recompute the
  2096.             // timezone offsets, strip the zone.
  2097.             if (isAmbigTimezone) {
  2098.                 if (+dateDelta || +durationDelta) {
  2099.                     newStart.stripZone();
  2100.                     if (newEnd) {
  2101.                         newEnd.stripZone();
  2102.                     }
  2103.                 }
  2104.             }
  2105.  
  2106.             event.allDay = newAllDay;
  2107.             event.start = newStart;
  2108.             event.end = newEnd;
  2109.             backupEventDates(event);
  2110.  
  2111.             undoFunctions.push(function() {
  2112.                 event.allDay = oldAllDay;
  2113.                 event.start = oldStart;
  2114.                 event.end = oldEnd;
  2115.                 backupEventDates(event);
  2116.             });
  2117.         });
  2118.  
  2119.         return function() {
  2120.             for (var i=0; i<undoFunctions.length; i++) {
  2121.                 undoFunctions[i]();
  2122.             }
  2123.         };
  2124.     }
  2125.  
  2126.  
  2127.     /* Business Hours
  2128.     -----------------------------------------------------------------------------------------*/
  2129.  
  2130.     t.getBusinessHoursEvents = getBusinessHoursEvents;
  2131.  
  2132.  
  2133.     // Returns an array of events as to when the business hours occur in the current view.
  2134.     // Abuse of our event system :(
  2135.     function getBusinessHoursEvents() {
  2136.         var optionVal = options.businessHours;
  2137.         var defaultVal = {
  2138.             className: 'fc-nonbusiness',
  2139.             start: '09:00',
  2140.             end: '17:00',
  2141.             dow: [ 1, 2, 3, 4, 5 ], // monday - friday
  2142.             rendering: 'inverse-background'
  2143.         };
  2144.         var eventInput;
  2145.  
  2146.         if (optionVal) {
  2147.             if (typeof optionVal === 'object') {
  2148.                 // option value is an object that can override the default business hours
  2149.                 eventInput = $.extend({}, defaultVal, optionVal);
  2150.             }
  2151.             else {
  2152.                 // option value is `true`. use default business hours
  2153.                 eventInput = defaultVal;
  2154.             }
  2155.         }
  2156.  
  2157.         if (eventInput) {
  2158.             return expandEvent(buildEventFromInput(eventInput));
  2159.         }
  2160.  
  2161.         return [];
  2162.     }
  2163.  
  2164.  
  2165.     /* Overlapping / Constraining
  2166.     -----------------------------------------------------------------------------------------*/
  2167.  
  2168.     t.isEventAllowedInRange = isEventAllowedInRange;
  2169.     t.isSelectionAllowedInRange = isSelectionAllowedInRange;
  2170.     t.isExternalDragAllowedInRange = isExternalDragAllowedInRange;
  2171.  
  2172.  
  2173.     function isEventAllowedInRange(event, start, end) {
  2174.         var source = event.source || {};
  2175.         var constraint = firstDefined(
  2176.             event.constraint,
  2177.             source.constraint,
  2178.             options.eventConstraint
  2179.         );
  2180.         var overlap = firstDefined(
  2181.             event.overlap,
  2182.             source.overlap,
  2183.             options.eventOverlap
  2184.         );
  2185.  
  2186.         return isRangeAllowed(start, end, constraint, overlap, event);
  2187.     }
  2188.  
  2189.  
  2190.     function isSelectionAllowedInRange(start, end) {
  2191.         return isRangeAllowed(
  2192.             start,
  2193.             end,
  2194.             options.selectConstraint,
  2195.             options.selectOverlap
  2196.         );
  2197.     }
  2198.  
  2199.  
  2200.     function isExternalDragAllowedInRange(start, end, eventInput) { // eventInput is optional associated event data
  2201.         var event;
  2202.  
  2203.         if (eventInput) {
  2204.             event = expandEvent(buildEventFromInput(eventInput))[0];
  2205.             if (event) {
  2206.                 return isEventAllowedInRange(event, start, end);
  2207.             }
  2208.         }
  2209.  
  2210.         return isSelectionAllowedInRange(start, end); // treat it as a selection
  2211.     }
  2212.  
  2213.  
  2214.     // Returns true if the given range (caused by an event drop/resize or a selection) is allowed to exist
  2215.     // according to the constraint/overlap settings.
  2216.     // `event` is not required if checking a selection.
  2217.     function isRangeAllowed(start, end, constraint, overlap, event) {
  2218.         var constraintEvents;
  2219.         var anyContainment;
  2220.         var i, otherEvent;
  2221.         var otherOverlap;
  2222.  
  2223.         // normalize. fyi, we're normalizing in too many places :(
  2224.         start = start.clone().stripZone();
  2225.         end = end.clone().stripZone();
  2226.  
  2227.         // the range must be fully contained by at least one of produced constraint events
  2228.         if (constraint != null) {
  2229.             constraintEvents = constraintToEvents(constraint);
  2230.             anyContainment = false;
  2231.  
  2232.             for (i = 0; i < constraintEvents.length; i++) {
  2233.                 if (eventContainsRange(constraintEvents[i], start, end)) {
  2234.                     anyContainment = true;
  2235.                     break;
  2236.                 }
  2237.             }
  2238.  
  2239.             if (!anyContainment) {
  2240.                 return false;
  2241.             }
  2242.         }
  2243.  
  2244.         for (i = 0; i < cache.length; i++) { // loop all events and detect overlap
  2245.             otherEvent = cache[i];
  2246.  
  2247.             // don't compare the event to itself or other related [repeating] events
  2248.             if (event && event._id === otherEvent._id) {
  2249.                 continue;
  2250.             }
  2251.  
  2252.             // there needs to be an actual intersection before disallowing anything
  2253.             if (eventIntersectsRange(otherEvent, start, end)) {
  2254.  
  2255.                 // evaluate overlap for the given range and short-circuit if necessary
  2256.                 if (overlap === false) {
  2257.                     return false;
  2258.                 }
  2259.                 else if (typeof overlap === 'function' && !overlap(otherEvent, event)) {
  2260.                     return false;
  2261.                 }
  2262.  
  2263.                 // if we are computing if the given range is allowable for an event, consider the other event's
  2264.                 // EventObject-specific or Source-specific `overlap` property
  2265.                 if (event) {
  2266.                     otherOverlap = firstDefined(
  2267.                         otherEvent.overlap,
  2268.                         (otherEvent.source || {}).overlap
  2269.                         // we already considered the global `eventOverlap`
  2270.                     );
  2271.                     if (otherOverlap === false) {
  2272.                         return false;
  2273.                     }
  2274.                     if (typeof otherOverlap === 'function' && !otherOverlap(event, otherEvent)) {
  2275.                         return false;
  2276.                     }
  2277.                 }
  2278.             }
  2279.         }
  2280.  
  2281.         return true;
  2282.     }
  2283.  
  2284.  
  2285.     // Given an event input from the API, produces an array of event objects. Possible event inputs:
  2286.     // 'businessHours'
  2287.     // An event ID (number or string)
  2288.     // An object with specific start/end dates or a recurring event (like what businessHours accepts)
  2289.     function constraintToEvents(constraintInput) {
  2290.  
  2291.         if (constraintInput === 'businessHours') {
  2292.             return getBusinessHoursEvents();
  2293.         }
  2294.  
  2295.         if (typeof constraintInput === 'object') {
  2296.             return expandEvent(buildEventFromInput(constraintInput));
  2297.         }
  2298.  
  2299.         return clientEvents(constraintInput); // probably an ID
  2300.     }
  2301.  
  2302.  
  2303.     // Is the event's date ranged fully contained by the given range?
  2304.     // start/end already assumed to have stripped zones :(
  2305.     function eventContainsRange(event, start, end) {
  2306.         var eventStart = event.start.clone().stripZone();
  2307.         var eventEnd = t.getEventEnd(event).stripZone();
  2308.  
  2309.         return start >= eventStart && end <= eventEnd;
  2310.     }
  2311.  
  2312.  
  2313.     // Does the event's date range intersect with the given range?
  2314.     // start/end already assumed to have stripped zones :(
  2315.     function eventIntersectsRange(event, start, end) {
  2316.         var eventStart = event.start.clone().stripZone();
  2317.         var eventEnd = t.getEventEnd(event).stripZone();
  2318.  
  2319.         return start < eventEnd && end > eventStart;
  2320.     }
  2321.  
  2322. }
  2323.  
  2324.  
  2325. // updates the "backup" properties, which are preserved in order to compute diffs later on.
  2326. function backupEventDates(event) {
  2327.     event._allDay = event.allDay;
  2328.     event._start = event.start.clone();
  2329.     event._end = event.end ? event.end.clone() : null;
  2330. }
  2331.  
  2332. ;;
  2333.  
  2334. /* FullCalendar-specific DOM Utilities
  2335. ----------------------------------------------------------------------------------------------------------------------*/
  2336.  
  2337.  
  2338. // Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left
  2339. // and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that.
  2340. function compensateScroll(rowEls, scrollbarWidths) {
  2341.     if (scrollbarWidths.left) {
  2342.         rowEls.css({
  2343.             'border-left-width': 1,
  2344.             'margin-left': scrollbarWidths.left - 1
  2345.         });
  2346.     }
  2347.     if (scrollbarWidths.right) {
  2348.         rowEls.css({
  2349.             'border-right-width': 1,
  2350.             'margin-right': scrollbarWidths.right - 1
  2351.         });
  2352.     }
  2353. }
  2354.  
  2355.  
  2356. // Undoes compensateScroll and restores all borders/margins
  2357. function uncompensateScroll(rowEls) {
  2358.     rowEls.css({
  2359.         'margin-left': '',
  2360.         'margin-right': '',
  2361.         'border-left-width': '',
  2362.         'border-right-width': ''
  2363.     });
  2364. }
  2365.  
  2366.  
  2367. // Make the mouse cursor express that an event is not allowed in the current area
  2368. function disableCursor() {
  2369.     $('body').addClass('fc-not-allowed');
  2370. }
  2371.  
  2372.  
  2373. // Returns the mouse cursor to its original look
  2374. function enableCursor() {
  2375.     $('body').removeClass('fc-not-allowed');
  2376. }
  2377.  
  2378.  
  2379. // Given a total available height to fill, have `els` (essentially child rows) expand to accomodate.
  2380. // By default, all elements that are shorter than the recommended height are expanded uniformly, not considering
  2381. // any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and
  2382. // reduces the available height.
  2383. function distributeHeight(els, availableHeight, shouldRedistribute) {
  2384.  
  2385.     // *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions,
  2386.     // and it is better to be shorter than taller, to avoid creating unnecessary scrollbars.
  2387.  
  2388.     var minOffset1 = Math.floor(availableHeight / els.length); // for non-last element
  2389.     var minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)); // for last element *FLOORING NOTE*
  2390.     var flexEls = []; // elements that are allowed to expand. array of DOM nodes
  2391.     var flexOffsets = []; // amount of vertical space it takes up
  2392.     var flexHeights = []; // actual css height
  2393.     var usedHeight = 0;
  2394.  
  2395.     undistributeHeight(els); // give all elements their natural height
  2396.  
  2397.     // find elements that are below the recommended height (expandable).
  2398.     // important to query for heights in a single first pass (to avoid reflow oscillation).
  2399.     els.each(function(i, el) {
  2400.         var minOffset = i === els.length - 1 ? minOffset2 : minOffset1;
  2401.         var naturalOffset = $(el).outerHeight(true);
  2402.  
  2403.         if (naturalOffset < minOffset) {
  2404.             flexEls.push(el);
  2405.             flexOffsets.push(naturalOffset);
  2406.             flexHeights.push($(el).height());
  2407.         }
  2408.         else {
  2409.             // this element stretches past recommended height (non-expandable). mark the space as occupied.
  2410.             usedHeight += naturalOffset;
  2411.         }
  2412.     });
  2413.  
  2414.     // readjust the recommended height to only consider the height available to non-maxed-out rows.
  2415.     if (shouldRedistribute) {
  2416.         availableHeight -= usedHeight;
  2417.         minOffset1 = Math.floor(availableHeight / flexEls.length);
  2418.         minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)); // *FLOORING NOTE*
  2419.     }
  2420.  
  2421.     // assign heights to all expandable elements
  2422.     $(flexEls).each(function(i, el) {
  2423.         var minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1;
  2424.         var naturalOffset = flexOffsets[i];
  2425.         var naturalHeight = flexHeights[i];
  2426.         var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding
  2427.  
  2428.         if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things
  2429.             $(el).height(newHeight);
  2430.         }
  2431.     });
  2432. }
  2433.  
  2434.  
  2435. // Undoes distrubuteHeight, restoring all els to their natural height
  2436. function undistributeHeight(els) {
  2437.     els.height('');
  2438. }
  2439.  
  2440.  
  2441. // Given `els`, a jQuery set of <td> cells, find the cell with the largest natural width and set the widths of all the
  2442. // cells to be that width.
  2443. // PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline
  2444. function matchCellWidths(els) {
  2445.     var maxInnerWidth = 0;
  2446.  
  2447.     els.find('> *').each(function(i, innerEl) {
  2448.         var innerWidth = $(innerEl).outerWidth();
  2449.         if (innerWidth > maxInnerWidth) {
  2450.             maxInnerWidth = innerWidth;
  2451.         }
  2452.     });
  2453.  
  2454.     maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance
  2455.  
  2456.     els.width(maxInnerWidth);
  2457.  
  2458.     return maxInnerWidth;
  2459. }
  2460.  
  2461.  
  2462. // Turns a container element into a scroller if its contents is taller than the allotted height.
  2463. // Returns true if the element is now a scroller, false otherwise.
  2464. // NOTE: this method is best because it takes weird zooming dimensions into account
  2465. function setPotentialScroller(containerEl, height) {
  2466.     containerEl.height(height).addClass('fc-scroller');
  2467.  
  2468.     // are scrollbars needed?
  2469.     if (containerEl[0].scrollHeight - 1 > containerEl[0].clientHeight) { // !!! -1 because IE is often off-by-one :(
  2470.         return true;
  2471.     }
  2472.  
  2473.     unsetScroller(containerEl); // undo
  2474.     return false;
  2475. }
  2476.  
  2477.  
  2478. // Takes an element that might have been a scroller, and turns it back into a normal element.
  2479. function unsetScroller(containerEl) {
  2480.     containerEl.height('').removeClass('fc-scroller');
  2481. }
  2482.  
  2483.  
  2484. /* General DOM Utilities
  2485. ----------------------------------------------------------------------------------------------------------------------*/
  2486.  
  2487.  
  2488. // borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51
  2489. function getScrollParent(el) {
  2490.     var position = el.css('position'),
  2491.         scrollParent = el.parents().filter(function() {
  2492.             var parent = $(this);
  2493.             return (/(auto|scroll)/).test(
  2494.                 parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x')
  2495.             );
  2496.         }).eq(0);
  2497.  
  2498.     return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent;
  2499. }
  2500.  
  2501.  
  2502. // Given a container element, return an object with the pixel values of the left/right scrollbars.
  2503. // Left scrollbars might occur on RTL browsers (IE maybe?) but I have not tested.
  2504. // PREREQUISITE: container element must have a single child with display:block
  2505. function getScrollbarWidths(container) {
  2506.     var containerLeft = container.offset().left;
  2507.     var containerRight = containerLeft + container.width();
  2508.     var inner = container.children();
  2509.     var innerLeft = inner.offset().left;
  2510.     var innerRight = innerLeft + inner.outerWidth();
  2511.  
  2512.     return {
  2513.         left: innerLeft - containerLeft,
  2514.         right: containerRight - innerRight
  2515.     };
  2516. }
  2517.  
  2518.  
  2519. // Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
  2520. function isPrimaryMouseButton(ev) {
  2521.     return ev.which == 1 && !ev.ctrlKey;
  2522. }
  2523.  
  2524.  
  2525. /* FullCalendar-specific Misc Utilities
  2526. ----------------------------------------------------------------------------------------------------------------------*/
  2527.  
  2528.  
  2529. // Creates a basic segment with the intersection of the two ranges. Returns undefined if no intersection.
  2530. // Expects all dates to be normalized to the same timezone beforehand.
  2531. function intersectionToSeg(subjectStart, subjectEnd, intervalStart, intervalEnd) {
  2532.     var segStart, segEnd;
  2533.     var isStart, isEnd;
  2534.  
  2535.     if (subjectEnd > intervalStart && subjectStart < intervalEnd) { // in bounds at all?
  2536.  
  2537.         if (subjectStart >= intervalStart) {
  2538.             segStart = subjectStart.clone();
  2539.             isStart = true;
  2540.         }
  2541.         else {
  2542.             segStart = intervalStart.clone();
  2543.             isStart =  false;
  2544.         }
  2545.  
  2546.         if (subjectEnd <= intervalEnd) {
  2547.             segEnd = subjectEnd.clone();
  2548.             isEnd = true;
  2549.         }
  2550.         else {
  2551.             segEnd = intervalEnd.clone();
  2552.             isEnd = false;
  2553.         }
  2554.  
  2555.         return {
  2556.             start: segStart,
  2557.             end: segEnd,
  2558.             isStart: isStart,
  2559.             isEnd: isEnd
  2560.         };
  2561.     }
  2562. }
  2563.  
  2564.  
  2565. function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object
  2566.     obj = obj || {};
  2567.     if (obj[name] !== undefined) {
  2568.         return obj[name];
  2569.     }
  2570.     var parts = name.split(/(?=[A-Z])/),
  2571.         i = parts.length - 1, res;
  2572.     for (; i>=0; i--) {
  2573.         res = obj[parts[i].toLowerCase()];
  2574.         if (res !== undefined) {
  2575.             return res;
  2576.         }
  2577.     }
  2578.     return obj['default'];
  2579. }
  2580.  
  2581.  
  2582. /* Date Utilities
  2583. ----------------------------------------------------------------------------------------------------------------------*/
  2584.  
  2585. var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ];
  2586.  
  2587.  
  2588. // Diffs the two moments into a Duration where full-days are recorded first, then the remaining time.
  2589. // Moments will have their timezones normalized.
  2590. function dayishDiff(a, b) {
  2591.     return moment.duration({
  2592.         days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'),
  2593.         ms: a.time() - b.time()
  2594.     });
  2595. }
  2596.  
  2597.  
  2598. function isNativeDate(input) {
  2599.     return  Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date;
  2600. }
  2601.  
  2602.  
  2603. function dateCompare(a, b) { // works with Moments and native Dates
  2604.     return a - b;
  2605. }
  2606.  
  2607.  
  2608. // Returns a boolean about whether the given input is a time string, like "06:40:00" or "06:00"
  2609. function isTimeString(str) {
  2610.     return /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str);
  2611. }
  2612.  
  2613.  
  2614. /* General Utilities
  2615. ----------------------------------------------------------------------------------------------------------------------*/
  2616.  
  2617. fc.applyAll = applyAll; // export
  2618.  
  2619.  
  2620. // Create an object that has the given prototype. Just like Object.create
  2621. function createObject(proto) {
  2622.     var f = function() {};
  2623.     f.prototype = proto;
  2624.     return new f();
  2625. }
  2626.  
  2627.  
  2628. function applyAll(functions, thisObj, args) {
  2629.     if ($.isFunction(functions)) {
  2630.         functions = [ functions ];
  2631.     }
  2632.     if (functions) {
  2633.         var i;
  2634.         var ret;
  2635.         for (i=0; i<functions.length; i++) {
  2636.             ret = functions[i].apply(thisObj, args) || ret;
  2637.         }
  2638.         return ret;
  2639.     }
  2640. }
  2641.  
  2642.  
  2643. function firstDefined() {
  2644.     for (var i=0; i<arguments.length; i++) {
  2645.         if (arguments[i] !== undefined) {
  2646.             return arguments[i];
  2647.         }
  2648.     }
  2649. }
  2650.  
  2651.  
  2652. function htmlEscape(s) {
  2653.     return (s + '').replace(/&/g, '&amp;')
  2654.         .replace(/</g, '&lt;')
  2655.         .replace(/>/g, '&gt;')
  2656.         .replace(/'/g, '&#039;')
  2657.         .replace(/"/g, '&quot;')
  2658.         .replace(/\n/g, '<br />');
  2659. }
  2660.  
  2661.  
  2662. function stripHtmlEntities(text) {
  2663.     return text.replace(/&.*?;/g, '');
  2664. }
  2665.  
  2666.  
  2667. function capitaliseFirstLetter(str) {
  2668.     return str.charAt(0).toUpperCase() + str.slice(1);
  2669. }
  2670.  
  2671.  
  2672. // Returns a function, that, as long as it continues to be invoked, will not
  2673. // be triggered. The function will be called after it stops being called for
  2674. // N milliseconds.
  2675. // https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714
  2676. function debounce(func, wait) {
  2677.     var timeoutId;
  2678.     var args;
  2679.     var context;
  2680.     var timestamp; // of most recent call
  2681.     var later = function() {
  2682.         var last = +new Date() - timestamp;
  2683.         if (last < wait && last > 0) {
  2684.             timeoutId = setTimeout(later, wait - last);
  2685.         }
  2686.         else {
  2687.             timeoutId = null;
  2688.             func.apply(context, args);
  2689.             if (!timeoutId) {
  2690.                 context = args = null;
  2691.             }
  2692.         }
  2693.     };
  2694.  
  2695.     return function() {
  2696.         context = this;
  2697.         args = arguments;
  2698.         timestamp = +new Date();
  2699.         if (!timeoutId) {
  2700.             timeoutId = setTimeout(later, wait);
  2701.         }
  2702.     };
  2703. }
  2704.  
  2705. ;;
  2706.  
  2707. var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/;
  2708. var ambigTimeOrZoneRegex =
  2709.     /^\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+)?)?)?)?)?$/;
  2710. var newMomentProto = moment.fn; // where we will attach our new methods
  2711. var oldMomentProto = $.extend({}, newMomentProto); // copy of original moment methods
  2712. var allowValueOptimization;
  2713. var setUTCValues; // function defined below
  2714. var setLocalValues; // function defined below
  2715.  
  2716.  
  2717. // Creating
  2718. // -------------------------------------------------------------------------------------------------
  2719.  
  2720. // Creates a new moment, similar to the vanilla moment(...) constructor, but with
  2721. // extra features (ambiguous time, enhanced formatting). When given an existing moment,
  2722. // it will function as a clone (and retain the zone of the moment). Anything else will
  2723. // result in a moment in the local zone.
  2724. fc.moment = function() {
  2725.     return makeMoment(arguments);
  2726. };
  2727.  
  2728. // Sames as fc.moment, but forces the resulting moment to be in the UTC timezone.
  2729. fc.moment.utc = function() {
  2730.     var mom = makeMoment(arguments, true);
  2731.  
  2732.     // Force it into UTC because makeMoment doesn't guarantee it
  2733.     // (if given a pre-existing moment for example)
  2734.     if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone
  2735.         mom.utc();
  2736.     }
  2737.  
  2738.     return mom;
  2739. };
  2740.  
  2741. // Same as fc.moment, but when given an ISO8601 string, the timezone offset is preserved.
  2742. // ISO8601 strings with no timezone offset will become ambiguously zoned.
  2743. fc.moment.parseZone = function() {
  2744.     return makeMoment(arguments, true, true);
  2745. };
  2746.  
  2747. // Builds an enhanced moment from args. When given an existing moment, it clones. When given a
  2748. // native Date, or called with no arguments (the current time), the resulting moment will be local.
  2749. // Anything else needs to be "parsed" (a string or an array), and will be affected by:
  2750. //    parseAsUTC - if there is no zone information, should we parse the input in UTC?
  2751. //    parseZone - if there is zone information, should we force the zone of the moment?
  2752. function makeMoment(args, parseAsUTC, parseZone) {
  2753.     var input = args[0];
  2754.     var isSingleString = args.length == 1 && typeof input === 'string';
  2755.     var isAmbigTime;
  2756.     var isAmbigZone;
  2757.     var ambigMatch;
  2758.     var mom;
  2759.  
  2760.     if (moment.isMoment(input)) {
  2761.         mom = moment.apply(null, args); // clone it
  2762.         transferAmbigs(input, mom); // the ambig flags weren't transfered with the clone
  2763.     }
  2764.     else if (isNativeDate(input) || input === undefined) {
  2765.         mom = moment.apply(null, args); // will be local
  2766.     }
  2767.     else { // "parsing" is required
  2768.         isAmbigTime = false;
  2769.         isAmbigZone = false;
  2770.  
  2771.         if (isSingleString) {
  2772.             if (ambigDateOfMonthRegex.test(input)) {
  2773.                 // accept strings like '2014-05', but convert to the first of the month
  2774.                 input += '-01';
  2775.                 args = [ input ]; // for when we pass it on to moment's constructor
  2776.                 isAmbigTime = true;
  2777.                 isAmbigZone = true;
  2778.             }
  2779.             else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) {
  2780.                 isAmbigTime = !ambigMatch[5]; // no time part?
  2781.                 isAmbigZone = true;
  2782.             }
  2783.         }
  2784.         else if ($.isArray(input)) {
  2785.             // arrays have no timezone information, so assume ambiguous zone
  2786.             isAmbigZone = true;
  2787.         }
  2788.         // otherwise, probably a string with a format
  2789.  
  2790.         if (parseAsUTC) {
  2791.             mom = moment.utc.apply(moment, args);
  2792.         }
  2793.         else {
  2794.             mom = moment.apply(null, args);
  2795.         }
  2796.  
  2797.         if (isAmbigTime) {
  2798.             mom._ambigTime = true;
  2799.             mom._ambigZone = true; // ambiguous time always means ambiguous zone
  2800.         }
  2801.         else if (parseZone) { // let's record the inputted zone somehow
  2802.             if (isAmbigZone) {
  2803.                 mom._ambigZone = true;
  2804.             }
  2805.             else if (isSingleString) {
  2806.                 mom.zone(input); // if not a valid zone, will assign UTC
  2807.             }
  2808.         }
  2809.     }
  2810.  
  2811.     mom._fullCalendar = true; // flag for extended functionality
  2812.  
  2813.     return mom;
  2814. }
  2815.  
  2816.  
  2817. // A clone method that works with the flags related to our enhanced functionality.
  2818. // In the future, use moment.momentProperties
  2819. newMomentProto.clone = function() {
  2820.     var mom = oldMomentProto.clone.apply(this, arguments);
  2821.  
  2822.     // these flags weren't transfered with the clone
  2823.     transferAmbigs(this, mom);
  2824.     if (this._fullCalendar) {
  2825.         mom._fullCalendar = true;
  2826.     }
  2827.  
  2828.     return mom;
  2829. };
  2830.  
  2831.  
  2832. // Time-of-day
  2833. // -------------------------------------------------------------------------------------------------
  2834.  
  2835. // GETTER
  2836. // Returns a Duration with the hours/minutes/seconds/ms values of the moment.
  2837. // If the moment has an ambiguous time, a duration of 00:00 will be returned.
  2838. //
  2839. // SETTER
  2840. // You can supply a Duration, a Moment, or a Duration-like argument.
  2841. // When setting the time, and the moment has an ambiguous time, it then becomes unambiguous.
  2842. newMomentProto.time = function(time) {
  2843.  
  2844.     // Fallback to the original method (if there is one) if this moment wasn't created via FullCalendar.
  2845.     // `time` is a generic enough method name where this precaution is necessary to avoid collisions w/ other plugins.
  2846.     if (!this._fullCalendar) {
  2847.         return oldMomentProto.time.apply(this, arguments);
  2848.     }
  2849.  
  2850.     if (time == null) { // getter
  2851.         return moment.duration({
  2852.             hours: this.hours(),
  2853.             minutes: this.minutes(),
  2854.             seconds: this.seconds(),
  2855.             milliseconds: this.milliseconds()
  2856.         });
  2857.     }
  2858.     else { // setter
  2859.  
  2860.         this._ambigTime = false; // mark that the moment now has a time
  2861.  
  2862.         if (!moment.isDuration(time) && !moment.isMoment(time)) {
  2863.             time = moment.duration(time);
  2864.         }
  2865.  
  2866.         // The day value should cause overflow (so 24 hours becomes 00:00:00 of next day).
  2867.         // Only for Duration times, not Moment times.
  2868.         var dayHours = 0;
  2869.         if (moment.isDuration(time)) {
  2870.             dayHours = Math.floor(time.asDays()) * 24;
  2871.         }
  2872.  
  2873.         // We need to set the individual fields.
  2874.         // Can't use startOf('day') then add duration. In case of DST at start of day.
  2875.         return this.hours(dayHours + time.hours())
  2876.             .minutes(time.minutes())
  2877.             .seconds(time.seconds())
  2878.             .milliseconds(time.milliseconds());
  2879.     }
  2880. };
  2881.  
  2882. // Converts the moment to UTC, stripping out its time-of-day and timezone offset,
  2883. // but preserving its YMD. A moment with a stripped time will display no time
  2884. // nor timezone offset when .format() is called.
  2885. newMomentProto.stripTime = function() {
  2886.     var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array
  2887.  
  2888.     this.utc(); // set the internal UTC flag (will clear the ambig flags)
  2889.     setUTCValues(this, a.slice(0, 3)); // set the year/month/date. time will be zero
  2890.  
  2891.     // Mark the time as ambiguous. This needs to happen after the .utc() call, which calls .zone(),
  2892.     // which clears all ambig flags. Same with setUTCValues with moment-timezone.
  2893.     this._ambigTime = true;
  2894.     this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset
  2895.  
  2896.     return this; // for chaining
  2897. };
  2898.  
  2899. // Returns if the moment has a non-ambiguous time (boolean)
  2900. newMomentProto.hasTime = function() {
  2901.     return !this._ambigTime;
  2902. };
  2903.  
  2904.  
  2905. // Timezone
  2906. // -------------------------------------------------------------------------------------------------
  2907.  
  2908. // Converts the moment to UTC, stripping out its timezone offset, but preserving its
  2909. // YMD and time-of-day. A moment with a stripped timezone offset will display no
  2910. // timezone offset when .format() is called.
  2911. newMomentProto.stripZone = function() {
  2912.     var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array
  2913.     var wasAmbigTime = this._ambigTime;
  2914.  
  2915.     this.utc(); // set the internal UTC flag (will clear the ambig flags)
  2916.     setUTCValues(this, a); // will set the year/month/date/hours/minutes/seconds/ms
  2917.  
  2918.     if (wasAmbigTime) {
  2919.         // the above call to .utc()/.zone() unfortunately clears the ambig flags, so reassign
  2920.         this._ambigTime = true;
  2921.     }
  2922.  
  2923.     // Mark the zone as ambiguous. This needs to happen after the .utc() call, which calls .zone(),
  2924.     // which clears all ambig flags. Same with setUTCValues with moment-timezone.
  2925.     this._ambigZone = true;
  2926.  
  2927.     return this; // for chaining
  2928. };
  2929.  
  2930. // Returns of the moment has a non-ambiguous timezone offset (boolean)
  2931. newMomentProto.hasZone = function() {
  2932.     return !this._ambigZone;
  2933. };
  2934.  
  2935. // this method implicitly marks a zone (will get called upon .utc() and .local())
  2936. newMomentProto.zone = function(tzo) {
  2937.  
  2938.     if (tzo != null) { // setter
  2939.         // these assignments needs to happen before the original zone method is called.
  2940.         // I forget why, something to do with a browser crash.
  2941.         this._ambigTime = false;
  2942.         this._ambigZone = false;
  2943.     }
  2944.  
  2945.     return oldMomentProto.zone.apply(this, arguments);
  2946. };
  2947.  
  2948. // this method implicitly marks a zone
  2949. newMomentProto.local = function() {
  2950.     var a = this.toArray(); // year,month,date,hours,minutes,seconds,ms as an array
  2951.     var wasAmbigZone = this._ambigZone;
  2952.  
  2953.     oldMomentProto.local.apply(this, arguments); // will clear ambig flags
  2954.  
  2955.     if (wasAmbigZone) {
  2956.         // If the moment was ambiguously zoned, the date fields were stored as UTC.
  2957.         // We want to preserve these, but in local time.
  2958.         setLocalValues(this, a);
  2959.     }
  2960.  
  2961.     return this; // for chaining
  2962. };
  2963.  
  2964.  
  2965. // Formatting
  2966. // -------------------------------------------------------------------------------------------------
  2967.  
  2968. newMomentProto.format = function() {
  2969.     if (this._fullCalendar && arguments[0]) { // an enhanced moment? and a format string provided?
  2970.         return formatDate(this, arguments[0]); // our extended formatting
  2971.     }
  2972.     if (this._ambigTime) {
  2973.         return oldMomentFormat(this, 'YYYY-MM-DD');
  2974.     }
  2975.     if (this._ambigZone) {
  2976.         return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
  2977.     }
  2978.     return oldMomentProto.format.apply(this, arguments);
  2979. };
  2980.  
  2981. newMomentProto.toISOString = function() {
  2982.     if (this._ambigTime) {
  2983.         return oldMomentFormat(this, 'YYYY-MM-DD');
  2984.     }
  2985.     if (this._ambigZone) {
  2986.         return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
  2987.     }
  2988.     return oldMomentProto.toISOString.apply(this, arguments);
  2989. };
  2990.  
  2991.  
  2992. // Querying
  2993. // -------------------------------------------------------------------------------------------------
  2994.  
  2995. // Is the moment within the specified range? `end` is exclusive.
  2996. // FYI, this method is not a standard Moment method, so always do our enhanced logic.
  2997. newMomentProto.isWithin = function(start, end) {
  2998.     var a = commonlyAmbiguate([ this, start, end ]);
  2999.     return a[0] >= a[1] && a[0] < a[2];
  3000. };
  3001.  
  3002. // When isSame is called with units, timezone ambiguity is normalized before the comparison happens.
  3003. // If no units specified, the two moments must be identically the same, with matching ambig flags.
  3004. newMomentProto.isSame = function(input, units) {
  3005.     var a;
  3006.  
  3007.     // only do custom logic if this is an enhanced moment
  3008.     if (!this._fullCalendar) {
  3009.         return oldMomentProto.isSame.apply(this, arguments);
  3010.     }
  3011.  
  3012.     if (units) {
  3013.         a = commonlyAmbiguate([ this, input ], true); // normalize timezones but don't erase times
  3014.         return oldMomentProto.isSame.call(a[0], a[1], units);
  3015.     }
  3016.     else {
  3017.         input = fc.moment.parseZone(input); // normalize input
  3018.         return oldMomentProto.isSame.call(this, input) &&
  3019.             Boolean(this._ambigTime) === Boolean(input._ambigTime) &&
  3020.             Boolean(this._ambigZone) === Boolean(input._ambigZone);
  3021.     }
  3022. };
  3023.  
  3024. // Make these query methods work with ambiguous moments
  3025. $.each([
  3026.     'isBefore',
  3027.     'isAfter'
  3028. ], function(i, methodName) {
  3029.     newMomentProto[methodName] = function(input, units) {
  3030.         var a;
  3031.  
  3032.         // only do custom logic if this is an enhanced moment
  3033.         if (!this._fullCalendar) {
  3034.             return oldMomentProto[methodName].apply(this, arguments);
  3035.         }
  3036.  
  3037.         a = commonlyAmbiguate([ this, input ]);
  3038.         return oldMomentProto[methodName].call(a[0], a[1], units);
  3039.     };
  3040. });
  3041.  
  3042.  
  3043. // Misc Internals
  3044. // -------------------------------------------------------------------------------------------------
  3045.  
  3046. // given an array of moment-like inputs, return a parallel array w/ moments similarly ambiguated.
  3047. // for example, of one moment has ambig time, but not others, all moments will have their time stripped.
  3048. // set `preserveTime` to `true` to keep times, but only normalize zone ambiguity.
  3049. function commonlyAmbiguate(inputs, preserveTime) {
  3050.     var outputs = [];
  3051.     var anyAmbigTime = false;
  3052.     var anyAmbigZone = false;
  3053.     var i;
  3054.  
  3055.     for (i=0; i<inputs.length; i++) {
  3056.         outputs.push(fc.moment.parseZone(inputs[i]));
  3057.         anyAmbigTime = anyAmbigTime || outputs[i]._ambigTime;
  3058.         anyAmbigZone = anyAmbigZone || outputs[i]._ambigZone;
  3059.     }
  3060.  
  3061.     for (i=0; i<outputs.length; i++) {
  3062.         if (anyAmbigTime && !preserveTime) {
  3063.             outputs[i].stripTime();
  3064.         }
  3065.         else if (anyAmbigZone) {
  3066.             outputs[i].stripZone();
  3067.         }
  3068.     }
  3069.  
  3070.     return outputs;
  3071. }
  3072.  
  3073. // Transfers all the flags related to ambiguous time/zone from the `src` moment to the `dest` moment
  3074. function transferAmbigs(src, dest) {
  3075.     if (src._ambigTime) {
  3076.         dest._ambigTime = true;
  3077.     }
  3078.     else if (dest._ambigTime) {
  3079.         dest._ambigTime = false;
  3080.     }
  3081.  
  3082.     if (src._ambigZone) {
  3083.         dest._ambigZone = true;
  3084.     }
  3085.     else if (dest._ambigZone) {
  3086.         dest._ambigZone = false;
  3087.     }
  3088. }
  3089.  
  3090.  
  3091. // Sets the year/month/date/etc values of the moment from the given array.
  3092. // Inefficient because it calls each individual setter.
  3093. function setMomentValues(mom, a) {
  3094.     mom.year(a[0] || 0)
  3095.         .month(a[1] || 0)
  3096.         .date(a[2] || 0)
  3097.         .hours(a[3] || 0)
  3098.         .minutes(a[4] || 0)
  3099.         .seconds(a[5] || 0)
  3100.         .milliseconds(a[6] || 0);
  3101. }
  3102.  
  3103. // Can we set the moment's internal date directly?
  3104. allowValueOptimization = '_d' in moment() && 'updateOffset' in moment;
  3105.  
  3106. // Utility function. Accepts a moment and an array of the UTC year/month/date/etc values to set.
  3107. // Assumes the given moment is already in UTC mode.
  3108. setUTCValues = allowValueOptimization ? function(mom, a) {
  3109.     // simlate what moment's accessors do
  3110.     mom._d.setTime(Date.UTC.apply(Date, a));
  3111.     moment.updateOffset(mom, false); // keepTime=false
  3112. } : setMomentValues;
  3113.  
  3114. // Utility function. Accepts a moment and an array of the local year/month/date/etc values to set.
  3115. // Assumes the given moment is already in local mode.
  3116. setLocalValues = allowValueOptimization ? function(mom, a) {
  3117.     // simlate what moment's accessors do
  3118.     mom._d.setTime(+new Date( // FYI, there is now way to apply an array of args to a constructor
  3119.         a[0] || 0,
  3120.         a[1] || 0,
  3121.         a[2] || 0,
  3122.         a[3] || 0,
  3123.         a[4] || 0,
  3124.         a[5] || 0,
  3125.         a[6] || 0
  3126.     ));
  3127.     moment.updateOffset(mom, false); // keepTime=false
  3128. } : setMomentValues;
  3129.  
  3130. ;;
  3131.  
  3132. // Single Date Formatting
  3133. // -------------------------------------------------------------------------------------------------
  3134.  
  3135.  
  3136. // call this if you want Moment's original format method to be used
  3137. function oldMomentFormat(mom, formatStr) {
  3138.     return oldMomentProto.format.call(mom, formatStr); // oldMomentProto defined in moment-ext.js
  3139. }
  3140.  
  3141.  
  3142. // Formats `date` with a Moment formatting string, but allow our non-zero areas and
  3143. // additional token.
  3144. function formatDate(date, formatStr) {
  3145.     return formatDateWithChunks(date, getFormatStringChunks(formatStr));
  3146. }
  3147.  
  3148.  
  3149. function formatDateWithChunks(date, chunks) {
  3150.     var s = '';
  3151.     var i;
  3152.  
  3153.     for (i=0; i<chunks.length; i++) {
  3154.         s += formatDateWithChunk(date, chunks[i]);
  3155.     }
  3156.  
  3157.     return s;
  3158. }
  3159.  
  3160.  
  3161. // addition formatting tokens we want recognized
  3162. var tokenOverrides = {
  3163.     t: function(date) { // "a" or "p"
  3164.         return oldMomentFormat(date, 'a').charAt(0);
  3165.     },
  3166.     T: function(date) { // "A" or "P"
  3167.         return oldMomentFormat(date, 'A').charAt(0);
  3168.     }
  3169. };
  3170.  
  3171.  
  3172. function formatDateWithChunk(date, chunk) {
  3173.     var token;
  3174.     var maybeStr;
  3175.  
  3176.     if (typeof chunk === 'string') { // a literal string
  3177.         return chunk;
  3178.     }
  3179.     else if ((token = chunk.token)) { // a token, like "YYYY"
  3180.         if (tokenOverrides[token]) {
  3181.             return tokenOverrides[token](date); // use our custom token
  3182.         }
  3183.         return oldMomentFormat(date, token);
  3184.     }
  3185.     else if (chunk.maybe) { // a grouping of other chunks that must be non-zero
  3186.         maybeStr = formatDateWithChunks(date, chunk.maybe);
  3187.         if (maybeStr.match(/[1-9]/)) {
  3188.             return maybeStr;
  3189.         }
  3190.     }
  3191.  
  3192.     return '';
  3193. }
  3194.  
  3195.  
  3196. // Date Range Formatting
  3197. // -------------------------------------------------------------------------------------------------
  3198. // TODO: make it work with timezone offset
  3199.  
  3200. // Using a formatting string meant for a single date, generate a range string, like
  3201. // "Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ.
  3202. // If the dates are the same as far as the format string is concerned, just return a single
  3203. // rendering of one date, without any separator.
  3204. function formatRange(date1, date2, formatStr, separator, isRTL) {
  3205.     var localeData;
  3206.  
  3207.     date1 = fc.moment.parseZone(date1);
  3208.     date2 = fc.moment.parseZone(date2);
  3209.  
  3210.     localeData = (date1.localeData || date1.lang).call(date1); // works with moment-pre-2.8
  3211.  
  3212.     // Expand localized format strings, like "LL" -> "MMMM D YYYY"
  3213.     formatStr = localeData.longDateFormat(formatStr) || formatStr;
  3214.     // BTW, this is not important for `formatDate` because it is impossible to put custom tokens
  3215.     // or non-zero areas in Moment's localized format strings.
  3216.  
  3217.     separator = separator || ' - ';
  3218.  
  3219.     return formatRangeWithChunks(
  3220.         date1,
  3221.         date2,
  3222.         getFormatStringChunks(formatStr),
  3223.         separator,
  3224.         isRTL
  3225.     );
  3226. }
  3227. fc.formatRange = formatRange; // expose
  3228.  
  3229.  
  3230. function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) {
  3231.     var chunkStr; // the rendering of the chunk
  3232.     var leftI;
  3233.     var leftStr = '';
  3234.     var rightI;
  3235.     var rightStr = '';
  3236.     var middleI;
  3237.     var middleStr1 = '';
  3238.     var middleStr2 = '';
  3239.     var middleStr = '';
  3240.  
  3241.     // Start at the leftmost side of the formatting string and continue until you hit a token
  3242.     // that is not the same between dates.
  3243.     for (leftI=0; leftI<chunks.length; leftI++) {
  3244.         chunkStr = formatSimilarChunk(date1, date2, chunks[leftI]);
  3245.         if (chunkStr === false) {
  3246.             break;
  3247.         }
  3248.         leftStr += chunkStr;
  3249.     }
  3250.  
  3251.     // Similarly, start at the rightmost side of the formatting string and move left
  3252.     for (rightI=chunks.length-1; rightI>leftI; rightI--) {
  3253.         chunkStr = formatSimilarChunk(date1, date2, chunks[rightI]);
  3254.         if (chunkStr === false) {
  3255.             break;
  3256.         }
  3257.         rightStr = chunkStr + rightStr;
  3258.     }
  3259.  
  3260.     // The area in the middle is different for both of the dates.
  3261.     // Collect them distinctly so we can jam them together later.
  3262.     for (middleI=leftI; middleI<=rightI; middleI++) {
  3263.         middleStr1 += formatDateWithChunk(date1, chunks[middleI]);
  3264.         middleStr2 += formatDateWithChunk(date2, chunks[middleI]);
  3265.     }
  3266.  
  3267.     if (middleStr1 || middleStr2) {
  3268.         if (isRTL) {
  3269.             middleStr = middleStr2 + separator + middleStr1;
  3270.         }
  3271.         else {
  3272.             middleStr = middleStr1 + separator + middleStr2;
  3273.         }
  3274.     }
  3275.  
  3276.     return leftStr + middleStr + rightStr;
  3277. }
  3278.  
  3279.  
  3280. var similarUnitMap = {
  3281.     Y: 'year',
  3282.     M: 'month',
  3283.     D: 'day', // day of month
  3284.     d: 'day', // day of week
  3285.     // prevents a separator between anything time-related...
  3286.     A: 'second', // AM/PM
  3287.     a: 'second', // am/pm
  3288.     T: 'second', // A/P
  3289.     t: 'second', // a/p
  3290.     H: 'second', // hour (24)
  3291.     h: 'second', // hour (12)
  3292.     m: 'second', // minute
  3293.     s: 'second' // second
  3294. };
  3295. // TODO: week maybe?
  3296.  
  3297.  
  3298. // Given a formatting chunk, and given that both dates are similar in the regard the
  3299. // formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`.
  3300. function formatSimilarChunk(date1, date2, chunk) {
  3301.     var token;
  3302.     var unit;
  3303.  
  3304.     if (typeof chunk === 'string') { // a literal string
  3305.         return chunk;
  3306.     }
  3307.     else if ((token = chunk.token)) {
  3308.         unit = similarUnitMap[token.charAt(0)];
  3309.         // are the dates the same for this unit of measurement?
  3310.         if (unit && date1.isSame(date2, unit)) {
  3311.             return oldMomentFormat(date1, token); // would be the same if we used `date2`
  3312.             // BTW, don't support custom tokens
  3313.         }
  3314.     }
  3315.  
  3316.     return false; // the chunk is NOT the same for the two dates
  3317.     // BTW, don't support splitting on non-zero areas
  3318. }
  3319.  
  3320.  
  3321. // Chunking Utils
  3322. // -------------------------------------------------------------------------------------------------
  3323.  
  3324.  
  3325. var formatStringChunkCache = {};
  3326.  
  3327.  
  3328. function getFormatStringChunks(formatStr) {
  3329.     if (formatStr in formatStringChunkCache) {
  3330.         return formatStringChunkCache[formatStr];
  3331.     }
  3332.     return (formatStringChunkCache[formatStr] = chunkFormatString(formatStr));
  3333. }
  3334.  
  3335.  
  3336. // Break the formatting string into an array of chunks
  3337. function chunkFormatString(formatStr) {
  3338.     var chunks = [];
  3339.     var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LT|(\w)\4*o?)|([^\w\[\(]+)/g; // TODO: more descrimination
  3340.     var match;
  3341.  
  3342.     while ((match = chunker.exec(formatStr))) {
  3343.         if (match[1]) { // a literal string inside [ ... ]
  3344.             chunks.push(match[1]);
  3345.         }
  3346.         else if (match[2]) { // non-zero formatting inside ( ... )
  3347.             chunks.push({ maybe: chunkFormatString(match[2]) });
  3348.         }
  3349.         else if (match[3]) { // a formatting token
  3350.             chunks.push({ token: match[3] });
  3351.         }
  3352.         else if (match[5]) { // an unenclosed literal string
  3353.             chunks.push(match[5]);
  3354.         }
  3355.     }
  3356.  
  3357.     return chunks;
  3358. }
  3359.  
  3360. ;;
  3361.  
  3362. /* A rectangular panel that is absolutely positioned over other content
  3363. ------------------------------------------------------------------------------------------------------------------------
  3364. Options:
  3365.     - className (string)
  3366.     - content (HTML string or jQuery element set)
  3367.     - parentEl
  3368.     - top
  3369.     - left
  3370.     - right (the x coord of where the right edge should be. not a "CSS" right)
  3371.     - autoHide (boolean)
  3372.     - show (callback)
  3373.     - hide (callback)
  3374. */
  3375.  
  3376. function Popover(options) {
  3377.     this.options = options || {};
  3378. }
  3379.  
  3380.  
  3381. Popover.prototype = {
  3382.  
  3383.     isHidden: true,
  3384.     options: null,
  3385.     el: null, // the container element for the popover. generated by this object
  3386.     documentMousedownProxy: null, // document mousedown handler bound to `this`
  3387.     margin: 10, // the space required between the popover and the edges of the scroll container
  3388.  
  3389.  
  3390.     // Shows the popover on the specified position. Renders it if not already
  3391.     show: function() {
  3392.         if (this.isHidden) {
  3393.             if (!this.el) {
  3394.                 this.render();
  3395.             }
  3396.             this.el.show();
  3397.             this.position();
  3398.             this.isHidden = false;
  3399.             this.trigger('show');
  3400.         }
  3401.     },
  3402.  
  3403.  
  3404.     // Hides the popover, through CSS, but does not remove it from the DOM
  3405.     hide: function() {
  3406.         if (!this.isHidden) {
  3407.             this.el.hide();
  3408.             this.isHidden = true;
  3409.             this.trigger('hide');
  3410.         }
  3411.     },
  3412.  
  3413.  
  3414.     // Creates `this.el` and renders content inside of it
  3415.     render: function() {
  3416.         var _this = this;
  3417.         var options = this.options;
  3418.  
  3419.         this.el = $('<div class="fc-popover"/>')
  3420.             .addClass(options.className || '')
  3421.             .css({
  3422.                 // position initially to the top left to avoid creating scrollbars
  3423.                 top: 0,
  3424.                 left: 0
  3425.             })
  3426.             .append(options.content)
  3427.             .appendTo(options.parentEl);
  3428.  
  3429.         // when a click happens on anything inside with a 'fc-close' className, hide the popover
  3430.         this.el.on('click', '.fc-close', function() {
  3431.             _this.hide();
  3432.         });
  3433.  
  3434.         if (options.autoHide) {
  3435.             $(document).on('mousedown', this.documentMousedownProxy = $.proxy(this, 'documentMousedown'));
  3436.         }
  3437.     },
  3438.  
  3439.  
  3440.     // Triggered when the user clicks *anywhere* in the document, for the autoHide feature
  3441.     documentMousedown: function(ev) {
  3442.         // only hide the popover if the click happened outside the popover
  3443.         if (this.el && !$(ev.target).closest(this.el).length) {
  3444.             this.hide();
  3445.         }
  3446.     },
  3447.  
  3448.  
  3449.     // Hides and unregisters any handlers
  3450.     destroy: function() {
  3451.         this.hide();
  3452.  
  3453.         if (this.el) {
  3454.             this.el.remove();
  3455.             this.el = null;
  3456.         }
  3457.  
  3458.         $(document).off('mousedown', this.documentMousedownProxy);
  3459.     },
  3460.  
  3461.  
  3462.     // Positions the popover optimally, using the top/left/right options
  3463.     position: function() {
  3464.         var options = this.options;
  3465.         var origin = this.el.offsetParent().offset();
  3466.         var width = this.el.outerWidth();
  3467.         var height = this.el.outerHeight();
  3468.         var windowEl = $(window);
  3469.         var viewportEl = getScrollParent(this.el);
  3470.         var viewportTop;
  3471.         var viewportLeft;
  3472.         var viewportOffset;
  3473.         var top; // the "position" (not "offset") values for the popover
  3474.         var left; //
  3475.  
  3476.         // compute top and left
  3477.         top = options.top || 0;
  3478.         if (options.left !== undefined) {
  3479.             left = options.left;
  3480.         }
  3481.         else if (options.right !== undefined) {
  3482.             left = options.right - width; // derive the left value from the right value
  3483.         }
  3484.         else {
  3485.             left = 0;
  3486.         }
  3487.  
  3488.         if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result
  3489.             viewportEl = windowEl;
  3490.             viewportTop = 0; // the window is always at the top left
  3491.             viewportLeft = 0; // (and .offset() won't work if called here)
  3492.         }
  3493.         else {
  3494.             viewportOffset = viewportEl.offset();
  3495.             viewportTop = viewportOffset.top;
  3496.             viewportLeft = viewportOffset.left;
  3497.         }
  3498.  
  3499.         // if the window is scrolled, it causes the visible area to be further down
  3500.         viewportTop += windowEl.scrollTop();
  3501.         viewportLeft += windowEl.scrollLeft();
  3502.  
  3503.         // constrain to the view port. if constrained by two edges, give precedence to top/left
  3504.         if (options.viewportConstrain !== false) {
  3505.             top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin);
  3506.             top = Math.max(top, viewportTop + this.margin);
  3507.             left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin);
  3508.             left = Math.max(left, viewportLeft + this.margin);
  3509.         }
  3510.  
  3511.         this.el.css({
  3512.             top: top - origin.top,
  3513.             left: left - origin.left
  3514.         });
  3515.     },
  3516.  
  3517.  
  3518.     // Triggers a callback. Calls a function in the option hash of the same name.
  3519.     // Arguments beyond the first `name` are forwarded on.
  3520.     // TODO: better code reuse for this. Repeat code
  3521.     trigger: function(name) {
  3522.         if (this.options[name]) {
  3523.             this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
  3524.         }
  3525.     }
  3526.  
  3527. };
  3528.  
  3529. ;;
  3530.  
  3531. /* A "coordinate map" converts pixel coordinates into an associated cell, which has an associated date
  3532. ------------------------------------------------------------------------------------------------------------------------
  3533. Common interface:
  3534.  
  3535.     CoordMap.prototype = {
  3536.         build: function() {},
  3537.         getCell: function(x, y) {}
  3538.     };
  3539.  
  3540. */
  3541.  
  3542. /* Coordinate map for a grid component
  3543. ----------------------------------------------------------------------------------------------------------------------*/
  3544.  
  3545. function GridCoordMap(grid) {
  3546.     this.grid = grid;
  3547. }
  3548.  
  3549.  
  3550. GridCoordMap.prototype = {
  3551.  
  3552.     grid: null, // reference to the Grid
  3553.     rows: null, // the top-to-bottom y coordinates. including the bottom of the last item
  3554.     cols: null, // the left-to-right x coordinates. including the right of the last item
  3555.  
  3556.     containerEl: null, // container element that all coordinates are constrained to. optionally assigned
  3557.     minX: null,
  3558.     maxX: null, // exclusive
  3559.     minY: null,
  3560.     maxY: null, // exclusive
  3561.  
  3562.  
  3563.     // Queries the grid for the coordinates of all the cells
  3564.     build: function() {
  3565.         this.grid.buildCoords(
  3566.             this.rows = [],
  3567.             this.cols = []
  3568.         );
  3569.         this.computeBounds();
  3570.     },
  3571.  
  3572.  
  3573.     // Given a coordinate of the document, gets the associated cell. If no cell is underneath, returns null
  3574.     getCell: function(x, y) {
  3575.         var cell = null;
  3576.         var rows = this.rows;
  3577.         var cols = this.cols;
  3578.         var r = -1;
  3579.         var c = -1;
  3580.         var i;
  3581.  
  3582.         if (this.inBounds(x, y)) {
  3583.  
  3584.             for (i = 0; i < rows.length; i++) {
  3585.                 if (y >= rows[i][0] && y < rows[i][1]) {
  3586.                     r = i;
  3587.                     break;
  3588.                 }
  3589.             }
  3590.  
  3591.             for (i = 0; i < cols.length; i++) {
  3592.                 if (x >= cols[i][0] && x < cols[i][1]) {
  3593.                     c = i;
  3594.                     break;
  3595.                 }
  3596.             }
  3597.  
  3598.             if (r >= 0 && c >= 0) {
  3599.                 cell = { row: r, col: c };
  3600.                 cell.grid = this.grid;
  3601.                 cell.date = this.grid.getCellDate(cell);
  3602.             }
  3603.         }
  3604.  
  3605.         return cell;
  3606.     },
  3607.  
  3608.  
  3609.     // If there is a containerEl, compute the bounds into min/max values
  3610.     computeBounds: function() {
  3611.         var containerOffset;
  3612.  
  3613.         if (this.containerEl) {
  3614.             containerOffset = this.containerEl.offset();
  3615.             this.minX = containerOffset.left;
  3616.             this.maxX = containerOffset.left + this.containerEl.outerWidth();
  3617.             this.minY = containerOffset.top;
  3618.             this.maxY = containerOffset.top + this.containerEl.outerHeight();
  3619.         }
  3620.     },
  3621.  
  3622.  
  3623.     // Determines if the given coordinates are in bounds. If no `containerEl`, always true
  3624.     inBounds: function(x, y) {
  3625.         if (this.containerEl) {
  3626.             return x >= this.minX && x < this.maxX && y >= this.minY && y < this.maxY;
  3627.         }
  3628.         return true;
  3629.     }
  3630.  
  3631. };
  3632.  
  3633.  
  3634. /* Coordinate map that is a combination of multiple other coordinate maps
  3635. ----------------------------------------------------------------------------------------------------------------------*/
  3636.  
  3637. function ComboCoordMap(coordMaps) {
  3638.     this.coordMaps = coordMaps;
  3639. }
  3640.  
  3641.  
  3642. ComboCoordMap.prototype = {
  3643.  
  3644.     coordMaps: null, // an array of CoordMaps
  3645.  
  3646.  
  3647.     // Builds all coordMaps
  3648.     build: function() {
  3649.         var coordMaps = this.coordMaps;
  3650.         var i;
  3651.  
  3652.         for (i = 0; i < coordMaps.length; i++) {
  3653.             coordMaps[i].build();
  3654.         }
  3655.     },
  3656.  
  3657.  
  3658.     // Queries all coordMaps for the cell underneath the given coordinates, returning the first result
  3659.     getCell: function(x, y) {
  3660.         var coordMaps = this.coordMaps;
  3661.         var cell = null;
  3662.         var i;
  3663.  
  3664.         for (i = 0; i < coordMaps.length && !cell; i++) {
  3665.             cell = coordMaps[i].getCell(x, y);
  3666.         }
  3667.  
  3668.         return cell;
  3669.     }
  3670.  
  3671. };
  3672.  
  3673. ;;
  3674.  
  3675. /* Tracks mouse movements over a CoordMap and raises events about which cell the mouse is over.
  3676. ----------------------------------------------------------------------------------------------------------------------*/
  3677. // TODO: very useful to have a handler that gets called upon cellOut OR when dragging stops (for cleanup)
  3678.  
  3679. function DragListener(coordMap, options) {
  3680.     this.coordMap = coordMap;
  3681.     this.options = options || {};
  3682. }
  3683.  
  3684.  
  3685. DragListener.prototype = {
  3686.  
  3687.     coordMap: null,
  3688.     options: null,
  3689.  
  3690.     isListening: false,
  3691.     isDragging: false,
  3692.  
  3693.     // the cell/date the mouse was over when listening started
  3694.     origCell: null,
  3695.     origDate: null,
  3696.  
  3697.     // the cell/date the mouse is over
  3698.     cell: null,
  3699.     date: null,
  3700.  
  3701.     // coordinates of the initial mousedown
  3702.     mouseX0: null,
  3703.     mouseY0: null,
  3704.  
  3705.     // handler attached to the document, bound to the DragListener's `this`
  3706.     mousemoveProxy: null,
  3707.     mouseupProxy: null,
  3708.  
  3709.     scrollEl: null,
  3710.     scrollBounds: null, // { top, bottom, left, right }
  3711.     scrollTopVel: null, // pixels per second
  3712.     scrollLeftVel: null, // pixels per second
  3713.     scrollIntervalId: null, // ID of setTimeout for scrolling animation loop
  3714.     scrollHandlerProxy: null, // this-scoped function for handling when scrollEl is scrolled
  3715.  
  3716.     scrollSensitivity: 30, // pixels from edge for scrolling to start
  3717.     scrollSpeed: 200, // pixels per second, at maximum speed
  3718.     scrollIntervalMs: 50, // millisecond wait between scroll increment
  3719.  
  3720.  
  3721.     // Call this when the user does a mousedown. Will probably lead to startListening
  3722.     mousedown: function(ev) {
  3723.         if (isPrimaryMouseButton(ev)) {
  3724.  
  3725.             ev.preventDefault(); // prevents native selection in most browsers
  3726.  
  3727.             this.startListening(ev);
  3728.  
  3729.             // start the drag immediately if there is no minimum distance for a drag start
  3730.             if (!this.options.distance) {
  3731.                 this.startDrag(ev);
  3732.             }
  3733.         }
  3734.     },
  3735.  
  3736.  
  3737.     // Call this to start tracking mouse movements
  3738.     startListening: function(ev) {
  3739.         var scrollParent;
  3740.         var cell;
  3741.  
  3742.         if (!this.isListening) {
  3743.  
  3744.             // grab scroll container and attach handler
  3745.             if (ev && this.options.scroll) {
  3746.                 scrollParent = getScrollParent($(ev.target));
  3747.                 if (!scrollParent.is(window) && !scrollParent.is(document)) {
  3748.                     this.scrollEl = scrollParent;
  3749.  
  3750.                     // scope to `this`, and use `debounce` to make sure rapid calls don't happen
  3751.                     this.scrollHandlerProxy = debounce($.proxy(this, 'scrollHandler'), 100);
  3752.                     this.scrollEl.on('scroll', this.scrollHandlerProxy);
  3753.                 }
  3754.             }
  3755.  
  3756.             this.computeCoords(); // relies on `scrollEl`
  3757.  
  3758.             // get info on the initial cell, date, and coordinates
  3759.             if (ev) {
  3760.                 cell = this.getCell(ev);
  3761.                 this.origCell = cell;
  3762.                 this.origDate = cell ? cell.date : null;
  3763.  
  3764.                 this.mouseX0 = ev.pageX;
  3765.                 this.mouseY0 = ev.pageY;
  3766.             }
  3767.  
  3768.             $(document)
  3769.                 .on('mousemove', this.mousemoveProxy = $.proxy(this, 'mousemove'))
  3770.                 .on('mouseup', this.mouseupProxy = $.proxy(this, 'mouseup'))
  3771.                 .on('selectstart', this.preventDefault); // prevents native selection in IE<=8
  3772.  
  3773.             this.isListening = true;
  3774.             this.trigger('listenStart', ev);
  3775.         }
  3776.     },
  3777.  
  3778.  
  3779.     // Recomputes the drag-critical positions of elements
  3780.     computeCoords: function() {
  3781.         this.coordMap.build();
  3782.         this.computeScrollBounds();
  3783.     },
  3784.  
  3785.  
  3786.     // Called when the user moves the mouse
  3787.     mousemove: function(ev) {
  3788.         var minDistance;
  3789.         var distanceSq; // current distance from mouseX0/mouseY0, squared
  3790.  
  3791.         if (!this.isDragging) { // if not already dragging...
  3792.             // then start the drag if the minimum distance criteria is met
  3793.             minDistance = this.options.distance || 1;
  3794.             distanceSq = Math.pow(ev.pageX - this.mouseX0, 2) + Math.pow(ev.pageY - this.mouseY0, 2);
  3795.             if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem
  3796.                 this.startDrag(ev);
  3797.             }
  3798.         }
  3799.  
  3800.         if (this.isDragging) {
  3801.             this.drag(ev); // report a drag, even if this mousemove initiated the drag
  3802.         }
  3803.     },
  3804.  
  3805.  
  3806.     // Call this to initiate a legitimate drag.
  3807.     // This function is called internally from this class, but can also be called explicitly from outside
  3808.     startDrag: function(ev) {
  3809.         var cell;
  3810.  
  3811.         if (!this.isListening) { // startDrag must have manually initiated
  3812.             this.startListening();
  3813.         }
  3814.  
  3815.         if (!this.isDragging) {
  3816.             this.isDragging = true;
  3817.             this.trigger('dragStart', ev);
  3818.  
  3819.             // report the initial cell the mouse is over
  3820.             cell = this.getCell(ev);
  3821.             if (cell) {
  3822.                 this.cellOver(cell, true);
  3823.             }
  3824.         }
  3825.     },
  3826.  
  3827.  
  3828.     // Called while the mouse is being moved and when we know a legitimate drag is taking place
  3829.     drag: function(ev) {
  3830.         var cell;
  3831.  
  3832.         if (this.isDragging) {
  3833.             cell = this.getCell(ev);
  3834.  
  3835.             if (!isCellsEqual(cell, this.cell)) { // a different cell than before?
  3836.                 if (this.cell) {
  3837.                     this.cellOut();
  3838.                 }
  3839.                 if (cell) {
  3840.                     this.cellOver(cell);
  3841.                 }
  3842.             }
  3843.  
  3844.             this.dragScroll(ev); // will possibly cause scrolling
  3845.         }
  3846.     },
  3847.  
  3848.  
  3849.     // Called when a the mouse has just moved over a new cell
  3850.     cellOver: function(cell) {
  3851.         this.cell = cell;
  3852.         this.date = cell.date;
  3853.         this.trigger('cellOver', cell, cell.date);
  3854.     },
  3855.  
  3856.  
  3857.     // Called when the mouse has just moved out of a cell
  3858.     cellOut: function() {
  3859.         if (this.cell) {
  3860.             this.trigger('cellOut', this.cell);
  3861.             this.cell = null;
  3862.             this.date = null;
  3863.         }
  3864.     },
  3865.  
  3866.  
  3867.     // Called when the user does a mouseup
  3868.     mouseup: function(ev) {
  3869.         this.stopDrag(ev);
  3870.         this.stopListening(ev);
  3871.     },
  3872.  
  3873.  
  3874.     // Called when the drag is over. Will not cause listening to stop however.
  3875.     // A concluding 'cellOut' event will NOT be triggered.
  3876.     stopDrag: function(ev) {
  3877.         if (this.isDragging) {
  3878.             this.stopScrolling();
  3879.             this.trigger('dragStop', ev);
  3880.             this.isDragging = false;
  3881.         }
  3882.     },
  3883.  
  3884.  
  3885.     // Call this to stop listening to the user's mouse events
  3886.     stopListening: function(ev) {
  3887.         if (this.isListening) {
  3888.  
  3889.             // remove the scroll handler if there is a scrollEl
  3890.             if (this.scrollEl) {
  3891.                 this.scrollEl.off('scroll', this.scrollHandlerProxy);
  3892.                 this.scrollHandlerProxy = null;
  3893.             }
  3894.  
  3895.             $(document)
  3896.                 .off('mousemove', this.mousemoveProxy)
  3897.                 .off('mouseup', this.mouseupProxy)
  3898.                 .off('selectstart', this.preventDefault);
  3899.  
  3900.             this.mousemoveProxy = null;
  3901.             this.mouseupProxy = null;
  3902.  
  3903.             this.isListening = false;
  3904.             this.trigger('listenStop', ev);
  3905.  
  3906.             this.origCell = this.cell = null;
  3907.             this.origDate = this.date = null;
  3908.         }
  3909.     },
  3910.  
  3911.  
  3912.     // Gets the cell underneath the coordinates for the given mouse event
  3913.     getCell: function(ev) {
  3914.         return this.coordMap.getCell(ev.pageX, ev.pageY);
  3915.     },
  3916.  
  3917.  
  3918.     // Triggers a callback. Calls a function in the option hash of the same name.
  3919.     // Arguments beyond the first `name` are forwarded on.
  3920.     trigger: function(name) {
  3921.         if (this.options[name]) {
  3922.             this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
  3923.         }
  3924.     },
  3925.  
  3926.  
  3927.     // Stops a given mouse event from doing it's native browser action. In our case, text selection.
  3928.     preventDefault: function(ev) {
  3929.         ev.preventDefault();
  3930.     },
  3931.  
  3932.  
  3933.     /* Scrolling
  3934.     ------------------------------------------------------------------------------------------------------------------*/
  3935.  
  3936.  
  3937.     // Computes and stores the bounding rectangle of scrollEl
  3938.     computeScrollBounds: function() {
  3939.         var el = this.scrollEl;
  3940.         var offset;
  3941.  
  3942.         if (el) {
  3943.             offset = el.offset();
  3944.             this.scrollBounds = {
  3945.                 top: offset.top,
  3946.                 left: offset.left,
  3947.                 bottom: offset.top + el.outerHeight(),
  3948.                 right: offset.left + el.outerWidth()
  3949.             };
  3950.         }
  3951.     },
  3952.  
  3953.  
  3954.     // Called when the dragging is in progress and scrolling should be updated
  3955.     dragScroll: function(ev) {
  3956.         var sensitivity = this.scrollSensitivity;
  3957.         var bounds = this.scrollBounds;
  3958.         var topCloseness, bottomCloseness;
  3959.         var leftCloseness, rightCloseness;
  3960.         var topVel = 0;
  3961.         var leftVel = 0;
  3962.  
  3963.         if (bounds) { // only scroll if scrollEl exists
  3964.  
  3965.             // compute closeness to edges. valid range is from 0.0 - 1.0
  3966.             topCloseness = (sensitivity - (ev.pageY - bounds.top)) / sensitivity;
  3967.             bottomCloseness = (sensitivity - (bounds.bottom - ev.pageY)) / sensitivity;
  3968.             leftCloseness = (sensitivity - (ev.pageX - bounds.left)) / sensitivity;
  3969.             rightCloseness = (sensitivity - (bounds.right - ev.pageX)) / sensitivity;
  3970.  
  3971.             // translate vertical closeness into velocity.
  3972.             // mouse must be completely in bounds for velocity to happen.
  3973.             if (topCloseness >= 0 && topCloseness <= 1) {
  3974.                 topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up
  3975.             }
  3976.             else if (bottomCloseness >= 0 && bottomCloseness <= 1) {
  3977.                 topVel = bottomCloseness * this.scrollSpeed;
  3978.             }
  3979.  
  3980.             // translate horizontal closeness into velocity
  3981.             if (leftCloseness >= 0 && leftCloseness <= 1) {
  3982.                 leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left
  3983.             }
  3984.             else if (rightCloseness >= 0 && rightCloseness <= 1) {
  3985.                 leftVel = rightCloseness * this.scrollSpeed;
  3986.             }
  3987.         }
  3988.  
  3989.         this.setScrollVel(topVel, leftVel);
  3990.     },
  3991.  
  3992.  
  3993.     // Sets the speed-of-scrolling for the scrollEl
  3994.     setScrollVel: function(topVel, leftVel) {
  3995.  
  3996.         this.scrollTopVel = topVel;
  3997.         this.scrollLeftVel = leftVel;
  3998.  
  3999.         this.constrainScrollVel(); // massages into realistic values
  4000.  
  4001.         // if there is non-zero velocity, and an animation loop hasn't already started, then START
  4002.         if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) {
  4003.             this.scrollIntervalId = setInterval(
  4004.                 $.proxy(this, 'scrollIntervalFunc'), // scope to `this`
  4005.                 this.scrollIntervalMs
  4006.             );
  4007.         }
  4008.     },
  4009.  
  4010.  
  4011.     // Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way
  4012.     constrainScrollVel: function() {
  4013.         var el = this.scrollEl;
  4014.  
  4015.         if (this.scrollTopVel < 0) { // scrolling up?
  4016.             if (el.scrollTop() <= 0) { // already scrolled all the way up?
  4017.                 this.scrollTopVel = 0;
  4018.             }
  4019.         }
  4020.         else if (this.scrollTopVel > 0) { // scrolling down?
  4021.             if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) { // already scrolled all the way down?
  4022.                 this.scrollTopVel = 0;
  4023.             }
  4024.         }
  4025.  
  4026.         if (this.scrollLeftVel < 0) { // scrolling left?
  4027.             if (el.scrollLeft() <= 0) { // already scrolled all the left?
  4028.                 this.scrollLeftVel = 0;
  4029.             }
  4030.         }
  4031.         else if (this.scrollLeftVel > 0) { // scrolling right?
  4032.             if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) { // already scrolled all the way right?
  4033.                 this.scrollLeftVel = 0;
  4034.             }
  4035.         }
  4036.     },
  4037.  
  4038.  
  4039.     // This function gets called during every iteration of the scrolling animation loop
  4040.     scrollIntervalFunc: function() {
  4041.         var el = this.scrollEl;
  4042.         var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by
  4043.  
  4044.         // change the value of scrollEl's scroll
  4045.         if (this.scrollTopVel) {
  4046.             el.scrollTop(el.scrollTop() + this.scrollTopVel * frac);
  4047.         }
  4048.         if (this.scrollLeftVel) {
  4049.             el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac);
  4050.         }
  4051.  
  4052.         this.constrainScrollVel(); // since the scroll values changed, recompute the velocities
  4053.  
  4054.         // if scrolled all the way, which causes the vels to be zero, stop the animation loop
  4055.         if (!this.scrollTopVel && !this.scrollLeftVel) {
  4056.             this.stopScrolling();
  4057.         }
  4058.     },
  4059.  
  4060.  
  4061.     // Kills any existing scrolling animation loop
  4062.     stopScrolling: function() {
  4063.         if (this.scrollIntervalId) {
  4064.             clearInterval(this.scrollIntervalId);
  4065.             this.scrollIntervalId = null;
  4066.  
  4067.             // when all done with scrolling, recompute positions since they probably changed
  4068.             this.computeCoords();
  4069.         }
  4070.     },
  4071.  
  4072.  
  4073.     // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce)
  4074.     scrollHandler: function() {
  4075.         // recompute all coordinates, but *only* if this is *not* part of our scrolling animation
  4076.         if (!this.scrollIntervalId) {
  4077.             this.computeCoords();
  4078.         }
  4079.     }
  4080.  
  4081. };
  4082.  
  4083.  
  4084. // Returns `true` if the cells are identically equal. `false` otherwise.
  4085. // They must have the same row, col, and be from the same grid.
  4086. // Two null values will be considered equal, as two "out of the grid" states are the same.
  4087. function isCellsEqual(cell1, cell2) {
  4088.  
  4089.     if (!cell1 && !cell2) {
  4090.         return true;
  4091.     }
  4092.  
  4093.     if (cell1 && cell2) {
  4094.         return cell1.grid === cell2.grid &&
  4095.             cell1.row === cell2.row &&
  4096.             cell1.col === cell2.col;
  4097.     }
  4098.  
  4099.     return false;
  4100. }
  4101.  
  4102. ;;
  4103.  
  4104. /* Creates a clone of an element and lets it track the mouse as it moves
  4105. ----------------------------------------------------------------------------------------------------------------------*/
  4106.  
  4107. function MouseFollower(sourceEl, options) {
  4108.     this.options = options = options || {};
  4109.     this.sourceEl = sourceEl;
  4110.     this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent
  4111. }
  4112.  
  4113.  
  4114. MouseFollower.prototype = {
  4115.  
  4116.     options: null,
  4117.  
  4118.     sourceEl: null, // the element that will be cloned and made to look like it is dragging
  4119.     el: null, // the clone of `sourceEl` that will track the mouse
  4120.     parentEl: null, // the element that `el` (the clone) will be attached to
  4121.  
  4122.     // the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl
  4123.     top0: null,
  4124.     left0: null,
  4125.  
  4126.     // the initial position of the mouse
  4127.     mouseY0: null,
  4128.     mouseX0: null,
  4129.  
  4130.     // the number of pixels the mouse has moved from its initial position
  4131.     topDelta: null,
  4132.     leftDelta: null,
  4133.  
  4134.     mousemoveProxy: null, // document mousemove handler, bound to the MouseFollower's `this`
  4135.  
  4136.     isFollowing: false,
  4137.     isHidden: false,
  4138.     isAnimating: false, // doing the revert animation?
  4139.  
  4140.  
  4141.     // Causes the element to start following the mouse
  4142.     start: function(ev) {
  4143.         if (!this.isFollowing) {
  4144.             this.isFollowing = true;
  4145.  
  4146.             this.mouseY0 = ev.pageY;
  4147.             this.mouseX0 = ev.pageX;
  4148.             this.topDelta = 0;
  4149.             this.leftDelta = 0;
  4150.  
  4151.             if (!this.isHidden) {
  4152.                 this.updatePosition();
  4153.             }
  4154.  
  4155.             $(document).on('mousemove', this.mousemoveProxy = $.proxy(this, 'mousemove'));
  4156.         }
  4157.     },
  4158.  
  4159.  
  4160.     // Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position.
  4161.     // `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately.
  4162.     stop: function(shouldRevert, callback) {
  4163.         var _this = this;
  4164.         var revertDuration = this.options.revertDuration;
  4165.  
  4166.         function complete() {
  4167.             this.isAnimating = false;
  4168.             _this.destroyEl();
  4169.  
  4170.             this.top0 = this.left0 = null; // reset state for future updatePosition calls
  4171.  
  4172.             if (callback) {
  4173.                 callback();
  4174.             }
  4175.         }
  4176.  
  4177.         if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time
  4178.             this.isFollowing = false;
  4179.  
  4180.             $(document).off('mousemove', this.mousemoveProxy);
  4181.  
  4182.             if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation?
  4183.                 this.isAnimating = true;
  4184.                 this.el.animate({
  4185.                     top: this.top0,
  4186.                     left: this.left0
  4187.                 }, {
  4188.                     duration: revertDuration,
  4189.                     complete: complete
  4190.                 });
  4191.             }
  4192.             else {
  4193.                 complete();
  4194.             }
  4195.         }
  4196.     },
  4197.  
  4198.  
  4199.     // Gets the tracking element. Create it if necessary
  4200.     getEl: function() {
  4201.         var el = this.el;
  4202.  
  4203.         if (!el) {
  4204.             this.sourceEl.width(); // hack to force IE8 to compute correct bounding box
  4205.             el = this.el = this.sourceEl.clone()
  4206.                 .css({
  4207.                     position: 'absolute',
  4208.                     visibility: '', // in case original element was hidden (commonly through hideEvents())
  4209.                     display: this.isHidden ? 'none' : '', // for when initially hidden
  4210.                     margin: 0,
  4211.                     right: 'auto', // erase and set width instead
  4212.                     bottom: 'auto', // erase and set height instead
  4213.                     width: this.sourceEl.width(), // explicit height in case there was a 'right' value
  4214.                     height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value
  4215.                     opacity: this.options.opacity || '',
  4216.                     zIndex: this.options.zIndex
  4217.                 })
  4218.                 .appendTo(this.parentEl);
  4219.         }
  4220.  
  4221.         return el;
  4222.     },
  4223.  
  4224.  
  4225.     // Removes the tracking element if it has already been created
  4226.     destroyEl: function() {
  4227.         if (this.el) {
  4228.             this.el.remove();
  4229.             this.el = null;
  4230.         }
  4231.     },
  4232.  
  4233.  
  4234.     // Update the CSS position of the tracking element
  4235.     updatePosition: function() {
  4236.         var sourceOffset;
  4237.         var origin;
  4238.  
  4239.         this.getEl(); // ensure this.el
  4240.  
  4241.         // make sure origin info was computed
  4242.         if (this.top0 === null) {
  4243.             this.sourceEl.width(); // hack to force IE8 to compute correct bounding box
  4244.             sourceOffset = this.sourceEl.offset();
  4245.             origin = this.el.offsetParent().offset();
  4246.             this.top0 = sourceOffset.top - origin.top;
  4247.             this.left0 = sourceOffset.left - origin.left;
  4248.         }
  4249.  
  4250.         this.el.css({
  4251.             top: this.top0 + this.topDelta,
  4252.             left: this.left0 + this.leftDelta
  4253.         });
  4254.     },
  4255.  
  4256.  
  4257.     // Gets called when the user moves the mouse
  4258.     mousemove: function(ev) {
  4259.         this.topDelta = ev.pageY - this.mouseY0;
  4260.         this.leftDelta = ev.pageX - this.mouseX0;
  4261.  
  4262.         if (!this.isHidden) {
  4263.             this.updatePosition();
  4264.         }
  4265.     },
  4266.  
  4267.  
  4268.     // Temporarily makes the tracking element invisible. Can be called before following starts
  4269.     hide: function() {
  4270.         if (!this.isHidden) {
  4271.             this.isHidden = true;
  4272.             if (this.el) {
  4273.                 this.el.hide();
  4274.             }
  4275.         }
  4276.     },
  4277.  
  4278.  
  4279.     // Show the tracking element after it has been temporarily hidden
  4280.     show: function() {
  4281.         if (this.isHidden) {
  4282.             this.isHidden = false;
  4283.             this.updatePosition();
  4284.             this.getEl().show();
  4285.         }
  4286.     }
  4287.  
  4288. };
  4289.  
  4290. ;;
  4291.  
  4292. /* A utility class for rendering <tr> rows.
  4293. ----------------------------------------------------------------------------------------------------------------------*/
  4294. // It leverages methods of the subclass and the View to determine custom rendering behavior for each row "type"
  4295. // (such as highlight rows, day rows, helper rows, etc).
  4296.  
  4297. function RowRenderer(view) {
  4298.     this.view = view;
  4299. }
  4300.  
  4301.  
  4302. RowRenderer.prototype = {
  4303.  
  4304.     view: null, // a View object
  4305.     cellHtml: '<td/>', // plain default HTML used for a cell when no other is available
  4306.  
  4307.  
  4308.     // Renders the HTML for a row, leveraging custom cell-HTML-renderers based on the `rowType`.
  4309.     // Also applies the "intro" and "outro" cells, which are specified by the subclass and views.
  4310.     // `row` is an optional row number.
  4311.     rowHtml: function(rowType, row) {
  4312.         var view = this.view;
  4313.         var renderCell = this.getHtmlRenderer('cell', rowType);
  4314.         var cellHtml = '';
  4315.         var col;
  4316.         var date;
  4317.  
  4318.         row = row || 0;
  4319.  
  4320.         for (col = 0; col < view.colCnt; col++) {
  4321.             date = view.cellToDate(row, col);
  4322.             cellHtml += renderCell(row, col, date);
  4323.         }
  4324.  
  4325.         cellHtml = this.bookendCells(cellHtml, rowType, row); // apply intro and outro
  4326.  
  4327.         return '<tr>' + cellHtml + '</tr>';
  4328.     },
  4329.  
  4330.  
  4331.     // Applies the "intro" and "outro" HTML to the given cells.
  4332.     // Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro.
  4333.     // `cells` can be an HTML string of <td>'s or a jQuery <tr> element
  4334.     // `row` is an optional row number.
  4335.     bookendCells: function(cells, rowType, row) {
  4336.         var view = this.view;
  4337.         var intro = this.getHtmlRenderer('intro', rowType)(row || 0);
  4338.         var outro = this.getHtmlRenderer('outro', rowType)(row || 0);
  4339.         var isRTL = view.opt('isRTL');
  4340.         var prependHtml = isRTL ? outro : intro;
  4341.         var appendHtml = isRTL ? intro : outro;
  4342.  
  4343.         if (typeof cells === 'string') {
  4344.             return prependHtml + cells + appendHtml;
  4345.         }
  4346.         else { // a jQuery <tr> element
  4347.             return cells.prepend(prependHtml).append(appendHtml);
  4348.         }
  4349.     },
  4350.  
  4351.  
  4352.     // Returns an HTML-rendering function given a specific `rendererName` (like cell, intro, or outro) and a specific
  4353.     // `rowType` (like day, eventSkeleton, helperSkeleton), which is optional.
  4354.     // If a renderer for the specific rowType doesn't exist, it will fall back to a generic renderer.
  4355.     // We will query the View object first for any custom rendering functions, then the methods of the subclass.
  4356.     getHtmlRenderer: function(rendererName, rowType) {
  4357.         var view = this.view;
  4358.         var generalName; // like "cellHtml"
  4359.         var specificName; // like "dayCellHtml". based on rowType
  4360.         var provider; // either the View or the RowRenderer subclass, whichever provided the method
  4361.         var renderer;
  4362.  
  4363.         generalName = rendererName + 'Html';
  4364.         if (rowType) {
  4365.             specificName = rowType + capitaliseFirstLetter(rendererName) + 'Html';
  4366.         }
  4367.  
  4368.         if (specificName && (renderer = view[specificName])) {
  4369.             provider = view;
  4370.         }
  4371.         else if (specificName && (renderer = this[specificName])) {
  4372.             provider = this;
  4373.         }
  4374.         else if ((renderer = view[generalName])) {
  4375.             provider = view;
  4376.         }
  4377.         else if ((renderer = this[generalName])) {
  4378.             provider = this;
  4379.         }
  4380.  
  4381.         if (typeof renderer === 'function') {
  4382.             return function() {
  4383.                 return renderer.apply(provider, arguments) || ''; // use correct `this` and always return a string
  4384.             };
  4385.         }
  4386.  
  4387.         // the rendered can be a plain string as well. if not specified, always an empty string.
  4388.         return function() {
  4389.             return renderer || '';
  4390.         };
  4391.     }
  4392.  
  4393. };
  4394.  
  4395. ;;
  4396.  
  4397. /* An abstract class comprised of a "grid" of cells that each represent a specific datetime
  4398. ----------------------------------------------------------------------------------------------------------------------*/
  4399.  
  4400. function Grid(view) {
  4401.     RowRenderer.call(this, view); // call the super-constructor
  4402.     this.coordMap = new GridCoordMap(this);
  4403.     this.elsByFill = {};
  4404. }
  4405.  
  4406.  
  4407. Grid.prototype = createObject(RowRenderer.prototype); // declare the super-class
  4408. $.extend(Grid.prototype, {
  4409.  
  4410.     el: null, // the containing element
  4411.     coordMap: null, // a GridCoordMap that converts pixel values to datetimes
  4412.     cellDuration: null, // a cell's duration. subclasses must assign this ASAP
  4413.     elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.
  4414.  
  4415.  
  4416.     // Renders the grid into the `el` element.
  4417.     // Subclasses should override and call this super-method when done.
  4418.     render: function() {
  4419.         this.bindHandlers();
  4420.     },
  4421.  
  4422.  
  4423.     // Called when the grid's resources need to be cleaned up
  4424.     destroy: function() {
  4425.         // subclasses can implement
  4426.     },
  4427.  
  4428.  
  4429.     /* Coordinates & Cells
  4430.     ------------------------------------------------------------------------------------------------------------------*/
  4431.  
  4432.  
  4433.     // Populates the given empty arrays with the y and x coordinates of the cells
  4434.     buildCoords: function(rows, cols) {
  4435.         // subclasses must implement
  4436.     },
  4437.  
  4438.  
  4439.     // Given a cell object, returns the date for that cell
  4440.     getCellDate: function(cell) {
  4441.         // subclasses must implement
  4442.     },
  4443.  
  4444.  
  4445.     // Given a cell object, returns the element that represents the cell's whole-day
  4446.     getCellDayEl: function(cell) {
  4447.         // subclasses must implement
  4448.     },
  4449.  
  4450.  
  4451.     // Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects
  4452.     rangeToSegs: function(start, end) {
  4453.         // subclasses must implement
  4454.     },
  4455.  
  4456.  
  4457.     /* Handlers
  4458.     ------------------------------------------------------------------------------------------------------------------*/
  4459.  
  4460.  
  4461.     // Attach handlers to `this.el`, using bubbling to listen to all ancestors.
  4462.     // We don't need to undo any of this in a "destroy" method, because the view will simply remove `this.el` from the
  4463.     // DOM and jQuery will be smart enough to garbage collect the handlers.
  4464.     bindHandlers: function() {
  4465.         var _this = this;
  4466.  
  4467.         this.el.on('mousedown', function(ev) {
  4468.             if (
  4469.                 !$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link
  4470.                 !$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one)
  4471.             ) {
  4472.                 _this.dayMousedown(ev);
  4473.             }
  4474.         });
  4475.  
  4476.         this.bindSegHandlers(); // attach event-element-related handlers. in Grid.events.js
  4477.     },
  4478.  
  4479.  
  4480.     // Process a mousedown on an element that represents a day. For day clicking and selecting.
  4481.     dayMousedown: function(ev) {
  4482.         var _this = this;
  4483.         var view = this.view;
  4484.         var calendar = view.calendar;
  4485.         var isSelectable = view.opt('selectable');
  4486.         var dates = null; // the inclusive dates of the selection. will be null if no selection
  4487.         var start; // the inclusive start of the selection
  4488.         var end; // the *exclusive* end of the selection
  4489.         var dayEl;
  4490.  
  4491.         // this listener tracks a mousedown on a day element, and a subsequent drag.
  4492.         // if the drag ends on the same day, it is a 'dayClick'.
  4493.         // if 'selectable' is enabled, this listener also detects selections.
  4494.         var dragListener = new DragListener(this.coordMap, {
  4495.             //distance: 5, // needs more work if we want dayClick to fire correctly
  4496.             scroll: view.opt('dragScroll'),
  4497.             dragStart: function() {
  4498.                 view.unselect(); // since we could be rendering a new selection, we want to clear any old one
  4499.             },
  4500.             cellOver: function(cell, date) {
  4501.                 if (dragListener.origDate) { // click needs to have started on a cell
  4502.  
  4503.                     dayEl = _this.getCellDayEl(cell);
  4504.  
  4505.                     dates = [ date, dragListener.origDate ].sort(dateCompare);
  4506.                     start = dates[0];
  4507.                     end = dates[1].clone().add(_this.cellDuration);
  4508.  
  4509.                     if (isSelectable) {
  4510.                         if (calendar.isSelectionAllowedInRange(start, end)) { // allowed to select within this range?
  4511.                             _this.renderSelection(start, end);
  4512.                         }
  4513.                         else {
  4514.                             dates = null; // flag for an invalid selection
  4515.                             disableCursor();
  4516.                         }
  4517.                     }
  4518.                 }
  4519.             },
  4520.             cellOut: function(cell, date) {
  4521.                 dates = null;
  4522.                 _this.destroySelection();
  4523.                 enableCursor();
  4524.             },
  4525.             listenStop: function(ev) {
  4526.                 if (dates) { // started and ended on a cell?
  4527.                     if (dates[0].isSame(dates[1])) {
  4528.                         view.trigger('dayClick', dayEl[0], start, ev);
  4529.                     }
  4530.                     if (isSelectable) {
  4531.                         // the selection will already have been rendered. just report it
  4532.                         view.reportSelection(start, end, ev);
  4533.                     }
  4534.                 }
  4535.                 enableCursor();
  4536.             }
  4537.         });
  4538.  
  4539.         dragListener.mousedown(ev); // start listening, which will eventually initiate a dragStart
  4540.     },
  4541.  
  4542.  
  4543.     /* Event Dragging
  4544.     ------------------------------------------------------------------------------------------------------------------*/
  4545.  
  4546.  
  4547.     // Renders a visual indication of a event being dragged over the given date(s).
  4548.     // `end` can be null, as well as `seg`. See View's documentation on renderDrag for more info.
  4549.     // A returned value of `true` signals that a mock "helper" event has been rendered.
  4550.     renderDrag: function(start, end, seg) {
  4551.         // subclasses must implement
  4552.     },
  4553.  
  4554.  
  4555.     // Unrenders a visual indication of an event being dragged
  4556.     destroyDrag: function() {
  4557.         // subclasses must implement
  4558.     },
  4559.  
  4560.  
  4561.     /* Event Resizing
  4562.     ------------------------------------------------------------------------------------------------------------------*/
  4563.  
  4564.  
  4565.     // Renders a visual indication of an event being resized.
  4566.     // `start` and `end` are the updated dates of the event. `seg` is the original segment object involved in the drag.
  4567.     renderResize: function(start, end, seg) {
  4568.         // subclasses must implement
  4569.     },
  4570.  
  4571.  
  4572.     // Unrenders a visual indication of an event being resized.
  4573.     destroyResize: function() {
  4574.         // subclasses must implement
  4575.     },
  4576.  
  4577.  
  4578.     /* Event Helper
  4579.     ------------------------------------------------------------------------------------------------------------------*/
  4580.  
  4581.  
  4582.     // Renders a mock event over the given date(s).
  4583.     // `end` can be null, in which case the mock event that is rendered will have a null end time.
  4584.     // `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging.
  4585.     renderRangeHelper: function(start, end, sourceSeg) {
  4586.         var view = this.view;
  4587.         var fakeEvent;
  4588.  
  4589.         // compute the end time if forced to do so (this is what EventManager does)
  4590.         if (!end && view.opt('forceEventDuration')) {
  4591.             end = view.calendar.getDefaultEventEnd(!start.hasTime(), start);
  4592.         }
  4593.  
  4594.         fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible
  4595.         fakeEvent.start = start;
  4596.         fakeEvent.end = end;
  4597.         fakeEvent.allDay = !(start.hasTime() || (end && end.hasTime())); // freshly compute allDay
  4598.  
  4599.         // this extra className will be useful for differentiating real events from mock events in CSS
  4600.         fakeEvent.className = (fakeEvent.className || []).concat('fc-helper');
  4601.  
  4602.         // if something external is being dragged in, don't render a resizer
  4603.         if (!sourceSeg) {
  4604.             fakeEvent.editable = false;
  4605.         }
  4606.  
  4607.         this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering
  4608.     },
  4609.  
  4610.  
  4611.     // Renders a mock event
  4612.     renderHelper: function(event, sourceSeg) {
  4613.         // subclasses must implement
  4614.     },
  4615.  
  4616.  
  4617.     // Unrenders a mock event
  4618.     destroyHelper: function() {
  4619.         // subclasses must implement
  4620.     },
  4621.  
  4622.  
  4623.     /* Selection
  4624.     ------------------------------------------------------------------------------------------------------------------*/
  4625.  
  4626.  
  4627.     // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses.
  4628.     renderSelection: function(start, end) {
  4629.         this.renderHighlight(start, end);
  4630.     },
  4631.  
  4632.  
  4633.     // Unrenders any visual indications of a selection. Will unrender a highlight by default.
  4634.     destroySelection: function() {
  4635.         this.destroyHighlight();
  4636.     },
  4637.  
  4638.  
  4639.     /* Highlight
  4640.     ------------------------------------------------------------------------------------------------------------------*/
  4641.  
  4642.  
  4643.     // Renders an emphasis on the given date range. `start` is inclusive. `end` is exclusive.
  4644.     renderHighlight: function(start, end) {
  4645.         this.renderFill('highlight', this.rangeToSegs(start, end));
  4646.     },
  4647.  
  4648.  
  4649.     // Unrenders the emphasis on a date range
  4650.     destroyHighlight: function() {
  4651.         this.destroyFill('highlight');
  4652.     },
  4653.  
  4654.  
  4655.     // Generates an array of classNames for rendering the highlight. Used by the fill system.
  4656.     highlightSegClasses: function() {
  4657.         return [ 'fc-highlight' ];
  4658.     },
  4659.  
  4660.  
  4661.     /* Fill System (highlight, background events, business hours)
  4662.     ------------------------------------------------------------------------------------------------------------------*/
  4663.  
  4664.  
  4665.     // Renders a set of rectangles over the given segments of time.
  4666.     // Returns a subset of segs, the segs that were actually rendered.
  4667.     // Responsible for populating this.elsByFill
  4668.     renderFill: function(type, segs) {
  4669.         // subclasses must implement
  4670.     },
  4671.  
  4672.  
  4673.     // Unrenders a specific type of fill that is currently rendered on the grid
  4674.     destroyFill: function(type) {
  4675.         var el = this.elsByFill[type];
  4676.  
  4677.         if (el) {
  4678.             el.remove();
  4679.             delete this.elsByFill[type];
  4680.         }
  4681.     },
  4682.  
  4683.  
  4684.     // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types.
  4685.     // Only returns segments that successfully rendered.
  4686.     // To be harnessed by renderFill (implemented by subclasses).
  4687.     // Analagous to renderFgSegEls.
  4688.     renderFillSegEls: function(type, segs) {
  4689.         var _this = this;
  4690.         var segElMethod = this[type + 'SegEl'];
  4691.         var html = '';
  4692.         var renderedSegs = [];
  4693.         var i;
  4694.  
  4695.         if (segs.length) {
  4696.  
  4697.             // build a large concatenation of segment HTML
  4698.             for (i = 0; i < segs.length; i++) {
  4699.                 html += this.fillSegHtml(type, segs[i]);
  4700.             }
  4701.  
  4702.             // Grab individual elements from the combined HTML string. Use each as the default rendering.
  4703.             // Then, compute the 'el' for each segment.
  4704.             $(html).each(function(i, node) {
  4705.                 var seg = segs[i];
  4706.                 var el = $(node);
  4707.  
  4708.                 // allow custom filter methods per-type
  4709.                 if (segElMethod) {
  4710.                     el = segElMethod.call(_this, seg, el);
  4711.                 }
  4712.  
  4713.                 if (el) { // custom filters did not cancel the render
  4714.                     el = $(el); // allow custom filter to return raw DOM node
  4715.  
  4716.                     // correct element type? (would be bad if a non-TD were inserted into a table for example)
  4717.                     if (el.is(_this.fillSegTag)) {
  4718.                         seg.el = el;
  4719.                         renderedSegs.push(seg);
  4720.                     }
  4721.                 }
  4722.             });
  4723.         }
  4724.  
  4725.         return renderedSegs;
  4726.     },
  4727.  
  4728.  
  4729.     fillSegTag: 'div', // subclasses can override
  4730.  
  4731.  
  4732.     // Builds the HTML needed for one fill segment. Generic enought o work with different types.
  4733.     fillSegHtml: function(type, seg) {
  4734.         var classesMethod = this[type + 'SegClasses']; // custom hooks per-type
  4735.         var stylesMethod = this[type + 'SegStyles']; //
  4736.         var classes = classesMethod ? classesMethod.call(this, seg) : [];
  4737.         var styles = stylesMethod ? stylesMethod.call(this, seg) : ''; // a semi-colon separated CSS property string
  4738.  
  4739.         return '<' + this.fillSegTag +
  4740.             (classes.length ? ' class="' + classes.join(' ') + '"' : '') +
  4741.             (styles ? ' style="' + styles + '"' : '') +
  4742.             ' />';
  4743.     },
  4744.  
  4745.  
  4746.     /* Generic rendering utilities for subclasses
  4747.     ------------------------------------------------------------------------------------------------------------------*/
  4748.  
  4749.  
  4750.     // Renders a day-of-week header row
  4751.     headHtml: function() {
  4752.         return '' +
  4753.             '<div class="fc-row ' + this.view.widgetHeaderClass + '">' +
  4754.                 '<table>' +
  4755.                     '<thead>' +
  4756.                         this.rowHtml('head') + // leverages RowRenderer
  4757.                     '</thead>' +
  4758.                 '</table>' +
  4759.             '</div>';
  4760.     },
  4761.  
  4762.  
  4763.     // Used by the `headHtml` method, via RowRenderer, for rendering the HTML of a day-of-week header cell
  4764.     headCellHtml: function(row, col, date) {
  4765.         var view = this.view;
  4766.         var calendar = view.calendar;
  4767.         var colFormat = view.opt('columnFormat');
  4768.  
  4769.         return '' +
  4770.             '<th class="fc-day-header ' + view.widgetHeaderClass + ' fc-' + dayIDs[date.day()] + '">' +
  4771.                 htmlEscape(calendar.formatDate(date, colFormat)) +
  4772.             '</th>';
  4773.     },
  4774.  
  4775.  
  4776.     // Renders the HTML for a single-day background cell
  4777.     bgCellHtml: function(row, col, date) {
  4778.         var view = this.view;
  4779.         var classes = this.getDayClasses(date);
  4780.  
  4781.         classes.unshift('fc-day', view.widgetContentClass);
  4782.  
  4783.         return '<td class="' + classes.join(' ') + '" data-date="' + date.format() + '"></td>';
  4784.     },
  4785.  
  4786.  
  4787.     // Computes HTML classNames for a single-day cell
  4788.     getDayClasses: function(date) {
  4789.         var view = this.view;
  4790.         var today = view.calendar.getNow().stripTime();
  4791.         var classes = [ 'fc-' + dayIDs[date.day()] ];
  4792.  
  4793.         if (
  4794.             view.name === 'month' &&
  4795.             date.month() != view.intervalStart.month()
  4796.         ) {
  4797.             classes.push('fc-other-month');
  4798.         }
  4799.  
  4800.         if (date.isSame(today, 'day')) {
  4801.             classes.push(
  4802.                 'fc-today',
  4803.                 view.highlightStateClass
  4804.             );
  4805.         }
  4806.         else if (date < today) {
  4807.             classes.push('fc-past');
  4808.         }
  4809.         else {
  4810.             classes.push('fc-future');
  4811.         }
  4812.  
  4813.         return classes;
  4814.     }
  4815.  
  4816. });
  4817.  
  4818. ;;
  4819.  
  4820. /* Event-rendering and event-interaction methods for the abstract Grid class
  4821. ----------------------------------------------------------------------------------------------------------------------*/
  4822.  
  4823. $.extend(Grid.prototype, {
  4824.  
  4825.     mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing
  4826.     isDraggingSeg: false, // is a segment being dragged? boolean
  4827.     isResizingSeg: false, // is a segment being resized? boolean
  4828.     segs: null, // the event segments currently rendered in the grid
  4829.  
  4830.  
  4831.     // Renders the given events onto the grid
  4832.     renderEvents: function(events) {
  4833.         var segs = this.eventsToSegs(events);
  4834.         var bgSegs = [];
  4835.         var fgSegs = [];
  4836.         var i, seg;
  4837.  
  4838.         for (i = 0; i < segs.length; i++) {
  4839.             seg = segs[i];
  4840.  
  4841.             if (isBgEvent(seg.event)) {
  4842.                 bgSegs.push(seg);
  4843.             }
  4844.             else {
  4845.                 fgSegs.push(seg);
  4846.             }
  4847.         }
  4848.  
  4849.         // Render each different type of segment.
  4850.         // Each function may return a subset of the segs, segs that were actually rendered.
  4851.         bgSegs = this.renderBgSegs(bgSegs) || bgSegs;
  4852.         fgSegs = this.renderFgSegs(fgSegs) || fgSegs;
  4853.  
  4854.         this.segs = bgSegs.concat(fgSegs);
  4855.     },
  4856.  
  4857.  
  4858.     // Unrenders all events currently rendered on the grid
  4859.     destroyEvents: function() {
  4860.         this.triggerSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
  4861.  
  4862.         this.destroyFgSegs();
  4863.         this.destroyBgSegs();
  4864.  
  4865.         this.segs = null;
  4866.     },
  4867.  
  4868.  
  4869.     // Retrieves all rendered segment objects currently rendered on the grid
  4870.     getSegs: function() {
  4871.         return this.segs || [];
  4872.     },
  4873.  
  4874.  
  4875.     /* Foreground Segment Rendering
  4876.     ------------------------------------------------------------------------------------------------------------------*/
  4877.  
  4878.  
  4879.     // Renders foreground event segments onto the grid. May return a subset of segs that were rendered.
  4880.     renderFgSegs: function(segs) {
  4881.         // subclasses must implement
  4882.     },
  4883.  
  4884.  
  4885.     // Unrenders all currently rendered foreground segments
  4886.     destroyFgSegs: function() {
  4887.         // subclasses must implement
  4888.     },
  4889.  
  4890.  
  4891.     // Renders and assigns an `el` property for each foreground event segment.
  4892.     // Only returns segments that successfully rendered.
  4893.     // A utility that subclasses may use.
  4894.     renderFgSegEls: function(segs, disableResizing) {
  4895.         var view = this.view;
  4896.         var html = '';
  4897.         var renderedSegs = [];
  4898.         var i;
  4899.  
  4900.         if (segs.length) { // don't build an empty html string
  4901.  
  4902.             // build a large concatenation of event segment HTML
  4903.             for (i = 0; i < segs.length; i++) {
  4904.                 html += this.fgSegHtml(segs[i], disableResizing);
  4905.             }
  4906.  
  4907.             // Grab individual elements from the combined HTML string. Use each as the default rendering.
  4908.             // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
  4909.             $(html).each(function(i, node) {
  4910.                 var seg = segs[i];
  4911.                 var el = view.resolveEventEl(seg.event, $(node));
  4912.  
  4913.                 if (el) {
  4914.                     el.data('fc-seg', seg); // used by handlers
  4915.                     seg.el = el;
  4916.                     renderedSegs.push(seg);
  4917.                 }
  4918.             });
  4919.         }
  4920.  
  4921.         return renderedSegs;
  4922.     },
  4923.  
  4924.  
  4925.     // Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls()
  4926.     fgSegHtml: function(seg, disableResizing) {
  4927.         // subclasses should implement
  4928.     },
  4929.  
  4930.  
  4931.     /* Background Segment Rendering
  4932.     ------------------------------------------------------------------------------------------------------------------*/
  4933.  
  4934.  
  4935.     // Renders the given background event segments onto the grid.
  4936.     // Returns a subset of the segs that were actually rendered.
  4937.     renderBgSegs: function(segs) {
  4938.         return this.renderFill('bgEvent', segs);
  4939.     },
  4940.  
  4941.  
  4942.     // Unrenders all the currently rendered background event segments
  4943.     destroyBgSegs: function() {
  4944.         this.destroyFill('bgEvent');
  4945.     },
  4946.  
  4947.  
  4948.     // Renders a background event element, given the default rendering. Called by the fill system.
  4949.     bgEventSegEl: function(seg, el) {
  4950.         return this.view.resolveEventEl(seg.event, el); // will filter through eventRender
  4951.     },
  4952.  
  4953.  
  4954.     // Generates an array of classNames to be used for the default rendering of a background event.
  4955.     // Called by the fill system.
  4956.     bgEventSegClasses: function(seg) {
  4957.         var event = seg.event;
  4958.         var source = event.source || {};
  4959.  
  4960.         return [ 'fc-bgevent' ].concat(
  4961.             event.className,
  4962.             source.className || []
  4963.         );
  4964.     },
  4965.  
  4966.  
  4967.     // Generates a semicolon-separated CSS string to be used for the default rendering of a background event.
  4968.     // Called by the fill system.
  4969.     // TODO: consolidate with getEventSkinCss?
  4970.     bgEventSegStyles: function(seg) {
  4971.         var view = this.view;
  4972.         var event = seg.event;
  4973.         var source = event.source || {};
  4974.         var eventColor = event.color;
  4975.         var sourceColor = source.color;
  4976.         var optionColor = view.opt('eventColor');
  4977.         var backgroundColor =
  4978.             event.backgroundColor ||
  4979.             eventColor ||
  4980.             source.backgroundColor ||
  4981.             sourceColor ||
  4982.             view.opt('eventBackgroundColor') ||
  4983.             optionColor;
  4984.  
  4985.         if (backgroundColor) {
  4986.             return 'background-color:' + backgroundColor;
  4987.         }
  4988.  
  4989.         return '';
  4990.     },
  4991.  
  4992.  
  4993.     // Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system.
  4994.     businessHoursSegClasses: function(seg) {
  4995.         return [ 'fc-nonbusiness', 'fc-bgevent' ];
  4996.     },
  4997.  
  4998.  
  4999.     /* Handlers
  5000.     ------------------------------------------------------------------------------------------------------------------*/
  5001.  
  5002.  
  5003.     // Attaches event-element-related handlers to the container element and leverage bubbling
  5004.     bindSegHandlers: function() {
  5005.         var _this = this;
  5006.         var view = this.view;
  5007.  
  5008.         $.each(
  5009.             {
  5010.                 mouseenter: function(seg, ev) {
  5011.                     _this.triggerSegMouseover(seg, ev);
  5012.                 },
  5013.                 mouseleave: function(seg, ev) {
  5014.                     _this.triggerSegMouseout(seg, ev);
  5015.                 },
  5016.                 click: function(seg, ev) {
  5017.                     return view.trigger('eventClick', this, seg.event, ev); // can return `false` to cancel
  5018.                 },
  5019.                 mousedown: function(seg, ev) {
  5020.                     if ($(ev.target).is('.fc-resizer') && view.isEventResizable(seg.event)) {
  5021.                         _this.segResizeMousedown(seg, ev);
  5022.                     }
  5023.                     else if (view.isEventDraggable(seg.event)) {
  5024.                         _this.segDragMousedown(seg, ev);
  5025.                     }
  5026.                 }
  5027.             },
  5028.             function(name, func) {
  5029.                 // attach the handler to the container element and only listen for real event elements via bubbling
  5030.                 _this.el.on(name, '.fc-event-container > *', function(ev) {
  5031.                     var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents
  5032.  
  5033.                     // only call the handlers if there is not a drag/resize in progress
  5034.                     if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) {
  5035.                         return func.call(this, seg, ev); // `this` will be the event element
  5036.                     }
  5037.                 });
  5038.             }
  5039.         );
  5040.     },
  5041.  
  5042.  
  5043.     // Updates internal state and triggers handlers for when an event element is moused over
  5044.     triggerSegMouseover: function(seg, ev) {
  5045.         if (!this.mousedOverSeg) {
  5046.             this.mousedOverSeg = seg;
  5047.             this.view.trigger('eventMouseover', seg.el[0], seg.event, ev);
  5048.         }
  5049.     },
  5050.  
  5051.  
  5052.     // Updates internal state and triggers handlers for when an event element is moused out.
  5053.     // Can be given no arguments, in which case it will mouseout the segment that was previously moused over.
  5054.     triggerSegMouseout: function(seg, ev) {
  5055.         ev = ev || {}; // if given no args, make a mock mouse event
  5056.  
  5057.         if (this.mousedOverSeg) {
  5058.             seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment
  5059.             this.mousedOverSeg = null;
  5060.             this.view.trigger('eventMouseout', seg.el[0], seg.event, ev);
  5061.         }
  5062.     },
  5063.  
  5064.  
  5065.     /* Dragging
  5066.     ------------------------------------------------------------------------------------------------------------------*/
  5067.  
  5068.  
  5069.     // Called when the user does a mousedown on an event, which might lead to dragging.
  5070.     // Generic enough to work with any type of Grid.
  5071.     segDragMousedown: function(seg, ev) {
  5072.         var _this = this;
  5073.         var view = this.view;
  5074.         var calendar = view.calendar;
  5075.         var el = seg.el;
  5076.         var event = seg.event;
  5077.         var newStart, newEnd;
  5078.  
  5079.         // A clone of the original element that will move with the mouse
  5080.         var mouseFollower = new MouseFollower(seg.el, {
  5081.             parentEl: view.el,
  5082.             opacity: view.opt('dragOpacity'),
  5083.             revertDuration: view.opt('dragRevertDuration'),
  5084.             zIndex: 2 // one above the .fc-view
  5085.         });
  5086.  
  5087.         // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents
  5088.         // of the view.
  5089.         var dragListener = new DragListener(view.coordMap, {
  5090.             distance: 5,
  5091.             scroll: view.opt('dragScroll'),
  5092.             listenStart: function(ev) {
  5093.                 mouseFollower.hide(); // don't show until we know this is a real drag
  5094.                 mouseFollower.start(ev);
  5095.             },
  5096.             dragStart: function(ev) {
  5097.                 _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
  5098.                 _this.isDraggingSeg = true;
  5099.                 view.hideEvent(event); // hide all event segments. our mouseFollower will take over
  5100.                 view.trigger('eventDragStart', el[0], event, ev, {}); // last argument is jqui dummy
  5101.             },
  5102.             cellOver: function(cell, date) {
  5103.                 var origDate = seg.cellDate || dragListener.origDate;
  5104.                 var res = _this.computeDraggedEventDates(seg, origDate, date);
  5105.                 newStart = res.start;
  5106.                 newEnd = res.end;
  5107.  
  5108.                 if (calendar.isEventAllowedInRange(event, newStart, res.visibleEnd)) { // allowed to drop here?
  5109.                     if (view.renderDrag(newStart, newEnd, seg)) { // have the view render a visual indication
  5110.                         mouseFollower.hide(); // if the view is already using a mock event "helper", hide our own
  5111.                     }
  5112.                     else {
  5113.                         mouseFollower.show();
  5114.                     }
  5115.                 }
  5116.                 else {
  5117.                     // have the helper follow the mouse (no snapping) with a warning-style cursor
  5118.                     newStart = null; // mark an invalid drop date
  5119.                     mouseFollower.show();
  5120.                     disableCursor();
  5121.                 }
  5122.             },
  5123.             cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells
  5124.                 newStart = null;
  5125.                 view.destroyDrag(); // unrender whatever was done in view.renderDrag
  5126.                 mouseFollower.show(); // show in case we are moving out of all cells
  5127.                 enableCursor();
  5128.             },
  5129.             dragStop: function(ev) {
  5130.                 var hasChanged = newStart && !newStart.isSame(event.start);
  5131.  
  5132.                 // do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
  5133.                 mouseFollower.stop(!hasChanged, function() {
  5134.                     _this.isDraggingSeg = false;
  5135.                     view.destroyDrag();
  5136.                     view.showEvent(event);
  5137.                     view.trigger('eventDragStop', el[0], event, ev, {}); // last argument is jqui dummy
  5138.  
  5139.                     if (hasChanged) {
  5140.                         view.eventDrop(el[0], event, newStart, ev); // will rerender all events...
  5141.                     }
  5142.                 });
  5143.  
  5144.                 enableCursor();
  5145.             },
  5146.             listenStop: function() {
  5147.                 mouseFollower.stop(); // put in listenStop in case there was a mousedown but the drag never started
  5148.             }
  5149.         });
  5150.  
  5151.         dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart
  5152.     },
  5153.  
  5154.  
  5155.     // Given a segment, the dates where a drag began and ended, calculates the Event Object's new start and end dates.
  5156.     // Might return a `null` end (even when forceEventDuration is on).
  5157.     computeDraggedEventDates: function(seg, dragStartDate, dropDate) {
  5158.         var view = this.view;
  5159.         var event = seg.event;
  5160.         var start = event.start;
  5161.         var end = view.calendar.getEventEnd(event);
  5162.         var delta;
  5163.         var newStart;
  5164.         var newEnd;
  5165.         var newAllDay;
  5166.         var visibleEnd;
  5167.  
  5168.         if (dropDate.hasTime() === dragStartDate.hasTime()) {
  5169.             delta = dayishDiff(dropDate, dragStartDate);
  5170.             newStart = start.clone().add(delta);
  5171.             if (event.end === null) { // do we need to compute an end?
  5172.                 newEnd = null;
  5173.             }
  5174.             else {
  5175.                 newEnd = end.clone().add(delta);
  5176.             }
  5177.             newAllDay = event.allDay; // keep it the same
  5178.         }
  5179.         else {
  5180.             // if switching from day <-> timed, start should be reset to the dropped date, and the end cleared
  5181.             newStart = dropDate;
  5182.             newEnd = null; // end should be cleared
  5183.             newAllDay = !dropDate.hasTime();
  5184.         }
  5185.  
  5186.         // compute what the end date will appear to be
  5187.         visibleEnd = newEnd || view.calendar.getDefaultEventEnd(newAllDay, newStart);
  5188.  
  5189.         return { start: newStart, end: newEnd, visibleEnd: visibleEnd };
  5190.     },
  5191.  
  5192.  
  5193.     /* Resizing
  5194.     ------------------------------------------------------------------------------------------------------------------*/
  5195.  
  5196.  
  5197.     // Called when the user does a mousedown on an event's resizer, which might lead to resizing.
  5198.     // Generic enough to work with any type of Grid.
  5199.     segResizeMousedown: function(seg, ev) {
  5200.         var _this = this;
  5201.         var view = this.view;
  5202.         var calendar = view.calendar;
  5203.         var el = seg.el;
  5204.         var event = seg.event;
  5205.         var start = event.start;
  5206.         var end = view.calendar.getEventEnd(event);
  5207.         var newEnd = null;
  5208.         var dragListener;
  5209.  
  5210.         function destroy() { // resets the rendering to show the original event
  5211.             _this.destroyResize();
  5212.             view.showEvent(event);
  5213.         }
  5214.  
  5215.         // Tracks mouse movement over the *grid's* coordinate map
  5216.         dragListener = new DragListener(this.coordMap, {
  5217.             distance: 5,
  5218.             scroll: view.opt('dragScroll'),
  5219.             dragStart: function(ev) {
  5220.                 _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
  5221.                 _this.isResizingSeg = true;
  5222.                 view.trigger('eventResizeStart', el[0], event, ev, {}); // last argument is jqui dummy
  5223.             },
  5224.             cellOver: function(cell, date) {
  5225.                 // compute the new end. don't allow it to go before the event's start
  5226.                 if (date.isBefore(start)) { // allows comparing ambig to non-ambig
  5227.                     date = start;
  5228.                 }
  5229.                 newEnd = date.clone().add(_this.cellDuration); // make it an exclusive end
  5230.  
  5231.                 if (calendar.isEventAllowedInRange(event, start, newEnd)) { // allowed to be resized here?
  5232.                     if (newEnd.isSame(end)) {
  5233.                         newEnd = null; // mark an invalid resize
  5234.                         destroy();
  5235.                     }
  5236.                     else {
  5237.                         _this.renderResize(start, newEnd, seg);
  5238.                         view.hideEvent(event);
  5239.                     }
  5240.                 }
  5241.                 else {
  5242.                     newEnd = null; // mark an invalid resize
  5243.                     destroy();
  5244.                     disableCursor();
  5245.                 }
  5246.             },
  5247.             cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells
  5248.                 newEnd = null;
  5249.                 destroy();
  5250.                 enableCursor();
  5251.             },
  5252.             dragStop: function(ev) {
  5253.                 _this.isResizingSeg = false;
  5254.                 destroy();
  5255.                 enableCursor();
  5256.                 view.trigger('eventResizeStop', el[0], event, ev, {}); // last argument is jqui dummy
  5257.  
  5258.                 if (newEnd) {
  5259.                     view.eventResize(el[0], event, newEnd, ev); // will rerender all events...
  5260.                 }
  5261.             }
  5262.         });
  5263.  
  5264.         dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart
  5265.     },
  5266.  
  5267.  
  5268.     /* Rendering Utils
  5269.     ------------------------------------------------------------------------------------------------------------------*/
  5270.  
  5271.  
  5272.     // Generic utility for generating the HTML classNames for an event segment's element
  5273.     getSegClasses: function(seg, isDraggable, isResizable) {
  5274.         var event = seg.event;
  5275.         var classes = [
  5276.             'fc-event',
  5277.             seg.isStart ? 'fc-start' : 'fc-not-start',
  5278.             seg.isEnd ? 'fc-end' : 'fc-not-end'
  5279.         ].concat(
  5280.             event.className,
  5281.             event.source ? event.source.className : []
  5282.         );
  5283.  
  5284.         if (isDraggable) {
  5285.             classes.push('fc-draggable');
  5286.         }
  5287.         if (isResizable) {
  5288.             classes.push('fc-resizable');
  5289.         }
  5290.  
  5291.         return classes;
  5292.     },
  5293.  
  5294.  
  5295.     // Utility for generating a CSS string with all the event skin-related properties
  5296.     getEventSkinCss: function(event) {
  5297.         var view = this.view;
  5298.         var source = event.source || {};
  5299.         var eventColor = event.color;
  5300.         var sourceColor = source.color;
  5301.         var optionColor = view.opt('eventColor');
  5302.         var backgroundColor =
  5303.             event.backgroundColor ||
  5304.             eventColor ||
  5305.             source.backgroundColor ||
  5306.             sourceColor ||
  5307.             view.opt('eventBackgroundColor') ||
  5308.             optionColor;
  5309.         var borderColor =
  5310.             event.borderColor ||
  5311.             eventColor ||
  5312.             source.borderColor ||
  5313.             sourceColor ||
  5314.             view.opt('eventBorderColor') ||
  5315.             optionColor;
  5316.         var textColor =
  5317.             event.textColor ||
  5318.             source.textColor ||
  5319.             view.opt('eventTextColor');
  5320.         var statements = [];
  5321.         if (backgroundColor) {
  5322.             statements.push('background-color:' + backgroundColor);
  5323.         }
  5324.         if (borderColor) {
  5325.             statements.push('border-color:' + borderColor);
  5326.         }
  5327.         if (textColor) {
  5328.             statements.push('color:' + textColor);
  5329.         }
  5330.         return statements.join(';');
  5331.     },
  5332.  
  5333.  
  5334.     /* Converting events -> ranges -> segs
  5335.     ------------------------------------------------------------------------------------------------------------------*/
  5336.  
  5337.  
  5338.     // Converts an array of event objects into an array of event segment objects.
  5339.     // A custom `rangeToSegsFunc` may be given for arbitrarily slicing up events.
  5340.     eventsToSegs: function(events, rangeToSegsFunc) {
  5341.         var eventRanges = this.eventsToRanges(events);
  5342.         var segs = [];
  5343.         var i;
  5344.  
  5345.         for (i = 0; i < eventRanges.length; i++) {
  5346.             segs.push.apply(
  5347.                 segs,
  5348.                 this.eventRangeToSegs(eventRanges[i], rangeToSegsFunc)
  5349.             );
  5350.         }
  5351.  
  5352.         return segs;
  5353.     },
  5354.  
  5355.  
  5356.     // Converts an array of events into an array of "range" objects.
  5357.     // A "range" object is a plain object with start/end properties denoting the time it covers. Also an event property.
  5358.     // For "normal" events, this will be identical to the event's start/end, but for "inverse-background" events,
  5359.     // will create an array of ranges that span the time *not* covered by the given event.
  5360.     eventsToRanges: function(events) {
  5361.         var _this = this;
  5362.         var eventsById = groupEventsById(events);
  5363.         var ranges = [];
  5364.  
  5365.         // group by ID so that related inverse-background events can be rendered together
  5366.         $.each(eventsById, function(id, eventGroup) {
  5367.             if (eventGroup.length) {
  5368.                 ranges.push.apply(
  5369.                     ranges,
  5370.                     isInverseBgEvent(eventGroup[0]) ?
  5371.                         _this.eventsToInverseRanges(eventGroup) :
  5372.                         _this.eventsToNormalRanges(eventGroup)
  5373.                 );
  5374.             }
  5375.         });
  5376.  
  5377.         return ranges;
  5378.     },
  5379.  
  5380.  
  5381.     // Converts an array of "normal" events (not inverted rendering) into a parallel array of ranges
  5382.     eventsToNormalRanges: function(events) {
  5383.         var calendar = this.view.calendar;
  5384.         var ranges = [];
  5385.         var i, event;
  5386.         var eventStart, eventEnd;
  5387.  
  5388.         for (i = 0; i < events.length; i++) {
  5389.             event = events[i];
  5390.  
  5391.             // make copies and normalize by stripping timezone
  5392.             eventStart = event.start.clone().stripZone();
  5393.             eventEnd = calendar.getEventEnd(event).stripZone();
  5394.  
  5395.             ranges.push({
  5396.                 event: event,
  5397.                 start: eventStart,
  5398.                 end: eventEnd,
  5399.                 eventStartMS: +eventStart,
  5400.                 eventDurationMS: eventEnd - eventStart
  5401.             });
  5402.         }
  5403.  
  5404.         return ranges;
  5405.     },
  5406.  
  5407.  
  5408.     // Converts an array of events, with inverse-background rendering, into an array of range objects.
  5409.     // The range objects will cover all the time NOT covered by the events.
  5410.     eventsToInverseRanges: function(events) {
  5411.         var view = this.view;
  5412.         var viewStart = view.start.clone().stripZone(); // normalize timezone
  5413.         var viewEnd = view.end.clone().stripZone(); // normalize timezone
  5414.         var normalRanges = this.eventsToNormalRanges(events); // will give us normalized dates we can use w/o copies
  5415.         var inverseRanges = [];
  5416.         var event0 = events[0]; // assign this to each range's `.event`
  5417.         var start = viewStart; // the end of the previous range. the start of the new range
  5418.         var i, normalRange;
  5419.  
  5420.         // ranges need to be in order. required for our date-walking algorithm
  5421.         normalRanges.sort(compareNormalRanges);
  5422.  
  5423.         for (i = 0; i < normalRanges.length; i++) {
  5424.             normalRange = normalRanges[i];
  5425.  
  5426.             // add the span of time before the event (if there is any)
  5427.             if (normalRange.start > start) { // compare millisecond time (skip any ambig logic)
  5428.                 inverseRanges.push({
  5429.                     event: event0,
  5430.                     start: start,
  5431.                     end: normalRange.start
  5432.                 });
  5433.             }
  5434.  
  5435.             start = normalRange.end;
  5436.         }
  5437.  
  5438.         // add the span of time after the last event (if there is any)
  5439.         if (start < viewEnd) { // compare millisecond time (skip any ambig logic)
  5440.             inverseRanges.push({
  5441.                 event: event0,
  5442.                 start: start,
  5443.                 end: viewEnd
  5444.             });
  5445.         }
  5446.  
  5447.         return inverseRanges;
  5448.     },
  5449.  
  5450.  
  5451.     // Slices the given event range into one or more segment objects.
  5452.     // A `rangeToSegsFunc` custom slicing function can be given.
  5453.     eventRangeToSegs: function(eventRange, rangeToSegsFunc) {
  5454.         var segs;
  5455.         var i, seg;
  5456.  
  5457.         if (rangeToSegsFunc) {
  5458.             segs = rangeToSegsFunc(eventRange.start, eventRange.end);
  5459.         }
  5460.         else {
  5461.             segs = this.rangeToSegs(eventRange.start, eventRange.end); // defined by the subclass
  5462.         }
  5463.  
  5464.         for (i = 0; i < segs.length; i++) {
  5465.             seg = segs[i];
  5466.             seg.event = eventRange.event;
  5467.             seg.eventStartMS = eventRange.eventStartMS;
  5468.             seg.eventDurationMS = eventRange.eventDurationMS;
  5469.         }
  5470.  
  5471.         return segs;
  5472.     }
  5473.  
  5474. });
  5475.  
  5476.  
  5477. /* Utilities
  5478. ----------------------------------------------------------------------------------------------------------------------*/
  5479.  
  5480.  
  5481. function isBgEvent(event) { // returns true if background OR inverse-background
  5482.     var rendering = getEventRendering(event);
  5483.     return rendering === 'background' || rendering === 'inverse-background';
  5484. }
  5485.  
  5486.  
  5487. function isInverseBgEvent(event) {
  5488.     return getEventRendering(event) === 'inverse-background';
  5489. }
  5490.  
  5491.  
  5492. function getEventRendering(event) {
  5493.     return firstDefined((event.source || {}).rendering, event.rendering);
  5494. }
  5495.  
  5496.  
  5497. function groupEventsById(events) {
  5498.     var eventsById = {};
  5499.     var i, event;
  5500.  
  5501.     for (i = 0; i < events.length; i++) {
  5502.         event = events[i];
  5503.         (eventsById[event._id] || (eventsById[event._id] = [])).push(event);
  5504.     }
  5505.  
  5506.     return eventsById;
  5507. }
  5508.  
  5509.  
  5510. // A cmp function for determining which non-inverted "ranges" (see above) happen earlier
  5511. function compareNormalRanges(range1, range2) {
  5512.     return range1.eventStartMS - range2.eventStartMS; // earlier ranges go first
  5513. }
  5514.  
  5515.  
  5516. // A cmp function for determining which segments should take visual priority
  5517. // DOES NOT WORK ON INVERTED BACKGROUND EVENTS because they have no eventStartMS/eventDurationMS
  5518. function compareSegs(seg1, seg2) {
  5519.     return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first
  5520.         seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first
  5521.         seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1)
  5522.         (seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title
  5523. }
  5524.  
  5525.  
  5526. ;;
  5527.  
  5528. /* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week.
  5529. ----------------------------------------------------------------------------------------------------------------------*/
  5530.  
  5531. function DayGrid(view) {
  5532.     Grid.call(this, view); // call the super-constructor
  5533. }
  5534.  
  5535.  
  5536. DayGrid.prototype = createObject(Grid.prototype); // declare the super-class
  5537. $.extend(DayGrid.prototype, {
  5538.  
  5539.     numbersVisible: false, // should render a row for day/week numbers? manually set by the view
  5540.     cellDuration: moment.duration({ days: 1 }), // required for Grid.event.js. Each cell is always a single day
  5541.     bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid
  5542.  
  5543.     rowEls: null, // set of fake row elements
  5544.     dayEls: null, // set of whole-day elements comprising the row's background
  5545.     helperEls: null, // set of cell skeleton elements for rendering the mock event "helper"
  5546.  
  5547.  
  5548.     // Renders the rows and columns into the component's `this.el`, which should already be assigned.
  5549.     // isRigid determins whether the individual rows should ignore the contents and be a constant height.
  5550.     // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient.
  5551.     render: function(isRigid) {
  5552.         var view = this.view;
  5553.         var html = '';
  5554.         var row;
  5555.  
  5556.         for (row = 0; row < view.rowCnt; row++) {
  5557.             html += this.dayRowHtml(row, isRigid);
  5558.         }
  5559.         this.el.html(html);
  5560.  
  5561.         this.rowEls = this.el.find('.fc-row');
  5562.         this.dayEls = this.el.find('.fc-day');
  5563.  
  5564.         // run all the day cells through the dayRender callback
  5565.         this.dayEls.each(function(i, node) {
  5566.             var date = view.cellToDate(Math.floor(i / view.colCnt), i % view.colCnt);
  5567.             view.trigger('dayRender', null, date, $(node));
  5568.         });
  5569.  
  5570.         Grid.prototype.render.call(this); // call the super-method
  5571.     },
  5572.  
  5573.  
  5574.     destroy: function() {
  5575.         this.destroySegPopover();
  5576.     },
  5577.  
  5578.  
  5579.     // Generates the HTML for a single row. `row` is the row number.
  5580.     dayRowHtml: function(row, isRigid) {
  5581.         var view = this.view;
  5582.         var classes = [ 'fc-row', 'fc-week', view.widgetContentClass ];
  5583.  
  5584.         if (isRigid) {
  5585.             classes.push('fc-rigid');
  5586.         }
  5587.  
  5588.         return '' +
  5589.             '<div class="' + classes.join(' ') + '">' +
  5590.                 '<div class="fc-bg">' +
  5591.                     '<table>' +
  5592.                         this.rowHtml('day', row) + // leverages RowRenderer. calls dayCellHtml()
  5593.                     '</table>' +
  5594.                 '</div>' +
  5595.                 '<div class="fc-content-skeleton">' +
  5596.                     '<table>' +
  5597.                         (this.numbersVisible ?
  5598.                             '<thead>' +
  5599.                                 this.rowHtml('number', row) + // leverages RowRenderer. View will define render method
  5600.                             '</thead>' :
  5601.                             ''
  5602.                             ) +
  5603.                     '</table>' +
  5604.                 '</div>' +
  5605.             '</div>';
  5606.     },
  5607.  
  5608.  
  5609.     // Renders the HTML for a whole-day cell. Will eventually end up in the day-row's background.
  5610.     // We go through a 'day' row type instead of just doing a 'bg' row type so that the View can do custom rendering
  5611.     // specifically for whole-day rows, whereas a 'bg' might also be used for other purposes (TimeGrid bg for example).
  5612.     dayCellHtml: function(row, col, date) {
  5613.         return this.bgCellHtml(row, col, date);
  5614.     },
  5615.  
  5616.  
  5617.     /* Coordinates & Cells
  5618.     ------------------------------------------------------------------------------------------------------------------*/
  5619.  
  5620.  
  5621.     // Populates the empty `rows` and `cols` arrays with coordinates of the cells. For CoordGrid.
  5622.     buildCoords: function(rows, cols) {
  5623.         var colCnt = this.view.colCnt;
  5624.         var e, n, p;
  5625.  
  5626.         this.dayEls.slice(0, colCnt).each(function(i, _e) { // iterate the first row of day elements
  5627.             e = $(_e);
  5628.             n = e.offset().left;
  5629.             if (i) {
  5630.                 p[1] = n;
  5631.             }
  5632.             p = [ n ];
  5633.             cols[i] = p;
  5634.         });
  5635.         p[1] = n + e.outerWidth();
  5636.  
  5637.         this.rowEls.each(function(i, _e) {
  5638.             e = $(_e);
  5639.             n = e.offset().top;
  5640.             if (i) {
  5641.                 p[1] = n;
  5642.             }
  5643.             p = [ n ];
  5644.             rows[i] = p;
  5645.         });
  5646.         p[1] = n + e.outerHeight() + this.bottomCoordPadding; // hack to extend hit area of last row
  5647.     },
  5648.  
  5649.  
  5650.     // Converts a cell to a date
  5651.     getCellDate: function(cell) {
  5652.         return this.view.cellToDate(cell); // leverages the View's cell system
  5653.     },
  5654.  
  5655.  
  5656.     // Gets the whole-day element associated with the cell
  5657.     getCellDayEl: function(cell) {
  5658.         return this.dayEls.eq(cell.row * this.view.colCnt + cell.col);
  5659.     },
  5660.  
  5661.  
  5662.     // Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects
  5663.     rangeToSegs: function(start, end) {
  5664.         return this.view.rangeToSegments(start, end); // leverages the View's cell system
  5665.     },
  5666.  
  5667.  
  5668.     /* Event Drag Visualization
  5669.     ------------------------------------------------------------------------------------------------------------------*/
  5670.  
  5671.  
  5672.     // Renders a visual indication of an event hovering over the given date(s).
  5673.     // `end` can be null, as well as `seg`. See View's documentation on renderDrag for more info.
  5674.     // A returned value of `true` signals that a mock "helper" event has been rendered.
  5675.     renderDrag: function(start, end, seg) {
  5676.         var opacity;
  5677.  
  5678.         // always render a highlight underneath
  5679.         this.renderHighlight(
  5680.             start,
  5681.             end || this.view.calendar.getDefaultEventEnd(true, start)
  5682.         );
  5683.  
  5684.         // if a segment from the same calendar but another component is being dragged, render a helper event
  5685.         if (seg && !seg.el.closest(this.el).length) {
  5686.  
  5687.             this.renderRangeHelper(start, end, seg);
  5688.  
  5689.             opacity = this.view.opt('dragOpacity');
  5690.             if (opacity !== undefined) {
  5691.                 this.helperEls.css('opacity', opacity);
  5692.             }
  5693.  
  5694.             return true; // a helper has been rendered
  5695.         }
  5696.     },
  5697.  
  5698.  
  5699.     // Unrenders any visual indication of a hovering event
  5700.     destroyDrag: function() {
  5701.         this.destroyHighlight();
  5702.         this.destroyHelper();
  5703.     },
  5704.  
  5705.  
  5706.     /* Event Resize Visualization
  5707.     ------------------------------------------------------------------------------------------------------------------*/
  5708.  
  5709.  
  5710.     // Renders a visual indication of an event being resized
  5711.     renderResize: function(start, end, seg) {
  5712.         this.renderHighlight(start, end);
  5713.         this.renderRangeHelper(start, end, seg);
  5714.     },
  5715.  
  5716.  
  5717.     // Unrenders a visual indication of an event being resized
  5718.     destroyResize: function() {
  5719.         this.destroyHighlight();
  5720.         this.destroyHelper();
  5721.     },
  5722.  
  5723.  
  5724.     /* Event Helper
  5725.     ------------------------------------------------------------------------------------------------------------------*/
  5726.  
  5727.  
  5728.     // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null.
  5729.     renderHelper: function(event, sourceSeg) {
  5730.         var helperNodes = [];
  5731.         var segs = this.eventsToSegs([ event ]);
  5732.         var rowStructs;
  5733.  
  5734.         segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered
  5735.         rowStructs = this.renderSegRows(segs);
  5736.  
  5737.         // inject each new event skeleton into each associated row
  5738.         this.rowEls.each(function(row, rowNode) {
  5739.             var rowEl = $(rowNode); // the .fc-row
  5740.             var skeletonEl = $('<div class="fc-helper-skeleton"><table/></div>'); // will be absolutely positioned
  5741.             var skeletonTop;
  5742.  
  5743.             // If there is an original segment, match the top position. Otherwise, put it at the row's top level
  5744.             if (sourceSeg && sourceSeg.row === row) {
  5745.                 skeletonTop = sourceSeg.el.position().top;
  5746.             }
  5747.             else {
  5748.                 skeletonTop = rowEl.find('.fc-content-skeleton tbody').position().top;
  5749.             }
  5750.  
  5751.             skeletonEl.css('top', skeletonTop)
  5752.                 .find('table')
  5753.                     .append(rowStructs[row].tbodyEl);
  5754.  
  5755.             rowEl.append(skeletonEl);
  5756.             helperNodes.push(skeletonEl[0]);
  5757.         });
  5758.  
  5759.         this.helperEls = $(helperNodes); // array -> jQuery set
  5760.     },
  5761.  
  5762.  
  5763.     // Unrenders any visual indication of a mock helper event
  5764.     destroyHelper: function() {
  5765.         if (this.helperEls) {
  5766.             this.helperEls.remove();
  5767.             this.helperEls = null;
  5768.         }
  5769.     },
  5770.  
  5771.  
  5772.     /* Fill System (highlight, background events, business hours)
  5773.     ------------------------------------------------------------------------------------------------------------------*/
  5774.  
  5775.  
  5776.     fillSegTag: 'td', // override the default tag name
  5777.  
  5778.  
  5779.     // Renders a set of rectangles over the given segments of days.
  5780.     // Only returns segments that successfully rendered.
  5781.     renderFill: function(type, segs) {
  5782.         var nodes = [];
  5783.         var i, seg;
  5784.         var skeletonEl;
  5785.  
  5786.         segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs
  5787.  
  5788.         for (i = 0; i < segs.length; i++) {
  5789.             seg = segs[i];
  5790.             skeletonEl = this.renderFillRow(type, seg);
  5791.             this.rowEls.eq(seg.row).append(skeletonEl);
  5792.             nodes.push(skeletonEl[0]);
  5793.         }
  5794.  
  5795.         this.elsByFill[type] = $(nodes);
  5796.  
  5797.         return segs;
  5798.     },
  5799.  
  5800.  
  5801.     // Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered.
  5802.     renderFillRow: function(type, seg) {
  5803.         var colCnt = this.view.colCnt;
  5804.         var startCol = seg.leftCol;
  5805.         var endCol = seg.rightCol + 1;
  5806.         var skeletonEl;
  5807.         var trEl;
  5808.  
  5809.         skeletonEl = $(
  5810.             '<div class="fc-' + type.toLowerCase() + '-skeleton">' +
  5811.                 '<table><tr/></table>' +
  5812.             '</div>'
  5813.         );
  5814.         trEl = skeletonEl.find('tr');
  5815.  
  5816.         if (startCol > 0) {
  5817.             trEl.append('<td colspan="' + startCol + '"/>');
  5818.         }
  5819.  
  5820.         trEl.append(
  5821.             seg.el.attr('colspan', endCol - startCol)
  5822.         );
  5823.  
  5824.         if (endCol < colCnt) {
  5825.             trEl.append('<td colspan="' + (colCnt - endCol) + '"/>');
  5826.         }
  5827.  
  5828.         this.bookendCells(trEl, type);
  5829.  
  5830.         return skeletonEl;
  5831.     }
  5832.  
  5833. });
  5834.  
  5835. ;;
  5836.  
  5837. /* Event-rendering methods for the DayGrid class
  5838. ----------------------------------------------------------------------------------------------------------------------*/
  5839.  
  5840. $.extend(DayGrid.prototype, {
  5841.  
  5842.     rowStructs: null, // an array of objects, each holding information about a row's foreground event-rendering
  5843.  
  5844.  
  5845.     // Unrenders all events currently rendered on the grid
  5846.     destroyEvents: function() {
  5847.         this.destroySegPopover(); // removes the "more.." events popover
  5848.         Grid.prototype.destroyEvents.apply(this, arguments); // calls the super-method
  5849.     },
  5850.  
  5851.  
  5852.     // Retrieves all rendered segment objects currently rendered on the grid
  5853.     getSegs: function() {
  5854.         return Grid.prototype.getSegs.call(this) // get the segments from the super-method
  5855.             .concat(this.popoverSegs || []); // append the segments from the "more..." popover
  5856.     },
  5857.  
  5858.  
  5859.     // Renders the given background event segments onto the grid
  5860.     renderBgSegs: function(segs) {
  5861.  
  5862.         // don't render timed background events
  5863.         var allDaySegs = $.grep(segs, function(seg) {
  5864.             return seg.event.allDay;
  5865.         });
  5866.  
  5867.         return Grid.prototype.renderBgSegs.call(this, allDaySegs); // call the super-method
  5868.     },
  5869.  
  5870.  
  5871.     // Renders the given foreground event segments onto the grid
  5872.     renderFgSegs: function(segs) {
  5873.         var rowStructs;
  5874.  
  5875.         // render an `.el` on each seg
  5876.         // returns a subset of the segs. segs that were actually rendered
  5877.         segs = this.renderFgSegEls(segs);
  5878.  
  5879.         rowStructs = this.rowStructs = this.renderSegRows(segs);
  5880.  
  5881.         // append to each row's content skeleton
  5882.         this.rowEls.each(function(i, rowNode) {
  5883.             $(rowNode).find('.fc-content-skeleton > table').append(
  5884.                 rowStructs[i].tbodyEl
  5885.             );
  5886.         });
  5887.  
  5888.         return segs; // return only the segs that were actually rendered
  5889.     },
  5890.  
  5891.  
  5892.     // Unrenders all currently rendered foreground event segments
  5893.     destroyFgSegs: function() {
  5894.         var rowStructs = this.rowStructs || [];
  5895.         var rowStruct;
  5896.  
  5897.         while ((rowStruct = rowStructs.pop())) {
  5898.             rowStruct.tbodyEl.remove();
  5899.         }
  5900.  
  5901.         this.rowStructs = null;
  5902.     },
  5903.  
  5904.  
  5905.     // Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton.
  5906.     // Returns an array of rowStruct objects (see the bottom of `renderSegRow`).
  5907.     // PRECONDITION: each segment shoud already have a rendered and assigned `.el`
  5908.     renderSegRows: function(segs) {
  5909.         var rowStructs = [];
  5910.         var segRows;
  5911.         var row;
  5912.  
  5913.         segRows = this.groupSegRows(segs); // group into nested arrays
  5914.  
  5915.         // iterate each row of segment groupings
  5916.         for (row = 0; row < segRows.length; row++) {
  5917.             rowStructs.push(
  5918.                 this.renderSegRow(row, segRows[row])
  5919.             );
  5920.         }
  5921.  
  5922.         return rowStructs;
  5923.     },
  5924.  
  5925.  
  5926.     // Builds the HTML to be used for the default element for an individual segment
  5927.     fgSegHtml: function(seg, disableResizing) {
  5928.         var view = this.view;
  5929.         var isRTL = view.opt('isRTL');
  5930.         var event = seg.event;
  5931.         var isDraggable = view.isEventDraggable(event);
  5932.         var isResizable = !disableResizing && event.allDay && seg.isEnd && view.isEventResizable(event);
  5933.         var classes = this.getSegClasses(seg, isDraggable, isResizable);
  5934.         var skinCss = this.getEventSkinCss(event);
  5935.         var timeHtml = '';
  5936.         var titleHtml;
  5937.  
  5938.         classes.unshift('fc-day-grid-event');
  5939.  
  5940.         // Only display a timed events time if it is the starting segment
  5941.         if (!event.allDay && seg.isStart) {
  5942.             timeHtml = '<span class="fc-time">' + htmlEscape(view.getEventTimeText(event)) + '</span>';
  5943.         }
  5944.  
  5945.         titleHtml =
  5946.             '<span class="fc-title">' +
  5947.                 (htmlEscape(event.title || '') || '&nbsp;') + // we always want one line of height
  5948.             '</span>';
  5949.        
  5950.         return '<a class="' + classes.join(' ') + '"' +
  5951.                 (event.url ?
  5952.                     ' href="' + htmlEscape(event.url) + '"' :
  5953.                     ''
  5954.                     ) +
  5955.                 (skinCss ?
  5956.                     ' style="' + skinCss + '"' :
  5957.                     ''
  5958.                     ) +
  5959.             '>' +
  5960.                 '<div class="fc-content">' +
  5961.                     (isRTL ?
  5962.                         titleHtml + ' ' + timeHtml : // put a natural space in between
  5963.                         timeHtml + ' ' + titleHtml   //
  5964.                         ) +
  5965.                 '</div>' +
  5966.                 (isResizable ?
  5967.                     '<div class="fc-resizer"/>' :
  5968.                     ''
  5969.                     ) +
  5970.             '</a>';
  5971.     },
  5972.  
  5973.  
  5974.     // Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains
  5975.     // the segments. Returns object with a bunch of internal data about how the render was calculated.
  5976.     renderSegRow: function(row, rowSegs) {
  5977.         var view = this.view;
  5978.         var colCnt = view.colCnt;
  5979.         var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels
  5980.         var levelCnt = Math.max(1, segLevels.length); // ensure at least one level
  5981.         var tbody = $('<tbody/>');
  5982.         var segMatrix = []; // lookup for which segments are rendered into which level+col cells
  5983.         var cellMatrix = []; // lookup for all <td> elements of the level+col matrix
  5984.         var loneCellMatrix = []; // lookup for <td> elements that only take up a single column
  5985.         var i, levelSegs;
  5986.         var col;
  5987.         var tr;
  5988.         var j, seg;
  5989.         var td;
  5990.  
  5991.         // populates empty cells from the current column (`col`) to `endCol`
  5992.         function emptyCellsUntil(endCol) {
  5993.             while (col < endCol) {
  5994.                 // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell
  5995.                 td = (loneCellMatrix[i - 1] || [])[col];
  5996.                 if (td) {
  5997.                     td.attr(
  5998.                         'rowspan',
  5999.                         parseInt(td.attr('rowspan') || 1, 10) + 1
  6000.                     );
  6001.                 }
  6002.                 else {
  6003.                     td = $('<td/>');
  6004.                     tr.append(td);
  6005.                 }
  6006.                 cellMatrix[i][col] = td;
  6007.                 loneCellMatrix[i][col] = td;
  6008.                 col++;
  6009.             }
  6010.         }
  6011.  
  6012.         for (i = 0; i < levelCnt; i++) { // iterate through all levels
  6013.             levelSegs = segLevels[i];
  6014.             col = 0;
  6015.             tr = $('<tr/>');
  6016.  
  6017.             segMatrix.push([]);
  6018.             cellMatrix.push([]);
  6019.             loneCellMatrix.push([]);
  6020.  
  6021.             // levelCnt might be 1 even though there are no actual levels. protect against this.
  6022.             // this single empty row is useful for styling.
  6023.             if (levelSegs) {
  6024.                 for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level
  6025.                     seg = levelSegs[j];
  6026.  
  6027.                     emptyCellsUntil(seg.leftCol);
  6028.  
  6029.                     // create a container that occupies or more columns. append the event element.
  6030.                     td = $('<td class="fc-event-container"/>').append(seg.el);
  6031.                     if (seg.leftCol != seg.rightCol) {
  6032.                         td.attr('colspan', seg.rightCol - seg.leftCol + 1);
  6033.                     }
  6034.                     else { // a single-column segment
  6035.                         loneCellMatrix[i][col] = td;
  6036.                     }
  6037.  
  6038.                     while (col <= seg.rightCol) {
  6039.                         cellMatrix[i][col] = td;
  6040.                         segMatrix[i][col] = seg;
  6041.                         col++;
  6042.                     }
  6043.  
  6044.                     tr.append(td);
  6045.                 }
  6046.             }
  6047.  
  6048.             emptyCellsUntil(colCnt); // finish off the row
  6049.             this.bookendCells(tr, 'eventSkeleton');
  6050.             tbody.append(tr);
  6051.         }
  6052.  
  6053.         return { // a "rowStruct"
  6054.             row: row, // the row number
  6055.             tbodyEl: tbody,
  6056.             cellMatrix: cellMatrix,
  6057.             segMatrix: segMatrix,
  6058.             segLevels: segLevels,
  6059.             segs: rowSegs
  6060.         };
  6061.     },
  6062.  
  6063.  
  6064.     // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels.
  6065.     buildSegLevels: function(segs) {
  6066.         var levels = [];
  6067.         var i, seg;
  6068.         var j;
  6069.  
  6070.         // Give preference to elements with certain criteria, so they have
  6071.         // a chance to be closer to the top.
  6072.         segs.sort(compareSegs);
  6073.        
  6074.         for (i = 0; i < segs.length; i++) {
  6075.             seg = segs[i];
  6076.  
  6077.             // loop through levels, starting with the topmost, until the segment doesn't collide with other segments
  6078.             for (j = 0; j < levels.length; j++) {
  6079.                 if (!isDaySegCollision(seg, levels[j])) {
  6080.                     break;
  6081.                 }
  6082.             }
  6083.             // `j` now holds the desired subrow index
  6084.             seg.level = j;
  6085.  
  6086.             // create new level array if needed and append segment
  6087.             (levels[j] || (levels[j] = [])).push(seg);
  6088.         }
  6089.  
  6090.         // order segments left-to-right. very important if calendar is RTL
  6091.         for (j = 0; j < levels.length; j++) {
  6092.             levels[j].sort(compareDaySegCols);
  6093.         }
  6094.  
  6095.         return levels;
  6096.     },
  6097.  
  6098.  
  6099.     // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row
  6100.     groupSegRows: function(segs) {
  6101.         var view = this.view;
  6102.         var segRows = [];
  6103.         var i;
  6104.  
  6105.         for (i = 0; i < view.rowCnt; i++) {
  6106.             segRows.push([]);
  6107.         }
  6108.  
  6109.         for (i = 0; i < segs.length; i++) {
  6110.             segRows[segs[i].row].push(segs[i]);
  6111.         }
  6112.  
  6113.         return segRows;
  6114.     }
  6115.  
  6116. });
  6117.  
  6118.  
  6119. // Computes whether two segments' columns collide. They are assumed to be in the same row.
  6120. function isDaySegCollision(seg, otherSegs) {
  6121.     var i, otherSeg;
  6122.  
  6123.     for (i = 0; i < otherSegs.length; i++) {
  6124.         otherSeg = otherSegs[i];
  6125.  
  6126.         if (
  6127.             otherSeg.leftCol <= seg.rightCol &&
  6128.             otherSeg.rightCol >= seg.leftCol
  6129.         ) {
  6130.             return true;
  6131.         }
  6132.     }
  6133.  
  6134.     return false;
  6135. }
  6136.  
  6137.  
  6138. // A cmp function for determining the leftmost event
  6139. function compareDaySegCols(a, b) {
  6140.     return a.leftCol - b.leftCol;
  6141. }
  6142.  
  6143. ;;
  6144.  
  6145. /* Methods relate to limiting the number events for a given day on a DayGrid
  6146. ----------------------------------------------------------------------------------------------------------------------*/
  6147. // NOTE: all the segs being passed around in here are foreground segs
  6148.  
  6149. $.extend(DayGrid.prototype, {
  6150.  
  6151.  
  6152.     segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible
  6153.     popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible
  6154.  
  6155.  
  6156.     destroySegPopover: function() {
  6157.         if (this.segPopover) {
  6158.             this.segPopover.hide(); // will trigger destruction of `segPopover` and `popoverSegs`
  6159.         }
  6160.     },
  6161.  
  6162.  
  6163.     // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid.
  6164.     // `levelLimit` can be false (don't limit), a number, or true (should be computed).
  6165.     limitRows: function(levelLimit) {
  6166.         var rowStructs = this.rowStructs || [];
  6167.         var row; // row #
  6168.         var rowLevelLimit;
  6169.  
  6170.         for (row = 0; row < rowStructs.length; row++) {
  6171.             this.unlimitRow(row);
  6172.  
  6173.             if (!levelLimit) {
  6174.                 rowLevelLimit = false;
  6175.             }
  6176.             else if (typeof levelLimit === 'number') {
  6177.                 rowLevelLimit = levelLimit;
  6178.             }
  6179.             else {
  6180.                 rowLevelLimit = this.computeRowLevelLimit(row);
  6181.             }
  6182.  
  6183.             if (rowLevelLimit !== false) {
  6184.                 this.limitRow(row, rowLevelLimit);
  6185.             }
  6186.         }
  6187.     },
  6188.  
  6189.  
  6190.     // Computes the number of levels a row will accomodate without going outside its bounds.
  6191.     // Assumes the row is "rigid" (maintains a constant height regardless of what is inside).
  6192.     // `row` is the row number.
  6193.     computeRowLevelLimit: function(row) {
  6194.         var rowEl = this.rowEls.eq(row); // the containing "fake" row div
  6195.         var rowHeight = rowEl.height(); // TODO: cache somehow?
  6196.         var trEls = this.rowStructs[row].tbodyEl.children();
  6197.         var i, trEl;
  6198.  
  6199.         // Reveal one level <tr> at a time and stop when we find one out of bounds
  6200.         for (i = 0; i < trEls.length; i++) {
  6201.             trEl = trEls.eq(i).removeClass('fc-limited'); // get and reveal
  6202.             if (trEl.position().top + trEl.outerHeight() > rowHeight) {
  6203.                 return i;
  6204.             }
  6205.         }
  6206.  
  6207.         return false; // should not limit at all
  6208.     },
  6209.  
  6210.  
  6211.     // Limits the given grid row to the maximum number of levels and injects "more" links if necessary.
  6212.     // `row` is the row number.
  6213.     // `levelLimit` is a number for the maximum (inclusive) number of levels allowed.
  6214.     limitRow: function(row, levelLimit) {
  6215.         var _this = this;
  6216.         var view = this.view;
  6217.         var rowStruct = this.rowStructs[row];
  6218.         var moreNodes = []; // array of "more" <a> links and <td> DOM nodes
  6219.         var col = 0; // col #
  6220.         var cell;
  6221.         var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right
  6222.         var cellMatrix; // a matrix (by level, then column) of all <td> jQuery elements in the row
  6223.         var limitedNodes; // array of temporarily hidden level <tr> and segment <td> DOM nodes
  6224.         var i, seg;
  6225.         var segsBelow; // array of segment objects below `seg` in the current `col`
  6226.         var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies
  6227.         var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column)
  6228.         var td, rowspan;
  6229.         var segMoreNodes; // array of "more" <td> cells that will stand-in for the current seg's cell
  6230.         var j;
  6231.         var moreTd, moreWrap, moreLink;
  6232.  
  6233.         // Iterates through empty level cells and places "more" links inside if need be
  6234.         function emptyCellsUntil(endCol) { // goes from current `col` to `endCol`
  6235.             while (col < endCol) {
  6236.                 cell = { row: row, col: col };
  6237.                 segsBelow = _this.getCellSegs(cell, levelLimit);
  6238.                 if (segsBelow.length) {
  6239.                     td = cellMatrix[levelLimit - 1][col];
  6240.                     moreLink = _this.renderMoreLink(cell, segsBelow);
  6241.                     moreWrap = $('<div/>').append(moreLink);
  6242.                     td.append(moreWrap);
  6243.                     moreNodes.push(moreWrap[0]);
  6244.                 }
  6245.                 col++;
  6246.             }
  6247.         }
  6248.  
  6249.         if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit?
  6250.             levelSegs = rowStruct.segLevels[levelLimit - 1];
  6251.             cellMatrix = rowStruct.cellMatrix;
  6252.  
  6253.             limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level <tr> elements past the limit
  6254.                 .addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array
  6255.  
  6256.             // iterate though segments in the last allowable level
  6257.             for (i = 0; i < levelSegs.length; i++) {
  6258.                 seg = levelSegs[i];
  6259.                 emptyCellsUntil(seg.leftCol); // process empty cells before the segment
  6260.  
  6261.                 // determine *all* segments below `seg` that occupy the same columns
  6262.                 colSegsBelow = [];
  6263.                 totalSegsBelow = 0;
  6264.                 while (col <= seg.rightCol) {
  6265.                     cell = { row: row, col: col };
  6266.                     segsBelow = this.getCellSegs(cell, levelLimit);
  6267.                     colSegsBelow.push(segsBelow);
  6268.                     totalSegsBelow += segsBelow.length;
  6269.                     col++;
  6270.                 }
  6271.  
  6272.                 if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links?
  6273.                     td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell
  6274.                     rowspan = td.attr('rowspan') || 1;
  6275.                     segMoreNodes = [];
  6276.  
  6277.                     // make a replacement <td> for each column the segment occupies. will be one for each colspan
  6278.                     for (j = 0; j < colSegsBelow.length; j++) {
  6279.                         moreTd = $('<td class="fc-more-cell"/>').attr('rowspan', rowspan);
  6280.                         segsBelow = colSegsBelow[j];
  6281.                         cell = { row: row, col: seg.leftCol + j };
  6282.                         moreLink = this.renderMoreLink(cell, [ seg ].concat(segsBelow)); // count seg as hidden too
  6283.                         moreWrap = $('<div/>').append(moreLink);
  6284.                         moreTd.append(moreWrap);
  6285.                         segMoreNodes.push(moreTd[0]);
  6286.                         moreNodes.push(moreTd[0]);
  6287.                     }
  6288.  
  6289.                     td.addClass('fc-limited').after($(segMoreNodes)); // hide original <td> and inject replacements
  6290.                     limitedNodes.push(td[0]);
  6291.                 }
  6292.             }
  6293.  
  6294.             emptyCellsUntil(view.colCnt); // finish off the level
  6295.             rowStruct.moreEls = $(moreNodes); // for easy undoing later
  6296.             rowStruct.limitedEls = $(limitedNodes); // for easy undoing later
  6297.         }
  6298.     },
  6299.  
  6300.  
  6301.     // Reveals all levels and removes all "more"-related elements for a grid's row.
  6302.     // `row` is a row number.
  6303.     unlimitRow: function(row) {
  6304.         var rowStruct = this.rowStructs[row];
  6305.  
  6306.         if (rowStruct.moreEls) {
  6307.             rowStruct.moreEls.remove();
  6308.             rowStruct.moreEls = null;
  6309.         }
  6310.  
  6311.         if (rowStruct.limitedEls) {
  6312.             rowStruct.limitedEls.removeClass('fc-limited');
  6313.             rowStruct.limitedEls = null;
  6314.         }
  6315.     },
  6316.  
  6317.  
  6318.     // Renders an <a> element that represents hidden event element for a cell.
  6319.     // Responsible for attaching click handler as well.
  6320.     renderMoreLink: function(cell, hiddenSegs) {
  6321.         var _this = this;
  6322.         var view = this.view;
  6323.  
  6324.         return $('<a class="fc-more"/>')
  6325.             .text(
  6326.                 this.getMoreLinkText(hiddenSegs.length)
  6327.             )
  6328.             .on('click', function(ev) {
  6329.                 var clickOption = view.opt('eventLimitClick');
  6330.                 var date = view.cellToDate(cell);
  6331.                 var moreEl = $(this);
  6332.                 var dayEl = _this.getCellDayEl(cell);
  6333.                 var allSegs = _this.getCellSegs(cell);
  6334.  
  6335.                 // rescope the segments to be within the cell's date
  6336.                 var reslicedAllSegs = _this.resliceDaySegs(allSegs, date);
  6337.                 var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date);
  6338.  
  6339.                 if (typeof clickOption === 'function') {
  6340.                     // the returned value can be an atomic option
  6341.                     clickOption = view.trigger('eventLimitClick', null, {
  6342.                         date: date,
  6343.                         dayEl: dayEl,
  6344.                         moreEl: moreEl,
  6345.                         segs: reslicedAllSegs,
  6346.                         hiddenSegs: reslicedHiddenSegs
  6347.                     }, ev);
  6348.                 }
  6349.  
  6350.                 if (clickOption === 'popover') {
  6351.                     _this.showSegPopover(date, cell, moreEl, reslicedAllSegs);
  6352.                 }
  6353.                 else if (typeof clickOption === 'string') { // a view name
  6354.                     view.calendar.zoomTo(date, clickOption);
  6355.                 }
  6356.             });
  6357.     },
  6358.  
  6359.  
  6360.     // Reveals the popover that displays all events within a cell
  6361.     showSegPopover: function(date, cell, moreLink, segs) {
  6362.         var _this = this;
  6363.         var view = this.view;
  6364.         var moreWrap = moreLink.parent(); // the <div> wrapper around the <a>
  6365.         var topEl; // the element we want to match the top coordinate of
  6366.         var options;
  6367.  
  6368.         if (view.rowCnt == 1) {
  6369.             topEl = this.view.el; // will cause the popover to cover any sort of header
  6370.         }
  6371.         else {
  6372.             topEl = this.rowEls.eq(cell.row); // will align with top of row
  6373.         }
  6374.  
  6375.         options = {
  6376.             className: 'fc-more-popover',
  6377.             content: this.renderSegPopoverContent(date, segs),
  6378.             parentEl: this.el,
  6379.             top: topEl.offset().top,
  6380.             autoHide: true, // when the user clicks elsewhere, hide the popover
  6381.             viewportConstrain: view.opt('popoverViewportConstrain'),
  6382.             hide: function() {
  6383.                 // destroy everything when the popover is hidden
  6384.                 _this.segPopover.destroy();
  6385.                 _this.segPopover = null;
  6386.                 _this.popoverSegs = null;
  6387.             }
  6388.         };
  6389.  
  6390.         // Determine horizontal coordinate.
  6391.         // We use the moreWrap instead of the <td> to avoid border confusion.
  6392.         if (view.opt('isRTL')) {
  6393.             options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border
  6394.         }
  6395.         else {
  6396.             options.left = moreWrap.offset().left - 1; // -1 to be over cell border
  6397.         }
  6398.  
  6399.         this.segPopover = new Popover(options);
  6400.         this.segPopover.show();
  6401.     },
  6402.  
  6403.  
  6404.     // Builds the inner DOM contents of the segment popover
  6405.     renderSegPopoverContent: function(date, segs) {
  6406.         var view = this.view;
  6407.         var isTheme = view.opt('theme');
  6408.         var title = date.format(view.opt('dayPopoverFormat'));
  6409.         var content = $(
  6410.             '<div class="fc-header ' + view.widgetHeaderClass + '">' +
  6411.                 '<span class="fc-close ' +
  6412.                     (isTheme ? 'ui-icon ui-icon-closethick' : 'fc-icon fc-icon-x') +
  6413.                 '"></span>' +
  6414.                 '<span class="fc-title">' +
  6415.                     htmlEscape(title) +
  6416.                 '</span>' +
  6417.                 '<div class="fc-clear"/>' +
  6418.             '</div>' +
  6419.             '<div class="fc-body ' + view.widgetContentClass + '">' +
  6420.                 '<div class="fc-event-container"></div>' +
  6421.             '</div>'
  6422.         );
  6423.         var segContainer = content.find('.fc-event-container');
  6424.         var i;
  6425.  
  6426.         // render each seg's `el` and only return the visible segs
  6427.         segs = this.renderFgSegEls(segs, true); // disableResizing=true
  6428.         this.popoverSegs = segs;
  6429.  
  6430.         for (i = 0; i < segs.length; i++) {
  6431.  
  6432.             // because segments in the popover are not part of a grid coordinate system, provide a hint to any
  6433.             // grids that want to do drag-n-drop about which cell it came from
  6434.             segs[i].cellDate = date;
  6435.  
  6436.             segContainer.append(segs[i].el);
  6437.         }
  6438.  
  6439.         return content;
  6440.     },
  6441.  
  6442.  
  6443.     // Given the events within an array of segment objects, reslice them to be in a single day
  6444.     resliceDaySegs: function(segs, dayDate) {
  6445.  
  6446.         // build an array of the original events
  6447.         var events = $.map(segs, function(seg) {
  6448.             return seg.event;
  6449.         });
  6450.  
  6451.         var dayStart = dayDate.clone().stripTime();
  6452.         var dayEnd = dayStart.clone().add(1, 'days');
  6453.  
  6454.         // slice the events with a custom slicing function
  6455.         return this.eventsToSegs(
  6456.             events,
  6457.             function(rangeStart, rangeEnd) {
  6458.                 var seg = intersectionToSeg(rangeStart, rangeEnd, dayStart, dayEnd); // if no intersection, undefined
  6459.                 return seg ? [ seg ] : []; // must return an array of segments
  6460.             }
  6461.         );
  6462.     },
  6463.  
  6464.  
  6465.     // Generates the text that should be inside a "more" link, given the number of events it represents
  6466.     getMoreLinkText: function(num) {
  6467.         var view = this.view;
  6468.         var opt = view.opt('eventLimitText');
  6469.  
  6470.         if (typeof opt === 'function') {
  6471.             return opt(num);
  6472.         }
  6473.         else {
  6474.             return '+' + num + ' ' + opt;
  6475.         }
  6476.     },
  6477.  
  6478.  
  6479.     // Returns segments within a given cell.
  6480.     // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs.
  6481.     getCellSegs: function(cell, startLevel) {
  6482.         var segMatrix = this.rowStructs[cell.row].segMatrix;
  6483.         var level = startLevel || 0;
  6484.         var segs = [];
  6485.         var seg;
  6486.  
  6487.         while (level < segMatrix.length) {
  6488.             seg = segMatrix[level][cell.col];
  6489.             if (seg) {
  6490.                 segs.push(seg);
  6491.             }
  6492.             level++;
  6493.         }
  6494.  
  6495.         return segs;
  6496.     }
  6497.  
  6498. });
  6499.  
  6500. ;;
  6501.  
  6502. /* A component that renders one or more columns of vertical time slots
  6503. ----------------------------------------------------------------------------------------------------------------------*/
  6504.  
  6505. function TimeGrid(view) {
  6506.     Grid.call(this, view); // call the super-constructor
  6507. }
  6508.  
  6509.  
  6510. TimeGrid.prototype = createObject(Grid.prototype); // define the super-class
  6511. $.extend(TimeGrid.prototype, {
  6512.  
  6513.     slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines
  6514.     snapDuration: null, // granularity of time for dragging and selecting
  6515.  
  6516.     minTime: null, // Duration object that denotes the first visible time of any given day
  6517.     maxTime: null, // Duration object that denotes the exclusive visible end time of any given day
  6518.  
  6519.     dayEls: null, // cells elements in the day-row background
  6520.     slatEls: null, // elements running horizontally across all columns
  6521.  
  6522.     slatTops: null, // an array of top positions, relative to the container. last item holds bottom of last slot
  6523.  
  6524.     helperEl: null, // cell skeleton element for rendering the mock event "helper"
  6525.  
  6526.     businessHourSegs: null,
  6527.  
  6528.  
  6529.     // Renders the time grid into `this.el`, which should already be assigned.
  6530.     // Relies on the view's colCnt. In the future, this component should probably be self-sufficient.
  6531.     render: function() {
  6532.         this.processOptions();
  6533.  
  6534.         this.el.html(this.renderHtml());
  6535.  
  6536.         this.dayEls = this.el.find('.fc-day');
  6537.         this.slatEls = this.el.find('.fc-slats tr');
  6538.  
  6539.         this.computeSlatTops();
  6540.  
  6541.         this.renderBusinessHours();
  6542.  
  6543.         Grid.prototype.render.call(this); // call the super-method
  6544.     },
  6545.  
  6546.  
  6547.     renderBusinessHours: function() {
  6548.         var events = this.view.calendar.getBusinessHoursEvents();
  6549.         this.businessHourSegs = this.renderFill('businessHours', this.eventsToSegs(events), 'bgevent');
  6550.     },
  6551.  
  6552.  
  6553.     // Renders the basic HTML skeleton for the grid
  6554.     renderHtml: function() {
  6555.         return '' +
  6556.             '<div class="fc-bg">' +
  6557.                 '<table>' +
  6558.                     this.rowHtml('slotBg') + // leverages RowRenderer, which will call slotBgCellHtml
  6559.                 '</table>' +
  6560.             '</div>' +
  6561.             '<div class="fc-slats">' +
  6562.                 '<table>' +
  6563.                     this.slatRowHtml() +
  6564.                 '</table>' +
  6565.             '</div>';
  6566.     },
  6567.  
  6568.  
  6569.     // Renders the HTML for a vertical background cell behind the slots.
  6570.     // This method is distinct from 'bg' because we wanted a new `rowType` so the View could customize the rendering.
  6571.     slotBgCellHtml: function(row, col, date) {
  6572.         return this.bgCellHtml(row, col, date);
  6573.     },
  6574.  
  6575.  
  6576.     // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL.
  6577.     slatRowHtml: function() {
  6578.         var view = this.view;
  6579.         var calendar = view.calendar;
  6580.         var isRTL = view.opt('isRTL');
  6581.         var html = '';
  6582.         var slotNormal = this.slotDuration.asMinutes() % 15 === 0;
  6583.         var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations
  6584.         var slotDate; // will be on the view's first day, but we only care about its time
  6585.         var minutes;
  6586.         var axisHtml;
  6587.  
  6588.         // Calculate the time for each slot
  6589.         while (slotTime < this.maxTime) {
  6590.             slotDate = view.start.clone().time(slotTime); // will be in UTC but that's good. to avoid DST issues
  6591.             minutes = slotDate.minutes();
  6592.  
  6593.             axisHtml =
  6594.                 '<td class="fc-axis fc-time ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' +
  6595.                     ((!slotNormal || !minutes) ? // if irregular slot duration, or on the hour, then display the time
  6596.                         '<span>' + // for matchCellWidths
  6597.                             htmlEscape(calendar.formatDate(slotDate, view.opt('axisFormat'))) +
  6598.                         '</span>' :
  6599.                         ''
  6600.                         ) +
  6601.                 '</td>';
  6602.  
  6603.             html +=
  6604.                 '<tr ' + (!minutes ? '' : 'class="fc-minor"') + '>' +
  6605.                     (!isRTL ? axisHtml : '') +
  6606.                     '<td class="' + view.widgetContentClass + '"/>' +
  6607.                     (isRTL ? axisHtml : '') +
  6608.                 "</tr>";
  6609.  
  6610.             slotTime.add(this.slotDuration);
  6611.         }
  6612.  
  6613.         return html;
  6614.     },
  6615.  
  6616.  
  6617.     // Parses various options into properties of this object
  6618.     processOptions: function() {
  6619.         var view = this.view;
  6620.         var slotDuration = view.opt('slotDuration');
  6621.         var snapDuration = view.opt('snapDuration');
  6622.  
  6623.         slotDuration = moment.duration(slotDuration);
  6624.         snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration;
  6625.  
  6626.         this.slotDuration = slotDuration;
  6627.         this.snapDuration = snapDuration;
  6628.         this.cellDuration = snapDuration; // important to assign this for Grid.events.js
  6629.  
  6630.         this.minTime = moment.duration(view.opt('minTime'));
  6631.         this.maxTime = moment.duration(view.opt('maxTime'));
  6632.     },
  6633.  
  6634.  
  6635.     // Slices up a date range into a segment for each column
  6636.     rangeToSegs: function(rangeStart, rangeEnd) {
  6637.         var view = this.view;
  6638.         var segs = [];
  6639.         var seg;
  6640.         var col;
  6641.         var cellDate;
  6642.         var colStart, colEnd;
  6643.  
  6644.         // normalize
  6645.         rangeStart = rangeStart.clone().stripZone();
  6646.         rangeEnd = rangeEnd.clone().stripZone();
  6647.  
  6648.         for (col = 0; col < view.colCnt; col++) {
  6649.             cellDate = view.cellToDate(0, col); // use the View's cell system for this
  6650.             colStart = cellDate.clone().time(this.minTime);
  6651.             colEnd = cellDate.clone().time(this.maxTime);
  6652.             seg = intersectionToSeg(rangeStart, rangeEnd, colStart, colEnd);
  6653.             if (seg) {
  6654.                 seg.col = col;
  6655.                 segs.push(seg);
  6656.             }
  6657.         }
  6658.  
  6659.         return segs;
  6660.     },
  6661.  
  6662.  
  6663.     /* Coordinates
  6664.     ------------------------------------------------------------------------------------------------------------------*/
  6665.  
  6666.  
  6667.     // Called when there is a window resize/zoom and we need to recalculate coordinates for the grid
  6668.     resize: function() {
  6669.         this.computeSlatTops();
  6670.         this.updateSegVerticals();
  6671.     },
  6672.  
  6673.  
  6674.     // Populates the given empty `rows` and `cols` arrays with offset positions of the "snap" cells.
  6675.     // "Snap" cells are different the slots because they might have finer granularity.
  6676.     buildCoords: function(rows, cols) {
  6677.         var colCnt = this.view.colCnt;
  6678.         var originTop = this.el.offset().top;
  6679.         var snapTime = moment.duration(+this.minTime);
  6680.         var p = null;
  6681.         var e, n;
  6682.  
  6683.         this.dayEls.slice(0, colCnt).each(function(i, _e) {
  6684.             e = $(_e);
  6685.             n = e.offset().left;
  6686.             if (p) {
  6687.                 p[1] = n;
  6688.             }
  6689.             p = [ n ];
  6690.             cols[i] = p;
  6691.         });
  6692.         p[1] = n + e.outerWidth();
  6693.  
  6694.         p = null;
  6695.         while (snapTime < this.maxTime) {
  6696.             n = originTop + this.computeTimeTop(snapTime);
  6697.             if (p) {
  6698.                 p[1] = n;
  6699.             }
  6700.             p = [ n ];
  6701.             rows.push(p);
  6702.             snapTime.add(this.snapDuration);
  6703.         }
  6704.         p[1] = originTop + this.computeTimeTop(snapTime); // the position of the exclusive end
  6705.     },
  6706.  
  6707.  
  6708.     // Gets the datetime for the given slot cell
  6709.     getCellDate: function(cell) {
  6710.         var view = this.view;
  6711.         var calendar = view.calendar;
  6712.  
  6713.         return calendar.rezoneDate( // since we are adding a time, it needs to be in the calendar's timezone
  6714.             view.cellToDate(0, cell.col) // View's coord system only accounts for start-of-day for column
  6715.                 .time(this.minTime + this.snapDuration * cell.row)
  6716.         );
  6717.     },
  6718.  
  6719.  
  6720.     // Gets the element that represents the whole-day the cell resides on
  6721.     getCellDayEl: function(cell) {
  6722.         return this.dayEls.eq(cell.col);
  6723.     },
  6724.  
  6725.  
  6726.     // Computes the top coordinate, relative to the bounds of the grid, of the given date.
  6727.     // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
  6728.     computeDateTop: function(date, startOfDayDate) {
  6729.         return this.computeTimeTop(
  6730.             moment.duration(
  6731.                 date.clone().stripZone() - startOfDayDate.clone().stripTime()
  6732.             )
  6733.         );
  6734.     },
  6735.  
  6736.  
  6737.     // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
  6738.     computeTimeTop: function(time) {
  6739.         var slatCoverage = (time - this.minTime) / this.slotDuration; // floating-point value of # of slots covered
  6740.         var slatIndex;
  6741.         var slatRemainder;
  6742.         var slatTop;
  6743.         var slatBottom;
  6744.  
  6745.         // constrain. because minTime/maxTime might be customized
  6746.         slatCoverage = Math.max(0, slatCoverage);
  6747.         slatCoverage = Math.min(this.slatEls.length, slatCoverage);
  6748.  
  6749.         slatIndex = Math.floor(slatCoverage); // an integer index of the furthest whole slot
  6750.         slatRemainder = slatCoverage - slatIndex;
  6751.         slatTop = this.slatTops[slatIndex]; // the top position of the furthest whole slot
  6752.  
  6753.         if (slatRemainder) { // time spans part-way into the slot
  6754.             slatBottom = this.slatTops[slatIndex + 1];
  6755.             return slatTop + (slatBottom - slatTop) * slatRemainder; // part-way between slots
  6756.         }
  6757.         else {
  6758.             return slatTop;
  6759.         }
  6760.     },
  6761.  
  6762.  
  6763.     // Queries each `slatEl` for its position relative to the grid's container and stores it in `slatTops`.
  6764.     // Includes the the bottom of the last slat as the last item in the array.
  6765.     computeSlatTops: function() {
  6766.         var tops = [];
  6767.         var top;
  6768.  
  6769.         this.slatEls.each(function(i, node) {
  6770.             top = $(node).position().top;
  6771.             tops.push(top);
  6772.         });
  6773.  
  6774.         tops.push(top + this.slatEls.last().outerHeight()); // bottom of the last slat
  6775.  
  6776.         this.slatTops = tops;
  6777.     },
  6778.  
  6779.  
  6780.     /* Event Drag Visualization
  6781.     ------------------------------------------------------------------------------------------------------------------*/
  6782.  
  6783.  
  6784.     // Renders a visual indication of an event being dragged over the specified date(s).
  6785.     // `end` and `seg` can be null. See View's documentation on renderDrag for more info.
  6786.     renderDrag: function(start, end, seg) {
  6787.         var opacity;
  6788.  
  6789.         if (seg) { // if there is event information for this drag, render a helper event
  6790.             this.renderRangeHelper(start, end, seg);
  6791.  
  6792.             opacity = this.view.opt('dragOpacity');
  6793.             if (opacity !== undefined) {
  6794.                 this.helperEl.css('opacity', opacity);
  6795.             }
  6796.  
  6797.             return true; // signal that a helper has been rendered
  6798.         }
  6799.         else {
  6800.             // otherwise, just render a highlight
  6801.             this.renderHighlight(
  6802.                 start,
  6803.                 end || this.view.calendar.getDefaultEventEnd(false, start)
  6804.             );
  6805.         }
  6806.     },
  6807.  
  6808.  
  6809.     // Unrenders any visual indication of an event being dragged
  6810.     destroyDrag: function() {
  6811.         this.destroyHelper();
  6812.         this.destroyHighlight();
  6813.     },
  6814.  
  6815.  
  6816.     /* Event Resize Visualization
  6817.     ------------------------------------------------------------------------------------------------------------------*/
  6818.  
  6819.  
  6820.     // Renders a visual indication of an event being resized
  6821.     renderResize: function(start, end, seg) {
  6822.         this.renderRangeHelper(start, end, seg);
  6823.     },
  6824.  
  6825.  
  6826.     // Unrenders any visual indication of an event being resized
  6827.     destroyResize: function() {
  6828.         this.destroyHelper();
  6829.     },
  6830.  
  6831.  
  6832.     /* Event Helper
  6833.     ------------------------------------------------------------------------------------------------------------------*/
  6834.  
  6835.  
  6836.     // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag)
  6837.     renderHelper: function(event, sourceSeg) {
  6838.         var segs = this.eventsToSegs([ event ]);
  6839.         var tableEl;
  6840.         var i, seg;
  6841.         var sourceEl;
  6842.  
  6843.         segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered
  6844.         tableEl = this.renderSegTable(segs);
  6845.  
  6846.         // Try to make the segment that is in the same row as sourceSeg look the same
  6847.         for (i = 0; i < segs.length; i++) {
  6848.             seg = segs[i];
  6849.             if (sourceSeg && sourceSeg.col === seg.col) {
  6850.                 sourceEl = sourceSeg.el;
  6851.                 seg.el.css({
  6852.                     left: sourceEl.css('left'),
  6853.                     right: sourceEl.css('right'),
  6854.                     'margin-left': sourceEl.css('margin-left'),
  6855.                     'margin-right': sourceEl.css('margin-right')
  6856.                 });
  6857.             }
  6858.         }
  6859.  
  6860.         this.helperEl = $('<div class="fc-helper-skeleton"/>')
  6861.             .append(tableEl)
  6862.                 .appendTo(this.el);
  6863.     },
  6864.  
  6865.  
  6866.     // Unrenders any mock helper event
  6867.     destroyHelper: function() {
  6868.         if (this.helperEl) {
  6869.             this.helperEl.remove();
  6870.             this.helperEl = null;
  6871.         }
  6872.     },
  6873.  
  6874.  
  6875.     /* Selection
  6876.     ------------------------------------------------------------------------------------------------------------------*/
  6877.  
  6878.  
  6879.     // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight.
  6880.     renderSelection: function(start, end) {
  6881.         if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered
  6882.             this.renderRangeHelper(start, end);
  6883.         }
  6884.         else {
  6885.             this.renderHighlight(start, end);
  6886.         }
  6887.     },
  6888.  
  6889.  
  6890.     // Unrenders any visual indication of a selection
  6891.     destroySelection: function() {
  6892.         this.destroyHelper();
  6893.         this.destroyHighlight();
  6894.     },
  6895.  
  6896.  
  6897.     /* Fill System (highlight, background events, business hours)
  6898.     ------------------------------------------------------------------------------------------------------------------*/
  6899.  
  6900.  
  6901.     // Renders a set of rectangles over the given time segments.
  6902.     // Only returns segments that successfully rendered.
  6903.     renderFill: function(type, segs, className) {
  6904.         var view = this.view;
  6905.         var segCols;
  6906.         var skeletonEl;
  6907.         var trEl;
  6908.         var col, colSegs;
  6909.         var tdEl;
  6910.         var containerEl;
  6911.         var dayDate;
  6912.         var i, seg;
  6913.  
  6914.         if (segs.length) {
  6915.  
  6916.             segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs
  6917.             segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg
  6918.  
  6919.             className = className || type.toLowerCase();
  6920.             skeletonEl = $(
  6921.                 '<div class="fc-' + className + '-skeleton">' +
  6922.                     '<table><tr/></table>' +
  6923.                 '</div>'
  6924.             );
  6925.             trEl = skeletonEl.find('tr');
  6926.  
  6927.             for (col = 0; col < segCols.length; col++) {
  6928.                 colSegs = segCols[col];
  6929.                 tdEl = $('<td/>').appendTo(trEl);
  6930.  
  6931.                 if (colSegs.length) {
  6932.                     containerEl = $('<div class="fc-' + className + '-container"/>').appendTo(tdEl);
  6933.                     dayDate = view.cellToDate(0, col);
  6934.  
  6935.                     for (i = 0; i < colSegs.length; i++) {
  6936.                         seg = colSegs[i];
  6937.                         containerEl.append(
  6938.                             seg.el.css({
  6939.                                 top: this.computeDateTop(seg.start, dayDate),
  6940.                                 bottom: -this.computeDateTop(seg.end, dayDate) // the y position of the bottom edge
  6941.                             })
  6942.                         );
  6943.                     }
  6944.                 }
  6945.             }
  6946.  
  6947.             this.bookendCells(trEl, type);
  6948.  
  6949.             this.el.append(skeletonEl);
  6950.             this.elsByFill[type] = skeletonEl;
  6951.         }
  6952.  
  6953.         return segs;
  6954.     }
  6955.  
  6956. });
  6957.  
  6958. ;;
  6959.  
  6960. /* Event-rendering methods for the TimeGrid class
  6961. ----------------------------------------------------------------------------------------------------------------------*/
  6962.  
  6963. $.extend(TimeGrid.prototype, {
  6964.  
  6965.     eventSkeletonEl: null, // has cells with event-containers, which contain absolutely positioned event elements
  6966.  
  6967.  
  6968.     // Renders the given foreground event segments onto the grid
  6969.     renderFgSegs: function(segs) {
  6970.         segs = this.renderFgSegEls(segs); // returns a subset of the segs. segs that were actually rendered
  6971.  
  6972.         this.el.append(
  6973.             this.eventSkeletonEl = $('<div class="fc-content-skeleton"/>')
  6974.                 .append(this.renderSegTable(segs))
  6975.         );
  6976.  
  6977.         return segs; // return only the segs that were actually rendered
  6978.     },
  6979.  
  6980.  
  6981.     // Unrenders all currently rendered foreground event segments
  6982.     destroyFgSegs: function(segs) {
  6983.         if (this.eventSkeletonEl) {
  6984.             this.eventSkeletonEl.remove();
  6985.             this.eventSkeletonEl = null;
  6986.         }
  6987.     },
  6988.  
  6989.  
  6990.     // Renders and returns the <table> portion of the event-skeleton.
  6991.     // Returns an object with properties 'tbodyEl' and 'segs'.
  6992.     renderSegTable: function(segs) {
  6993.         var tableEl = $('<table><tr/></table>');
  6994.         var trEl = tableEl.find('tr');
  6995.         var segCols;
  6996.         var i, seg;
  6997.         var col, colSegs;
  6998.         var containerEl;
  6999.  
  7000.         segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg
  7001.  
  7002.         this.computeSegVerticals(segs); // compute and assign top/bottom
  7003.  
  7004.         for (col = 0; col < segCols.length; col++) { // iterate each column grouping
  7005.             colSegs = segCols[col];
  7006.             placeSlotSegs(colSegs); // compute horizontal coordinates, z-index's, and reorder the array
  7007.  
  7008.             containerEl = $('<div class="fc-event-container"/>');
  7009.  
  7010.             // assign positioning CSS and insert into container
  7011.             for (i = 0; i < colSegs.length; i++) {
  7012.                 seg = colSegs[i];
  7013.                 seg.el.css(this.generateSegPositionCss(seg));
  7014.  
  7015.                 // if the height is short, add a className for alternate styling
  7016.                 if (seg.bottom - seg.top < 30) {
  7017.                     seg.el.addClass('fc-short');
  7018.                 }
  7019.  
  7020.                 containerEl.append(seg.el);
  7021.             }
  7022.  
  7023.             trEl.append($('<td/>').append(containerEl));
  7024.         }
  7025.  
  7026.         this.bookendCells(trEl, 'eventSkeleton');
  7027.  
  7028.         return tableEl;
  7029.     },
  7030.  
  7031.  
  7032.     // Refreshes the CSS top/bottom coordinates for each segment element. Probably after a window resize/zoom.
  7033.     // Repositions business hours segs too, so not just for events. Maybe shouldn't be here.
  7034.     updateSegVerticals: function() {
  7035.         var allSegs = (this.segs || []).concat(this.businessHourSegs || []);
  7036.         var i;
  7037.  
  7038.         this.computeSegVerticals(allSegs);
  7039.  
  7040.         for (i = 0; i < allSegs.length; i++) {
  7041.             allSegs[i].el.css(
  7042.                 this.generateSegVerticalCss(allSegs[i])
  7043.             );
  7044.         }
  7045.     },
  7046.  
  7047.  
  7048.     // For each segment in an array, computes and assigns its top and bottom properties
  7049.     computeSegVerticals: function(segs) {
  7050.         var i, seg;
  7051.  
  7052.         for (i = 0; i < segs.length; i++) {
  7053.             seg = segs[i];
  7054.             seg.top = this.computeDateTop(seg.start, seg.start);
  7055.             seg.bottom = this.computeDateTop(seg.end, seg.start);
  7056.         }
  7057.     },
  7058.  
  7059.  
  7060.     // Renders the HTML for a single event segment's default rendering
  7061.     fgSegHtml: function(seg, disableResizing) {
  7062.         var view = this.view;
  7063.         var event = seg.event;
  7064.         var isDraggable = view.isEventDraggable(event);
  7065.         var isResizable = !disableResizing && seg.isEnd && view.isEventResizable(event);
  7066.         var classes = this.getSegClasses(seg, isDraggable, isResizable);
  7067.         var skinCss = this.getEventSkinCss(event);
  7068.         var timeText;
  7069.         var fullTimeText; // more verbose time text. for the print stylesheet
  7070.         var startTimeText; // just the start time text
  7071.  
  7072.         classes.unshift('fc-time-grid-event');
  7073.  
  7074.         if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day...
  7075.             // Don't display time text on segments that run entirely through a day.
  7076.             // That would appear as midnight-midnight and would look dumb.
  7077.             // Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am)
  7078.             if (seg.isStart || seg.isEnd) {
  7079.                 timeText = view.getEventTimeText(seg.start, seg.end);
  7080.                 fullTimeText = view.getEventTimeText(seg.start, seg.end, 'LT');
  7081.                 startTimeText = view.getEventTimeText(seg.start, null);
  7082.             }
  7083.         } else {
  7084.             // Display the normal time text for the *event's* times
  7085.             timeText = view.getEventTimeText(event);
  7086.             fullTimeText = view.getEventTimeText(event, 'LT');
  7087.             startTimeText = view.getEventTimeText(event.start, null);
  7088.         }
  7089.  
  7090.         return '<a class="' + classes.join(' ') + '"' +
  7091.             (event.url ?
  7092.                 ' href="' + htmlEscape(event.url) + '"' :
  7093.                 ''
  7094.                 ) +
  7095.             (skinCss ?
  7096.                 ' style="' + skinCss + '"' :
  7097.                 ''
  7098.                 ) +
  7099.             '>' +
  7100.                 '<div class="fc-content">' +
  7101.                     (timeText ?
  7102.                         '<div class="fc-time"' +
  7103.                         ' data-start="' + htmlEscape(startTimeText) + '"' +
  7104.                         ' data-full="' + htmlEscape(fullTimeText) + '"' +
  7105.                         '>' +
  7106.                             '<span>' + htmlEscape(timeText) + '</span>' +
  7107.                         '</div>' :
  7108.                         ''
  7109.                         ) +
  7110.                     (event.title ?
  7111.                         '<div class="fc-title">' +
  7112.                             htmlEscape(event.title) +
  7113.                         '</div>' :
  7114.                         ''
  7115.                         ) +
  7116.                 '</div>' +
  7117.                 '<div class="fc-bg"/>' +
  7118.                 (isResizable ?
  7119.                     '<div class="fc-resizer"/>' :
  7120.                     ''
  7121.                     ) +
  7122.             '</a>';
  7123.     },
  7124.  
  7125.  
  7126.     // Generates an object with CSS properties/values that should be applied to an event segment element.
  7127.     // Contains important positioning-related properties that should be applied to any event element, customized or not.
  7128.     generateSegPositionCss: function(seg) {
  7129.         var view = this.view;
  7130.         var isRTL = view.opt('isRTL');
  7131.         var shouldOverlap = view.opt('slotEventOverlap');
  7132.         var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point
  7133.         var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point
  7134.         var props = this.generateSegVerticalCss(seg); // get top/bottom first
  7135.         var left; // amount of space from left edge, a fraction of the total width
  7136.         var right; // amount of space from right edge, a fraction of the total width
  7137.  
  7138.         if (shouldOverlap) {
  7139.             // double the width, but don't go beyond the maximum forward coordinate (1.0)
  7140.             forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2);
  7141.         }
  7142.  
  7143.         if (isRTL) {
  7144.             left = 1 - forwardCoord;
  7145.             right = backwardCoord;
  7146.         }
  7147.         else {
  7148.             left = backwardCoord;
  7149.             right = 1 - forwardCoord;
  7150.         }
  7151.  
  7152.         props.zIndex = seg.level + 1; // convert from 0-base to 1-based
  7153.         props.left = left * 100 + '%';
  7154.         props.right = right * 100 + '%';
  7155.  
  7156.         if (shouldOverlap && seg.forwardPressure) {
  7157.             // add padding to the edge so that forward stacked events don't cover the resizer's icon
  7158.             props[isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width
  7159.         }
  7160.  
  7161.         return props;
  7162.     },
  7163.  
  7164.  
  7165.     // Generates an object with CSS properties for the top/bottom coordinates of a segment element
  7166.     generateSegVerticalCss: function(seg) {
  7167.         return {
  7168.             top: seg.top,
  7169.             bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container
  7170.         };
  7171.     },
  7172.  
  7173.  
  7174.     // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col
  7175.     groupSegCols: function(segs) {
  7176.         var view = this.view;
  7177.         var segCols = [];
  7178.         var i;
  7179.  
  7180.         for (i = 0; i < view.colCnt; i++) {
  7181.             segCols.push([]);
  7182.         }
  7183.  
  7184.         for (i = 0; i < segs.length; i++) {
  7185.             segCols[segs[i].col].push(segs[i]);
  7186.         }
  7187.  
  7188.         return segCols;
  7189.     }
  7190.  
  7191. });
  7192.  
  7193.  
  7194. // Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each.
  7195. // Also reorders the given array by date!
  7196. function placeSlotSegs(segs) {
  7197.     var levels;
  7198.     var level0;
  7199.     var i;
  7200.  
  7201.     segs.sort(compareSegs); // order by date
  7202.     levels = buildSlotSegLevels(segs);
  7203.     computeForwardSlotSegs(levels);
  7204.  
  7205.     if ((level0 = levels[0])) {
  7206.  
  7207.         for (i = 0; i < level0.length; i++) {
  7208.             computeSlotSegPressures(level0[i]);
  7209.         }
  7210.  
  7211.         for (i = 0; i < level0.length; i++) {
  7212.             computeSlotSegCoords(level0[i], 0, 0);
  7213.         }
  7214.     }
  7215. }
  7216.  
  7217.  
  7218. // Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is
  7219. // left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date.
  7220. function buildSlotSegLevels(segs) {
  7221.     var levels = [];
  7222.     var i, seg;
  7223.     var j;
  7224.  
  7225.     for (i=0; i<segs.length; i++) {
  7226.         seg = segs[i];
  7227.  
  7228.         // go through all the levels and stop on the first level where there are no collisions
  7229.         for (j=0; j<levels.length; j++) {
  7230.             if (!computeSlotSegCollisions(seg, levels[j]).length) {
  7231.                 break;
  7232.             }
  7233.         }
  7234.  
  7235.         seg.level = j;
  7236.  
  7237.         (levels[j] || (levels[j] = [])).push(seg);
  7238.     }
  7239.  
  7240.     return levels;
  7241. }
  7242.  
  7243.  
  7244. // For every segment, figure out the other segments that are in subsequent
  7245. // levels that also occupy the same vertical space. Accumulate in seg.forwardSegs
  7246. function computeForwardSlotSegs(levels) {
  7247.     var i, level;
  7248.     var j, seg;
  7249.     var k;
  7250.  
  7251.     for (i=0; i<levels.length; i++) {
  7252.         level = levels[i];
  7253.  
  7254.         for (j=0; j<level.length; j++) {
  7255.             seg = level[j];
  7256.  
  7257.             seg.forwardSegs = [];
  7258.             for (k=i+1; k<levels.length; k++) {
  7259.                 computeSlotSegCollisions(seg, levels[k], seg.forwardSegs);
  7260.             }
  7261.         }
  7262.     }
  7263. }
  7264.  
  7265.  
  7266. // Figure out which path forward (via seg.forwardSegs) results in the longest path until
  7267. // the furthest edge is reached. The number of segments in this path will be seg.forwardPressure
  7268. function computeSlotSegPressures(seg) {
  7269.     var forwardSegs = seg.forwardSegs;
  7270.     var forwardPressure = 0;
  7271.     var i, forwardSeg;
  7272.  
  7273.     if (seg.forwardPressure === undefined) { // not already computed
  7274.  
  7275.         for (i=0; i<forwardSegs.length; i++) {
  7276.             forwardSeg = forwardSegs[i];
  7277.  
  7278.             // figure out the child's maximum forward path
  7279.             computeSlotSegPressures(forwardSeg);
  7280.  
  7281.             // either use the existing maximum, or use the child's forward pressure
  7282.             // plus one (for the forwardSeg itself)
  7283.             forwardPressure = Math.max(
  7284.                 forwardPressure,
  7285.                 1 + forwardSeg.forwardPressure
  7286.             );
  7287.         }
  7288.  
  7289.         seg.forwardPressure = forwardPressure;
  7290.     }
  7291. }
  7292.  
  7293.  
  7294. // Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
  7295. // from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
  7296. // seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
  7297. //
  7298. // The segment might be part of a "series", which means consecutive segments with the same pressure
  7299. // who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
  7300. // segments behind this one in the current series, and `seriesBackwardCoord` is the starting
  7301. // coordinate of the first segment in the series.
  7302. function computeSlotSegCoords(seg, seriesBackwardPressure, seriesBackwardCoord) {
  7303.     var forwardSegs = seg.forwardSegs;
  7304.     var i;
  7305.  
  7306.     if (seg.forwardCoord === undefined) { // not already computed
  7307.  
  7308.         if (!forwardSegs.length) {
  7309.  
  7310.             // if there are no forward segments, this segment should butt up against the edge
  7311.             seg.forwardCoord = 1;
  7312.         }
  7313.         else {
  7314.  
  7315.             // sort highest pressure first
  7316.             forwardSegs.sort(compareForwardSlotSegs);
  7317.  
  7318.             // this segment's forwardCoord will be calculated from the backwardCoord of the
  7319.             // highest-pressure forward segment.
  7320.             computeSlotSegCoords(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord);
  7321.             seg.forwardCoord = forwardSegs[0].backwardCoord;
  7322.         }
  7323.  
  7324.         // calculate the backwardCoord from the forwardCoord. consider the series
  7325.         seg.backwardCoord = seg.forwardCoord -
  7326.             (seg.forwardCoord - seriesBackwardCoord) / // available width for series
  7327.             (seriesBackwardPressure + 1); // # of segments in the series
  7328.  
  7329.         // use this segment's coordinates to computed the coordinates of the less-pressurized
  7330.         // forward segments
  7331.         for (i=0; i<forwardSegs.length; i++) {
  7332.             computeSlotSegCoords(forwardSegs[i], 0, seg.forwardCoord);
  7333.         }
  7334.     }
  7335. }
  7336.  
  7337.  
  7338. // Find all the segments in `otherSegs` that vertically collide with `seg`.
  7339. // Append into an optionally-supplied `results` array and return.
  7340. function computeSlotSegCollisions(seg, otherSegs, results) {
  7341.     results = results || [];
  7342.  
  7343.     for (var i=0; i<otherSegs.length; i++) {
  7344.         if (isSlotSegCollision(seg, otherSegs[i])) {
  7345.             results.push(otherSegs[i]);
  7346.         }
  7347.     }
  7348.  
  7349.     return results;
  7350. }
  7351.  
  7352.  
  7353. // Do these segments occupy the same vertical space?
  7354. function isSlotSegCollision(seg1, seg2) {
  7355.     return seg1.bottom > seg2.top && seg1.top < seg2.bottom;
  7356. }
  7357.  
  7358.  
  7359. // A cmp function for determining which forward segment to rely on more when computing coordinates.
  7360. function compareForwardSlotSegs(seg1, seg2) {
  7361.     // put higher-pressure first
  7362.     return seg2.forwardPressure - seg1.forwardPressure ||
  7363.         // put segments that are closer to initial edge first (and favor ones with no coords yet)
  7364.         (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) ||
  7365.         // do normal sorting...
  7366.         compareSegs(seg1, seg2);
  7367. }
  7368.  
  7369. ;;
  7370.  
  7371. /* An abstract class from which other views inherit from
  7372. ----------------------------------------------------------------------------------------------------------------------*/
  7373. // Newer methods should be written as prototype methods, not in the monster `View` function at the bottom.
  7374.  
  7375. View.prototype = {
  7376.  
  7377.     calendar: null, // owner Calendar object
  7378.     coordMap: null, // a CoordMap object for converting pixel regions to dates
  7379.     el: null, // the view's containing element. set by Calendar
  7380.  
  7381.     // important Moments
  7382.     start: null, // the date of the very first cell
  7383.     end: null, // the date after the very last cell
  7384.     intervalStart: null, // the start of the interval of time the view represents (1st of month for month view)
  7385.     intervalEnd: null, // the exclusive end of the interval of time the view represents
  7386.  
  7387.     // used for cell-to-date and date-to-cell calculations
  7388.     rowCnt: null, // # of weeks
  7389.     colCnt: null, // # of days displayed in a week
  7390.  
  7391.     isSelected: false, // boolean whether cells are user-selected or not
  7392.  
  7393.     // subclasses can optionally use a scroll container
  7394.     scrollerEl: null, // the element that will most likely scroll when content is too tall
  7395.     scrollTop: null, // cached vertical scroll value
  7396.  
  7397.     // classNames styled by jqui themes
  7398.     widgetHeaderClass: null,
  7399.     widgetContentClass: null,
  7400.     highlightStateClass: null,
  7401.  
  7402.     // document handlers, bound to `this` object
  7403.     documentMousedownProxy: null,
  7404.     documentDragStartProxy: null,
  7405.  
  7406.  
  7407.     // Serves as a "constructor" to suppliment the monster `View` constructor below
  7408.     init: function() {
  7409.         var tm = this.opt('theme') ? 'ui' : 'fc';
  7410.  
  7411.         this.widgetHeaderClass = tm + '-widget-header';
  7412.         this.widgetContentClass = tm + '-widget-content';
  7413.         this.highlightStateClass = tm + '-state-highlight';
  7414.  
  7415.         // save references to `this`-bound handlers
  7416.         this.documentMousedownProxy = $.proxy(this, 'documentMousedown');
  7417.         this.documentDragStartProxy = $.proxy(this, 'documentDragStart');
  7418.     },
  7419.  
  7420.  
  7421.     // Renders the view inside an already-defined `this.el`.
  7422.     // Subclasses should override this and then call the super method afterwards.
  7423.     render: function() {
  7424.         this.updateSize();
  7425.         this.trigger('viewRender', this, this, this.el);
  7426.  
  7427.         // attach handlers to document. do it here to allow for destroy/rerender
  7428.         $(document)
  7429.             .on('mousedown', this.documentMousedownProxy)
  7430.             .on('dragstart', this.documentDragStartProxy); // jqui drag
  7431.     },
  7432.  
  7433.  
  7434.     // Clears all view rendering, event elements, and unregisters handlers
  7435.     destroy: function() {
  7436.         this.unselect();
  7437.         this.trigger('viewDestroy', this, this, this.el);
  7438.         this.destroyEvents();
  7439.         this.el.empty(); // removes inner contents but leaves the element intact
  7440.  
  7441.         $(document)
  7442.             .off('mousedown', this.documentMousedownProxy)
  7443.             .off('dragstart', this.documentDragStartProxy);
  7444.     },
  7445.  
  7446.  
  7447.     // Used to determine what happens when the users clicks next/prev. Given -1 for prev, 1 for next.
  7448.     // Should apply the delta to `date` (a Moment) and return it.
  7449.     incrementDate: function(date, delta) {
  7450.         // subclasses should implement
  7451.     },
  7452.  
  7453.  
  7454.     /* Dimensions
  7455.     ------------------------------------------------------------------------------------------------------------------*/
  7456.  
  7457.  
  7458.     // Refreshes anything dependant upon sizing of the container element of the grid
  7459.     updateSize: function(isResize) {
  7460.         if (isResize) {
  7461.             this.recordScroll();
  7462.         }
  7463.         this.updateHeight();
  7464.         this.updateWidth();
  7465.     },
  7466.  
  7467.  
  7468.     // Refreshes the horizontal dimensions of the calendar
  7469.     updateWidth: function() {
  7470.         // subclasses should implement
  7471.     },
  7472.  
  7473.  
  7474.     // Refreshes the vertical dimensions of the calendar
  7475.     updateHeight: function() {
  7476.         var calendar = this.calendar; // we poll the calendar for height information
  7477.  
  7478.         this.setHeight(
  7479.             calendar.getSuggestedViewHeight(),
  7480.             calendar.isHeightAuto()
  7481.         );
  7482.     },
  7483.  
  7484.  
  7485.     // Updates the vertical dimensions of the calendar to the specified height.
  7486.     // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height.
  7487.     setHeight: function(height, isAuto) {
  7488.         // subclasses should implement
  7489.     },
  7490.  
  7491.  
  7492.     // Given the total height of the view, return the number of pixels that should be used for the scroller.
  7493.     // Utility for subclasses.
  7494.     computeScrollerHeight: function(totalHeight) {
  7495.         var both = this.el.add(this.scrollerEl);
  7496.         var otherHeight; // cumulative height of everything that is not the scrollerEl in the view (header+borders)
  7497.  
  7498.         // fuckin IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked
  7499.         both.css({
  7500.             position: 'relative', // cause a reflow, which will force fresh dimension recalculation
  7501.             left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll
  7502.         });
  7503.         otherHeight = this.el.outerHeight() - this.scrollerEl.height(); // grab the dimensions
  7504.         both.css({ position: '', left: '' }); // undo hack
  7505.  
  7506.         return totalHeight - otherHeight;
  7507.     },
  7508.  
  7509.  
  7510.     // Called for remembering the current scroll value of the scroller.
  7511.     // Should be called before there is a destructive operation (like removing DOM elements) that might inadvertently
  7512.     // change the scroll of the container.
  7513.     recordScroll: function() {
  7514.         if (this.scrollerEl) {
  7515.             this.scrollTop = this.scrollerEl.scrollTop();
  7516.         }
  7517.     },
  7518.  
  7519.  
  7520.     // Set the scroll value of the scroller to the previously recorded value.
  7521.     // Should be called after we know the view's dimensions have been restored following some type of destructive
  7522.     // operation (like temporarily removing DOM elements).
  7523.     restoreScroll: function() {
  7524.         if (this.scrollTop !== null) {
  7525.             this.scrollerEl.scrollTop(this.scrollTop);
  7526.         }
  7527.     },
  7528.  
  7529.  
  7530.     /* Events
  7531.     ------------------------------------------------------------------------------------------------------------------*/
  7532.  
  7533.  
  7534.     // Renders the events onto the view.
  7535.     // Should be overriden by subclasses. Subclasses should call the super-method afterwards.
  7536.     renderEvents: function(events) {
  7537.         this.segEach(function(seg) {
  7538.             this.trigger('eventAfterRender', seg.event, seg.event, seg.el);
  7539.         });
  7540.         this.trigger('eventAfterAllRender');
  7541.     },
  7542.  
  7543.  
  7544.     // Removes event elements from the view.
  7545.     // Should be overridden by subclasses. Should call this super-method FIRST, then subclass DOM destruction.
  7546.     destroyEvents: function() {
  7547.         this.segEach(function(seg) {
  7548.             this.trigger('eventDestroy', seg.event, seg.event, seg.el);
  7549.         });
  7550.     },
  7551.  
  7552.  
  7553.     // Given an event and the default element used for rendering, returns the element that should actually be used.
  7554.     // Basically runs events and elements through the eventRender hook.
  7555.     resolveEventEl: function(event, el) {
  7556.         var custom = this.trigger('eventRender', event, event, el);
  7557.  
  7558.         if (custom === false) { // means don't render at all
  7559.             el = null;
  7560.         }
  7561.         else if (custom && custom !== true) {
  7562.             el = $(custom);
  7563.         }
  7564.  
  7565.         return el;
  7566.     },
  7567.  
  7568.  
  7569.     // Hides all rendered event segments linked to the given event
  7570.     showEvent: function(event) {
  7571.         this.segEach(function(seg) {
  7572.             seg.el.css('visibility', '');
  7573.         }, event);
  7574.     },
  7575.  
  7576.  
  7577.     // Shows all rendered event segments linked to the given event
  7578.     hideEvent: function(event) {
  7579.         this.segEach(function(seg) {
  7580.             seg.el.css('visibility', 'hidden');
  7581.         }, event);
  7582.     },
  7583.  
  7584.  
  7585.     // Iterates through event segments. Goes through all by default.
  7586.     // If the optional `event` argument is specified, only iterates through segments linked to that event.
  7587.     // The `this` value of the callback function will be the view.
  7588.     segEach: function(func, event) {
  7589.         var segs = this.getSegs();
  7590.         var i;
  7591.  
  7592.         for (i = 0; i < segs.length; i++) {
  7593.             if (!event || segs[i].event._id === event._id) {
  7594.                 func.call(this, segs[i]);
  7595.             }
  7596.         }
  7597.     },
  7598.  
  7599.  
  7600.     // Retrieves all the rendered segment objects for the view
  7601.     getSegs: function() {
  7602.         // subclasses must implement
  7603.     },
  7604.  
  7605.  
  7606.     /* Event Drag Visualization
  7607.     ------------------------------------------------------------------------------------------------------------------*/
  7608.  
  7609.  
  7610.     // Renders a visual indication of an event hovering over the specified date.
  7611.     // `end` is a Moment and might be null.
  7612.     // `seg` might be null. if specified, it is the segment object of the event being dragged.
  7613.     //       otherwise, an external event from outside the calendar is being dragged.
  7614.     renderDrag: function(start, end, seg) {
  7615.         // subclasses should implement
  7616.     },
  7617.  
  7618.  
  7619.     // Unrenders a visual indication of event hovering
  7620.     destroyDrag: function() {
  7621.         // subclasses should implement
  7622.     },
  7623.  
  7624.  
  7625.     // Handler for accepting externally dragged events being dropped in the view.
  7626.     // Gets called when jqui's 'dragstart' is fired.
  7627.     documentDragStart: function(ev, ui) {
  7628.         var _this = this;
  7629.         var calendar = this.calendar;
  7630.         var eventStart = null; // a null value signals an unsuccessful drag
  7631.         var eventEnd = null;
  7632.         var visibleEnd = null; // will be calculated event when no eventEnd
  7633.         var el;
  7634.         var accept;
  7635.         var meta;
  7636.         var eventProps; // if an object, signals an event should be created upon drop
  7637.         var dragListener;
  7638.  
  7639.         if (this.opt('droppable')) { // only listen if this setting is on
  7640.             el = $(ev.target);
  7641.  
  7642.             // Test that the dragged element passes the dropAccept selector or filter function.
  7643.             // FYI, the default is "*" (matches all)
  7644.             accept = this.opt('dropAccept');
  7645.             if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) {
  7646.  
  7647.                 meta = getDraggedElMeta(el); // data for possibly creating an event
  7648.                 eventProps = meta.eventProps;
  7649.  
  7650.                 // listener that tracks mouse movement over date-associated pixel regions
  7651.                 dragListener = new DragListener(this.coordMap, {
  7652.                     cellOver: function(cell, cellDate) {
  7653.                         eventStart = cellDate;
  7654.                         eventEnd = meta.duration ? eventStart.clone().add(meta.duration) : null;
  7655.                         visibleEnd = eventEnd || calendar.getDefaultEventEnd(!eventStart.hasTime(), eventStart);
  7656.  
  7657.                         // keep the start/end up to date when dragging
  7658.                         if (eventProps) {
  7659.                             $.extend(eventProps, { start: eventStart, end: eventEnd });
  7660.                         }
  7661.  
  7662.                         if (calendar.isExternalDragAllowedInRange(eventStart, visibleEnd, eventProps)) {
  7663.                             _this.renderDrag(eventStart, visibleEnd);
  7664.                         }
  7665.                         else {
  7666.                             eventStart = null; // signal unsuccessful
  7667.                             disableCursor();
  7668.                         }
  7669.                     },
  7670.                     cellOut: function() {
  7671.                         eventStart = null;
  7672.                         _this.destroyDrag();
  7673.                         enableCursor();
  7674.                     }
  7675.                 });
  7676.  
  7677.                 // gets called, only once, when jqui drag is finished
  7678.                 $(document).one('dragstop', function(ev, ui) {
  7679.                     var renderedEvents;
  7680.  
  7681.                     _this.destroyDrag();
  7682.                     enableCursor();
  7683.  
  7684.                     if (eventStart) { // element was dropped on a valid date/time cell
  7685.  
  7686.                         // if dropped on an all-day cell, and element's metadata specified a time, set it
  7687.                         if (meta.startTime && !eventStart.hasTime()) {
  7688.                             eventStart.time(meta.startTime);
  7689.                         }
  7690.  
  7691.                         // trigger 'drop' regardless of whether element represents an event
  7692.                         _this.trigger('drop', el[0], eventStart, ev, ui);
  7693.  
  7694.                         // create an event from the given properties and the latest dates
  7695.                         if (eventProps) {
  7696.                             renderedEvents = calendar.renderEvent(eventProps, meta.stick);
  7697.                             _this.trigger('eventReceive', null, renderedEvents[0]); // signal an external event landed
  7698.                         }
  7699.                     }
  7700.                 });
  7701.  
  7702.                 dragListener.startDrag(ev); // start listening immediately
  7703.             }
  7704.         }
  7705.     },
  7706.  
  7707.  
  7708.     /* Selection
  7709.     ------------------------------------------------------------------------------------------------------------------*/
  7710.  
  7711.  
  7712.     // Selects a date range on the view. `start` and `end` are both Moments.
  7713.     // `ev` is the native mouse event that begin the interaction.
  7714.     select: function(start, end, ev) {
  7715.         this.unselect(ev);
  7716.         this.renderSelection(start, end);
  7717.         this.reportSelection(start, end, ev);
  7718.     },
  7719.  
  7720.  
  7721.     // Renders a visual indication of the selection
  7722.     renderSelection: function(start, end) {
  7723.         // subclasses should implement
  7724.     },
  7725.  
  7726.  
  7727.     // Called when a new selection is made. Updates internal state and triggers handlers.
  7728.     reportSelection: function(start, end, ev) {
  7729.         this.isSelected = true;
  7730.         this.trigger('select', null, start, end, ev);
  7731.     },
  7732.  
  7733.  
  7734.     // Undoes a selection. updates in the internal state and triggers handlers.
  7735.     // `ev` is the native mouse event that began the interaction.
  7736.     unselect: function(ev) {
  7737.         if (this.isSelected) {
  7738.             this.isSelected = false;
  7739.             this.destroySelection();
  7740.             this.trigger('unselect', null, ev);
  7741.         }
  7742.     },
  7743.  
  7744.  
  7745.     // Unrenders a visual indication of selection
  7746.     destroySelection: function() {
  7747.         // subclasses should implement
  7748.     },
  7749.  
  7750.  
  7751.     // Handler for unselecting when the user clicks something and the 'unselectAuto' setting is on
  7752.     documentMousedown: function(ev) {
  7753.         var ignore;
  7754.  
  7755.         // is there a selection, and has the user made a proper left click?
  7756.         if (this.isSelected && this.opt('unselectAuto') && isPrimaryMouseButton(ev)) {
  7757.  
  7758.             // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element
  7759.             ignore = this.opt('unselectCancel');
  7760.             if (!ignore || !$(ev.target).closest(ignore).length) {
  7761.                 this.unselect(ev);
  7762.             }
  7763.         }
  7764.     }
  7765.  
  7766. };
  7767.  
  7768.  
  7769. // We are mixing JavaScript OOP design patterns here by putting methods and member variables in the closed scope of the
  7770. // constructor. Going forward, methods should be part of the prototype.
  7771. function View(calendar) {
  7772.     var t = this;
  7773.    
  7774.     // exports
  7775.     t.calendar = calendar;
  7776.     t.opt = opt;
  7777.     t.trigger = trigger;
  7778.     t.isEventDraggable = isEventDraggable;
  7779.     t.isEventResizable = isEventResizable;
  7780.     t.eventDrop = eventDrop;
  7781.     t.eventResize = eventResize;
  7782.    
  7783.     // imports
  7784.     var reportEventChange = calendar.reportEventChange;
  7785.    
  7786.     // locals
  7787.     var options = calendar.options;
  7788.     var nextDayThreshold = moment.duration(options.nextDayThreshold);
  7789.  
  7790.  
  7791.     t.init(); // the "constructor" that concerns the prototype methods
  7792.    
  7793.    
  7794.     function opt(name) {
  7795.         var v = options[name];
  7796.         if ($.isPlainObject(v) && !isForcedAtomicOption(name)) {
  7797.             return smartProperty(v, t.name);
  7798.         }
  7799.         return v;
  7800.     }
  7801.  
  7802.    
  7803.     function trigger(name, thisObj) {
  7804.         return calendar.trigger.apply(
  7805.             calendar,
  7806.             [name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t])
  7807.         );
  7808.     }
  7809.    
  7810.  
  7811.  
  7812.     /* Event Editable Boolean Calculations
  7813.     ------------------------------------------------------------------------------*/
  7814.  
  7815.    
  7816.     function isEventDraggable(event) {
  7817.         var source = event.source || {};
  7818.  
  7819.         return firstDefined(
  7820.             event.startEditable,
  7821.             source.startEditable,
  7822.             opt('eventStartEditable'),
  7823.             event.editable,
  7824.             source.editable,
  7825.             opt('editable')
  7826.         );
  7827.     }
  7828.    
  7829.    
  7830.     function isEventResizable(event) {
  7831.         var source = event.source || {};
  7832.  
  7833.         return firstDefined(
  7834.             event.durationEditable,
  7835.             source.durationEditable,
  7836.             opt('eventDurationEditable'),
  7837.             event.editable,
  7838.             source.editable,
  7839.             opt('editable')
  7840.         );
  7841.     }
  7842.    
  7843.    
  7844.    
  7845.     /* Event Elements
  7846.     ------------------------------------------------------------------------------*/
  7847.  
  7848.  
  7849.     // Compute the text that should be displayed on an event's element.
  7850.     // Based off the settings of the view. Possible signatures:
  7851.     //   .getEventTimeText(event, formatStr)
  7852.     //   .getEventTimeText(startMoment, endMoment, formatStr)
  7853.     //   .getEventTimeText(startMoment, null, formatStr)
  7854.     // `timeFormat` is used but the `formatStr` argument can be used to override.
  7855.     t.getEventTimeText = function(event, formatStr) {
  7856.         var start;
  7857.         var end;
  7858.  
  7859.         if (typeof event === 'object' && typeof formatStr === 'object') {
  7860.             // first two arguments are actually moments (or null). shift arguments.
  7861.             start = event;
  7862.             end = formatStr;
  7863.             formatStr = arguments[2];
  7864.         }
  7865.         else {
  7866.             // otherwise, an event object was the first argument
  7867.             start = event.start;
  7868.             end = event.end;
  7869.         }
  7870.  
  7871.         formatStr = formatStr || opt('timeFormat');
  7872.  
  7873.         if (end && opt('displayEventEnd')) {
  7874.             return calendar.formatRange(start, end, formatStr);
  7875.         }
  7876.         else {
  7877.             return calendar.formatDate(start, formatStr);
  7878.         }
  7879.     };
  7880.  
  7881.    
  7882.    
  7883.     /* Event Modification Reporting
  7884.     ---------------------------------------------------------------------------------*/
  7885.  
  7886.    
  7887.     function eventDrop(el, event, newStart, ev) {
  7888.         var mutateResult = calendar.mutateEvent(event, newStart, null);
  7889.  
  7890.         trigger(
  7891.             'eventDrop',
  7892.             el,
  7893.             event,
  7894.             mutateResult.dateDelta,
  7895.             function() {
  7896.                 mutateResult.undo();
  7897.                 reportEventChange();
  7898.             },
  7899.             ev,
  7900.             {} // jqui dummy
  7901.         );
  7902.  
  7903.         reportEventChange();
  7904.     }
  7905.  
  7906.  
  7907.     function eventResize(el, event, newEnd, ev) {
  7908.         var mutateResult = calendar.mutateEvent(event, null, newEnd);
  7909.  
  7910.         trigger(
  7911.             'eventResize',
  7912.             el,
  7913.             event,
  7914.             mutateResult.durationDelta,
  7915.             function() {
  7916.                 mutateResult.undo();
  7917.                 reportEventChange();
  7918.             },
  7919.             ev,
  7920.             {} // jqui dummy
  7921.         );
  7922.  
  7923.         reportEventChange();
  7924.     }
  7925.  
  7926.  
  7927.     // ====================================================================================================
  7928.     // Utilities for day "cells"
  7929.     // ====================================================================================================
  7930.     // The "basic" views are completely made up of day cells.
  7931.     // The "agenda" views have day cells at the top "all day" slot.
  7932.     // This was the obvious common place to put these utilities, but they should be abstracted out into
  7933.     // a more meaningful class (like DayEventRenderer).
  7934.     // ====================================================================================================
  7935.  
  7936.  
  7937.     // For determining how a given "cell" translates into a "date":
  7938.     //
  7939.     // 1. Convert the "cell" (row and column) into a "cell offset" (the # of the cell, cronologically from the first).
  7940.     //    Keep in mind that column indices are inverted with isRTL. This is taken into account.
  7941.     //
  7942.     // 2. Convert the "cell offset" to a "day offset" (the # of days since the first visible day in the view).
  7943.     //
  7944.     // 3. Convert the "day offset" into a "date" (a Moment).
  7945.     //
  7946.     // The reverse transformation happens when transforming a date into a cell.
  7947.  
  7948.  
  7949.     // exports
  7950.     t.isHiddenDay = isHiddenDay;
  7951.     t.skipHiddenDays = skipHiddenDays;
  7952.     t.getCellsPerWeek = getCellsPerWeek;
  7953.     t.dateToCell = dateToCell;
  7954.     t.dateToDayOffset = dateToDayOffset;
  7955.     t.dayOffsetToCellOffset = dayOffsetToCellOffset;
  7956.     t.cellOffsetToCell = cellOffsetToCell;
  7957.     t.cellToDate = cellToDate;
  7958.     t.cellToCellOffset = cellToCellOffset;
  7959.     t.cellOffsetToDayOffset = cellOffsetToDayOffset;
  7960.     t.dayOffsetToDate = dayOffsetToDate;
  7961.     t.rangeToSegments = rangeToSegments;
  7962.     t.isMultiDayEvent = isMultiDayEvent;
  7963.  
  7964.  
  7965.     // internals
  7966.     var hiddenDays = opt('hiddenDays') || []; // array of day-of-week indices that are hidden
  7967.     var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
  7968.     var cellsPerWeek;
  7969.     var dayToCellMap = []; // hash from dayIndex -> cellIndex, for one week
  7970.     var cellToDayMap = []; // hash from cellIndex -> dayIndex, for one week
  7971.     var isRTL = opt('isRTL');
  7972.  
  7973.  
  7974.     // initialize important internal variables
  7975.     (function() {
  7976.  
  7977.         if (opt('weekends') === false) {
  7978.             hiddenDays.push(0, 6); // 0=sunday, 6=saturday
  7979.         }
  7980.  
  7981.         // Loop through a hypothetical week and determine which
  7982.         // days-of-week are hidden. Record in both hashes (one is the reverse of the other).
  7983.         for (var dayIndex=0, cellIndex=0; dayIndex<7; dayIndex++) {
  7984.             dayToCellMap[dayIndex] = cellIndex;
  7985.             isHiddenDayHash[dayIndex] = $.inArray(dayIndex, hiddenDays) != -1;
  7986.             if (!isHiddenDayHash[dayIndex]) {
  7987.                 cellToDayMap[cellIndex] = dayIndex;
  7988.                 cellIndex++;
  7989.             }
  7990.         }
  7991.  
  7992.         cellsPerWeek = cellIndex;
  7993.         if (!cellsPerWeek) {
  7994.             throw 'invalid hiddenDays'; // all days were hidden? bad.
  7995.         }
  7996.  
  7997.     })();
  7998.  
  7999.  
  8000.     // Is the current day hidden?
  8001.     // `day` is a day-of-week index (0-6), or a Moment
  8002.     function isHiddenDay(day) {
  8003.         if (moment.isMoment(day)) {
  8004.             day = day.day();
  8005.         }
  8006.         return isHiddenDayHash[day];
  8007.     }
  8008.  
  8009.  
  8010.     function getCellsPerWeek() {
  8011.         return cellsPerWeek;
  8012.     }
  8013.  
  8014.  
  8015.     // Incrementing the current day until it is no longer a hidden day, returning a copy.
  8016.     // If the initial value of `date` is not a hidden day, don't do anything.
  8017.     // Pass `isExclusive` as `true` if you are dealing with an end date.
  8018.     // `inc` defaults to `1` (increment one day forward each time)
  8019.     function skipHiddenDays(date, inc, isExclusive) {
  8020.         var out = date.clone();
  8021.         inc = inc || 1;
  8022.         while (
  8023.             isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7]
  8024.         ) {
  8025.             out.add(inc, 'days');
  8026.         }
  8027.         return out;
  8028.     }
  8029.  
  8030.  
  8031.     //
  8032.     // TRANSFORMATIONS: cell -> cell offset -> day offset -> date
  8033.     //
  8034.  
  8035.     // cell -> date (combines all transformations)
  8036.     // Possible arguments:
  8037.     // - row, col
  8038.     // - { row:#, col: # }
  8039.     function cellToDate() {
  8040.         var cellOffset = cellToCellOffset.apply(null, arguments);
  8041.         var dayOffset = cellOffsetToDayOffset(cellOffset);
  8042.         var date = dayOffsetToDate(dayOffset);
  8043.         return date;
  8044.     }
  8045.  
  8046.     // cell -> cell offset
  8047.     // Possible arguments:
  8048.     // - row, col
  8049.     // - { row:#, col:# }
  8050.     function cellToCellOffset(row, col) {
  8051.         var colCnt = t.colCnt;
  8052.  
  8053.         // rtl variables. wish we could pre-populate these. but where?
  8054.         var dis = isRTL ? -1 : 1;
  8055.         var dit = isRTL ? colCnt - 1 : 0;
  8056.  
  8057.         if (typeof row == 'object') {
  8058.             col = row.col;
  8059.             row = row.row;
  8060.         }
  8061.         var cellOffset = row * colCnt + (col * dis + dit); // column, adjusted for RTL (dis & dit)
  8062.  
  8063.         return cellOffset;
  8064.     }
  8065.  
  8066.     // cell offset -> day offset
  8067.     function cellOffsetToDayOffset(cellOffset) {
  8068.         var day0 = t.start.day(); // first date's day of week
  8069.         cellOffset += dayToCellMap[day0]; // normlize cellOffset to beginning-of-week
  8070.         return Math.floor(cellOffset / cellsPerWeek) * 7 + // # of days from full weeks
  8071.             cellToDayMap[ // # of days from partial last week
  8072.                 (cellOffset % cellsPerWeek + cellsPerWeek) % cellsPerWeek // crazy math to handle negative cellOffsets
  8073.             ] -
  8074.             day0; // adjustment for beginning-of-week normalization
  8075.     }
  8076.  
  8077.     // day offset -> date
  8078.     function dayOffsetToDate(dayOffset) {
  8079.         return t.start.clone().add(dayOffset, 'days');
  8080.     }
  8081.  
  8082.  
  8083.     //
  8084.     // TRANSFORMATIONS: date -> day offset -> cell offset -> cell
  8085.     //
  8086.  
  8087.     // date -> cell (combines all transformations)
  8088.     function dateToCell(date) {
  8089.         var dayOffset = dateToDayOffset(date);
  8090.         var cellOffset = dayOffsetToCellOffset(dayOffset);
  8091.         var cell = cellOffsetToCell(cellOffset);
  8092.         return cell;
  8093.     }
  8094.  
  8095.     // date -> day offset
  8096.     function dateToDayOffset(date) {
  8097.         return date.clone().stripTime().diff(t.start, 'days');
  8098.     }
  8099.  
  8100.     // day offset -> cell offset
  8101.     function dayOffsetToCellOffset(dayOffset) {
  8102.         var day0 = t.start.day(); // first date's day of week
  8103.         dayOffset += day0; // normalize dayOffset to beginning-of-week
  8104.         return Math.floor(dayOffset / 7) * cellsPerWeek + // # of cells from full weeks
  8105.             dayToCellMap[ // # of cells from partial last week
  8106.                 (dayOffset % 7 + 7) % 7 // crazy math to handle negative dayOffsets
  8107.             ] -
  8108.             dayToCellMap[day0]; // adjustment for beginning-of-week normalization
  8109.     }
  8110.  
  8111.     // cell offset -> cell (object with row & col keys)
  8112.     function cellOffsetToCell(cellOffset) {
  8113.         var colCnt = t.colCnt;
  8114.  
  8115.         // rtl variables. wish we could pre-populate these. but where?
  8116.         var dis = isRTL ? -1 : 1;
  8117.         var dit = isRTL ? colCnt - 1 : 0;
  8118.  
  8119.         var row = Math.floor(cellOffset / colCnt);
  8120.         var col = ((cellOffset % colCnt + colCnt) % colCnt) * dis + dit; // column, adjusted for RTL (dis & dit)
  8121.         return {
  8122.             row: row,
  8123.             col: col
  8124.         };
  8125.     }
  8126.  
  8127.  
  8128.     //
  8129.     // Converts a date range into an array of segment objects.
  8130.     // "Segments" are horizontal stretches of time, sliced up by row.
  8131.     // A segment object has the following properties:
  8132.     // - row
  8133.     // - cols
  8134.     // - isStart
  8135.     // - isEnd
  8136.     //
  8137.     function rangeToSegments(start, end) {
  8138.  
  8139.         var rowCnt = t.rowCnt;
  8140.         var colCnt = t.colCnt;
  8141.         var segments = []; // array of segments to return
  8142.  
  8143.         // day offset for given date range
  8144.         var dayRange = computeDayRange(start, end); // convert to a whole-day range
  8145.         var rangeDayOffsetStart = dateToDayOffset(dayRange.start);
  8146.         var rangeDayOffsetEnd = dateToDayOffset(dayRange.end); // an exclusive value
  8147.  
  8148.         // first and last cell offset for the given date range
  8149.         // "last" implies inclusivity
  8150.         var rangeCellOffsetFirst = dayOffsetToCellOffset(rangeDayOffsetStart);
  8151.         var rangeCellOffsetLast = dayOffsetToCellOffset(rangeDayOffsetEnd) - 1;
  8152.  
  8153.         // loop through all the rows in the view
  8154.         for (var row=0; row<rowCnt; row++) {
  8155.  
  8156.             // first and last cell offset for the row
  8157.             var rowCellOffsetFirst = row * colCnt;
  8158.             var rowCellOffsetLast = rowCellOffsetFirst + colCnt - 1;
  8159.  
  8160.             // get the segment's cell offsets by constraining the range's cell offsets to the bounds of the row
  8161.             var segmentCellOffsetFirst = Math.max(rangeCellOffsetFirst, rowCellOffsetFirst);
  8162.             var segmentCellOffsetLast = Math.min(rangeCellOffsetLast, rowCellOffsetLast);
  8163.  
  8164.             // make sure segment's offsets are valid and in view
  8165.             if (segmentCellOffsetFirst <= segmentCellOffsetLast) {
  8166.  
  8167.                 // translate to cells
  8168.                 var segmentCellFirst = cellOffsetToCell(segmentCellOffsetFirst);
  8169.                 var segmentCellLast = cellOffsetToCell(segmentCellOffsetLast);
  8170.  
  8171.                 // view might be RTL, so order by leftmost column
  8172.                 var cols = [ segmentCellFirst.col, segmentCellLast.col ].sort();
  8173.  
  8174.                 // Determine if segment's first/last cell is the beginning/end of the date range.
  8175.                 // We need to compare "day offset" because "cell offsets" are often ambiguous and
  8176.                 // can translate to multiple days, and an edge case reveals itself when we the
  8177.                 // range's first cell is hidden (we don't want isStart to be true).
  8178.                 var isStart = cellOffsetToDayOffset(segmentCellOffsetFirst) == rangeDayOffsetStart;
  8179.                 var isEnd = cellOffsetToDayOffset(segmentCellOffsetLast) + 1 == rangeDayOffsetEnd;
  8180.                                                                    // +1 for comparing exclusively
  8181.  
  8182.                 segments.push({
  8183.                     row: row,
  8184.                     leftCol: cols[0],
  8185.                     rightCol: cols[1],
  8186.                     isStart: isStart,
  8187.                     isEnd: isEnd
  8188.                 });
  8189.             }
  8190.         }
  8191.  
  8192.         return segments;
  8193.     }
  8194.  
  8195.  
  8196.     // Returns the date range of the full days the given range visually appears to occupy.
  8197.     // Returns object with properties `start` (moment) and `end` (moment, exclusive end).
  8198.     function computeDayRange(start, end) {
  8199.         var startDay = start.clone().stripTime(); // the beginning of the day the range starts
  8200.         var endDay;
  8201.         var endTimeMS;
  8202.  
  8203.         if (end) {
  8204.             endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends
  8205.             endTimeMS = +end.time(); // # of milliseconds into `endDay`
  8206.  
  8207.             // If the end time is actually inclusively part of the next day and is equal to or
  8208.             // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
  8209.             // Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
  8210.             if (endTimeMS && endTimeMS >= nextDayThreshold) {
  8211.                 endDay.add(1, 'days');
  8212.             }
  8213.         }
  8214.  
  8215.         // If no end was specified, or if it is within `startDay` but not past nextDayThreshold,
  8216.         // assign the default duration of one day.
  8217.         if (!end || endDay <= startDay) {
  8218.             endDay = startDay.clone().add(1, 'days');
  8219.         }
  8220.  
  8221.         return { start: startDay, end: endDay };
  8222.     }
  8223.  
  8224.  
  8225.     // Does the given event visually appear to occupy more than one day?
  8226.     function isMultiDayEvent(event) {
  8227.         var range = computeDayRange(event.start, event.end);
  8228.  
  8229.         return range.end.diff(range.start, 'days') > 1;
  8230.     }
  8231.  
  8232. }
  8233.  
  8234.  
  8235. /* Utils
  8236. ----------------------------------------------------------------------------------------------------------------------*/
  8237.  
  8238. // Require all HTML5 data-* attributes used by FullCalendar to have this prefix.
  8239. // A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event.
  8240. fc.dataAttrPrefix = '';
  8241.  
  8242. // Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure
  8243. // to be used for Event Object creation.
  8244. // A defined `.eventProps`, even when empty, indicates that an event should be created.
  8245. function getDraggedElMeta(el) {
  8246.     var prefix = fc.dataAttrPrefix;
  8247.     var eventProps; // properties for creating the event, not related to date/time
  8248.     var startTime; // a Duration
  8249.     var duration;
  8250.     var stick;
  8251.  
  8252.     if (prefix) { prefix += '-'; }
  8253.     eventProps = el.data(prefix + 'event') || null;
  8254.  
  8255.     if (eventProps) {
  8256.         if (typeof eventProps === 'object') {
  8257.             eventProps = $.extend({}, eventProps); // make a copy
  8258.         }
  8259.         else { // something like 1 or true. still signal event creation
  8260.             eventProps = {};
  8261.         }
  8262.  
  8263.         // pluck special-cased date/time properties
  8264.         startTime = eventProps.start;
  8265.         if (startTime == null) { startTime = eventProps.time; } // accept 'time' as well
  8266.         duration = eventProps.duration;
  8267.         stick = eventProps.stick;
  8268.         delete eventProps.start;
  8269.         delete eventProps.time;
  8270.         delete eventProps.duration;
  8271.         delete eventProps.stick;
  8272.     }
  8273.  
  8274.     // fallback to standalone attribute values for each of the date/time properties
  8275.     if (startTime == null) { startTime = el.data(prefix + 'start'); }
  8276.     if (startTime == null) { startTime = el.data(prefix + 'time'); } // accept 'time' as well
  8277.     if (duration == null) { duration = el.data(prefix + 'duration'); }
  8278.     if (stick == null) { stick = el.data(prefix + 'stick'); }
  8279.  
  8280.     // massage into correct data types
  8281.     startTime = startTime != null ? moment.duration(startTime) : null;
  8282.     duration = duration != null ? moment.duration(duration) : null;
  8283.     stick = Boolean(stick);
  8284.  
  8285.     return { eventProps: eventProps, startTime: startTime, duration: duration, stick: stick };
  8286. }
  8287.  
  8288. ;;
  8289.  
  8290. /* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells.
  8291. ----------------------------------------------------------------------------------------------------------------------*/
  8292. // It is a manager for a DayGrid subcomponent, which does most of the heavy lifting.
  8293. // It is responsible for managing width/height.
  8294.  
  8295. function BasicView(calendar) {
  8296.     View.call(this, calendar); // call the super-constructor
  8297.     this.dayGrid = new DayGrid(this);
  8298.     this.coordMap = this.dayGrid.coordMap; // the view's date-to-cell mapping is identical to the subcomponent's
  8299. }
  8300.  
  8301.  
  8302. BasicView.prototype = createObject(View.prototype); // define the super-class
  8303. $.extend(BasicView.prototype, {
  8304.  
  8305.     dayGrid: null, // the main subcomponent that does most of the heavy lifting
  8306.  
  8307.     dayNumbersVisible: false, // display day numbers on each day cell?
  8308.     weekNumbersVisible: false, // display week numbers along the side?
  8309.  
  8310.     weekNumberWidth: null, // width of all the week-number cells running down the side
  8311.  
  8312.     headRowEl: null, // the fake row element of the day-of-week header
  8313.  
  8314.  
  8315.     // Renders the view into `this.el`, which should already be assigned.
  8316.     // rowCnt, colCnt, and dayNumbersVisible have been calculated by a subclass and passed here.
  8317.     render: function(rowCnt, colCnt, dayNumbersVisible) {
  8318.  
  8319.         // needed for cell-to-date and date-to-cell calculations in View
  8320.         this.rowCnt = rowCnt;
  8321.         this.colCnt = colCnt;
  8322.  
  8323.         this.dayNumbersVisible = dayNumbersVisible;
  8324.         this.weekNumbersVisible = this.opt('weekNumbers');
  8325.         this.dayGrid.numbersVisible = this.dayNumbersVisible || this.weekNumbersVisible;
  8326.  
  8327.         this.el.addClass('fc-basic-view').html(this.renderHtml());
  8328.  
  8329.         this.headRowEl = this.el.find('thead .fc-row');
  8330.  
  8331.         this.scrollerEl = this.el.find('.fc-day-grid-container');
  8332.         this.dayGrid.coordMap.containerEl = this.scrollerEl; // constrain clicks/etc to the dimensions of the scroller
  8333.  
  8334.         this.dayGrid.el = this.el.find('.fc-day-grid');
  8335.         this.dayGrid.render(this.hasRigidRows());
  8336.  
  8337.         View.prototype.render.call(this); // call the super-method
  8338.     },
  8339.  
  8340.  
  8341.     // Make subcomponents ready for cleanup
  8342.     destroy: function() {
  8343.         this.dayGrid.destroy();
  8344.         View.prototype.destroy.call(this); // call the super-method
  8345.     },
  8346.  
  8347.  
  8348.     // Builds the HTML skeleton for the view.
  8349.     // The day-grid component will render inside of a container defined by this HTML.
  8350.     renderHtml: function() {
  8351.         return '' +
  8352.             '<table>' +
  8353.                 '<thead>' +
  8354.                     '<tr>' +
  8355.                         '<td class="' + this.widgetHeaderClass + '">' +
  8356.                             this.dayGrid.headHtml() + // render the day-of-week headers
  8357.                         '</td>' +
  8358.                     '</tr>' +
  8359.                 '</thead>' +
  8360.                 '<tbody>' +
  8361.                     '<tr>' +
  8362.                         '<td class="' + this.widgetContentClass + '">' +
  8363.                             '<div class="fc-day-grid-container">' +
  8364.                                 '<div class="fc-day-grid"/>' +
  8365.                             '</div>' +
  8366.                         '</td>' +
  8367.                     '</tr>' +
  8368.                 '</tbody>' +
  8369.             '</table>';
  8370.     },
  8371.  
  8372.  
  8373.     // Generates the HTML that will go before the day-of week header cells.
  8374.     // Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL.
  8375.     headIntroHtml: function() {
  8376.         if (this.weekNumbersVisible) {
  8377.             return '' +
  8378.                 '<th class="fc-week-number ' + this.widgetHeaderClass + '" ' + this.weekNumberStyleAttr() + '>' +
  8379.                     '<span>' + // needed for matchCellWidths
  8380.                         htmlEscape(this.opt('weekNumberTitle')) +
  8381.                     '</span>' +
  8382.                 '</th>';
  8383.         }
  8384.     },
  8385.  
  8386.  
  8387.     // Generates the HTML that will go before content-skeleton cells that display the day/week numbers.
  8388.     // Queried by the DayGrid subcomponent. Ordering depends on isRTL.
  8389.     numberIntroHtml: function(row) {
  8390.         if (this.weekNumbersVisible) {
  8391.             return '' +
  8392.                 '<td class="fc-week-number" ' + this.weekNumberStyleAttr() + '>' +
  8393.                     '<span>' + // needed for matchCellWidths
  8394.                         this.calendar.calculateWeekNumber(this.cellToDate(row, 0)) +
  8395.                     '</span>' +
  8396.                 '</td>';
  8397.         }
  8398.     },
  8399.  
  8400.  
  8401.     // Generates the HTML that goes before the day bg cells for each day-row.
  8402.     // Queried by the DayGrid subcomponent. Ordering depends on isRTL.
  8403.     dayIntroHtml: function() {
  8404.         if (this.weekNumbersVisible) {
  8405.             return '<td class="fc-week-number ' + this.widgetContentClass + '" ' +
  8406.                 this.weekNumberStyleAttr() + '></td>';
  8407.         }
  8408.     },
  8409.  
  8410.  
  8411.     // Generates the HTML that goes before every other type of row generated by DayGrid. Ordering depends on isRTL.
  8412.     // Affects helper-skeleton and highlight-skeleton rows.
  8413.     introHtml: function() {
  8414.         if (this.weekNumbersVisible) {
  8415.             return '<td class="fc-week-number" ' + this.weekNumberStyleAttr() + '></td>';
  8416.         }
  8417.     },
  8418.  
  8419.  
  8420.     // Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton.
  8421.     // The number row will only exist if either day numbers or week numbers are turned on.
  8422.     numberCellHtml: function(row, col, date) {
  8423.         var classes;
  8424.  
  8425.         if (!this.dayNumbersVisible) { // if there are week numbers but not day numbers
  8426.             return '<td/>'; //  will create an empty space above events :(
  8427.         }
  8428.  
  8429.         classes = this.dayGrid.getDayClasses(date);
  8430.         classes.unshift('fc-day-number');
  8431.  
  8432.         return '' +
  8433.             '<td class="' + classes.join(' ') + '" data-date="' + date.format() + '">' +
  8434.                 date.date() +
  8435.             '</td>';
  8436.     },
  8437.  
  8438.  
  8439.     // Generates an HTML attribute string for setting the width of the week number column, if it is known
  8440.     weekNumberStyleAttr: function() {
  8441.         if (this.weekNumberWidth !== null) {
  8442.             return 'style="width:' + this.weekNumberWidth + 'px"';
  8443.         }
  8444.         return '';
  8445.     },
  8446.  
  8447.  
  8448.     // Determines whether each row should have a constant height
  8449.     hasRigidRows: function() {
  8450.         var eventLimit = this.opt('eventLimit');
  8451.         return eventLimit && typeof eventLimit !== 'number';
  8452.     },
  8453.  
  8454.  
  8455.     /* Dimensions
  8456.     ------------------------------------------------------------------------------------------------------------------*/
  8457.  
  8458.  
  8459.     // Refreshes the horizontal dimensions of the view
  8460.     updateWidth: function() {
  8461.         if (this.weekNumbersVisible) {
  8462.             // Make sure all week number cells running down the side have the same width.
  8463.             // Record the width for cells created later.
  8464.             this.weekNumberWidth = matchCellWidths(
  8465.                 this.el.find('.fc-week-number')
  8466.             );
  8467.         }
  8468.     },
  8469.  
  8470.  
  8471.     // Adjusts the vertical dimensions of the view to the specified values
  8472.     setHeight: function(totalHeight, isAuto) {
  8473.         var eventLimit = this.opt('eventLimit');
  8474.         var scrollerHeight;
  8475.  
  8476.         // reset all heights to be natural
  8477.         unsetScroller(this.scrollerEl);
  8478.         uncompensateScroll(this.headRowEl);
  8479.  
  8480.         this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed
  8481.  
  8482.         // is the event limit a constant level number?
  8483.         if (eventLimit && typeof eventLimit === 'number') {
  8484.             this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after
  8485.         }
  8486.  
  8487.         scrollerHeight = this.computeScrollerHeight(totalHeight);
  8488.         this.setGridHeight(scrollerHeight, isAuto);
  8489.  
  8490.         // is the event limit dynamically calculated?
  8491.         if (eventLimit && typeof eventLimit !== 'number') {
  8492.             this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set
  8493.         }
  8494.  
  8495.         if (!isAuto && setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars?
  8496.  
  8497.             compensateScroll(this.headRowEl, getScrollbarWidths(this.scrollerEl));
  8498.  
  8499.             // doing the scrollbar compensation might have created text overflow which created more height. redo
  8500.             scrollerHeight = this.computeScrollerHeight(totalHeight);
  8501.             this.scrollerEl.height(scrollerHeight);
  8502.  
  8503.             this.restoreScroll();
  8504.         }
  8505.     },
  8506.  
  8507.  
  8508.     // Sets the height of just the DayGrid component in this view
  8509.     setGridHeight: function(height, isAuto) {
  8510.         if (isAuto) {
  8511.             undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding
  8512.         }
  8513.         else {
  8514.             distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows
  8515.         }
  8516.     },
  8517.  
  8518.  
  8519.     /* Events
  8520.     ------------------------------------------------------------------------------------------------------------------*/
  8521.  
  8522.  
  8523.     // Renders the given events onto the view and populates the segments array
  8524.     renderEvents: function(events) {
  8525.         this.dayGrid.renderEvents(events);
  8526.  
  8527.         this.updateHeight(); // must compensate for events that overflow the row
  8528.  
  8529.         View.prototype.renderEvents.call(this, events); // call the super-method
  8530.     },
  8531.  
  8532.  
  8533.     // Retrieves all segment objects that are rendered in the view
  8534.     getSegs: function() {
  8535.         return this.dayGrid.getSegs();
  8536.     },
  8537.  
  8538.  
  8539.     // Unrenders all event elements and clears internal segment data
  8540.     destroyEvents: function() {
  8541.         View.prototype.destroyEvents.call(this); // do this before dayGrid's segs have been cleared
  8542.  
  8543.         this.recordScroll(); // removing events will reduce height and mess with the scroll, so record beforehand
  8544.         this.dayGrid.destroyEvents();
  8545.  
  8546.         // we DON'T need to call updateHeight() because:
  8547.         // A) a renderEvents() call always happens after this, which will eventually call updateHeight()
  8548.         // B) in IE8, this causes a flash whenever events are rerendered
  8549.     },
  8550.  
  8551.  
  8552.     /* Event Dragging
  8553.     ------------------------------------------------------------------------------------------------------------------*/
  8554.  
  8555.  
  8556.     // Renders a visual indication of an event being dragged over the view.
  8557.     // A returned value of `true` signals that a mock "helper" event has been rendered.
  8558.     renderDrag: function(start, end, seg) {
  8559.         return this.dayGrid.renderDrag(start, end, seg);
  8560.     },
  8561.  
  8562.  
  8563.     // Unrenders the visual indication of an event being dragged over the view
  8564.     destroyDrag: function() {
  8565.         this.dayGrid.destroyDrag();
  8566.     },
  8567.  
  8568.  
  8569.     /* Selection
  8570.     ------------------------------------------------------------------------------------------------------------------*/
  8571.  
  8572.  
  8573.     // Renders a visual indication of a selection
  8574.     renderSelection: function(start, end) {
  8575.         this.dayGrid.renderSelection(start, end);
  8576.     },
  8577.  
  8578.  
  8579.     // Unrenders a visual indications of a selection
  8580.     destroySelection: function() {
  8581.         this.dayGrid.destroySelection();
  8582.     }
  8583.  
  8584. });
  8585.  
  8586. ;;
  8587.  
  8588. /* A month view with day cells running in rows (one-per-week) and columns
  8589. ----------------------------------------------------------------------------------------------------------------------*/
  8590.  
  8591. setDefaults({
  8592.     fixedWeekCount: true
  8593. });
  8594.  
  8595. fcViews.month = MonthView; // register the view
  8596.  
  8597. function MonthView(calendar) {
  8598.     BasicView.call(this, calendar); // call the super-constructor
  8599. }
  8600.  
  8601.  
  8602. MonthView.prototype = createObject(BasicView.prototype); // define the super-class
  8603. $.extend(MonthView.prototype, {
  8604.  
  8605.     name: 'month',
  8606.  
  8607.  
  8608.     incrementDate: function(date, delta) {
  8609.         return date.clone().stripTime().add(delta, 'months').startOf('month');
  8610.     },
  8611.  
  8612.  
  8613.     render: function(date) {
  8614.         var rowCnt;
  8615.  
  8616.         this.intervalStart = date.clone().stripTime().startOf('month');
  8617.         this.intervalEnd = this.intervalStart.clone().add(1, 'months');
  8618.  
  8619.         this.start = this.intervalStart.clone();
  8620.         this.start = this.skipHiddenDays(this.start); // move past the first week if no visible days
  8621.         this.start.startOf('week');
  8622.         this.start = this.skipHiddenDays(this.start); // move past the first invisible days of the week
  8623.  
  8624.         this.end = this.intervalEnd.clone();
  8625.         this.end = this.skipHiddenDays(this.end, -1, true); // move in from the last week if no visible days
  8626.         this.end.add((7 - this.end.weekday()) % 7, 'days'); // move to end of week if not already
  8627.         this.end = this.skipHiddenDays(this.end, -1, true); // move in from the last invisible days of the week
  8628.  
  8629.         rowCnt = Math.ceil( // need to ceil in case there are hidden days
  8630.             this.end.diff(this.start, 'weeks', true) // returnfloat=true
  8631.         );
  8632.         if (this.isFixedWeeks()) {
  8633.             this.end.add(6 - rowCnt, 'weeks');
  8634.             rowCnt = 6;
  8635.         }
  8636.  
  8637.         this.title = this.calendar.formatDate(this.intervalStart, this.opt('titleFormat'));
  8638.  
  8639.         BasicView.prototype.render.call(this, rowCnt, this.getCellsPerWeek(), true); // call the super-method
  8640.     },
  8641.  
  8642.  
  8643.     // Overrides the default BasicView behavior to have special multi-week auto-height logic
  8644.     setGridHeight: function(height, isAuto) {
  8645.  
  8646.         isAuto = isAuto || this.opt('weekMode') === 'variable'; // LEGACY: weekMode is deprecated
  8647.  
  8648.         // if auto, make the height of each row the height that it would be if there were 6 weeks
  8649.         if (isAuto) {
  8650.             height *= this.rowCnt / 6;
  8651.         }
  8652.  
  8653.         distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows
  8654.     },
  8655.  
  8656.  
  8657.     isFixedWeeks: function() {
  8658.         var weekMode = this.opt('weekMode'); // LEGACY: weekMode is deprecated
  8659.         if (weekMode) {
  8660.             return weekMode === 'fixed'; // if any other type of weekMode, assume NOT fixed
  8661.         }
  8662.  
  8663.         return this.opt('fixedWeekCount');
  8664.     }
  8665.  
  8666. });
  8667.  
  8668. ;;
  8669.  
  8670. /* A week view with simple day cells running horizontally
  8671. ----------------------------------------------------------------------------------------------------------------------*/
  8672. // TODO: a WeekView mixin for calculating dates and titles
  8673.  
  8674. fcViews.basicWeek = BasicWeekView; // register this view
  8675.  
  8676. function BasicWeekView(calendar) {
  8677.     BasicView.call(this, calendar); // call the super-constructor
  8678. }
  8679.  
  8680.  
  8681. BasicWeekView.prototype = createObject(BasicView.prototype); // define the super-class
  8682. $.extend(BasicWeekView.prototype, {
  8683.  
  8684.     name: 'basicWeek',
  8685.  
  8686.  
  8687.     incrementDate: function(date, delta) {
  8688.         return date.clone().stripTime().add(delta, 'weeks').startOf('week');
  8689.     },
  8690.  
  8691.  
  8692.     render: function(date) {
  8693.  
  8694.         this.intervalStart = date.clone().stripTime().startOf('week');
  8695.         this.intervalEnd = this.intervalStart.clone().add(1, 'weeks');
  8696.  
  8697.         this.start = this.skipHiddenDays(this.intervalStart);
  8698.         this.end = this.skipHiddenDays(this.intervalEnd, -1, true);
  8699.  
  8700.         this.title = this.calendar.formatRange(
  8701.             this.start,
  8702.             this.end.clone().subtract(1), // make inclusive by subtracting 1 ms
  8703.             this.opt('titleFormat'),
  8704.             ' \u2014 ' // emphasized dash
  8705.         );
  8706.  
  8707.         BasicView.prototype.render.call(this, 1, this.getCellsPerWeek(), false); // call the super-method
  8708.     }
  8709.    
  8710. });
  8711. ;;
  8712.  
  8713. /* A view with a single simple day cell
  8714. ----------------------------------------------------------------------------------------------------------------------*/
  8715.  
  8716. fcViews.basicDay = BasicDayView; // register this view
  8717.  
  8718. function BasicDayView(calendar) {
  8719.     BasicView.call(this, calendar); // call the super-constructor
  8720. }
  8721.  
  8722.  
  8723. BasicDayView.prototype = createObject(BasicView.prototype); // define the super-class
  8724. $.extend(BasicDayView.prototype, {
  8725.  
  8726.     name: 'basicDay',
  8727.  
  8728.  
  8729.     incrementDate: function(date, delta) {
  8730.         var out = date.clone().stripTime().add(delta, 'days');
  8731.         out = this.skipHiddenDays(out, delta < 0 ? -1 : 1);
  8732.         return out;
  8733.     },
  8734.  
  8735.  
  8736.     render: function(date) {
  8737.  
  8738.         this.start = this.intervalStart = date.clone().stripTime();
  8739.         this.end = this.intervalEnd = this.start.clone().add(1, 'days');
  8740.  
  8741.         this.title = this.calendar.formatDate(this.start, this.opt('titleFormat'));
  8742.  
  8743.         BasicView.prototype.render.call(this, 1, 1, false); // call the super-method
  8744.     }
  8745.  
  8746. });
  8747. ;;
  8748.  
  8749. /* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically.
  8750. ----------------------------------------------------------------------------------------------------------------------*/
  8751. // Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on).
  8752. // Responsible for managing width/height.
  8753.  
  8754. setDefaults({
  8755.     allDaySlot: true,
  8756.     allDayText: 'all-day',
  8757.  
  8758.     scrollTime: '06:00:00',
  8759.  
  8760.     slotDuration: '00:30:00',
  8761.  
  8762.     axisFormat: generateAgendaAxisFormat,
  8763.     timeFormat: {
  8764.         agenda: generateAgendaTimeFormat
  8765.     },
  8766.  
  8767.     minTime: '00:00:00',
  8768.     maxTime: '24:00:00',
  8769.     slotEventOverlap: true
  8770. });
  8771.  
  8772. var AGENDA_ALL_DAY_EVENT_LIMIT = 5;
  8773.  
  8774.  
  8775. function generateAgendaAxisFormat(options, langData) {
  8776.     return langData.longDateFormat('LT')
  8777.         .replace(':mm', '(:mm)')
  8778.         .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs
  8779.         .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
  8780. }
  8781.  
  8782.  
  8783. function generateAgendaTimeFormat(options, langData) {
  8784.     return langData.longDateFormat('LT')
  8785.         .replace(/\s*a$/i, ''); // remove trailing AM/PM
  8786. }
  8787.  
  8788.  
  8789. function AgendaView(calendar) {
  8790.     View.call(this, calendar); // call the super-constructor
  8791.  
  8792.     this.timeGrid = new TimeGrid(this);
  8793.  
  8794.     if (this.opt('allDaySlot')) { // should we display the "all-day" area?
  8795.         this.dayGrid = new DayGrid(this); // the all-day subcomponent of this view
  8796.  
  8797.         // the coordinate grid will be a combination of both subcomponents' grids
  8798.         this.coordMap = new ComboCoordMap([
  8799.             this.dayGrid.coordMap,
  8800.             this.timeGrid.coordMap
  8801.         ]);
  8802.     }
  8803.     else {
  8804.         this.coordMap = this.timeGrid.coordMap;
  8805.     }
  8806. }
  8807.  
  8808.  
  8809. AgendaView.prototype = createObject(View.prototype); // define the super-class
  8810. $.extend(AgendaView.prototype, {
  8811.  
  8812.     timeGrid: null, // the main time-grid subcomponent of this view
  8813.     dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null
  8814.  
  8815.     axisWidth: null, // the width of the time axis running down the side
  8816.  
  8817.     noScrollRowEls: null, // set of fake row elements that must compensate when scrollerEl has scrollbars
  8818.  
  8819.     // when the time-grid isn't tall enough to occupy the given height, we render an <hr> underneath
  8820.     bottomRuleEl: null,
  8821.     bottomRuleHeight: null,
  8822.  
  8823.  
  8824.     /* Rendering
  8825.     ------------------------------------------------------------------------------------------------------------------*/
  8826.  
  8827.  
  8828.     // Renders the view into `this.el`, which has already been assigned.
  8829.     // `colCnt` has been calculated by a subclass and passed here.
  8830.     render: function(colCnt) {
  8831.  
  8832.         // needed for cell-to-date and date-to-cell calculations in View
  8833.         this.rowCnt = 1;
  8834.         this.colCnt = colCnt;
  8835.  
  8836.         this.el.addClass('fc-agenda-view').html(this.renderHtml());
  8837.  
  8838.         // the element that wraps the time-grid that will probably scroll
  8839.         this.scrollerEl = this.el.find('.fc-time-grid-container');
  8840.         this.timeGrid.coordMap.containerEl = this.scrollerEl; // don't accept clicks/etc outside of this
  8841.  
  8842.         this.timeGrid.el = this.el.find('.fc-time-grid');
  8843.         this.timeGrid.render();
  8844.  
  8845.         // the <hr> that sometimes displays under the time-grid
  8846.         this.bottomRuleEl = $('<hr class="' + this.widgetHeaderClass + '"/>')
  8847.             .appendTo(this.timeGrid.el); // inject it into the time-grid
  8848.  
  8849.         if (this.dayGrid) {
  8850.             this.dayGrid.el = this.el.find('.fc-day-grid');
  8851.             this.dayGrid.render();
  8852.  
  8853.             // have the day-grid extend it's coordinate area over the <hr> dividing the two grids
  8854.             this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight();
  8855.         }
  8856.  
  8857.         this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller
  8858.  
  8859.         View.prototype.render.call(this); // call the super-method
  8860.  
  8861.         this.resetScroll(); // do this after sizes have been set
  8862.     },
  8863.  
  8864.  
  8865.     // Make subcomponents ready for cleanup
  8866.     destroy: function() {
  8867.         this.timeGrid.destroy();
  8868.         if (this.dayGrid) {
  8869.             this.dayGrid.destroy();
  8870.         }
  8871.         View.prototype.destroy.call(this); // call the super-method
  8872.     },
  8873.  
  8874.  
  8875.     // Builds the HTML skeleton for the view.
  8876.     // The day-grid and time-grid components will render inside containers defined by this HTML.
  8877.     renderHtml: function() {
  8878.         return '' +
  8879.             '<table>' +
  8880.                 '<thead>' +
  8881.                     '<tr>' +
  8882.                         '<td class="' + this.widgetHeaderClass + '">' +
  8883.                             this.timeGrid.headHtml() + // render the day-of-week headers
  8884.                         '</td>' +
  8885.                     '</tr>' +
  8886.                 '</thead>' +
  8887.                 '<tbody>' +
  8888.                     '<tr>' +
  8889.                         '<td class="' + this.widgetContentClass + '">' +
  8890.                             (this.dayGrid ?
  8891.                                 '<div class="fc-day-grid"/>' +
  8892.                                 '<hr class="' + this.widgetHeaderClass + '"/>' :
  8893.                                 ''
  8894.                                 ) +
  8895.                             '<div class="fc-time-grid-container">' +
  8896.                                 '<div class="fc-time-grid"/>' +
  8897.                             '</div>' +
  8898.                         '</td>' +
  8899.                     '</tr>' +
  8900.                 '</tbody>' +
  8901.             '</table>';
  8902.     },
  8903.  
  8904.  
  8905.     // Generates the HTML that will go before the day-of week header cells.
  8906.     // Queried by the TimeGrid subcomponent when generating rows. Ordering depends on isRTL.
  8907.     headIntroHtml: function() {
  8908.         var date;
  8909.         var weekNumber;
  8910.         var weekTitle;
  8911.         var weekText;
  8912.  
  8913.         if (this.opt('weekNumbers')) {
  8914.             date = this.cellToDate(0, 0);
  8915.             weekNumber = this.calendar.calculateWeekNumber(date);
  8916.             weekTitle = this.opt('weekNumberTitle');
  8917.  
  8918.             if (this.opt('isRTL')) {
  8919.                 weekText = weekNumber + weekTitle;
  8920.             }
  8921.             else {
  8922.                 weekText = weekTitle + weekNumber;
  8923.             }
  8924.  
  8925.             return '' +
  8926.                 '<th class="fc-axis fc-week-number ' + this.widgetHeaderClass + '" ' + this.axisStyleAttr() + '>' +
  8927.                     '<span>' + // needed for matchCellWidths
  8928.                         htmlEscape(weekText) +
  8929.                     '</span>' +
  8930.                 '</th>';
  8931.         }
  8932.         else {
  8933.             return '<th class="fc-axis ' + this.widgetHeaderClass + '" ' + this.axisStyleAttr() + '></th>';
  8934.         }
  8935.     },
  8936.  
  8937.  
  8938.     // Generates the HTML that goes before the all-day cells.
  8939.     // Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL.
  8940.     dayIntroHtml: function() {
  8941.         return '' +
  8942.             '<td class="fc-axis ' + this.widgetContentClass + '" ' + this.axisStyleAttr() + '>' +
  8943.                 '<span>' + // needed for matchCellWidths
  8944.                     (this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'))) +
  8945.                 '</span>' +
  8946.             '</td>';
  8947.     },
  8948.  
  8949.  
  8950.     // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column.
  8951.     slotBgIntroHtml: function() {
  8952.         return '<td class="fc-axis ' + this.widgetContentClass + '" ' + this.axisStyleAttr() + '></td>';
  8953.     },
  8954.  
  8955.  
  8956.     // Generates the HTML that goes before all other types of cells.
  8957.     // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
  8958.     // Queried by the TimeGrid and DayGrid subcomponents when generating rows. Ordering depends on isRTL.
  8959.     introHtml: function() {
  8960.         return '<td class="fc-axis" ' + this.axisStyleAttr() + '></td>';
  8961.     },
  8962.  
  8963.  
  8964.     // Generates an HTML attribute string for setting the width of the axis, if it is known
  8965.     axisStyleAttr: function() {
  8966.         if (this.axisWidth !== null) {
  8967.              return 'style="width:' + this.axisWidth + 'px"';
  8968.         }
  8969.         return '';
  8970.     },
  8971.  
  8972.  
  8973.     /* Dimensions
  8974.     ------------------------------------------------------------------------------------------------------------------*/
  8975.  
  8976.     updateSize: function(isResize) {
  8977.         if (isResize) {
  8978.             this.timeGrid.resize();
  8979.         }
  8980.         View.prototype.updateSize.call(this, isResize);
  8981.     },
  8982.  
  8983.  
  8984.     // Refreshes the horizontal dimensions of the view
  8985.     updateWidth: function() {
  8986.         // make all axis cells line up, and record the width so newly created axis cells will have it
  8987.         this.axisWidth = matchCellWidths(this.el.find('.fc-axis'));
  8988.     },
  8989.  
  8990.  
  8991.     // Adjusts the vertical dimensions of the view to the specified values
  8992.     setHeight: function(totalHeight, isAuto) {
  8993.         var eventLimit;
  8994.         var scrollerHeight;
  8995.  
  8996.         if (this.bottomRuleHeight === null) {
  8997.             // calculate the height of the rule the very first time
  8998.             this.bottomRuleHeight = this.bottomRuleEl.outerHeight();
  8999.         }
  9000.         this.bottomRuleEl.hide(); // .show() will be called later if this <hr> is necessary
  9001.  
  9002.         // reset all dimensions back to the original state
  9003.         this.scrollerEl.css('overflow', '');
  9004.         unsetScroller(this.scrollerEl);
  9005.         uncompensateScroll(this.noScrollRowEls);
  9006.  
  9007.         // limit number of events in the all-day area
  9008.         if (this.dayGrid) {
  9009.             this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed
  9010.  
  9011.             eventLimit = this.opt('eventLimit');
  9012.             if (eventLimit && typeof eventLimit !== 'number') {
  9013.                 eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number
  9014.             }
  9015.             if (eventLimit) {
  9016.                 this.dayGrid.limitRows(eventLimit);
  9017.             }
  9018.         }
  9019.  
  9020.         if (!isAuto) { // should we force dimensions of the scroll container, or let the contents be natural height?
  9021.  
  9022.             scrollerHeight = this.computeScrollerHeight(totalHeight);
  9023.             if (setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars?
  9024.  
  9025.                 // make the all-day and header rows lines up
  9026.                 compensateScroll(this.noScrollRowEls, getScrollbarWidths(this.scrollerEl));
  9027.  
  9028.                 // the scrollbar compensation might have changed text flow, which might affect height, so recalculate
  9029.                 // and reapply the desired height to the scroller.
  9030.                 scrollerHeight = this.computeScrollerHeight(totalHeight);
  9031.                 this.scrollerEl.height(scrollerHeight);
  9032.  
  9033.                 this.restoreScroll();
  9034.             }
  9035.             else { // no scrollbars
  9036.                 // still, force a height and display the bottom rule (marks the end of day)
  9037.                 this.scrollerEl.height(scrollerHeight).css('overflow', 'hidden'); // in case <hr> goes outside
  9038.                 this.bottomRuleEl.show();
  9039.             }
  9040.         }
  9041.     },
  9042.  
  9043.  
  9044.     // Sets the scroll value of the scroller to the intial pre-configured state prior to allowing the user to change it.
  9045.     resetScroll: function() {
  9046.         var _this = this;
  9047.         var scrollTime = moment.duration(this.opt('scrollTime'));
  9048.         var top = this.timeGrid.computeTimeTop(scrollTime);
  9049.  
  9050.         // zoom can give weird floating-point values. rather scroll a little bit further
  9051.         top = Math.ceil(top);
  9052.  
  9053.         if (top) {
  9054.             top++; // to overcome top border that slots beyond the first have. looks better
  9055.         }
  9056.  
  9057.         function scroll() {
  9058.             _this.scrollerEl.scrollTop(top);
  9059.         }
  9060.  
  9061.         scroll();
  9062.         setTimeout(scroll, 0); // overrides any previous scroll state made by the browser
  9063.     },
  9064.  
  9065.  
  9066.     /* Events
  9067.     ------------------------------------------------------------------------------------------------------------------*/
  9068.  
  9069.  
  9070.     // Renders events onto the view and populates the View's segment array
  9071.     renderEvents: function(events) {
  9072.         var dayEvents = [];
  9073.         var timedEvents = [];
  9074.         var daySegs = [];
  9075.         var timedSegs;
  9076.         var i;
  9077.  
  9078.         // separate the events into all-day and timed
  9079.         for (i = 0; i < events.length; i++) {
  9080.             if (events[i].allDay) {
  9081.                 dayEvents.push(events[i]);
  9082.             }
  9083.             else {
  9084.                 timedEvents.push(events[i]);
  9085.             }
  9086.         }
  9087.  
  9088.         // render the events in the subcomponents
  9089.         timedSegs = this.timeGrid.renderEvents(timedEvents);
  9090.         if (this.dayGrid) {
  9091.             daySegs = this.dayGrid.renderEvents(dayEvents);
  9092.         }
  9093.  
  9094.         // the all-day area is flexible and might have a lot of events, so shift the height
  9095.         this.updateHeight();
  9096.  
  9097.         View.prototype.renderEvents.call(this, events); // call the super-method
  9098.     },
  9099.  
  9100.  
  9101.     // Retrieves all segment objects that are rendered in the view
  9102.     getSegs: function() {
  9103.         return this.timeGrid.getSegs().concat(
  9104.             this.dayGrid ? this.dayGrid.getSegs() : []
  9105.         );
  9106.     },
  9107.  
  9108.  
  9109.     // Unrenders all event elements and clears internal segment data
  9110.     destroyEvents: function() {
  9111.         View.prototype.destroyEvents.call(this); // do this before the grids' segs have been cleared
  9112.  
  9113.         // if destroyEvents is being called as part of an event rerender, renderEvents will be called shortly
  9114.         // after, so remember what the scroll value was so we can restore it.
  9115.         this.recordScroll();
  9116.  
  9117.         // destroy the events in the subcomponents
  9118.         this.timeGrid.destroyEvents();
  9119.         if (this.dayGrid) {
  9120.             this.dayGrid.destroyEvents();
  9121.         }
  9122.  
  9123.         // we DON'T need to call updateHeight() because:
  9124.         // A) a renderEvents() call always happens after this, which will eventually call updateHeight()
  9125.         // B) in IE8, this causes a flash whenever events are rerendered
  9126.     },
  9127.  
  9128.  
  9129.     /* Event Dragging
  9130.     ------------------------------------------------------------------------------------------------------------------*/
  9131.  
  9132.  
  9133.     // Renders a visual indication of an event being dragged over the view.
  9134.     // A returned value of `true` signals that a mock "helper" event has been rendered.
  9135.     renderDrag: function(start, end, seg) {
  9136.         if (start.hasTime()) {
  9137.             return this.timeGrid.renderDrag(start, end, seg);
  9138.         }
  9139.         else if (this.dayGrid) {
  9140.             return this.dayGrid.renderDrag(start, end, seg);
  9141.         }
  9142.     },
  9143.  
  9144.  
  9145.     // Unrenders a visual indications of an event being dragged over the view
  9146.     destroyDrag: function() {
  9147.         this.timeGrid.destroyDrag();
  9148.         if (this.dayGrid) {
  9149.             this.dayGrid.destroyDrag();
  9150.         }
  9151.     },
  9152.  
  9153.  
  9154.     /* Selection
  9155.     ------------------------------------------------------------------------------------------------------------------*/
  9156.  
  9157.  
  9158.     // Renders a visual indication of a selection
  9159.     renderSelection: function(start, end) {
  9160.         if (start.hasTime() || end.hasTime()) {
  9161.             this.timeGrid.renderSelection(start, end);
  9162.         }
  9163.         else if (this.dayGrid) {
  9164.             this.dayGrid.renderSelection(start, end);
  9165.         }
  9166.     },
  9167.  
  9168.  
  9169.     // Unrenders a visual indications of a selection
  9170.     destroySelection: function() {
  9171.         this.timeGrid.destroySelection();
  9172.         if (this.dayGrid) {
  9173.             this.dayGrid.destroySelection();
  9174.         }
  9175.     }
  9176.  
  9177. });
  9178.  
  9179. ;;
  9180.  
  9181. /* A week view with an all-day cell area at the top, and a time grid below
  9182. ----------------------------------------------------------------------------------------------------------------------*/
  9183. // TODO: a WeekView mixin for calculating dates and titles
  9184.  
  9185. fcViews.agendaWeek = AgendaWeekView; // register the view
  9186.  
  9187. function AgendaWeekView(calendar) {
  9188.     AgendaView.call(this, calendar); // call the super-constructor
  9189. }
  9190.  
  9191.  
  9192. AgendaWeekView.prototype = createObject(AgendaView.prototype); // define the super-class
  9193. $.extend(AgendaWeekView.prototype, {
  9194.  
  9195.     name: 'agendaWeek',
  9196.  
  9197.  
  9198.     incrementDate: function(date, delta) {
  9199.         return date.clone().stripTime().add(delta, 'weeks').startOf('week');
  9200.     },
  9201.  
  9202.  
  9203.     render: function(date) {
  9204.  
  9205.         this.intervalStart = date.clone().stripTime().startOf('week');
  9206.         this.intervalEnd = this.intervalStart.clone().add(1, 'weeks');
  9207.  
  9208.         this.start = this.skipHiddenDays(this.intervalStart);
  9209.         this.end = this.skipHiddenDays(this.intervalEnd, -1, true);
  9210.  
  9211.         this.title = this.calendar.formatRange(
  9212.             this.start,
  9213.             this.end.clone().subtract(1), // make inclusive by subtracting 1 ms
  9214.             this.opt('titleFormat'),
  9215.             ' \u2014 ' // emphasized dash
  9216.         );
  9217.  
  9218.         AgendaView.prototype.render.call(this, this.getCellsPerWeek()); // call the super-method
  9219.     }
  9220.  
  9221. });
  9222.  
  9223. ;;
  9224.  
  9225. /* A day view with an all-day cell area at the top, and a time grid below
  9226. ----------------------------------------------------------------------------------------------------------------------*/
  9227.  
  9228. fcViews.agendaDay = AgendaDayView; // register the view
  9229.  
  9230. function AgendaDayView(calendar) {
  9231.     AgendaView.call(this, calendar); // call the super-constructor
  9232. }
  9233.  
  9234.  
  9235. AgendaDayView.prototype = createObject(AgendaView.prototype); // define the super-class
  9236. $.extend(AgendaDayView.prototype, {
  9237.  
  9238.     name: 'agendaDay',
  9239.  
  9240.  
  9241.     incrementDate: function(date, delta) {
  9242.         var out = date.clone().stripTime().add(delta, 'days');
  9243.         out = this.skipHiddenDays(out, delta < 0 ? -1 : 1);
  9244.         return out;
  9245.     },
  9246.  
  9247.  
  9248.     render: function(date) {
  9249.  
  9250.         this.start = this.intervalStart = date.clone().stripTime();
  9251.         this.end = this.intervalEnd = this.start.clone().add(1, 'days');
  9252.  
  9253.         this.title = this.calendar.formatDate(this.start, this.opt('titleFormat'));
  9254.  
  9255.         AgendaView.prototype.render.call(this, 1); // call the super-method
  9256.     }
  9257.  
  9258. });
  9259.  
  9260. ;;
  9261.  
  9262. });
Add Comment
Please, Sign In to add comment