Guest User

Untitled

a guest
Oct 31st, 2017
36
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. /*!
  2.  * FullCalendar v2.3.2
  3.  * Docs & License: http://fullcalendar.io/
  4.  * (c) 2015 Adam Shaw
  5.  */
  6.  
  7. (function(factory) {
  8.     if (typeof define === 'function' && define.amd) {
  9.         define([ 'jquery', 'moment' ], factory);
  10.     }
  11.     else if (typeof exports === 'object') { // Node/CommonJS
  12.         module.exports = factory(require('jquery'), require('moment'));
  13.     }
  14.     else {
  15.         factory(jQuery, moment);
  16.     }
  17. })(function($, moment) {
  18.  
  19. ;;
  20.  
  21. var fc = $.fullCalendar = { version: "2.3.2" };
  22. var fcViews = fc.views = {};
  23.  
  24.  
  25. $.fn.fullCalendar = function(options) {
  26.     var args = Array.prototype.slice.call(arguments, 1); // for a possible method call
  27.     var res = this; // what this function will return (this jQuery object by default)
  28.  
  29.     this.each(function(i, _element) { // loop each DOM element involved
  30.         var element = $(_element);
  31.         var calendar = element.data('fullCalendar'); // get the existing calendar object (if any)
  32.         var singleRes; // the returned value of this single method call
  33.  
  34.         // a method call
  35.         if (typeof options === 'string') {
  36.             if (calendar && $.isFunction(calendar[options])) {
  37.                 singleRes = calendar[options].apply(calendar, args);
  38.                 if (!i) {
  39.                     res = singleRes; // record the first method call result
  40.                 }
  41.                 if (options === 'destroy') { // for the destroy method, must remove Calendar object data
  42.                     element.removeData('fullCalendar');
  43.                 }
  44.             }
  45.         }
  46.         // a new calendar initialization
  47.         else if (!calendar) { // don't initialize twice
  48.             calendar = new Calendar(element, options);
  49.             element.data('fullCalendar', calendar);
  50.             calendar.render();
  51.         }
  52.     });
  53.    
  54.     return res;
  55. };
  56.  
  57.  
  58. var complexOptions = [ // names of options that are objects whose properties should be combined
  59.     'header',
  60.     'buttonText',
  61.     'buttonIcons',
  62.     'themeButtonIcons'
  63. ];
  64.  
  65.  
  66. // Merges an array of option objects into a single object
  67. function mergeOptions(optionObjs) {
  68.     return mergeProps(optionObjs, complexOptions);
  69. }
  70.  
  71.  
  72. // Given options specified for the calendar's constructor, massages any legacy options into a non-legacy form.
  73. // Converts View-Option-Hashes into the View-Specific-Options format.
  74. function massageOverrides(input) {
  75.     var overrides = { views: input.views || {} }; // the output. ensure a `views` hash
  76.     var subObj;
  77.  
  78.     // iterate through all option override properties (except `views`)
  79.     $.each(input, function(name, val) {
  80.         if (name != 'views') {
  81.  
  82.             // could the value be a legacy View-Option-Hash?
  83.             if (
  84.                 $.isPlainObject(val) &&
  85.                 !/(time|duration|interval)$/i.test(name) && // exclude duration options. might be given as objects
  86.                 $.inArray(name, complexOptions) == -1 // complex options aren't allowed to be View-Option-Hashes
  87.             ) {
  88.                 subObj = null;
  89.  
  90.                 // iterate through the properties of this possible View-Option-Hash value
  91.                 $.each(val, function(subName, subVal) {
  92.  
  93.                     // is the property targeting a view?
  94.                     if (/^(month|week|day|default|basic(Week|Day)?|agenda(Week|Day)?)$/.test(subName)) {
  95.                         if (!overrides.views[subName]) { // ensure the view-target entry exists
  96.                             overrides.views[subName] = {};
  97.                         }
  98.                         overrides.views[subName][name] = subVal; // record the value in the `views` object
  99.                     }
  100.                     else { // a non-View-Option-Hash property
  101.                         if (!subObj) {
  102.                             subObj = {};
  103.                         }
  104.                         subObj[subName] = subVal; // accumulate these unrelated values for later
  105.                     }
  106.                 });
  107.  
  108.                 if (subObj) { // non-View-Option-Hash properties? transfer them as-is
  109.                     overrides[name] = subObj;
  110.                 }
  111.             }
  112.             else {
  113.                 overrides[name] = val; // transfer normal options as-is
  114.             }
  115.         }
  116.     });
  117.  
  118.     return overrides;
  119. }
  120.  
  121. ;;
  122.  
  123. // exports
  124. fc.intersectionToSeg = intersectionToSeg;
  125. fc.applyAll = applyAll;
  126. fc.debounce = debounce;
  127. fc.isInt = isInt;
  128. fc.htmlEscape = htmlEscape;
  129. fc.cssToStr = cssToStr;
  130. fc.proxy = proxy;
  131. fc.capitaliseFirstLetter = capitaliseFirstLetter;
  132.  
  133.  
  134. /* FullCalendar-specific DOM Utilities
  135. ----------------------------------------------------------------------------------------------------------------------*/
  136.  
  137.  
  138. // Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left
  139. // and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that.
  140. function compensateScroll(rowEls, scrollbarWidths) {
  141.     if (scrollbarWidths.left) {
  142.         rowEls.css({
  143.             'border-left-width': 1,
  144.             'margin-left': scrollbarWidths.left - 1
  145.         });
  146.     }
  147.     if (scrollbarWidths.right) {
  148.         rowEls.css({
  149.             'border-right-width': 1,
  150.             'margin-right': scrollbarWidths.right - 1
  151.         });
  152.     }
  153. }
  154.  
  155.  
  156. // Undoes compensateScroll and restores all borders/margins
  157. function uncompensateScroll(rowEls) {
  158.     rowEls.css({
  159.         'margin-left': '',
  160.         'margin-right': '',
  161.         'border-left-width': '',
  162.         'border-right-width': ''
  163.     });
  164. }
  165.  
  166.  
  167. // Make the mouse cursor express that an event is not allowed in the current area
  168. function disableCursor() {
  169.     $('body').addClass('fc-not-allowed');
  170. }
  171.  
  172.  
  173. // Returns the mouse cursor to its original look
  174. function enableCursor() {
  175.     $('body').removeClass('fc-not-allowed');
  176. }
  177.  
  178.  
  179. // Given a total available height to fill, have `els` (essentially child rows) expand to accomodate.
  180. // By default, all elements that are shorter than the recommended height are expanded uniformly, not considering
  181. // any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and
  182. // reduces the available height.
  183. function distributeHeight(els, availableHeight, shouldRedistribute) {
  184.  
  185.     // *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions,
  186.     // and it is better to be shorter than taller, to avoid creating unnecessary scrollbars.
  187.  
  188.     var minOffset1 = Math.floor(availableHeight / els.length); // for non-last element
  189.     var minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)); // for last element *FLOORING NOTE*
  190.     var flexEls = []; // elements that are allowed to expand. array of DOM nodes
  191.     var flexOffsets = []; // amount of vertical space it takes up
  192.     var flexHeights = []; // actual css height
  193.     var usedHeight = 0;
  194.  
  195.     undistributeHeight(els); // give all elements their natural height
  196.  
  197.     // find elements that are below the recommended height (expandable).
  198.     // important to query for heights in a single first pass (to avoid reflow oscillation).
  199.     els.each(function(i, el) {
  200.         var minOffset = i === els.length - 1 ? minOffset2 : minOffset1;
  201.         var naturalOffset = $(el).outerHeight(true);
  202.  
  203.         if (naturalOffset < minOffset) {
  204.             flexEls.push(el);
  205.             flexOffsets.push(naturalOffset);
  206.             flexHeights.push($(el).height());
  207.         }
  208.         else {
  209.             // this element stretches past recommended height (non-expandable). mark the space as occupied.
  210.             usedHeight += naturalOffset;
  211.         }
  212.     });
  213.  
  214.     // readjust the recommended height to only consider the height available to non-maxed-out rows.
  215.     if (shouldRedistribute) {
  216.         availableHeight -= usedHeight;
  217.         minOffset1 = Math.floor(availableHeight / flexEls.length);
  218.         minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)); // *FLOORING NOTE*
  219.     }
  220.  
  221.     // assign heights to all expandable elements
  222.     $(flexEls).each(function(i, el) {
  223.         var minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1;
  224.         var naturalOffset = flexOffsets[i];
  225.         var naturalHeight = flexHeights[i];
  226.         var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding
  227.  
  228.         if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things
  229.             $(el).height(newHeight);
  230.         }
  231.     });
  232. }
  233.  
  234.  
  235. // Undoes distrubuteHeight, restoring all els to their natural height
  236. function undistributeHeight(els) {
  237.     els.height('');
  238. }
  239.  
  240.  
  241. // Given `els`, a jQuery set of <td> cells, find the cell with the largest natural width and set the widths of all the
  242. // cells to be that width.
  243. // PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline
  244. function matchCellWidths(els) {
  245.     var maxInnerWidth = 0;
  246.  
  247.     els.find('> *').each(function(i, innerEl) {
  248.         var innerWidth = $(innerEl).outerWidth();
  249.         if (innerWidth > maxInnerWidth) {
  250.             maxInnerWidth = innerWidth;
  251.         }
  252.     });
  253.  
  254.     maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance
  255.  
  256.     els.width(maxInnerWidth);
  257.  
  258.     return maxInnerWidth;
  259. }
  260.  
  261.  
  262. // Turns a container element into a scroller if its contents is taller than the allotted height.
  263. // Returns true if the element is now a scroller, false otherwise.
  264. // NOTE: this method is best because it takes weird zooming dimensions into account
  265. function setPotentialScroller(containerEl, height) {
  266.     containerEl.height(height).addClass('fc-scroller');
  267.  
  268.     // are scrollbars needed?
  269.     if (containerEl[0].scrollHeight - 1 > containerEl[0].clientHeight) { // !!! -1 because IE is often off-by-one :(
  270.         return true;
  271.     }
  272.  
  273.     unsetScroller(containerEl); // undo
  274.     return false;
  275. }
  276.  
  277.  
  278. // Takes an element that might have been a scroller, and turns it back into a normal element.
  279. function unsetScroller(containerEl) {
  280.     containerEl.height('').removeClass('fc-scroller');
  281. }
  282.  
  283.  
  284. /* General DOM Utilities
  285. ----------------------------------------------------------------------------------------------------------------------*/
  286.  
  287. fc.getClientRect = getClientRect;
  288. fc.getContentRect = getContentRect;
  289. fc.getScrollbarWidths = getScrollbarWidths;
  290.  
  291.  
  292. // borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51
  293. function getScrollParent(el) {
  294.     var position = el.css('position'),
  295.         scrollParent = el.parents().filter(function() {
  296.             var parent = $(this);
  297.             return (/(auto|scroll)/).test(
  298.                 parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x')
  299.             );
  300.         }).eq(0);
  301.  
  302.     return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent;
  303. }
  304.  
  305.  
  306. // Queries the outer bounding area of a jQuery element.
  307. // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
  308. function getOuterRect(el) {
  309.     var offset = el.offset();
  310.  
  311.     return {
  312.         left: offset.left,
  313.         right: offset.left + el.outerWidth(),
  314.         top: offset.top,
  315.         bottom: offset.top + el.outerHeight()
  316.     };
  317. }
  318.  
  319.  
  320. // Queries the area within the margin/border/scrollbars of a jQuery element. Does not go within the padding.
  321. // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
  322. // NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
  323. function getClientRect(el) {
  324.     var offset = el.offset();
  325.     var scrollbarWidths = getScrollbarWidths(el);
  326.     var left = offset.left + getCssFloat(el, 'border-left-width') + scrollbarWidths.left;
  327.     var top = offset.top + getCssFloat(el, 'border-top-width') + scrollbarWidths.top;
  328.  
  329.     return {
  330.         left: left,
  331.         right: left + el[0].clientWidth, // clientWidth includes padding but NOT scrollbars
  332.         top: top,
  333.         bottom: top + el[0].clientHeight // clientHeight includes padding but NOT scrollbars
  334.     };
  335. }
  336.  
  337.  
  338. // Queries the area within the margin/border/padding of a jQuery element. Assumed not to have scrollbars.
  339. // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
  340. function getContentRect(el) {
  341.     var offset = el.offset(); // just outside of border, margin not included
  342.     var left = offset.left + getCssFloat(el, 'border-left-width') + getCssFloat(el, 'padding-left');
  343.     var top = offset.top + getCssFloat(el, 'border-top-width') + getCssFloat(el, 'padding-top');
  344.  
  345.     return {
  346.         left: left,
  347.         right: left + el.width(),
  348.         top: top,
  349.         bottom: top + el.height()
  350.     };
  351. }
  352.  
  353.  
  354. // Returns the computed left/right/top/bottom scrollbar widths for the given jQuery element.
  355. // NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
  356. function getScrollbarWidths(el) {
  357.     var leftRightWidth = el.innerWidth() - el[0].clientWidth; // the paddings cancel out, leaving the scrollbars
  358.     var widths = {
  359.         left: 0,
  360.         right: 0,
  361.         top: 0,
  362.         bottom: el.innerHeight() - el[0].clientHeight // the paddings cancel out, leaving the bottom scrollbar
  363.     };
  364.  
  365.     if (getIsLeftRtlScrollbars() && el.css('direction') == 'rtl') { // is the scrollbar on the left side?
  366.         widths.left = leftRightWidth;
  367.     }
  368.     else {
  369.         widths.right = leftRightWidth;
  370.     }
  371.  
  372.     return widths;
  373. }
  374.  
  375.  
  376. // Logic for determining if, when the element is right-to-left, the scrollbar appears on the left side
  377.  
  378. var _isLeftRtlScrollbars = null;
  379.  
  380. function getIsLeftRtlScrollbars() { // responsible for caching the computation
  381.     if (_isLeftRtlScrollbars === null) {
  382.         _isLeftRtlScrollbars = computeIsLeftRtlScrollbars();
  383.     }
  384.     return _isLeftRtlScrollbars;
  385. }
  386.  
  387. function computeIsLeftRtlScrollbars() { // creates an offscreen test element, then removes it
  388.     var el = $('<div><div/></div>')
  389.         .css({
  390.             position: 'absolute',
  391.             top: -1000,
  392.             left: 0,
  393.             border: 0,
  394.             padding: 0,
  395.             overflow: 'scroll',
  396.             direction: 'rtl'
  397.         })
  398.         .appendTo('body');
  399.     var innerEl = el.children();
  400.     var res = innerEl.offset().left > el.offset().left; // is the inner div shifted to accommodate a left scrollbar?
  401.     el.remove();
  402.     return res;
  403. }
  404.  
  405.  
  406. // Retrieves a jQuery element's computed CSS value as a floating-point number.
  407. // If the queried value is non-numeric (ex: IE can return "medium" for border width), will just return zero.
  408. function getCssFloat(el, prop) {
  409.     return parseFloat(el.css(prop)) || 0;
  410. }
  411.  
  412.  
  413. // Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
  414. function isPrimaryMouseButton(ev) {
  415.     return ev.which == 1 && !ev.ctrlKey;
  416. }
  417.  
  418.  
  419. /* Geometry
  420. ----------------------------------------------------------------------------------------------------------------------*/
  421.  
  422.  
  423. // Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false
  424. function intersectRects(rect1, rect2) {
  425.     var res = {
  426.         left: Math.max(rect1.left, rect2.left),
  427.         right: Math.min(rect1.right, rect2.right),
  428.         top: Math.max(rect1.top, rect2.top),
  429.         bottom: Math.min(rect1.bottom, rect2.bottom)
  430.     };
  431.  
  432.     if (res.left < res.right && res.top < res.bottom) {
  433.         return res;
  434.     }
  435.     return false;
  436. }
  437.  
  438.  
  439. // Returns a new point that will have been moved to reside within the given rectangle
  440. function constrainPoint(point, rect) {
  441.     return {
  442.         left: Math.min(Math.max(point.left, rect.left), rect.right),
  443.         top: Math.min(Math.max(point.top, rect.top), rect.bottom)
  444.     };
  445. }
  446.  
  447.  
  448. // Returns a point that is the center of the given rectangle
  449. function getRectCenter(rect) {
  450.     return {
  451.         left: (rect.left + rect.right) / 2,
  452.         top: (rect.top + rect.bottom) / 2
  453.     };
  454. }
  455.  
  456.  
  457. // Subtracts point2's coordinates from point1's coordinates, returning a delta
  458. function diffPoints(point1, point2) {
  459.     return {
  460.         left: point1.left - point2.left,
  461.         top: point1.top - point2.top
  462.     };
  463. }
  464.  
  465.  
  466. /* FullCalendar-specific Misc Utilities
  467. ----------------------------------------------------------------------------------------------------------------------*/
  468.  
  469.  
  470. // Creates a basic segment with the intersection of the two ranges. Returns undefined if no intersection.
  471. // Expects all dates to be normalized to the same timezone beforehand.
  472. // TODO: move to date section?
  473. function intersectionToSeg(subjectRange, constraintRange) {
  474.     var subjectStart = subjectRange.start;
  475.     var subjectEnd = subjectRange.end;
  476.     var constraintStart = constraintRange.start;
  477.     var constraintEnd = constraintRange.end;
  478.     var segStart, segEnd;
  479.     var isStart, isEnd;
  480.  
  481.     if (subjectEnd > constraintStart && subjectStart < constraintEnd) { // in bounds at all?
  482.  
  483.         if (subjectStart >= constraintStart) {
  484.             segStart = subjectStart.clone();
  485.             isStart = true;
  486.         }
  487.         else {
  488.             segStart = constraintStart.clone();
  489.             isStart =  false;
  490.         }
  491.  
  492.         if (subjectEnd <= constraintEnd) {
  493.             segEnd = subjectEnd.clone();
  494.             isEnd = true;
  495.         }
  496.         else {
  497.             segEnd = constraintEnd.clone();
  498.             isEnd = false;
  499.         }
  500.  
  501.         return {
  502.             start: segStart,
  503.             end: segEnd,
  504.             isStart: isStart,
  505.             isEnd: isEnd
  506.         };
  507.     }
  508. }
  509.  
  510.  
  511. /* Date Utilities
  512. ----------------------------------------------------------------------------------------------------------------------*/
  513.  
  514. fc.computeIntervalUnit = computeIntervalUnit;
  515. fc.durationHasTime = durationHasTime;
  516.  
  517. var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ];
  518. var intervalUnits = [ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' ];
  519.  
  520.  
  521. // Diffs the two moments into a Duration where full-days are recorded first, then the remaining time.
  522. // Moments will have their timezones normalized.
  523. function diffDayTime(a, b) {
  524.     return moment.duration({
  525.         days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'),
  526.         ms: a.time() - b.time() // time-of-day from day start. disregards timezone
  527.     });
  528. }
  529.  
  530.  
  531. // Diffs the two moments via their start-of-day (regardless of timezone). Produces whole-day durations.
  532. function diffDay(a, b) {
  533.     return moment.duration({
  534.         days: a.clone().stripTime().diff(b.clone().stripTime(), 'days')
  535.     });
  536. }
  537.  
  538.  
  539. // Diffs two moments, producing a duration, made of a whole-unit-increment of the given unit. Uses rounding.
  540. function diffByUnit(a, b, unit) {
  541.     return moment.duration(
  542.         Math.round(a.diff(b, unit, true)), // returnFloat=true
  543.         unit
  544.     );
  545. }
  546.  
  547.  
  548. // Computes the unit name of the largest whole-unit period of time.
  549. // For example, 48 hours will be "days" whereas 49 hours will be "hours".
  550. // Accepts start/end, a range object, or an original duration object.
  551. function computeIntervalUnit(start, end) {
  552.     var i, unit;
  553.     var val;
  554.  
  555.     for (i = 0; i < intervalUnits.length; i++) {
  556.         unit = intervalUnits[i];
  557.         val = computeRangeAs(unit, start, end);
  558.  
  559.         if (val >= 1 && isInt(val)) {
  560.             break;
  561.         }
  562.     }
  563.  
  564.     return unit; // will be "milliseconds" if nothing else matches
  565. }
  566.  
  567.  
  568. // Computes the number of units (like "hours") in the given range.
  569. // Range can be a {start,end} object, separate start/end args, or a Duration.
  570. // Results are based on Moment's .as() and .diff() methods, so results can depend on internal handling
  571. // of month-diffing logic (which tends to vary from version to version).
  572. function computeRangeAs(unit, start, end) {
  573.  
  574.     if (end != null) { // given start, end
  575.         return end.diff(start, unit, true);
  576.     }
  577.     else if (moment.isDuration(start)) { // given duration
  578.         return start.as(unit);
  579.     }
  580.     else { // given { start, end } range object
  581.         return start.end.diff(start.start, unit, true);
  582.     }
  583. }
  584.  
  585.  
  586. // Returns a boolean about whether the given duration has any time parts (hours/minutes/seconds/ms)
  587. function durationHasTime(dur) {
  588.     return Boolean(dur.hours() || dur.minutes() || dur.seconds() || dur.milliseconds());
  589. }
  590.  
  591.  
  592. function isNativeDate(input) {
  593.     return  Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date;
  594. }
  595.  
  596.  
  597. // Returns a boolean about whether the given input is a time string, like "06:40:00" or "06:00"
  598. function isTimeString(str) {
  599.     return /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str);
  600. }
  601.  
  602.  
  603. /* General Utilities
  604. ----------------------------------------------------------------------------------------------------------------------*/
  605.  
  606. var hasOwnPropMethod = {}.hasOwnProperty;
  607.  
  608.  
  609. // Merges an array of objects into a single object.
  610. // The second argument allows for an array of property names who's object values will be merged together.
  611. function mergeProps(propObjs, complexProps) {
  612.     var dest = {};
  613.     var i, name;
  614.     var complexObjs;
  615.     var j, val;
  616.     var props;
  617.  
  618.     if (complexProps) {
  619.         for (i = 0; i < complexProps.length; i++) {
  620.             name = complexProps[i];
  621.             complexObjs = [];
  622.  
  623.             // collect the trailing object values, stopping when a non-object is discovered
  624.             for (j = propObjs.length - 1; j >= 0; j--) {
  625.                 val = propObjs[j][name];
  626.  
  627.                 if (typeof val === 'object') {
  628.                     complexObjs.unshift(val);
  629.                 }
  630.                 else if (val !== undefined) {
  631.                     dest[name] = val; // if there were no objects, this value will be used
  632.                     break;
  633.                 }
  634.             }
  635.  
  636.             // if the trailing values were objects, use the merged value
  637.             if (complexObjs.length) {
  638.                 dest[name] = mergeProps(complexObjs);
  639.             }
  640.         }
  641.     }
  642.  
  643.     // copy values into the destination, going from last to first
  644.     for (i = propObjs.length - 1; i >= 0; i--) {
  645.         props = propObjs[i];
  646.  
  647.         for (name in props) {
  648.             if (!(name in dest)) { // if already assigned by previous props or complex props, don't reassign
  649.                 dest[name] = props[name];
  650.             }
  651.         }
  652.     }
  653.  
  654.     return dest;
  655. }
  656.  
  657.  
  658. // Create an object that has the given prototype. Just like Object.create
  659. function createObject(proto) {
  660.     var f = function() {};
  661.     f.prototype = proto;
  662.     return new f();
  663. }
  664.  
  665.  
  666. function copyOwnProps(src, dest) {
  667.     for (var name in src) {
  668.         if (hasOwnProp(src, name)) {
  669.             dest[name] = src[name];
  670.         }
  671.     }
  672. }
  673.  
  674.  
  675. // Copies over certain methods with the same names as Object.prototype methods. Overcomes an IE<=8 bug:
  676. // https://developer.mozilla.org/en-US/docs/ECMAScript_DontEnum_attribute#JScript_DontEnum_Bug
  677. function copyNativeMethods(src, dest) {
  678.     var names = [ 'constructor', 'toString', 'valueOf' ];
  679.     var i, name;
  680.  
  681.     for (i = 0; i < names.length; i++) {
  682.         name = names[i];
  683.  
  684.         if (src[name] !== Object.prototype[name]) {
  685.             dest[name] = src[name];
  686.         }
  687.     }
  688. }
  689.  
  690.  
  691. function hasOwnProp(obj, name) {
  692.     return hasOwnPropMethod.call(obj, name);
  693. }
  694.  
  695.  
  696. // Is the given value a non-object non-function value?
  697. function isAtomic(val) {
  698.     return /undefined|null|boolean|number|string/.test($.type(val));
  699. }
  700.  
  701.  
  702. function applyAll(functions, thisObj, args) {
  703.     if ($.isFunction(functions)) {
  704.         functions = [ functions ];
  705.     }
  706.     if (functions) {
  707.         var i;
  708.         var ret;
  709.         for (i=0; i<functions.length; i++) {
  710.             ret = functions[i].apply(thisObj, args) || ret;
  711.         }
  712.         return ret;
  713.     }
  714. }
  715.  
  716.  
  717. function firstDefined() {
  718.     for (var i=0; i<arguments.length; i++) {
  719.         if (arguments[i] !== undefined) {
  720.             return arguments[i];
  721.         }
  722.     }
  723. }
  724.  
  725.  
  726. function htmlEscape(s) {
  727.     return (s + '').replace(/&/g, '&amp;')
  728.         .replace(/</g, '&lt;')
  729.         .replace(/>/g, '&gt;')
  730.         .replace(/'/g, '&#039;')
  731.         .replace(/"/g, '&quot;')
  732.         .replace(/\n/g, '<br />');
  733. }
  734.  
  735.  
  736. function stripHtmlEntities(text) {
  737.     return text.replace(/&.*?;/g, '');
  738. }
  739.  
  740.  
  741. // Given a hash of CSS properties, returns a string of CSS.
  742. // Uses property names as-is (no camel-case conversion). Will not make statements for null/undefined values.
  743. function cssToStr(cssProps) {
  744.     var statements = [];
  745.  
  746.     $.each(cssProps, function(name, val) {
  747.         if (val != null) {
  748.             statements.push(name + ':' + val);
  749.         }
  750.     });
  751.  
  752.     return statements.join(';');
  753. }
  754.  
  755.  
  756. function capitaliseFirstLetter(str) {
  757.     return str.charAt(0).toUpperCase() + str.slice(1);
  758. }
  759.  
  760.  
  761. function compareNumbers(a, b) { // for .sort()
  762.     return a - b;
  763. }
  764.  
  765.  
  766. function isInt(n) {
  767.     return n % 1 === 0;
  768. }
  769.  
  770.  
  771. // Returns a method bound to the given object context.
  772. // Just like one of the jQuery.proxy signatures, but without the undesired behavior of treating the same method with
  773. // different contexts as identical when binding/unbinding events.
  774. function proxy(obj, methodName) {
  775.     var method = obj[methodName];
  776.  
  777.     return function() {
  778.         return method.apply(obj, arguments);
  779.     };
  780. }
  781.  
  782.  
  783. // Returns a function, that, as long as it continues to be invoked, will not
  784. // be triggered. The function will be called after it stops being called for
  785. // N milliseconds.
  786. // https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714
  787. function debounce(func, wait) {
  788.     var timeoutId;
  789.     var args;
  790.     var context;
  791.     var timestamp; // of most recent call
  792.     var later = function() {
  793.         var last = +new Date() - timestamp;
  794.         if (last < wait && last > 0) {
  795.             timeoutId = setTimeout(later, wait - last);
  796.         }
  797.         else {
  798.             timeoutId = null;
  799.             func.apply(context, args);
  800.             if (!timeoutId) {
  801.                 context = args = null;
  802.             }
  803.         }
  804.     };
  805.  
  806.     return function() {
  807.         context = this;
  808.         args = arguments;
  809.         timestamp = +new Date();
  810.         if (!timeoutId) {
  811.             timeoutId = setTimeout(later, wait);
  812.         }
  813.     };
  814. }
  815.  
  816. ;;
  817.  
  818. var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/;
  819. var ambigTimeOrZoneRegex =
  820.     /^\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+)?)?)?)?)?$/;
  821. var newMomentProto = moment.fn; // where we will attach our new methods
  822. var oldMomentProto = $.extend({}, newMomentProto); // copy of original moment methods
  823. var allowValueOptimization;
  824. var setUTCValues; // function defined below
  825. var setLocalValues; // function defined below
  826.  
  827.  
  828. // Creating
  829. // -------------------------------------------------------------------------------------------------
  830.  
  831. // Creates a new moment, similar to the vanilla moment(...) constructor, but with
  832. // extra features (ambiguous time, enhanced formatting). When given an existing moment,
  833. // it will function as a clone (and retain the zone of the moment). Anything else will
  834. // result in a moment in the local zone.
  835. fc.moment = function() {
  836.     return makeMoment(arguments);
  837. };
  838.  
  839. // Sames as fc.moment, but forces the resulting moment to be in the UTC timezone.
  840. fc.moment.utc = function() {
  841.     var mom = makeMoment(arguments, true);
  842.  
  843.     // Force it into UTC because makeMoment doesn't guarantee it
  844.     // (if given a pre-existing moment for example)
  845.     if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone
  846.         mom.utc();
  847.     }
  848.  
  849.     return mom;
  850. };
  851.  
  852. // Same as fc.moment, but when given an ISO8601 string, the timezone offset is preserved.
  853. // ISO8601 strings with no timezone offset will become ambiguously zoned.
  854. fc.moment.parseZone = function() {
  855.     return makeMoment(arguments, true, true);
  856. };
  857.  
  858. // Builds an enhanced moment from args. When given an existing moment, it clones. When given a
  859. // native Date, or called with no arguments (the current time), the resulting moment will be local.
  860. // Anything else needs to be "parsed" (a string or an array), and will be affected by:
  861. //    parseAsUTC - if there is no zone information, should we parse the input in UTC?
  862. //    parseZone - if there is zone information, should we force the zone of the moment?
  863. function makeMoment(args, parseAsUTC, parseZone) {
  864.     var input = args[0];
  865.     var isSingleString = args.length == 1 && typeof input === 'string';
  866.     var isAmbigTime;
  867.     var isAmbigZone;
  868.     var ambigMatch;
  869.     var mom;
  870.  
  871.     if (moment.isMoment(input)) {
  872.         mom = moment.apply(null, args); // clone it
  873.         transferAmbigs(input, mom); // the ambig flags weren't transfered with the clone
  874.     }
  875.     else if (isNativeDate(input) || input === undefined) {
  876.         mom = moment.apply(null, args); // will be local
  877.     }
  878.     else { // "parsing" is required
  879.         isAmbigTime = false;
  880.         isAmbigZone = false;
  881.  
  882.         if (isSingleString) {
  883.             if (ambigDateOfMonthRegex.test(input)) {
  884.                 // accept strings like '2014-05', but convert to the first of the month
  885.                 input += '-01';
  886.                 args = [ input ]; // for when we pass it on to moment's constructor
  887.                 isAmbigTime = true;
  888.                 isAmbigZone = true;
  889.             }
  890.             else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) {
  891.                 isAmbigTime = !ambigMatch[5]; // no time part?
  892.                 isAmbigZone = true;
  893.             }
  894.         }
  895.         else if ($.isArray(input)) {
  896.             // arrays have no timezone information, so assume ambiguous zone
  897.             isAmbigZone = true;
  898.         }
  899.         // otherwise, probably a string with a format
  900.  
  901.         if (parseAsUTC || isAmbigTime) {
  902.             mom = moment.utc.apply(moment, args);
  903.         }
  904.         else {
  905.             mom = moment.apply(null, args);
  906.         }
  907.  
  908.         if (isAmbigTime) {
  909.             mom._ambigTime = true;
  910.             mom._ambigZone = true; // ambiguous time always means ambiguous zone
  911.         }
  912.         else if (parseZone) { // let's record the inputted zone somehow
  913.             if (isAmbigZone) {
  914.                 mom._ambigZone = true;
  915.             }
  916.             else if (isSingleString) {
  917.                 if (mom.utcOffset) {
  918.                     mom.utcOffset(input); // if not a valid zone, will assign UTC
  919.                 }
  920.                 else {
  921.                     mom.zone(input); // for moment-pre-2.9
  922.                 }
  923.             }
  924.         }
  925.     }
  926.  
  927.     mom._fullCalendar = true; // flag for extended functionality
  928.  
  929.     return mom;
  930. }
  931.  
  932.  
  933. // A clone method that works with the flags related to our enhanced functionality.
  934. // In the future, use moment.momentProperties
  935. newMomentProto.clone = function() {
  936.     var mom = oldMomentProto.clone.apply(this, arguments);
  937.  
  938.     // these flags weren't transfered with the clone
  939.     transferAmbigs(this, mom);
  940.     if (this._fullCalendar) {
  941.         mom._fullCalendar = true;
  942.     }
  943.  
  944.     return mom;
  945. };
  946.  
  947.  
  948. // Week Number
  949. // -------------------------------------------------------------------------------------------------
  950.  
  951.  
  952. // Returns the week number, considering the locale's custom week number calcuation
  953. // `weeks` is an alias for `week`
  954. newMomentProto.week = newMomentProto.weeks = function(input) {
  955.     var weekCalc = (this._locale || this._lang) // works pre-moment-2.8
  956.         ._fullCalendar_weekCalc;
  957.  
  958.     if (input == null && typeof weekCalc === 'function') { // custom function only works for getter
  959.         return weekCalc(this);
  960.     }
  961.     else if (weekCalc === 'ISO') {
  962.         return oldMomentProto.isoWeek.apply(this, arguments); // ISO getter/setter
  963.     }
  964.  
  965.     return oldMomentProto.week.apply(this, arguments); // local getter/setter
  966. };
  967.  
  968.  
  969. // Time-of-day
  970. // -------------------------------------------------------------------------------------------------
  971.  
  972. // GETTER
  973. // Returns a Duration with the hours/minutes/seconds/ms values of the moment.
  974. // If the moment has an ambiguous time, a duration of 00:00 will be returned.
  975. //
  976. // SETTER
  977. // You can supply a Duration, a Moment, or a Duration-like argument.
  978. // When setting the time, and the moment has an ambiguous time, it then becomes unambiguous.
  979. newMomentProto.time = function(time) {
  980.  
  981.     // Fallback to the original method (if there is one) if this moment wasn't created via FullCalendar.
  982.     // `time` is a generic enough method name where this precaution is necessary to avoid collisions w/ other plugins.
  983.     if (!this._fullCalendar) {
  984.         return oldMomentProto.time.apply(this, arguments);
  985.     }
  986.  
  987.     if (time == null) { // getter
  988.         return moment.duration({
  989.             hours: this.hours(),
  990.             minutes: this.minutes(),
  991.             seconds: this.seconds(),
  992.             milliseconds: this.milliseconds()
  993.         });
  994.     }
  995.     else { // setter
  996.  
  997.         this._ambigTime = false; // mark that the moment now has a time
  998.  
  999.         if (!moment.isDuration(time) && !moment.isMoment(time)) {
  1000.             time = moment.duration(time);
  1001.         }
  1002.  
  1003.         // The day value should cause overflow (so 24 hours becomes 00:00:00 of next day).
  1004.         // Only for Duration times, not Moment times.
  1005.         var dayHours = 0;
  1006.         if (moment.isDuration(time)) {
  1007.             dayHours = Math.floor(time.asDays()) * 24;
  1008.         }
  1009.  
  1010.         // We need to set the individual fields.
  1011.         // Can't use startOf('day') then add duration. In case of DST at start of day.
  1012.         return this.hours(dayHours + time.hours())
  1013.             .minutes(time.minutes())
  1014.             .seconds(time.seconds())
  1015.             .milliseconds(time.milliseconds());
  1016.     }
  1017. };
  1018.  
  1019. // Converts the moment to UTC, stripping out its time-of-day and timezone offset,
  1020. // but preserving its YMD. A moment with a stripped time will display no time
  1021. // nor timezone offset when .format() is called.
  1022. newMomentProto.stripTime = function() {
  1023.     var a;
  1024.  
  1025.     if (!this._ambigTime) {
  1026.  
  1027.         // get the values before any conversion happens
  1028.         a = this.toArray(); // array of y/m/d/h/m/s/ms
  1029.  
  1030.         // TODO: use keepLocalTime in the future
  1031.         this.utc(); // set the internal UTC flag (will clear the ambig flags)
  1032.         setUTCValues(this, a.slice(0, 3)); // set the year/month/date. time will be zero
  1033.  
  1034.         // Mark the time as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(),
  1035.         // which clears all ambig flags. Same with setUTCValues with moment-timezone.
  1036.         this._ambigTime = true;
  1037.         this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset
  1038.     }
  1039.  
  1040.     return this; // for chaining
  1041. };
  1042.  
  1043. // Returns if the moment has a non-ambiguous time (boolean)
  1044. newMomentProto.hasTime = function() {
  1045.     return !this._ambigTime;
  1046. };
  1047.  
  1048.  
  1049. // Timezone
  1050. // -------------------------------------------------------------------------------------------------
  1051.  
  1052. // Converts the moment to UTC, stripping out its timezone offset, but preserving its
  1053. // YMD and time-of-day. A moment with a stripped timezone offset will display no
  1054. // timezone offset when .format() is called.
  1055. // TODO: look into Moment's keepLocalTime functionality
  1056. newMomentProto.stripZone = function() {
  1057.     var a, wasAmbigTime;
  1058.  
  1059.     if (!this._ambigZone) {
  1060.  
  1061.         // get the values before any conversion happens
  1062.         a = this.toArray(); // array of y/m/d/h/m/s/ms
  1063.         wasAmbigTime = this._ambigTime;
  1064.  
  1065.         this.utc(); // set the internal UTC flag (might clear the ambig flags, depending on Moment internals)
  1066.         setUTCValues(this, a); // will set the year/month/date/hours/minutes/seconds/ms
  1067.  
  1068.         // the above call to .utc()/.utcOffset() unfortunately might clear the ambig flags, so restore
  1069.         this._ambigTime = wasAmbigTime || false;
  1070.  
  1071.         // Mark the zone as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(),
  1072.         // which clears the ambig flags. Same with setUTCValues with moment-timezone.
  1073.         this._ambigZone = true;
  1074.     }
  1075.  
  1076.     return this; // for chaining
  1077. };
  1078.  
  1079. // Returns of the moment has a non-ambiguous timezone offset (boolean)
  1080. newMomentProto.hasZone = function() {
  1081.     return !this._ambigZone;
  1082. };
  1083.  
  1084.  
  1085. // this method implicitly marks a zone
  1086. newMomentProto.local = function() {
  1087.     var a = this.toArray(); // year,month,date,hours,minutes,seconds,ms as an array
  1088.     var wasAmbigZone = this._ambigZone;
  1089.  
  1090.     oldMomentProto.local.apply(this, arguments);
  1091.  
  1092.     // ensure non-ambiguous
  1093.     // this probably already happened via local() -> utcOffset(), but don't rely on Moment's internals
  1094.     this._ambigTime = false;
  1095.     this._ambigZone = false;
  1096.  
  1097.     if (wasAmbigZone) {
  1098.         // If the moment was ambiguously zoned, the date fields were stored as UTC.
  1099.         // We want to preserve these, but in local time.
  1100.         // TODO: look into Moment's keepLocalTime functionality
  1101.         setLocalValues(this, a);
  1102.     }
  1103.  
  1104.     return this; // for chaining
  1105. };
  1106.  
  1107.  
  1108. // implicitly marks a zone
  1109. newMomentProto.utc = function() {
  1110.     oldMomentProto.utc.apply(this, arguments);
  1111.  
  1112.     // ensure non-ambiguous
  1113.     // this probably already happened via utc() -> utcOffset(), but don't rely on Moment's internals
  1114.     this._ambigTime = false;
  1115.     this._ambigZone = false;
  1116.  
  1117.     return this;
  1118. };
  1119.  
  1120.  
  1121. // methods for arbitrarily manipulating timezone offset.
  1122. // should clear time/zone ambiguity when called.
  1123. $.each([
  1124.     'zone', // only in moment-pre-2.9. deprecated afterwards
  1125.     'utcOffset'
  1126. ], function(i, name) {
  1127.     if (oldMomentProto[name]) { // original method exists?
  1128.  
  1129.         // this method implicitly marks a zone (will probably get called upon .utc() and .local())
  1130.         newMomentProto[name] = function(tzo) {
  1131.  
  1132.             if (tzo != null) { // setter
  1133.                 // these assignments needs to happen before the original zone method is called.
  1134.                 // I forget why, something to do with a browser crash.
  1135.                 this._ambigTime = false;
  1136.                 this._ambigZone = false;
  1137.             }
  1138.  
  1139.             return oldMomentProto[name].apply(this, arguments);
  1140.         };
  1141.     }
  1142. });
  1143.  
  1144.  
  1145. // Formatting
  1146. // -------------------------------------------------------------------------------------------------
  1147.  
  1148. newMomentProto.format = function() {
  1149.     if (this._fullCalendar && arguments[0]) { // an enhanced moment? and a format string provided?
  1150.         return formatDate(this, arguments[0]); // our extended formatting
  1151.     }
  1152.     if (this._ambigTime) {
  1153.         return oldMomentFormat(this, 'YYYY-MM-DD');
  1154.     }
  1155.     if (this._ambigZone) {
  1156.         return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
  1157.     }
  1158.     return oldMomentProto.format.apply(this, arguments);
  1159. };
  1160.  
  1161. newMomentProto.toISOString = function() {
  1162.     if (this._ambigTime) {
  1163.         return oldMomentFormat(this, 'YYYY-MM-DD');
  1164.     }
  1165.     if (this._ambigZone) {
  1166.         return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
  1167.     }
  1168.     return oldMomentProto.toISOString.apply(this, arguments);
  1169. };
  1170.  
  1171.  
  1172. // Querying
  1173. // -------------------------------------------------------------------------------------------------
  1174.  
  1175. // Is the moment within the specified range? `end` is exclusive.
  1176. // FYI, this method is not a standard Moment method, so always do our enhanced logic.
  1177. newMomentProto.isWithin = function(start, end) {
  1178.     var a = commonlyAmbiguate([ this, start, end ]);
  1179.     return a[0] >= a[1] && a[0] < a[2];
  1180. };
  1181.  
  1182. // When isSame is called with units, timezone ambiguity is normalized before the comparison happens.
  1183. // If no units specified, the two moments must be identically the same, with matching ambig flags.
  1184. newMomentProto.isSame = function(input, units) {
  1185.     var a;
  1186.  
  1187.     // only do custom logic if this is an enhanced moment
  1188.     if (!this._fullCalendar) {
  1189.         return oldMomentProto.isSame.apply(this, arguments);
  1190.     }
  1191.  
  1192.     if (units) {
  1193.         a = commonlyAmbiguate([ this, input ], true); // normalize timezones but don't erase times
  1194.         return oldMomentProto.isSame.call(a[0], a[1], units);
  1195.     }
  1196.     else {
  1197.         input = fc.moment.parseZone(input); // normalize input
  1198.         return oldMomentProto.isSame.call(this, input) &&
  1199.             Boolean(this._ambigTime) === Boolean(input._ambigTime) &&
  1200.             Boolean(this._ambigZone) === Boolean(input._ambigZone);
  1201.     }
  1202. };
  1203.  
  1204. // Make these query methods work with ambiguous moments
  1205. $.each([
  1206.     'isBefore',
  1207.     'isAfter'
  1208. ], function(i, methodName) {
  1209.     newMomentProto[methodName] = function(input, units) {
  1210.         var a;
  1211.  
  1212.         // only do custom logic if this is an enhanced moment
  1213.         if (!this._fullCalendar) {
  1214.             return oldMomentProto[methodName].apply(this, arguments);
  1215.         }
  1216.  
  1217.         a = commonlyAmbiguate([ this, input ]);
  1218.         return oldMomentProto[methodName].call(a[0], a[1], units);
  1219.     };
  1220. });
  1221.  
  1222.  
  1223. // Misc Internals
  1224. // -------------------------------------------------------------------------------------------------
  1225.  
  1226. // given an array of moment-like inputs, return a parallel array w/ moments similarly ambiguated.
  1227. // for example, of one moment has ambig time, but not others, all moments will have their time stripped.
  1228. // set `preserveTime` to `true` to keep times, but only normalize zone ambiguity.
  1229. // returns the original moments if no modifications are necessary.
  1230. function commonlyAmbiguate(inputs, preserveTime) {
  1231.     var anyAmbigTime = false;
  1232.     var anyAmbigZone = false;
  1233.     var len = inputs.length;
  1234.     var moms = [];
  1235.     var i, mom;
  1236.  
  1237.     // parse inputs into real moments and query their ambig flags
  1238.     for (i = 0; i < len; i++) {
  1239.         mom = inputs[i];
  1240.         if (!moment.isMoment(mom)) {
  1241.             mom = fc.moment.parseZone(mom);
  1242.         }
  1243.         anyAmbigTime = anyAmbigTime || mom._ambigTime;
  1244.         anyAmbigZone = anyAmbigZone || mom._ambigZone;
  1245.         moms.push(mom);
  1246.     }
  1247.  
  1248.     // strip each moment down to lowest common ambiguity
  1249.     // use clones to avoid modifying the original moments
  1250.     for (i = 0; i < len; i++) {
  1251.         mom = moms[i];
  1252.         if (!preserveTime && anyAmbigTime && !mom._ambigTime) {
  1253.             moms[i] = mom.clone().stripTime();
  1254.         }
  1255.         else if (anyAmbigZone && !mom._ambigZone) {
  1256.             moms[i] = mom.clone().stripZone();
  1257.         }
  1258.     }
  1259.  
  1260.     return moms;
  1261. }
  1262.  
  1263. // Transfers all the flags related to ambiguous time/zone from the `src` moment to the `dest` moment
  1264. // TODO: look into moment.momentProperties for this.
  1265. function transferAmbigs(src, dest) {
  1266.     if (src._ambigTime) {
  1267.         dest._ambigTime = true;
  1268.     }
  1269.     else if (dest._ambigTime) {
  1270.         dest._ambigTime = false;
  1271.     }
  1272.  
  1273.     if (src._ambigZone) {
  1274.         dest._ambigZone = true;
  1275.     }
  1276.     else if (dest._ambigZone) {
  1277.         dest._ambigZone = false;
  1278.     }
  1279. }
  1280.  
  1281.  
  1282. // Sets the year/month/date/etc values of the moment from the given array.
  1283. // Inefficient because it calls each individual setter.
  1284. function setMomentValues(mom, a) {
  1285.     mom.year(a[0] || 0)
  1286.         .month(a[1] || 0)
  1287.         .date(a[2] || 0)
  1288.         .hours(a[3] || 0)
  1289.         .minutes(a[4] || 0)
  1290.         .seconds(a[5] || 0)
  1291.         .milliseconds(a[6] || 0);
  1292. }
  1293.  
  1294. // Can we set the moment's internal date directly?
  1295. allowValueOptimization = '_d' in moment() && 'updateOffset' in moment;
  1296.  
  1297. // Utility function. Accepts a moment and an array of the UTC year/month/date/etc values to set.
  1298. // Assumes the given moment is already in UTC mode.
  1299. setUTCValues = allowValueOptimization ? function(mom, a) {
  1300.     // simlate what moment's accessors do
  1301.     mom._d.setTime(Date.UTC.apply(Date, a));
  1302.     moment.updateOffset(mom, false); // keepTime=false
  1303. } : setMomentValues;
  1304.  
  1305. // Utility function. Accepts a moment and an array of the local year/month/date/etc values to set.
  1306. // Assumes the given moment is already in local mode.
  1307. setLocalValues = allowValueOptimization ? function(mom, a) {
  1308.     // simlate what moment's accessors do
  1309.     mom._d.setTime(+new Date( // FYI, there is now way to apply an array of args to a constructor
  1310.         a[0] || 0,
  1311.         a[1] || 0,
  1312.         a[2] || 0,
  1313.         a[3] || 0,
  1314.         a[4] || 0,
  1315.         a[5] || 0,
  1316.         a[6] || 0
  1317.     ));
  1318.     moment.updateOffset(mom, false); // keepTime=false
  1319. } : setMomentValues;
  1320.  
  1321. ;;
  1322.  
  1323. // Single Date Formatting
  1324. // -------------------------------------------------------------------------------------------------
  1325.  
  1326.  
  1327. // call this if you want Moment's original format method to be used
  1328. function oldMomentFormat(mom, formatStr) {
  1329.     return oldMomentProto.format.call(mom, formatStr); // oldMomentProto defined in moment-ext.js
  1330. }
  1331.  
  1332.  
  1333. // Formats `date` with a Moment formatting string, but allow our non-zero areas and
  1334. // additional token.
  1335. function formatDate(date, formatStr) {
  1336.     return formatDateWithChunks(date, getFormatStringChunks(formatStr));
  1337. }
  1338.  
  1339.  
  1340. function formatDateWithChunks(date, chunks) {
  1341.     var s = '';
  1342.     var i;
  1343.  
  1344.     for (i=0; i<chunks.length; i++) {
  1345.         s += formatDateWithChunk(date, chunks[i]);
  1346.     }
  1347.  
  1348.     return s;
  1349. }
  1350.  
  1351.  
  1352. // addition formatting tokens we want recognized
  1353. var tokenOverrides = {
  1354.     t: function(date) { // "a" or "p"
  1355.         return oldMomentFormat(date, 'a').charAt(0);
  1356.     },
  1357.     T: function(date) { // "A" or "P"
  1358.         return oldMomentFormat(date, 'A').charAt(0);
  1359.     }
  1360. };
  1361.  
  1362.  
  1363. function formatDateWithChunk(date, chunk) {
  1364.     var token;
  1365.     var maybeStr;
  1366.  
  1367.     if (typeof chunk === 'string') { // a literal string
  1368.         return chunk;
  1369.     }
  1370.     else if ((token = chunk.token)) { // a token, like "YYYY"
  1371.         if (tokenOverrides[token]) {
  1372.             return tokenOverrides[token](date); // use our custom token
  1373.         }
  1374.         return oldMomentFormat(date, token);
  1375.     }
  1376.     else if (chunk.maybe) { // a grouping of other chunks that must be non-zero
  1377.         maybeStr = formatDateWithChunks(date, chunk.maybe);
  1378.         if (maybeStr.match(/[1-9]/)) {
  1379.             return maybeStr;
  1380.         }
  1381.     }
  1382.  
  1383.     return '';
  1384. }
  1385.  
  1386.  
  1387. // Date Range Formatting
  1388. // -------------------------------------------------------------------------------------------------
  1389. // TODO: make it work with timezone offset
  1390.  
  1391. // Using a formatting string meant for a single date, generate a range string, like
  1392. // "Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ.
  1393. // If the dates are the same as far as the format string is concerned, just return a single
  1394. // rendering of one date, without any separator.
  1395. function formatRange(date1, date2, formatStr, separator, isRTL) {
  1396.     var localeData;
  1397.  
  1398.     date1 = fc.moment.parseZone(date1);
  1399.     date2 = fc.moment.parseZone(date2);
  1400.  
  1401.     localeData = (date1.localeData || date1.lang).call(date1); // works with moment-pre-2.8
  1402.  
  1403.     // Expand localized format strings, like "LL" -> "MMMM D YYYY"
  1404.     formatStr = localeData.longDateFormat(formatStr) || formatStr;
  1405.     // BTW, this is not important for `formatDate` because it is impossible to put custom tokens
  1406.     // or non-zero areas in Moment's localized format strings.
  1407.  
  1408.     separator = separator || ' - ';
  1409.  
  1410.     return formatRangeWithChunks(
  1411.         date1,
  1412.         date2,
  1413.         getFormatStringChunks(formatStr),
  1414.         separator,
  1415.         isRTL
  1416.     );
  1417. }
  1418. fc.formatRange = formatRange; // expose
  1419.  
  1420.  
  1421. function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) {
  1422.     var chunkStr; // the rendering of the chunk
  1423.     var leftI;
  1424.     var leftStr = '';
  1425.     var rightI;
  1426.     var rightStr = '';
  1427.     var middleI;
  1428.     var middleStr1 = '';
  1429.     var middleStr2 = '';
  1430.     var middleStr = '';
  1431.  
  1432.     // Start at the leftmost side of the formatting string and continue until you hit a token
  1433.     // that is not the same between dates.
  1434.     for (leftI=0; leftI<chunks.length; leftI++) {
  1435.         chunkStr = formatSimilarChunk(date1, date2, chunks[leftI]);
  1436.         if (chunkStr === false) {
  1437.             break;
  1438.         }
  1439.         leftStr += chunkStr;
  1440.     }
  1441.  
  1442.     // Similarly, start at the rightmost side of the formatting string and move left
  1443.     for (rightI=chunks.length-1; rightI>leftI; rightI--) {
  1444.         chunkStr = formatSimilarChunk(date1, date2, chunks[rightI]);
  1445.         if (chunkStr === false) {
  1446.             break;
  1447.         }
  1448.         rightStr = chunkStr + rightStr;
  1449.     }
  1450.  
  1451.     // The area in the middle is different for both of the dates.
  1452.     // Collect them distinctly so we can jam them together later.
  1453.     for (middleI=leftI; middleI<=rightI; middleI++) {
  1454.         middleStr1 += formatDateWithChunk(date1, chunks[middleI]);
  1455.         middleStr2 += formatDateWithChunk(date2, chunks[middleI]);
  1456.     }
  1457.  
  1458.     if (middleStr1 || middleStr2) {
  1459.         if (isRTL) {
  1460.             middleStr = middleStr2 + separator + middleStr1;
  1461.         }
  1462.         else {
  1463.             middleStr = middleStr1 + separator + middleStr2;
  1464.         }
  1465.     }
  1466.  
  1467.     return leftStr + middleStr + rightStr;
  1468. }
  1469.  
  1470.  
  1471. var similarUnitMap = {
  1472.     Y: 'year',
  1473.     M: 'month',
  1474.     D: 'day', // day of month
  1475.     d: 'day', // day of week
  1476.     // prevents a separator between anything time-related...
  1477.     A: 'second', // AM/PM
  1478.     a: 'second', // am/pm
  1479.     T: 'second', // A/P
  1480.     t: 'second', // a/p
  1481.     H: 'second', // hour (24)
  1482.     h: 'second', // hour (12)
  1483.     m: 'second', // minute
  1484.     s: 'second' // second
  1485. };
  1486. // TODO: week maybe?
  1487.  
  1488.  
  1489. // Given a formatting chunk, and given that both dates are similar in the regard the
  1490. // formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`.
  1491. function formatSimilarChunk(date1, date2, chunk) {
  1492.     var token;
  1493.     var unit;
  1494.  
  1495.     if (typeof chunk === 'string') { // a literal string
  1496.         return chunk;
  1497.     }
  1498.     else if ((token = chunk.token)) {
  1499.         unit = similarUnitMap[token.charAt(0)];
  1500.         // are the dates the same for this unit of measurement?
  1501.         if (unit && date1.isSame(date2, unit)) {
  1502.             return oldMomentFormat(date1, token); // would be the same if we used `date2`
  1503.             // BTW, don't support custom tokens
  1504.         }
  1505.     }
  1506.  
  1507.     return false; // the chunk is NOT the same for the two dates
  1508.     // BTW, don't support splitting on non-zero areas
  1509. }
  1510.  
  1511.  
  1512. // Chunking Utils
  1513. // -------------------------------------------------------------------------------------------------
  1514.  
  1515.  
  1516. var formatStringChunkCache = {};
  1517.  
  1518.  
  1519. function getFormatStringChunks(formatStr) {
  1520.     if (formatStr in formatStringChunkCache) {
  1521.         return formatStringChunkCache[formatStr];
  1522.     }
  1523.     return (formatStringChunkCache[formatStr] = chunkFormatString(formatStr));
  1524. }
  1525.  
  1526.  
  1527. // Break the formatting string into an array of chunks
  1528. function chunkFormatString(formatStr) {
  1529.     var chunks = [];
  1530.     var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g; // TODO: more descrimination
  1531.     var match;
  1532.  
  1533.     while ((match = chunker.exec(formatStr))) {
  1534.         if (match[1]) { // a literal string inside [ ... ]
  1535.             chunks.push(match[1]);
  1536.         }
  1537.         else if (match[2]) { // non-zero formatting inside ( ... )
  1538.             chunks.push({ maybe: chunkFormatString(match[2]) });
  1539.         }
  1540.         else if (match[3]) { // a formatting token
  1541.             chunks.push({ token: match[3] });
  1542.         }
  1543.         else if (match[5]) { // an unenclosed literal string
  1544.             chunks.push(match[5]);
  1545.         }
  1546.     }
  1547.  
  1548.     return chunks;
  1549. }
  1550.  
  1551. ;;
  1552.  
  1553. fc.Class = Class; // export
  1554.  
  1555. // class that all other classes will inherit from
  1556. function Class() { }
  1557.  
  1558. // called upon a class to create a subclass
  1559. Class.extend = function(members) {
  1560.     var superClass = this;
  1561.     var subClass;
  1562.  
  1563.     members = members || {};
  1564.  
  1565.     // ensure a constructor for the subclass, forwarding all arguments to the super-constructor if it doesn't exist
  1566.     if (hasOwnProp(members, 'constructor')) {
  1567.         subClass = members.constructor;
  1568.     }
  1569.     if (typeof subClass !== 'function') {
  1570.         subClass = members.constructor = function() {
  1571.             superClass.apply(this, arguments);
  1572.         };
  1573.     }
  1574.  
  1575.     // build the base prototype for the subclass, which is an new object chained to the superclass's prototype
  1576.     subClass.prototype = createObject(superClass.prototype);
  1577.  
  1578.     // copy each member variable/method onto the the subclass's prototype
  1579.     copyOwnProps(members, subClass.prototype);
  1580.     copyNativeMethods(members, subClass.prototype); // hack for IE8
  1581.  
  1582.     // copy over all class variables/methods to the subclass, such as `extend` and `mixin`
  1583.     copyOwnProps(superClass, subClass);
  1584.  
  1585.     return subClass;
  1586. };
  1587.  
  1588. // adds new member variables/methods to the class's prototype.
  1589. // can be called with another class, or a plain object hash containing new members.
  1590. Class.mixin = function(members) {
  1591.     copyOwnProps(members.prototype || members, this.prototype); // TODO: copyNativeMethods?
  1592. };
  1593. ;;
  1594.  
  1595. /* A rectangular panel that is absolutely positioned over other content
  1596. ------------------------------------------------------------------------------------------------------------------------
  1597. Options:
  1598.     - className (string)
  1599.     - content (HTML string or jQuery element set)
  1600.     - parentEl
  1601.     - top
  1602.     - left
  1603.     - right (the x coord of where the right edge should be. not a "CSS" right)
  1604.     - autoHide (boolean)
  1605.     - show (callback)
  1606.     - hide (callback)
  1607. */
  1608.  
  1609. var Popover = Class.extend({
  1610.  
  1611.     isHidden: true,
  1612.     options: null,
  1613.     el: null, // the container element for the popover. generated by this object
  1614.     documentMousedownProxy: null, // document mousedown handler bound to `this`
  1615.     margin: 10, // the space required between the popover and the edges of the scroll container
  1616.  
  1617.  
  1618.     constructor: function(options) {
  1619.         this.options = options || {};
  1620.     },
  1621.  
  1622.  
  1623.     // Shows the popover on the specified position. Renders it if not already
  1624.     show: function() {
  1625.         if (this.isHidden) {
  1626.             if (!this.el) {
  1627.                 this.render();
  1628.             }
  1629.             this.el.show();
  1630.             this.position();
  1631.             this.isHidden = false;
  1632.             this.trigger('show');
  1633.         }
  1634.     },
  1635.  
  1636.  
  1637.     // Hides the popover, through CSS, but does not remove it from the DOM
  1638.     hide: function() {
  1639.         if (!this.isHidden) {
  1640.             this.el.hide();
  1641.             this.isHidden = true;
  1642.             this.trigger('hide');
  1643.         }
  1644.     },
  1645.  
  1646.  
  1647.     // Creates `this.el` and renders content inside of it
  1648.     render: function() {
  1649.         var _this = this;
  1650.         var options = this.options;
  1651.  
  1652.         this.el = $('<div class="fc-popover"/>')
  1653.             .addClass(options.className || '')
  1654.             .css({
  1655.                 // position initially to the top left to avoid creating scrollbars
  1656.                 top: 0,
  1657.                 left: 0
  1658.             })
  1659.             .append(options.content)
  1660.             .appendTo(options.parentEl);
  1661.  
  1662.         // when a click happens on anything inside with a 'fc-close' className, hide the popover
  1663.         this.el.on('click', '.fc-close', function() {
  1664.             _this.hide();
  1665.         });
  1666.  
  1667.         if (options.autoHide) {
  1668.             $(document).on('mousedown', this.documentMousedownProxy = proxy(this, 'documentMousedown'));
  1669.         }
  1670.     },
  1671.  
  1672.  
  1673.     // Triggered when the user clicks *anywhere* in the document, for the autoHide feature
  1674.     documentMousedown: function(ev) {
  1675.         // only hide the popover if the click happened outside the popover
  1676.         if (this.el && !$(ev.target).closest(this.el).length) {
  1677.             this.hide();
  1678.         }
  1679.     },
  1680.  
  1681.  
  1682.     // Hides and unregisters any handlers
  1683.     removeElement: function() {
  1684.         this.hide();
  1685.  
  1686.         if (this.el) {
  1687.             this.el.remove();
  1688.             this.el = null;
  1689.         }
  1690.  
  1691.         $(document).off('mousedown', this.documentMousedownProxy);
  1692.     },
  1693.  
  1694.  
  1695.     // Positions the popover optimally, using the top/left/right options
  1696.     position: function() {
  1697.         var options = this.options;
  1698.         var origin = this.el.offsetParent().offset();
  1699.         var width = this.el.outerWidth();
  1700.         var height = this.el.outerHeight();
  1701.         var windowEl = $(window);
  1702.         var viewportEl = getScrollParent(this.el);
  1703.         var viewportTop;
  1704.         var viewportLeft;
  1705.         var viewportOffset;
  1706.         var top; // the "position" (not "offset") values for the popover
  1707.         var left; //
  1708.  
  1709.         // compute top and left
  1710.         top = options.top || 0;
  1711.         if (options.left !== undefined) {
  1712.             left = options.left;
  1713.         }
  1714.         else if (options.right !== undefined) {
  1715.             left = options.right - width; // derive the left value from the right value
  1716.         }
  1717.         else {
  1718.             left = 0;
  1719.         }
  1720.  
  1721.         if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result
  1722.             viewportEl = windowEl;
  1723.             viewportTop = 0; // the window is always at the top left
  1724.             viewportLeft = 0; // (and .offset() won't work if called here)
  1725.         }
  1726.         else {
  1727.             viewportOffset = viewportEl.offset();
  1728.             viewportTop = viewportOffset.top;
  1729.             viewportLeft = viewportOffset.left;
  1730.         }
  1731.  
  1732.         // if the window is scrolled, it causes the visible area to be further down
  1733.         viewportTop += windowEl.scrollTop();
  1734.         viewportLeft += windowEl.scrollLeft();
  1735.  
  1736.         // constrain to the view port. if constrained by two edges, give precedence to top/left
  1737.         if (options.viewportConstrain !== false) {
  1738.             top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin);
  1739.             top = Math.max(top, viewportTop + this.margin);
  1740.             left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin);
  1741.             left = Math.max(left, viewportLeft + this.margin);
  1742.         }
  1743.  
  1744.         this.el.css({
  1745.             top: top - origin.top,
  1746.             left: left - origin.left
  1747.         });
  1748.     },
  1749.  
  1750.  
  1751.     // Triggers a callback. Calls a function in the option hash of the same name.
  1752.     // Arguments beyond the first `name` are forwarded on.
  1753.     // TODO: better code reuse for this. Repeat code
  1754.     trigger: function(name) {
  1755.         if (this.options[name]) {
  1756.             this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
  1757.         }
  1758.     }
  1759.  
  1760. });
  1761.  
  1762. ;;
  1763.  
  1764. /* A "coordinate map" converts pixel coordinates into an associated cell, which has an associated date
  1765. ------------------------------------------------------------------------------------------------------------------------
  1766. Common interface:
  1767.  
  1768.     CoordMap.prototype = {
  1769.         build: function() {},
  1770.         getCell: function(x, y) {}
  1771.     };
  1772.  
  1773. */
  1774.  
  1775. /* Coordinate map for a grid component
  1776. ----------------------------------------------------------------------------------------------------------------------*/
  1777.  
  1778. var GridCoordMap = Class.extend({
  1779.  
  1780.     grid: null, // reference to the Grid
  1781.     rowCoords: null, // array of {top,bottom} objects
  1782.     colCoords: null, // array of {left,right} objects
  1783.  
  1784.     containerEl: null, // container element that all coordinates are constrained to. optionally assigned
  1785.     bounds: null,
  1786.  
  1787.  
  1788.     constructor: function(grid) {
  1789.         this.grid = grid;
  1790.     },
  1791.  
  1792.  
  1793.     // Queries the grid for the coordinates of all the cells
  1794.     build: function() {
  1795.         this.grid.build();
  1796.         this.rowCoords = this.grid.computeRowCoords();
  1797.         this.colCoords = this.grid.computeColCoords();
  1798.         this.computeBounds();
  1799.     },
  1800.  
  1801.  
  1802.     // Clears the coordinates data to free up memory
  1803.     clear: function() {
  1804.         this.grid.clear();
  1805.         this.rowCoords = null;
  1806.         this.colCoords = null;
  1807.     },
  1808.  
  1809.  
  1810.     // Given a coordinate of the document, gets the associated cell. If no cell is underneath, returns null
  1811.     getCell: function(x, y) {
  1812.         var rowCoords = this.rowCoords;
  1813.         var rowCnt = rowCoords.length;
  1814.         var colCoords = this.colCoords;
  1815.         var colCnt = colCoords.length;
  1816.         var hitRow = null;
  1817.         var hitCol = null;
  1818.         var i, coords;
  1819.         var cell;
  1820.  
  1821.         if (this.inBounds(x, y)) {
  1822.  
  1823.             for (i = 0; i < rowCnt; i++) {
  1824.                 coords = rowCoords[i];
  1825.                 if (y >= coords.top && y < coords.bottom) {
  1826.                     hitRow = i;
  1827.                     break;
  1828.                 }
  1829.             }
  1830.  
  1831.             for (i = 0; i < colCnt; i++) {
  1832.                 coords = colCoords[i];
  1833.                 if (x >= coords.left && x < coords.right) {
  1834.                     hitCol = i;
  1835.                     break;
  1836.                 }
  1837.             }
  1838.  
  1839.             if (hitRow !== null && hitCol !== null) {
  1840.  
  1841.                 cell = this.grid.getCell(hitRow, hitCol); // expected to return a fresh object we can modify
  1842.                 cell.grid = this.grid; // for CellDragListener's isCellsEqual. dragging between grids
  1843.  
  1844.                 // make the coordinates available on the cell object
  1845.                 $.extend(cell, rowCoords[hitRow], colCoords[hitCol]);
  1846.  
  1847.                 return cell;
  1848.             }
  1849.         }
  1850.  
  1851.         return null;
  1852.     },
  1853.  
  1854.  
  1855.     // If there is a containerEl, compute the bounds into min/max values
  1856.     computeBounds: function() {
  1857.         this.bounds = this.containerEl ?
  1858.             getClientRect(this.containerEl) : // area within scrollbars
  1859.             null;
  1860.     },
  1861.  
  1862.  
  1863.     // Determines if the given coordinates are in bounds. If no `containerEl`, always true
  1864.     inBounds: function(x, y) {
  1865.         var bounds = this.bounds;
  1866.  
  1867.         if (bounds) {
  1868.             return x >= bounds.left && x < bounds.right && y >= bounds.top && y < bounds.bottom;
  1869.         }
  1870.  
  1871.         return true;
  1872.     }
  1873.  
  1874. });
  1875.  
  1876.  
  1877. /* Coordinate map that is a combination of multiple other coordinate maps
  1878. ----------------------------------------------------------------------------------------------------------------------*/
  1879.  
  1880. var ComboCoordMap = Class.extend({
  1881.  
  1882.     coordMaps: null, // an array of CoordMaps
  1883.  
  1884.  
  1885.     constructor: function(coordMaps) {
  1886.         this.coordMaps = coordMaps;
  1887.     },
  1888.  
  1889.  
  1890.     // Builds all coordMaps
  1891.     build: function() {
  1892.         var coordMaps = this.coordMaps;
  1893.         var i;
  1894.  
  1895.         for (i = 0; i < coordMaps.length; i++) {
  1896.             coordMaps[i].build();
  1897.         }
  1898.     },
  1899.  
  1900.  
  1901.     // Queries all coordMaps for the cell underneath the given coordinates, returning the first result
  1902.     getCell: function(x, y) {
  1903.         var coordMaps = this.coordMaps;
  1904.         var cell = null;
  1905.         var i;
  1906.  
  1907.         for (i = 0; i < coordMaps.length && !cell; i++) {
  1908.             cell = coordMaps[i].getCell(x, y);
  1909.         }
  1910.  
  1911.         return cell;
  1912.     },
  1913.  
  1914.  
  1915.     // Clears all coordMaps
  1916.     clear: function() {
  1917.         var coordMaps = this.coordMaps;
  1918.         var i;
  1919.  
  1920.         for (i = 0; i < coordMaps.length; i++) {
  1921.             coordMaps[i].clear();
  1922.         }
  1923.     }
  1924.  
  1925. });
  1926.  
  1927. ;;
  1928.  
  1929. /* Tracks a drag's mouse movement, firing various handlers
  1930. ----------------------------------------------------------------------------------------------------------------------*/
  1931.  
  1932. var DragListener = fc.DragListener = Class.extend({
  1933.  
  1934.     options: null,
  1935.  
  1936.     isListening: false,
  1937.     isDragging: false,
  1938.  
  1939.     // coordinates of the initial mousedown
  1940.     originX: null,
  1941.     originY: null,
  1942.  
  1943.     // handler attached to the document, bound to the DragListener's `this`
  1944.     mousemoveProxy: null,
  1945.     mouseupProxy: null,
  1946.  
  1947.     // for IE8 bug-fighting behavior, for now
  1948.     subjectEl: null, // the element being draged. optional
  1949.     subjectHref: null,
  1950.  
  1951.     scrollEl: null,
  1952.     scrollBounds: null, // { top, bottom, left, right }
  1953.     scrollTopVel: null, // pixels per second
  1954.     scrollLeftVel: null, // pixels per second
  1955.     scrollIntervalId: null, // ID of setTimeout for scrolling animation loop
  1956.     scrollHandlerProxy: null, // this-scoped function for handling when scrollEl is scrolled
  1957.  
  1958.     scrollSensitivity: 30, // pixels from edge for scrolling to start
  1959.     scrollSpeed: 200, // pixels per second, at maximum speed
  1960.     scrollIntervalMs: 50, // millisecond wait between scroll increment
  1961.  
  1962.  
  1963.     constructor: function(options) {
  1964.         options = options || {};
  1965.         this.options = options;
  1966.         this.subjectEl = options.subjectEl;
  1967.     },
  1968.  
  1969.  
  1970.     // Call this when the user does a mousedown. Will probably lead to startListening
  1971.     mousedown: function(ev) {
  1972.         if (isPrimaryMouseButton(ev)) {
  1973.  
  1974.             ev.preventDefault(); // prevents native selection in most browsers
  1975.  
  1976.             this.startListening(ev);
  1977.  
  1978.             // start the drag immediately if there is no minimum distance for a drag start
  1979.             if (!this.options.distance) {
  1980.                 this.startDrag(ev);
  1981.             }
  1982.         }
  1983.     },
  1984.  
  1985.  
  1986.     // Call this to start tracking mouse movements
  1987.     startListening: function(ev) {
  1988.         var scrollParent;
  1989.  
  1990.         if (!this.isListening) {
  1991.  
  1992.             // grab scroll container and attach handler
  1993.             if (ev && this.options.scroll) {
  1994.                 scrollParent = getScrollParent($(ev.target));
  1995.                 if (!scrollParent.is(window) && !scrollParent.is(document)) {
  1996.                     this.scrollEl = scrollParent;
  1997.  
  1998.                     // scope to `this`, and use `debounce` to make sure rapid calls don't happen
  1999.                     this.scrollHandlerProxy = debounce(proxy(this, 'scrollHandler'), 100);
  2000.                     this.scrollEl.on('scroll', this.scrollHandlerProxy);
  2001.                 }
  2002.             }
  2003.  
  2004.             $(document)
  2005.                 .on('mousemove', this.mousemoveProxy = proxy(this, 'mousemove'))
  2006.                 .on('mouseup', this.mouseupProxy = proxy(this, 'mouseup'))
  2007.                 .on('selectstart', this.preventDefault); // prevents native selection in IE<=8
  2008.  
  2009.             if (ev) {
  2010.                 this.originX = ev.pageX;
  2011.                 this.originY = ev.pageY;
  2012.             }
  2013.             else {
  2014.                 // if no starting information was given, origin will be the topleft corner of the screen.
  2015.                 // if so, dx/dy in the future will be the absolute coordinates.
  2016.                 this.originX = 0;
  2017.                 this.originY = 0;
  2018.             }
  2019.  
  2020.             this.isListening = true;
  2021.             this.listenStart(ev);
  2022.         }
  2023.     },
  2024.  
  2025.  
  2026.     // Called when drag listening has started (but a real drag has not necessarily began)
  2027.     listenStart: function(ev) {
  2028.         this.trigger('listenStart', ev);
  2029.     },
  2030.  
  2031.  
  2032.     // Called when the user moves the mouse
  2033.     mousemove: function(ev) {
  2034.         var dx = ev.pageX - this.originX;
  2035.         var dy = ev.pageY - this.originY;
  2036.         var minDistance;
  2037.         var distanceSq; // current distance from the origin, squared
  2038.  
  2039.         if (!this.isDragging) { // if not already dragging...
  2040.             // then start the drag if the minimum distance criteria is met
  2041.             minDistance = this.options.distance || 1;
  2042.             distanceSq = dx * dx + dy * dy;
  2043.             if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem
  2044.                 this.startDrag(ev);
  2045.             }
  2046.         }
  2047.  
  2048.         if (this.isDragging) {
  2049.             this.drag(dx, dy, ev); // report a drag, even if this mousemove initiated the drag
  2050.         }
  2051.     },
  2052.  
  2053.  
  2054.     // Call this to initiate a legitimate drag.
  2055.     // This function is called internally from this class, but can also be called explicitly from outside
  2056.     startDrag: function(ev) {
  2057.  
  2058.         if (!this.isListening) { // startDrag must have manually initiated
  2059.             this.startListening();
  2060.         }
  2061.  
  2062.         if (!this.isDragging) {
  2063.             this.isDragging = true;
  2064.             this.dragStart(ev);
  2065.         }
  2066.     },
  2067.  
  2068.  
  2069.     // Called when the actual drag has started (went beyond minDistance)
  2070.     dragStart: function(ev) {
  2071.         var subjectEl = this.subjectEl;
  2072.  
  2073.         this.trigger('dragStart', ev);
  2074.  
  2075.         // remove a mousedown'd <a>'s href so it is not visited (IE8 bug)
  2076.         if ((this.subjectHref = subjectEl ? subjectEl.attr('href') : null)) {
  2077.             subjectEl.removeAttr('href');
  2078.         }
  2079.     },
  2080.  
  2081.  
  2082.     // Called while the mouse is being moved and when we know a legitimate drag is taking place
  2083.     drag: function(dx, dy, ev) {
  2084.         this.trigger('drag', dx, dy, ev);
  2085.         this.updateScroll(ev); // will possibly cause scrolling
  2086.     },
  2087.  
  2088.  
  2089.     // Called when the user does a mouseup
  2090.     mouseup: function(ev) {
  2091.         this.stopListening(ev);
  2092.     },
  2093.  
  2094.  
  2095.     // Called when the drag is over. Will not cause listening to stop however.
  2096.     // A concluding 'cellOut' event will NOT be triggered.
  2097.     stopDrag: function(ev) {
  2098.         if (this.isDragging) {
  2099.             this.stopScrolling();
  2100.             this.dragStop(ev);
  2101.             this.isDragging = false;
  2102.         }
  2103.     },
  2104.  
  2105.  
  2106.     // Called when dragging has been stopped
  2107.     dragStop: function(ev) {
  2108.         var _this = this;
  2109.  
  2110.         this.trigger('dragStop', ev);
  2111.  
  2112.         // restore a mousedown'd <a>'s href (for IE8 bug)
  2113.         setTimeout(function() { // must be outside of the click's execution
  2114.             if (_this.subjectHref) {
  2115.                 _this.subjectEl.attr('href', _this.subjectHref);
  2116.             }
  2117.         }, 0);
  2118.     },
  2119.  
  2120.  
  2121.     // Call this to stop listening to the user's mouse events
  2122.     stopListening: function(ev) {
  2123.         this.stopDrag(ev); // if there's a current drag, kill it
  2124.  
  2125.         if (this.isListening) {
  2126.  
  2127.             // remove the scroll handler if there is a scrollEl
  2128.             if (this.scrollEl) {
  2129.                 this.scrollEl.off('scroll', this.scrollHandlerProxy);
  2130.                 this.scrollHandlerProxy = null;
  2131.             }
  2132.  
  2133.             $(document)
  2134.                 .off('mousemove', this.mousemoveProxy)
  2135.                 .off('mouseup', this.mouseupProxy)
  2136.                 .off('selectstart', this.preventDefault);
  2137.  
  2138.             this.mousemoveProxy = null;
  2139.             this.mouseupProxy = null;
  2140.  
  2141.             this.isListening = false;
  2142.             this.listenStop(ev);
  2143.         }
  2144.     },
  2145.  
  2146.  
  2147.     // Called when drag listening has stopped
  2148.     listenStop: function(ev) {
  2149.         this.trigger('listenStop', ev);
  2150.     },
  2151.  
  2152.  
  2153.     // Triggers a callback. Calls a function in the option hash of the same name.
  2154.     // Arguments beyond the first `name` are forwarded on.
  2155.     trigger: function(name) {
  2156.         if (this.options[name]) {
  2157.             this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
  2158.         }
  2159.     },
  2160.  
  2161.  
  2162.     // Stops a given mouse event from doing it's native browser action. In our case, text selection.
  2163.     preventDefault: function(ev) {
  2164.         ev.preventDefault();
  2165.     },
  2166.  
  2167.  
  2168.     /* Scrolling
  2169.     ------------------------------------------------------------------------------------------------------------------*/
  2170.  
  2171.  
  2172.     // Computes and stores the bounding rectangle of scrollEl
  2173.     computeScrollBounds: function() {
  2174.         var el = this.scrollEl;
  2175.  
  2176.         this.scrollBounds = el ? getOuterRect(el) : null;
  2177.             // TODO: use getClientRect in future. but prevents auto scrolling when on top of scrollbars
  2178.     },
  2179.  
  2180.  
  2181.     // Called when the dragging is in progress and scrolling should be updated
  2182.     updateScroll: function(ev) {
  2183.         var sensitivity = this.scrollSensitivity;
  2184.         var bounds = this.scrollBounds;
  2185.         var topCloseness, bottomCloseness;
  2186.         var leftCloseness, rightCloseness;
  2187.         var topVel = 0;
  2188.         var leftVel = 0;
  2189.  
  2190.         if (bounds) { // only scroll if scrollEl exists
  2191.  
  2192.             // compute closeness to edges. valid range is from 0.0 - 1.0
  2193.             topCloseness = (sensitivity - (ev.pageY - bounds.top)) / sensitivity;
  2194.             bottomCloseness = (sensitivity - (bounds.bottom - ev.pageY)) / sensitivity;
  2195.             leftCloseness = (sensitivity - (ev.pageX - bounds.left)) / sensitivity;
  2196.             rightCloseness = (sensitivity - (bounds.right - ev.pageX)) / sensitivity;
  2197.  
  2198.             // translate vertical closeness into velocity.
  2199.             // mouse must be completely in bounds for velocity to happen.
  2200.             if (topCloseness >= 0 && topCloseness <= 1) {
  2201.                 topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up
  2202.             }
  2203.             else if (bottomCloseness >= 0 && bottomCloseness <= 1) {
  2204.                 topVel = bottomCloseness * this.scrollSpeed;
  2205.             }
  2206.  
  2207.             // translate horizontal closeness into velocity
  2208.             if (leftCloseness >= 0 && leftCloseness <= 1) {
  2209.                 leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left
  2210.             }
  2211.             else if (rightCloseness >= 0 && rightCloseness <= 1) {
  2212.                 leftVel = rightCloseness * this.scrollSpeed;
  2213.             }
  2214.         }
  2215.  
  2216.         this.setScrollVel(topVel, leftVel);
  2217.     },
  2218.  
  2219.  
  2220.     // Sets the speed-of-scrolling for the scrollEl
  2221.     setScrollVel: function(topVel, leftVel) {
  2222.  
  2223.         this.scrollTopVel = topVel;
  2224.         this.scrollLeftVel = leftVel;
  2225.  
  2226.         this.constrainScrollVel(); // massages into realistic values
  2227.  
  2228.         // if there is non-zero velocity, and an animation loop hasn't already started, then START
  2229.         if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) {
  2230.             this.scrollIntervalId = setInterval(
  2231.                 proxy(this, 'scrollIntervalFunc'), // scope to `this`
  2232.                 this.scrollIntervalMs
  2233.             );
  2234.         }
  2235.     },
  2236.  
  2237.  
  2238.     // Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way
  2239.     constrainScrollVel: function() {
  2240.         var el = this.scrollEl;
  2241.  
  2242.         if (this.scrollTopVel < 0) { // scrolling up?
  2243.             if (el.scrollTop() <= 0) { // already scrolled all the way up?
  2244.                 this.scrollTopVel = 0;
  2245.             }
  2246.         }
  2247.         else if (this.scrollTopVel > 0) { // scrolling down?
  2248.             if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) { // already scrolled all the way down?
  2249.                 this.scrollTopVel = 0;
  2250.             }
  2251.         }
  2252.  
  2253.         if (this.scrollLeftVel < 0) { // scrolling left?
  2254.             if (el.scrollLeft() <= 0) { // already scrolled all the left?
  2255.                 this.scrollLeftVel = 0;
  2256.             }
  2257.         }
  2258.         else if (this.scrollLeftVel > 0) { // scrolling right?
  2259.             if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) { // already scrolled all the way right?
  2260.                 this.scrollLeftVel = 0;
  2261.             }
  2262.         }
  2263.     },
  2264.  
  2265.  
  2266.     // This function gets called during every iteration of the scrolling animation loop
  2267.     scrollIntervalFunc: function() {
  2268.         var el = this.scrollEl;
  2269.         var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by
  2270.  
  2271.         // change the value of scrollEl's scroll
  2272.         if (this.scrollTopVel) {
  2273.             el.scrollTop(el.scrollTop() + this.scrollTopVel * frac);
  2274.         }
  2275.         if (this.scrollLeftVel) {
  2276.             el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac);
  2277.         }
  2278.  
  2279.         this.constrainScrollVel(); // since the scroll values changed, recompute the velocities
  2280.  
  2281.         // if scrolled all the way, which causes the vels to be zero, stop the animation loop
  2282.         if (!this.scrollTopVel && !this.scrollLeftVel) {
  2283.             this.stopScrolling();
  2284.         }
  2285.     },
  2286.  
  2287.  
  2288.     // Kills any existing scrolling animation loop
  2289.     stopScrolling: function() {
  2290.         if (this.scrollIntervalId) {
  2291.             clearInterval(this.scrollIntervalId);
  2292.             this.scrollIntervalId = null;
  2293.  
  2294.             // when all done with scrolling, recompute positions since they probably changed
  2295.             this.scrollStop();
  2296.         }
  2297.     },
  2298.  
  2299.  
  2300.     // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce)
  2301.     scrollHandler: function() {
  2302.         // recompute all coordinates, but *only* if this is *not* part of our scrolling animation
  2303.         if (!this.scrollIntervalId) {
  2304.             this.scrollStop();
  2305.         }
  2306.     },
  2307.  
  2308.  
  2309.     // Called when scrolling has stopped, whether through auto scroll, or the user scrolling
  2310.     scrollStop: function() {
  2311.     }
  2312.  
  2313. });
  2314.  
  2315. ;;
  2316.  
  2317. /* Tracks mouse movements over a CoordMap and raises events about which cell the mouse is over.
  2318. ------------------------------------------------------------------------------------------------------------------------
  2319. options:
  2320. - subjectEl
  2321. - subjectCenter
  2322. */
  2323.  
  2324. var CellDragListener = DragListener.extend({
  2325.  
  2326.     coordMap: null, // converts coordinates to date cells
  2327.     origCell: null, // the cell the mouse was over when listening started
  2328.     cell: null, // the cell the mouse is over
  2329.     coordAdjust: null, // delta that will be added to the mouse coordinates when computing collisions
  2330.  
  2331.  
  2332.     constructor: function(coordMap, options) {
  2333.         DragListener.prototype.constructor.call(this, options); // call the super-constructor
  2334.  
  2335.         this.coordMap = coordMap;
  2336.     },
  2337.  
  2338.  
  2339.     // Called when drag listening starts (but a real drag has not necessarily began).
  2340.     // ev might be undefined if dragging was started manually.
  2341.     listenStart: function(ev) {
  2342.         var subjectEl = this.subjectEl;
  2343.         var subjectRect;
  2344.         var origPoint;
  2345.         var point;
  2346.  
  2347.         DragListener.prototype.listenStart.apply(this, arguments); // call the super-method
  2348.  
  2349.         this.computeCoords();
  2350.  
  2351.         if (ev) {
  2352.             origPoint = { left: ev.pageX, top: ev.pageY };
  2353.             point = origPoint;
  2354.  
  2355.             // constrain the point to bounds of the element being dragged
  2356.             if (subjectEl) {
  2357.                 subjectRect = getOuterRect(subjectEl); // used for centering as well
  2358.                 point = constrainPoint(point, subjectRect);
  2359.             }
  2360.  
  2361.             this.origCell = this.getCell(point.left, point.top);
  2362.  
  2363.             // treat the center of the subject as the collision point?
  2364.             if (subjectEl && this.options.subjectCenter) {
  2365.  
  2366.                 // only consider the area the subject overlaps the cell. best for large subjects
  2367.                 if (this.origCell) {
  2368.                     subjectRect = intersectRects(this.origCell, subjectRect) ||
  2369.                         subjectRect; // in case there is no intersection
  2370.                 }
  2371.  
  2372.                 point = getRectCenter(subjectRect);
  2373.             }
  2374.  
  2375.             this.coordAdjust = diffPoints(point, origPoint); // point - origPoint
  2376.         }
  2377.         else {
  2378.             this.origCell = null;
  2379.             this.coordAdjust = null;
  2380.         }
  2381.     },
  2382.  
  2383.  
  2384.     // Recomputes the drag-critical positions of elements
  2385.     computeCoords: function() {
  2386.         this.coordMap.build();
  2387.         this.computeScrollBounds();
  2388.     },
  2389.  
  2390.  
  2391.     // Called when the actual drag has started
  2392.     dragStart: function(ev) {
  2393.         var cell;
  2394.  
  2395.         DragListener.prototype.dragStart.apply(this, arguments); // call the super-method
  2396.  
  2397.         cell = this.getCell(ev.pageX, ev.pageY); // might be different from this.origCell if the min-distance is large
  2398.  
  2399.         // report the initial cell the mouse is over
  2400.         // especially important if no min-distance and drag starts immediately
  2401.         if (cell) {
  2402.             this.cellOver(cell);
  2403.         }
  2404.     },
  2405.  
  2406.  
  2407.     // Called when the drag moves
  2408.     drag: function(dx, dy, ev) {
  2409.         var cell;
  2410.  
  2411.         DragListener.prototype.drag.apply(this, arguments); // call the super-method
  2412.  
  2413.         cell = this.getCell(ev.pageX, ev.pageY);
  2414.  
  2415.         if (!isCellsEqual(cell, this.cell)) { // a different cell than before?
  2416.             if (this.cell) {
  2417.                 this.cellOut();
  2418.             }
  2419.             if (cell) {
  2420.                 this.cellOver(cell);
  2421.             }
  2422.         }
  2423.     },
  2424.  
  2425.  
  2426.     // Called when dragging has been stopped
  2427.     dragStop: function() {
  2428.         this.cellDone();
  2429.         DragListener.prototype.dragStop.apply(this, arguments); // call the super-method
  2430.     },
  2431.  
  2432.  
  2433.     // Called when a the mouse has just moved over a new cell
  2434.     cellOver: function(cell) {
  2435.         this.cell = cell;
  2436.         this.trigger('cellOver', cell, isCellsEqual(cell, this.origCell), this.origCell);
  2437.     },
  2438.  
  2439.  
  2440.     // Called when the mouse has just moved out of a cell
  2441.     cellOut: function() {
  2442.         if (this.cell) {
  2443.             this.trigger('cellOut', this.cell);
  2444.             this.cellDone();
  2445.             this.cell = null;
  2446.         }
  2447.     },
  2448.  
  2449.  
  2450.     // Called after a cellOut. Also called before a dragStop
  2451.     cellDone: function() {
  2452.         if (this.cell) {
  2453.             this.trigger('cellDone', this.cell);
  2454.         }
  2455.     },
  2456.  
  2457.  
  2458.     // Called when drag listening has stopped
  2459.     listenStop: function() {
  2460.         DragListener.prototype.listenStop.apply(this, arguments); // call the super-method
  2461.  
  2462.         this.origCell = this.cell = null;
  2463.         this.coordMap.clear();
  2464.     },
  2465.  
  2466.  
  2467.     // Called when scrolling has stopped, whether through auto scroll, or the user scrolling
  2468.     scrollStop: function() {
  2469.         DragListener.prototype.scrollStop.apply(this, arguments); // call the super-method
  2470.  
  2471.         this.computeCoords(); // cells' absolute positions will be in new places. recompute
  2472.     },
  2473.  
  2474.  
  2475.     // Gets the cell underneath the coordinates for the given mouse event
  2476.     getCell: function(left, top) {
  2477.  
  2478.         if (this.coordAdjust) {
  2479.             left += this.coordAdjust.left;
  2480.             top += this.coordAdjust.top;
  2481.         }
  2482.  
  2483.         return this.coordMap.getCell(left, top);
  2484.     }
  2485.  
  2486. });
  2487.  
  2488.  
  2489. // Returns `true` if the cells are identically equal. `false` otherwise.
  2490. // They must have the same row, col, and be from the same grid.
  2491. // Two null values will be considered equal, as two "out of the grid" states are the same.
  2492. function isCellsEqual(cell1, cell2) {
  2493.  
  2494.     if (!cell1 && !cell2) {
  2495.         return true;
  2496.     }
  2497.  
  2498.     if (cell1 && cell2) {
  2499.         return cell1.grid === cell2.grid &&
  2500.             cell1.row === cell2.row &&
  2501.             cell1.col === cell2.col;
  2502.     }
  2503.  
  2504.     return false;
  2505. }
  2506.  
  2507. ;;
  2508.  
  2509. /* Creates a clone of an element and lets it track the mouse as it moves
  2510. ----------------------------------------------------------------------------------------------------------------------*/
  2511.  
  2512. var MouseFollower = Class.extend({
  2513.  
  2514.     options: null,
  2515.  
  2516.     sourceEl: null, // the element that will be cloned and made to look like it is dragging
  2517.     el: null, // the clone of `sourceEl` that will track the mouse
  2518.     parentEl: null, // the element that `el` (the clone) will be attached to
  2519.  
  2520.     // the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl
  2521.     top0: null,
  2522.     left0: null,
  2523.  
  2524.     // the initial position of the mouse
  2525.     mouseY0: null,
  2526.     mouseX0: null,
  2527.  
  2528.     // the number of pixels the mouse has moved from its initial position
  2529.     topDelta: null,
  2530.     leftDelta: null,
  2531.  
  2532.     mousemoveProxy: null, // document mousemove handler, bound to the MouseFollower's `this`
  2533.  
  2534.     isFollowing: false,
  2535.     isHidden: false,
  2536.     isAnimating: false, // doing the revert animation?
  2537.  
  2538.     constructor: function(sourceEl, options) {
  2539.         this.options = options = options || {};
  2540.         this.sourceEl = sourceEl;
  2541.         this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent
  2542.     },
  2543.  
  2544.  
  2545.     // Causes the element to start following the mouse
  2546.     start: function(ev) {
  2547.         if (!this.isFollowing) {
  2548.             this.isFollowing = true;
  2549.  
  2550.             this.mouseY0 = ev.pageY;
  2551.             this.mouseX0 = ev.pageX;
  2552.             this.topDelta = 0;
  2553.             this.leftDelta = 0;
  2554.  
  2555.             if (!this.isHidden) {
  2556.                 this.updatePosition();
  2557.             }
  2558.  
  2559.             $(document).on('mousemove', this.mousemoveProxy = proxy(this, 'mousemove'));
  2560.         }
  2561.     },
  2562.  
  2563.  
  2564.     // Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position.
  2565.     // `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately.
  2566.     stop: function(shouldRevert, callback) {
  2567.         var _this = this;
  2568.         var revertDuration = this.options.revertDuration;
  2569.  
  2570.         function complete() {
  2571.             this.isAnimating = false;
  2572.             _this.removeElement();
  2573.  
  2574.             this.top0 = this.left0 = null; // reset state for future updatePosition calls
  2575.  
  2576.             if (callback) {
  2577.                 callback();
  2578.             }
  2579.         }
  2580.  
  2581.         if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time
  2582.             this.isFollowing = false;
  2583.  
  2584.             $(document).off('mousemove', this.mousemoveProxy);
  2585.  
  2586.             if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation?
  2587.                 this.isAnimating = true;
  2588.                 this.el.animate({
  2589.                     top: this.top0,
  2590.                     left: this.left0
  2591.                 }, {
  2592.                     duration: revertDuration,
  2593.                     complete: complete
  2594.                 });
  2595.             }
  2596.             else {
  2597.                 complete();
  2598.             }
  2599.         }
  2600.     },
  2601.  
  2602.  
  2603.     // Gets the tracking element. Create it if necessary
  2604.     getEl: function() {
  2605.         var el = this.el;
  2606.  
  2607.         if (!el) {
  2608.             this.sourceEl.width(); // hack to force IE8 to compute correct bounding box
  2609.             el = this.el = this.sourceEl.clone()
  2610.                 .css({
  2611.                     position: 'absolute',
  2612.                     visibility: '', // in case original element was hidden (commonly through hideEvents())
  2613.                     display: this.isHidden ? 'none' : '', // for when initially hidden
  2614.                     margin: 0,
  2615.                     right: 'auto', // erase and set width instead
  2616.                     bottom: 'auto', // erase and set height instead
  2617.                     width: this.sourceEl.width(), // explicit height in case there was a 'right' value
  2618.                     height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value
  2619.                     opacity: this.options.opacity || '',
  2620.                     zIndex: this.options.zIndex
  2621.                 })
  2622.                 .appendTo(this.parentEl);
  2623.         }
  2624.  
  2625.         return el;
  2626.     },
  2627.  
  2628.  
  2629.     // Removes the tracking element if it has already been created
  2630.     removeElement: function() {
  2631.         if (this.el) {
  2632.             this.el.remove();
  2633.             this.el = null;
  2634.         }
  2635.     },
  2636.  
  2637.  
  2638.     // Update the CSS position of the tracking element
  2639.     updatePosition: function() {
  2640.         var sourceOffset;
  2641.         var origin;
  2642.  
  2643.         this.getEl(); // ensure this.el
  2644.  
  2645.         // make sure origin info was computed
  2646.         if (this.top0 === null) {
  2647.             this.sourceEl.width(); // hack to force IE8 to compute correct bounding box
  2648.             sourceOffset = this.sourceEl.offset();
  2649.             origin = this.el.offsetParent().offset();
  2650.             this.top0 = sourceOffset.top - origin.top;
  2651.             this.left0 = sourceOffset.left - origin.left;
  2652.         }
  2653.  
  2654.         this.el.css({
  2655.             top: this.top0 + this.topDelta,
  2656.             left: this.left0 + this.leftDelta
  2657.         });
  2658.     },
  2659.  
  2660.  
  2661.     // Gets called when the user moves the mouse
  2662.     mousemove: function(ev) {
  2663.         this.topDelta = ev.pageY - this.mouseY0;
  2664.         this.leftDelta = ev.pageX - this.mouseX0;
  2665.  
  2666.         if (!this.isHidden) {
  2667.             this.updatePosition();
  2668.         }
  2669.     },
  2670.  
  2671.  
  2672.     // Temporarily makes the tracking element invisible. Can be called before following starts
  2673.     hide: function() {
  2674.         if (!this.isHidden) {
  2675.             this.isHidden = true;
  2676.             if (this.el) {
  2677.                 this.el.hide();
  2678.             }
  2679.         }
  2680.     },
  2681.  
  2682.  
  2683.     // Show the tracking element after it has been temporarily hidden
  2684.     show: function() {
  2685.         if (this.isHidden) {
  2686.             this.isHidden = false;
  2687.             this.updatePosition();
  2688.             this.getEl().show();
  2689.         }
  2690.     }
  2691.  
  2692. });
  2693.  
  2694. ;;
  2695.  
  2696. /* A utility class for rendering <tr> rows.
  2697. ----------------------------------------------------------------------------------------------------------------------*/
  2698. // It leverages methods of the subclass and the View to determine custom rendering behavior for each row "type"
  2699. // (such as highlight rows, day rows, helper rows, etc).
  2700.  
  2701. var RowRenderer = Class.extend({
  2702.  
  2703.     view: null, // a View object
  2704.     isRTL: null, // shortcut to the view's isRTL option
  2705.     cellHtml: '<td/>', // plain default HTML used for a cell when no other is available
  2706.  
  2707.  
  2708.     constructor: function(view) {
  2709.         this.view = view;
  2710.         this.isRTL = view.opt('isRTL');
  2711.     },
  2712.  
  2713.  
  2714.     // Renders the HTML for a row, leveraging custom cell-HTML-renderers based on the `rowType`.
  2715.     // Also applies the "intro" and "outro" cells, which are specified by the subclass and views.
  2716.     // `row` is an optional row number.
  2717.     rowHtml: function(rowType, row) {
  2718.         var renderCell = this.getHtmlRenderer('cell', rowType);
  2719.         var rowCellHtml = '';
  2720.         var col;
  2721.         var cell;
  2722.  
  2723.         row = row || 0;
  2724.  
  2725.         for (col = 0; col < this.colCnt; col++) {
  2726.             cell = this.getCell(row, col);
  2727.             rowCellHtml += renderCell(cell);
  2728.         }
  2729.  
  2730.         rowCellHtml = this.bookendCells(rowCellHtml, rowType, row); // apply intro and outro
  2731.  
  2732.         return '<tr>' + rowCellHtml + '</tr>';
  2733.     },
  2734.  
  2735.  
  2736.     // Applies the "intro" and "outro" HTML to the given cells.
  2737.     // Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro.
  2738.     // `cells` can be an HTML string of <td>'s or a jQuery <tr> element
  2739.     // `row` is an optional row number.
  2740.     bookendCells: function(cells, rowType, row) {
  2741.         var intro = this.getHtmlRenderer('intro', rowType)(row || 0);
  2742.         var outro = this.getHtmlRenderer('outro', rowType)(row || 0);
  2743.         var prependHtml = this.isRTL ? outro : intro;
  2744.         var appendHtml = this.isRTL ? intro : outro;
  2745.  
  2746.         if (typeof cells === 'string') {
  2747.             return prependHtml + cells + appendHtml;
  2748.         }
  2749.         else { // a jQuery <tr> element
  2750.             return cells.prepend(prependHtml).append(appendHtml);
  2751.         }
  2752.     },
  2753.  
  2754.  
  2755.     // Returns an HTML-rendering function given a specific `rendererName` (like cell, intro, or outro) and a specific
  2756.     // `rowType` (like day, eventSkeleton, helperSkeleton), which is optional.
  2757.     // If a renderer for the specific rowType doesn't exist, it will fall back to a generic renderer.
  2758.     // We will query the View object first for any custom rendering functions, then the methods of the subclass.
  2759.     getHtmlRenderer: function(rendererName, rowType) {
  2760.         var view = this.view;
  2761.         var generalName; // like "cellHtml"
  2762.         var specificName; // like "dayCellHtml". based on rowType
  2763.         var provider; // either the View or the RowRenderer subclass, whichever provided the method
  2764.         var renderer;
  2765.  
  2766.         generalName = rendererName + 'Html';
  2767.         if (rowType) {
  2768.             specificName = rowType + capitaliseFirstLetter(rendererName) + 'Html';
  2769.         }
  2770.  
  2771.         if (specificName && (renderer = view[specificName])) {
  2772.             provider = view;
  2773.         }
  2774.         else if (specificName && (renderer = this[specificName])) {
  2775.             provider = this;
  2776.         }
  2777.         else if ((renderer = view[generalName])) {
  2778.             provider = view;
  2779.         }
  2780.         else if ((renderer = this[generalName])) {
  2781.             provider = this;
  2782.         }
  2783.  
  2784.         if (typeof renderer === 'function') {
  2785.             return function() {
  2786.                 return renderer.apply(provider, arguments) || ''; // use correct `this` and always return a string
  2787.             };
  2788.         }
  2789.  
  2790.         // the rendered can be a plain string as well. if not specified, always an empty string.
  2791.         return function() {
  2792.             return renderer || '';
  2793.         };
  2794.     }
  2795.  
  2796. });
  2797.  
  2798. ;;
  2799.  
  2800. /* An abstract class comprised of a "grid" of cells that each represent a specific datetime
  2801. ----------------------------------------------------------------------------------------------------------------------*/
  2802.  
  2803. var Grid = fc.Grid = RowRenderer.extend({
  2804.  
  2805.     start: null, // the date of the first cell
  2806.     end: null, // the date after the last cell
  2807.  
  2808.     rowCnt: 0, // number of rows
  2809.     colCnt: 0, // number of cols
  2810.  
  2811.     el: null, // the containing element
  2812.     coordMap: null, // a GridCoordMap that converts pixel values to datetimes
  2813.     elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.
  2814.  
  2815.     externalDragStartProxy: null, // binds the Grid's scope to externalDragStart (in DayGrid.events)
  2816.  
  2817.     // derived from options
  2818.     colHeadFormat: null, // TODO: move to another class. not applicable to all Grids
  2819.     eventTimeFormat: null,
  2820.     displayEventTime: null,
  2821.     displayEventEnd: null,
  2822.  
  2823.     // if all cells are the same length of time, the duration they all share. optional.
  2824.     // when defined, allows the computeCellRange shortcut, as well as improved resizing behavior.
  2825.     cellDuration: null,
  2826.  
  2827.     // if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity
  2828.     // of the date cells. if not defined, assumes to be day and time granularity.
  2829.     largeUnit: null,
  2830.  
  2831.  
  2832.     constructor: function() {
  2833.         RowRenderer.apply(this, arguments); // call the super-constructor
  2834.  
  2835.         this.coordMap = new GridCoordMap(this);
  2836.         this.elsByFill = {};
  2837.         this.externalDragStartProxy = proxy(this, 'externalDragStart');
  2838.     },
  2839.  
  2840.  
  2841.     /* Options
  2842.     ------------------------------------------------------------------------------------------------------------------*/
  2843.  
  2844.  
  2845.     // Generates the format string used for the text in column headers, if not explicitly defined by 'columnFormat'
  2846.     // TODO: move to another class. not applicable to all Grids
  2847.     computeColHeadFormat: function() {
  2848.         // subclasses must implement if they want to use headHtml()
  2849.     },
  2850.  
  2851.  
  2852.     // Generates the format string used for event time text, if not explicitly defined by 'timeFormat'
  2853.     computeEventTimeFormat: function() {
  2854.         return this.view.opt('smallTimeFormat');
  2855.     },
  2856.  
  2857.  
  2858.     // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventTime'.
  2859.     // Only applies to non-all-day events.
  2860.     computeDisplayEventTime: function() {
  2861.         return true;
  2862.     },
  2863.  
  2864.  
  2865.     // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd'
  2866.     computeDisplayEventEnd: function() {
  2867.         return true;
  2868.     },
  2869.  
  2870.  
  2871.     /* Dates
  2872.     ------------------------------------------------------------------------------------------------------------------*/
  2873.  
  2874.  
  2875.     // Tells the grid about what period of time to display.
  2876.     // Any date-related cell system internal data should be generated.
  2877.     setRange: function(range) {
  2878.         this.start = range.start.clone();
  2879.         this.end = range.end.clone();
  2880.  
  2881.         this.rangeUpdated();
  2882.         this.processRangeOptions();
  2883.     },
  2884.  
  2885.  
  2886.     // Called when internal variables that rely on the range should be updated
  2887.     rangeUpdated: function() {
  2888.     },
  2889.  
  2890.  
  2891.     // Updates values that rely on options and also relate to range
  2892.     processRangeOptions: function() {
  2893.         var view = this.view;
  2894.         var displayEventTime;
  2895.         var displayEventEnd;
  2896.  
  2897.         // Populate option-derived settings. Look for override first, then compute if necessary.
  2898.         this.colHeadFormat = view.opt('columnFormat') || this.computeColHeadFormat();
  2899.  
  2900.         this.eventTimeFormat =
  2901.             view.opt('eventTimeFormat') ||
  2902.             view.opt('timeFormat') || // deprecated
  2903.             this.computeEventTimeFormat();
  2904.  
  2905.         displayEventTime = view.opt('displayEventTime');
  2906.         if (displayEventTime == null) {
  2907.             displayEventTime = this.computeDisplayEventTime(); // might be based off of range
  2908.         }
  2909.  
  2910.         displayEventEnd = view.opt('displayEventEnd');
  2911.         if (displayEventEnd == null) {
  2912.             displayEventEnd = this.computeDisplayEventEnd(); // might be based off of range
  2913.         }
  2914.  
  2915.         this.displayEventTime = displayEventTime;
  2916.         this.displayEventEnd = displayEventEnd;
  2917.     },
  2918.  
  2919.  
  2920.     // Called before the grid's coordinates will need to be queried for cells.
  2921.     // Any non-date-related cell system internal data should be built.
  2922.     build: function() {
  2923.     },
  2924.  
  2925.  
  2926.     // Called after the grid's coordinates are done being relied upon.
  2927.     // Any non-date-related cell system internal data should be cleared.
  2928.     clear: function() {
  2929.     },
  2930.  
  2931.  
  2932.     // Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects
  2933.     rangeToSegs: function(range) {
  2934.         // subclasses must implement
  2935.     },
  2936.  
  2937.  
  2938.     // Diffs the two dates, returning a duration, based on granularity of the grid
  2939.     diffDates: function(a, b) {
  2940.         if (this.largeUnit) {
  2941.             return diffByUnit(a, b, this.largeUnit);
  2942.         }
  2943.         else {
  2944.             return diffDayTime(a, b);
  2945.         }
  2946.     },
  2947.  
  2948.  
  2949.     /* Cells
  2950.     ------------------------------------------------------------------------------------------------------------------*/
  2951.     // NOTE: columns are ordered left-to-right
  2952.  
  2953.  
  2954.     // Gets an object containing row/col number, misc data, and range information about the cell.
  2955.     // Accepts row/col values, an object with row/col properties, or a single-number offset from the first cell.
  2956.     getCell: function(row, col) {
  2957.         var cell;
  2958.  
  2959.         if (col == null) {
  2960.             if (typeof row === 'number') { // a single-number offset
  2961.                 col = row % this.colCnt;
  2962.                 row = Math.floor(row / this.colCnt);
  2963.             }
  2964.             else { // an object with row/col properties
  2965.                 col = row.col;
  2966.                 row = row.row;
  2967.             }
  2968.         }
  2969.  
  2970.         cell = { row: row, col: col };
  2971.  
  2972.         $.extend(cell, this.getRowData(row), this.getColData(col));
  2973.         $.extend(cell, this.computeCellRange(cell));
  2974.  
  2975.         return cell;
  2976.     },
  2977.  
  2978.  
  2979.     // Given a cell object with index and misc data, generates a range object
  2980.     // If the grid is leveraging cellDuration, this doesn't need to be defined. Only computeCellDate does.
  2981.     // If being overridden, should return a range with reference-free date copies.
  2982.     computeCellRange: function(cell) {
  2983.         var date = this.computeCellDate(cell);
  2984.  
  2985.         return {
  2986.             start: date,
  2987.             end: date.clone().add(this.cellDuration)
  2988.         };
  2989.     },
  2990.  
  2991.  
  2992.     // Given a cell, returns its start date. Should return a reference-free date copy.
  2993.     computeCellDate: function(cell) {
  2994.         // subclasses can implement
  2995.     },
  2996.  
  2997.  
  2998.     // Retrieves misc data about the given row
  2999.     getRowData: function(row) {
  3000.         return {};
  3001.     },
  3002.  
  3003.  
  3004.     // Retrieves misc data baout the given column
  3005.     getColData: function(col) {
  3006.         return {};
  3007.     },
  3008.  
  3009.  
  3010.     // Retrieves the element representing the given row
  3011.     getRowEl: function(row) {
  3012.         // subclasses should implement if leveraging the default getCellDayEl() or computeRowCoords()
  3013.     },
  3014.  
  3015.  
  3016.     // Retrieves the element representing the given column
  3017.     getColEl: function(col) {
  3018.         // subclasses should implement if leveraging the default getCellDayEl() or computeColCoords()
  3019.     },
  3020.  
  3021.  
  3022.     // Given a cell object, returns the element that represents the cell's whole-day
  3023.     getCellDayEl: function(cell) {
  3024.         return this.getColEl(cell.col) || this.getRowEl(cell.row);
  3025.     },
  3026.  
  3027.  
  3028.     /* Cell Coordinates
  3029.     ------------------------------------------------------------------------------------------------------------------*/
  3030.  
  3031.  
  3032.     // Computes the top/bottom coordinates of all rows.
  3033.     // By default, queries the dimensions of the element provided by getRowEl().
  3034.     computeRowCoords: function() {
  3035.         var items = [];
  3036.         var i, el;
  3037.         var top;
  3038.  
  3039.         for (i = 0; i < this.rowCnt; i++) {
  3040.             el = this.getRowEl(i);
  3041.             top = el.offset().top;
  3042.             items.push({
  3043.                 top: top,
  3044.                 bottom: top + el.outerHeight()
  3045.             });
  3046.         }
  3047.  
  3048.         return items;
  3049.     },
  3050.  
  3051.  
  3052.     // Computes the left/right coordinates of all rows.
  3053.     // By default, queries the dimensions of the element provided by getColEl(). Columns can be LTR or RTL.
  3054.     computeColCoords: function() {
  3055.         var items = [];
  3056.         var i, el;
  3057.         var left;
  3058.  
  3059.         for (i = 0; i < this.colCnt; i++) {
  3060.             el = this.getColEl(i);
  3061.             left = el.offset().left;
  3062.             items.push({
  3063.                 left: left,
  3064.                 right: left + el.outerWidth()
  3065.             });
  3066.         }
  3067.  
  3068.         return items;
  3069.     },
  3070.  
  3071.  
  3072.     /* Rendering
  3073.     ------------------------------------------------------------------------------------------------------------------*/
  3074.  
  3075.  
  3076.     // Sets the container element that the grid should render inside of.
  3077.     // Does other DOM-related initializations.
  3078.     setElement: function(el) {
  3079.         var _this = this;
  3080.  
  3081.         this.el = el;
  3082.  
  3083.         // attach a handler to the grid's root element.
  3084.         // jQuery will take care of unregistering them when removeElement gets called.
  3085.         el.on('mousedown', function(ev) {
  3086.             if (
  3087.                 !$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link
  3088.                 !$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one)
  3089.             ) {
  3090.                 _this.dayMousedown(ev);
  3091.             }
  3092.         });
  3093.  
  3094.         // attach event-element-related handlers. in Grid.events
  3095.         // same garbage collection note as above.
  3096.         this.bindSegHandlers();
  3097.  
  3098.         this.bindGlobalHandlers();
  3099.     },
  3100.  
  3101.  
  3102.     // Removes the grid's container element from the DOM. Undoes any other DOM-related attachments.
  3103.     // DOES NOT remove any content beforehand (doesn't clear events or call unrenderDates), unlike View
  3104.     removeElement: function() {
  3105.         this.unbindGlobalHandlers();
  3106.  
  3107.         this.el.remove();
  3108.  
  3109.         // NOTE: we don't null-out this.el for the same reasons we don't do it within View::removeElement
  3110.     },
  3111.  
  3112.  
  3113.     // Renders the basic structure of grid view before any content is rendered
  3114.     renderSkeleton: function() {
  3115.         // subclasses should implement
  3116.     },
  3117.  
  3118.  
  3119.     // Renders the grid's date-related content (like cells that represent days/times).
  3120.     // Assumes setRange has already been called and the skeleton has already been rendered.
  3121.     renderDates: function() {
  3122.         // subclasses should implement
  3123.     },
  3124.  
  3125.  
  3126.     // Unrenders the grid's date-related content
  3127.     unrenderDates: function() {
  3128.         // subclasses should implement
  3129.     },
  3130.  
  3131.  
  3132.     /* Handlers
  3133.     ------------------------------------------------------------------------------------------------------------------*/
  3134.  
  3135.  
  3136.     // Binds DOM handlers to elements that reside outside the grid, such as the document
  3137.     bindGlobalHandlers: function() {
  3138.         $(document).on('dragstart sortstart', this.externalDragStartProxy); // jqui
  3139.     },
  3140.  
  3141.  
  3142.     // Unbinds DOM handlers from elements that reside outside the grid
  3143.     unbindGlobalHandlers: function() {
  3144.         $(document).off('dragstart sortstart', this.externalDragStartProxy); // jqui
  3145.     },
  3146.  
  3147.  
  3148.     // Process a mousedown on an element that represents a day. For day clicking and selecting.
  3149.     dayMousedown: function(ev) {
  3150.         var _this = this;
  3151.         var view = this.view;
  3152.         var isSelectable = view.opt('selectable');
  3153.         var dayClickCell; // null if invalid dayClick
  3154.         var selectionRange; // null if invalid selection
  3155.  
  3156.         // this listener tracks a mousedown on a day element, and a subsequent drag.
  3157.         // if the drag ends on the same day, it is a 'dayClick'.
  3158.         // if 'selectable' is enabled, this listener also detects selections.
  3159.         var dragListener = new CellDragListener(this.coordMap, {
  3160.             //distance: 5, // needs more work if we want dayClick to fire correctly
  3161.             scroll: view.opt('dragScroll'),
  3162.             dragStart: function() {
  3163.                 view.unselect(); // since we could be rendering a new selection, we want to clear any old one
  3164.             },
  3165.             cellOver: function(cell, isOrig, origCell) {
  3166.                 if (origCell) { // click needs to have started on a cell
  3167.                     dayClickCell = isOrig ? cell : null; // single-cell selection is a day click
  3168.                     if (isSelectable) {
  3169.                         selectionRange = _this.computeSelection(origCell, cell);
  3170.                         if (selectionRange) {
  3171.                             _this.renderSelection(selectionRange);
  3172.                         }
  3173.                         else {
  3174.                             disableCursor();
  3175.                         }
  3176.                     }
  3177.                 }
  3178.             },
  3179.             cellOut: function(cell) {
  3180.                 dayClickCell = null;
  3181.                 selectionRange = null;
  3182.                 _this.unrenderSelection();
  3183.                 enableCursor();
  3184.             },
  3185.             listenStop: function(ev) {
  3186.                 if (dayClickCell) {
  3187.                     view.triggerDayClick(dayClickCell, _this.getCellDayEl(dayClickCell), ev);
  3188.                 }
  3189.                 if (selectionRange) {
  3190.                     // the selection will already have been rendered. just report it
  3191.                     view.reportSelection(selectionRange, ev);
  3192.                 }
  3193.                 enableCursor();
  3194.             }
  3195.         });
  3196.  
  3197.         dragListener.mousedown(ev); // start listening, which will eventually initiate a dragStart
  3198.     },
  3199.  
  3200.  
  3201.     /* Event Helper
  3202.     ------------------------------------------------------------------------------------------------------------------*/
  3203.     // TODO: should probably move this to Grid.events, like we did event dragging / resizing
  3204.  
  3205.  
  3206.     // Renders a mock event over the given range
  3207.     renderRangeHelper: function(range, sourceSeg) {
  3208.         var fakeEvent = this.fabricateHelperEvent(range, sourceSeg);
  3209.  
  3210.         this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering
  3211.     },
  3212.  
  3213.  
  3214.     // Builds a fake event given a date range it should cover, and a segment is should be inspired from.
  3215.     // The range's end can be null, in which case the mock event that is rendered will have a null end time.
  3216.     // `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging.
  3217.     fabricateHelperEvent: function(range, sourceSeg) {
  3218.         var fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible
  3219.  
  3220.         fakeEvent.start = range.start.clone();
  3221.         fakeEvent.end = range.end ? range.end.clone() : null;
  3222.         fakeEvent.allDay = null; // force it to be freshly computed by normalizeEventRange
  3223.         this.view.calendar.normalizeEventRange(fakeEvent);
  3224.  
  3225.         // this extra className will be useful for differentiating real events from mock events in CSS
  3226.         fakeEvent.className = (fakeEvent.className || []).concat('fc-helper');
  3227.  
  3228.         // if something external is being dragged in, don't render a resizer
  3229.         if (!sourceSeg) {
  3230.             fakeEvent.editable = false;
  3231.         }
  3232.  
  3233.         return fakeEvent;
  3234.     },
  3235.  
  3236.  
  3237.     // Renders a mock event
  3238.     renderHelper: function(event, sourceSeg) {
  3239.         // subclasses must implement
  3240.     },
  3241.  
  3242.  
  3243.     // Unrenders a mock event
  3244.     unrenderHelper: function() {
  3245.         // subclasses must implement
  3246.     },
  3247.  
  3248.  
  3249.     /* Selection
  3250.     ------------------------------------------------------------------------------------------------------------------*/
  3251.  
  3252.  
  3253.     // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses.
  3254.     renderSelection: function(range) {
  3255.         this.renderHighlight(this.selectionRangeToSegs(range));
  3256.     },
  3257.  
  3258.  
  3259.     // Unrenders any visual indications of a selection. Will unrender a highlight by default.
  3260.     unrenderSelection: function() {
  3261.         this.unrenderHighlight();
  3262.     },
  3263.  
  3264.  
  3265.     // Given the first and last cells of a selection, returns a range object.
  3266.     // Will return something falsy if the selection is invalid (when outside of selectionConstraint for example).
  3267.     // Subclasses can override and provide additional data in the range object. Will be passed to renderSelection().
  3268.     computeSelection: function(firstCell, lastCell) {
  3269.         var dates = [
  3270.             firstCell.start,
  3271.             firstCell.end,
  3272.             lastCell.start,
  3273.             lastCell.end
  3274.         ];
  3275.         var range;
  3276.  
  3277.         dates.sort(compareNumbers); // sorts chronologically. works with Moments
  3278.  
  3279.         range = {
  3280.             start: dates[0].clone(),
  3281.             end: dates[3].clone()
  3282.         };
  3283.  
  3284.         if (!this.view.calendar.isSelectionRangeAllowed(range)) {
  3285.             return null;
  3286.         }
  3287.  
  3288.         return range;
  3289.     },
  3290.  
  3291.  
  3292.     selectionRangeToSegs: function(range) {
  3293.         return this.rangeToSegs(range);
  3294.     },
  3295.  
  3296.  
  3297.     /* Highlight
  3298.     ------------------------------------------------------------------------------------------------------------------*/
  3299.  
  3300.  
  3301.     // Renders an emphasis on the given date range. Given an array of segments.
  3302.     renderHighlight: function(segs) {
  3303.         this.renderFill('highlight', segs);
  3304.     },
  3305.  
  3306.  
  3307.     // Unrenders the emphasis on a date range
  3308.     unrenderHighlight: function() {
  3309.         this.unrenderFill('highlight');
  3310.     },
  3311.  
  3312.  
  3313.     // Generates an array of classNames for rendering the highlight. Used by the fill system.
  3314.     highlightSegClasses: function() {
  3315.         return [ 'fc-highlight' ];
  3316.     },
  3317.  
  3318.  
  3319.     /* Fill System (highlight, background events, business hours)
  3320.     ------------------------------------------------------------------------------------------------------------------*/
  3321.  
  3322.  
  3323.     // Renders a set of rectangles over the given segments of time.
  3324.     // MUST RETURN a subset of segs, the segs that were actually rendered.
  3325.     // Responsible for populating this.elsByFill. TODO: better API for expressing this requirement
  3326.     renderFill: function(type, segs) {
  3327.         // subclasses must implement
  3328.     },
  3329.  
  3330.  
  3331.     // Unrenders a specific type of fill that is currently rendered on the grid
  3332.     unrenderFill: function(type) {
  3333.         var el = this.elsByFill[type];
  3334.  
  3335.         if (el) {
  3336.             el.remove();
  3337.             delete this.elsByFill[type];
  3338.         }
  3339.     },
  3340.  
  3341.  
  3342.     // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types.
  3343.     // Only returns segments that successfully rendered.
  3344.     // To be harnessed by renderFill (implemented by subclasses).
  3345.     // Analagous to renderFgSegEls.
  3346.     renderFillSegEls: function(type, segs) {
  3347.         var _this = this;
  3348.         var segElMethod = this[type + 'SegEl'];
  3349.         var html = '';
  3350.         var renderedSegs = [];
  3351.         var i;
  3352.  
  3353.         if (segs.length) {
  3354.  
  3355.             // build a large concatenation of segment HTML
  3356.             for (i = 0; i < segs.length; i++) {
  3357.                 html += this.fillSegHtml(type, segs[i]);
  3358.             }
  3359.  
  3360.             // Grab individual elements from the combined HTML string. Use each as the default rendering.
  3361.             // Then, compute the 'el' for each segment.
  3362.             $(html).each(function(i, node) {
  3363.                 var seg = segs[i];
  3364.                 var el = $(node);
  3365.  
  3366.                 // allow custom filter methods per-type
  3367.                 if (segElMethod) {
  3368.                     el = segElMethod.call(_this, seg, el);
  3369.                 }
  3370.  
  3371.                 if (el) { // custom filters did not cancel the render
  3372.                     el = $(el); // allow custom filter to return raw DOM node
  3373.  
  3374.                     // correct element type? (would be bad if a non-TD were inserted into a table for example)
  3375.                     if (el.is(_this.fillSegTag)) {
  3376.                         seg.el = el;
  3377.                         renderedSegs.push(seg);
  3378.                     }
  3379.                 }
  3380.             });
  3381.         }
  3382.  
  3383.         return renderedSegs;
  3384.     },
  3385.  
  3386.  
  3387.     fillSegTag: 'div', // subclasses can override
  3388.  
  3389.  
  3390.     // Builds the HTML needed for one fill segment. Generic enought o work with different types.
  3391.     fillSegHtml: function(type, seg) {
  3392.  
  3393.         // custom hooks per-type
  3394.         var classesMethod = this[type + 'SegClasses'];
  3395.         var cssMethod = this[type + 'SegCss'];
  3396.  
  3397.         var classes = classesMethod ? classesMethod.call(this, seg) : [];
  3398.         var css = cssToStr(cssMethod ? cssMethod.call(this, seg) : {});
  3399.  
  3400.         return '<' + this.fillSegTag +
  3401.             (classes.length ? ' class="' + classes.join(' ') + '"' : '') +
  3402.             (css ? ' style="' + css + '"' : '') +
  3403.             ' />';
  3404.     },
  3405.  
  3406.  
  3407.     /* Generic rendering utilities for subclasses
  3408.     ------------------------------------------------------------------------------------------------------------------*/
  3409.  
  3410.  
  3411.     // Renders a day-of-week header row.
  3412.     // TODO: move to another class. not applicable to all Grids
  3413.     headHtml: function() {
  3414.         return '' +
  3415.             '<div class="fc-row ' + this.view.widgetHeaderClass + '">' +
  3416.                 '<table>' +
  3417.                     '<thead>' +
  3418.                         this.rowHtml('head') + // leverages RowRenderer
  3419.                     '</thead>' +
  3420.                 '</table>' +
  3421.             '</div>';
  3422.     },
  3423.  
  3424.  
  3425.     // Used by the `headHtml` method, via RowRenderer, for rendering the HTML of a day-of-week header cell
  3426.     // TODO: move to another class. not applicable to all Grids
  3427.     headCellHtml: function(cell) {
  3428.         var view = this.view;
  3429.         var date = cell.start;
  3430.  
  3431.         return '' +
  3432.             '<th class="fc-day-header ' + view.widgetHeaderClass + ' fc-' + dayIDs[date.day()] + '">' +
  3433.                 htmlEscape(date.format(this.colHeadFormat)) +
  3434.             '</th>';
  3435.     },
  3436.  
  3437.  
  3438.     // Renders the HTML for a single-day background cell
  3439.     bgCellHtml: function(cell) {
  3440.         var view = this.view;
  3441.         var date = cell.start;
  3442.         var classes = this.getDayClasses(date);
  3443.  
  3444.         classes.unshift('fc-day', view.widgetContentClass);
  3445.  
  3446.         return '<td class="' + classes.join(' ') + '"' +
  3447.             ' data-date="' + date.format('YYYY-MM-DD') + '"' + // if date has a time, won't format it
  3448.             '></td>';
  3449.     },
  3450.  
  3451.  
  3452.     // Computes HTML classNames for a single-day cell
  3453.     getDayClasses: function(date) {
  3454.         var view = this.view;
  3455.         var today = view.calendar.getNow().stripTime();
  3456.         var classes = [ 'fc-' + dayIDs[date.day()] ];
  3457.  
  3458.         if (
  3459.             view.intervalDuration.as('months') == 1 &&
  3460.             date.month() != view.intervalStart.month()
  3461.         ) {
  3462.             classes.push('fc-other-month');
  3463.         }
  3464.  
  3465.         if (date.isSame(today, 'day')) {
  3466.             classes.push(
  3467.                 'fc-today',
  3468.                 view.highlightStateClass
  3469.             );
  3470.         }
  3471.         else if (date < today) {
  3472.             classes.push('fc-past');
  3473.         }
  3474.         else {
  3475.             classes.push('fc-future');
  3476.         }
  3477.  
  3478.         return classes;
  3479.     }
  3480.  
  3481. });
  3482.  
  3483. ;;
  3484.  
  3485. /* Event-rendering and event-interaction methods for the abstract Grid class
  3486. ----------------------------------------------------------------------------------------------------------------------*/
  3487.  
  3488. Grid.mixin({
  3489.  
  3490.     mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing
  3491.     isDraggingSeg: false, // is a segment being dragged? boolean
  3492.     isResizingSeg: false, // is a segment being resized? boolean
  3493.     isDraggingExternal: false, // jqui-dragging an external element? boolean
  3494.     segs: null, // the event segments currently rendered in the grid
  3495.  
  3496.  
  3497.     // Renders the given events onto the grid
  3498.     renderEvents: function(events) {
  3499.         var segs = this.eventsToSegs(events);
  3500.         var bgSegs = [];
  3501.         var fgSegs = [];
  3502.         var i, seg;
  3503.  
  3504.         for (i = 0; i < segs.length; i++) {
  3505.             seg = segs[i];
  3506.  
  3507.             if (isBgEvent(seg.event)) {
  3508.                 bgSegs.push(seg);
  3509.             }
  3510.             else {
  3511.                 fgSegs.push(seg);
  3512.             }
  3513.         }
  3514.  
  3515.         // Render each different type of segment.
  3516.         // Each function may return a subset of the segs, segs that were actually rendered.
  3517.         bgSegs = this.renderBgSegs(bgSegs) || bgSegs;
  3518.         fgSegs = this.renderFgSegs(fgSegs) || fgSegs;
  3519.  
  3520.         this.segs = bgSegs.concat(fgSegs);
  3521.     },
  3522.  
  3523.  
  3524.     // Unrenders all events currently rendered on the grid
  3525.     unrenderEvents: function() {
  3526.         this.triggerSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
  3527.  
  3528.         this.unrenderFgSegs();
  3529.         this.unrenderBgSegs();
  3530.  
  3531.         this.segs = null;
  3532.     },
  3533.  
  3534.  
  3535.     // Retrieves all rendered segment objects currently rendered on the grid
  3536.     getEventSegs: function() {
  3537.         return this.segs || [];
  3538.     },
  3539.  
  3540.  
  3541.     /* Foreground Segment Rendering
  3542.     ------------------------------------------------------------------------------------------------------------------*/
  3543.  
  3544.  
  3545.     // Renders foreground event segments onto the grid. May return a subset of segs that were rendered.
  3546.     renderFgSegs: function(segs) {
  3547.         // subclasses must implement
  3548.     },
  3549.  
  3550.  
  3551.     // Unrenders all currently rendered foreground segments
  3552.     unrenderFgSegs: function() {
  3553.         // subclasses must implement
  3554.     },
  3555.  
  3556.  
  3557.     // Renders and assigns an `el` property for each foreground event segment.
  3558.     // Only returns segments that successfully rendered.
  3559.     // A utility that subclasses may use.
  3560.     renderFgSegEls: function(segs, disableResizing) {
  3561.         var view = this.view;
  3562.         var html = '';
  3563.         var renderedSegs = [];
  3564.         var i;
  3565.  
  3566.         if (segs.length) { // don't build an empty html string
  3567.  
  3568.             // build a large concatenation of event segment HTML
  3569.             for (i = 0; i < segs.length; i++) {
  3570.                 html += this.fgSegHtml(segs[i], disableResizing);
  3571.             }
  3572.  
  3573.             // Grab individual elements from the combined HTML string. Use each as the default rendering.
  3574.             // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
  3575.             $(html).each(function(i, node) {
  3576.                 var seg = segs[i];
  3577.                 var el = view.resolveEventEl(seg.event, $(node));
  3578.  
  3579.                 if (el) {
  3580.                     el.data('fc-seg', seg); // used by handlers
  3581.                     seg.el = el;
  3582.                     renderedSegs.push(seg);
  3583.                 }
  3584.             });
  3585.         }
  3586.  
  3587.         return renderedSegs;
  3588.     },
  3589.  
  3590.  
  3591.     // Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls()
  3592.     fgSegHtml: function(seg, disableResizing) {
  3593.         // subclasses should implement
  3594.     },
  3595.  
  3596.  
  3597.     /* Background Segment Rendering
  3598.     ------------------------------------------------------------------------------------------------------------------*/
  3599.  
  3600.  
  3601.     // Renders the given background event segments onto the grid.
  3602.     // Returns a subset of the segs that were actually rendered.
  3603.     renderBgSegs: function(segs) {
  3604.         return this.renderFill('bgEvent', segs);
  3605.     },
  3606.  
  3607.  
  3608.     // Unrenders all the currently rendered background event segments
  3609.     unrenderBgSegs: function() {
  3610.         this.unrenderFill('bgEvent');
  3611.     },
  3612.  
  3613.  
  3614.     // Renders a background event element, given the default rendering. Called by the fill system.
  3615.     bgEventSegEl: function(seg, el) {
  3616.         return this.view.resolveEventEl(seg.event, el); // will filter through eventRender
  3617.     },
  3618.  
  3619.  
  3620.     // Generates an array of classNames to be used for the default rendering of a background event.
  3621.     // Called by the fill system.
  3622.     bgEventSegClasses: function(seg) {
  3623.         var event = seg.event;
  3624.         var source = event.source || {};
  3625.  
  3626.         return [ 'fc-bgevent' ].concat(
  3627.             event.className,
  3628.             source.className || []
  3629.         );
  3630.     },
  3631.  
  3632.  
  3633.     // Generates a semicolon-separated CSS string to be used for the default rendering of a background event.
  3634.     // Called by the fill system.
  3635.     // TODO: consolidate with getEventSkinCss?
  3636.     bgEventSegCss: function(seg) {
  3637.         var view = this.view;
  3638.         var event = seg.event;
  3639.         var source = event.source || {};
  3640.  
  3641.         return {
  3642.             'background-color':
  3643.                 event.backgroundColor ||
  3644.                 event.color ||
  3645.                 source.backgroundColor ||
  3646.                 source.color ||
  3647.                 view.opt('eventBackgroundColor') ||
  3648.                 view.opt('eventColor')
  3649.         };
  3650.     },
  3651.  
  3652.  
  3653.     // Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system.
  3654.     businessHoursSegClasses: function(seg) {
  3655.         return [ 'fc-nonbusiness', 'fc-bgevent' ];
  3656.     },
  3657.  
  3658.  
  3659.     /* Handlers
  3660.     ------------------------------------------------------------------------------------------------------------------*/
  3661.  
  3662.  
  3663.     // Attaches event-element-related handlers to the container element and leverage bubbling
  3664.     bindSegHandlers: function() {
  3665.         var _this = this;
  3666.         var view = this.view;
  3667.  
  3668.         $.each(
  3669.             {
  3670.                 mouseenter: function(seg, ev) {
  3671.                     _this.triggerSegMouseover(seg, ev);
  3672.                 },
  3673.                 mouseleave: function(seg, ev) {
  3674.                     _this.triggerSegMouseout(seg, ev);
  3675.                 },
  3676.                 click: function(seg, ev) {
  3677.                     return view.trigger('eventClick', this, seg.event, ev); // can return `false` to cancel
  3678.                 },
  3679.                 mousedown: function(seg, ev) {
  3680.                     if ($(ev.target).is('.fc-resizer') && view.isEventResizable(seg.event)) {
  3681.                         _this.segResizeMousedown(seg, ev, $(ev.target).is('.fc-start-resizer'));
  3682.                     }
  3683.                     else if (view.isEventDraggable(seg.event)) {
  3684.                         _this.segDragMousedown(seg, ev);
  3685.                     }
  3686.                 }
  3687.             },
  3688.             function(name, func) {
  3689.                 // attach the handler to the container element and only listen for real event elements via bubbling
  3690.                 _this.el.on(name, '.fc-event-container > *', function(ev) {
  3691.                     var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents
  3692.  
  3693.                     // only call the handlers if there is not a drag/resize in progress
  3694.                     if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) {
  3695.                         return func.call(this, seg, ev); // `this` will be the event element
  3696.                     }
  3697.                 });
  3698.             }
  3699.         );
  3700.     },
  3701.  
  3702.  
  3703.     // Updates internal state and triggers handlers for when an event element is moused over
  3704.     triggerSegMouseover: function(seg, ev) {
  3705.         if (!this.mousedOverSeg) {
  3706.             this.mousedOverSeg = seg;
  3707.             this.view.trigger('eventMouseover', seg.el[0], seg.event, ev);
  3708.         }
  3709.     },
  3710.  
  3711.  
  3712.     // Updates internal state and triggers handlers for when an event element is moused out.
  3713.     // Can be given no arguments, in which case it will mouseout the segment that was previously moused over.
  3714.     triggerSegMouseout: function(seg, ev) {
  3715.         ev = ev || {}; // if given no args, make a mock mouse event
  3716.  
  3717.         if (this.mousedOverSeg) {
  3718.             seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment
  3719.             this.mousedOverSeg = null;
  3720.             this.view.trigger('eventMouseout', seg.el[0], seg.event, ev);
  3721.         }
  3722.     },
  3723.  
  3724.  
  3725.     /* Event Dragging
  3726.     ------------------------------------------------------------------------------------------------------------------*/
  3727.  
  3728.  
  3729.     // Called when the user does a mousedown on an event, which might lead to dragging.
  3730.     // Generic enough to work with any type of Grid.
  3731.     segDragMousedown: function(seg, ev) {
  3732.         var _this = this;
  3733.         var view = this.view;
  3734.         var calendar = view.calendar;
  3735.         var el = seg.el;
  3736.         var event = seg.event;
  3737.         var dropLocation;
  3738.  
  3739.         // A clone of the original element that will move with the mouse
  3740.         var mouseFollower = new MouseFollower(seg.el, {
  3741.             parentEl: view.el,
  3742.             opacity: view.opt('dragOpacity'),
  3743.             revertDuration: view.opt('dragRevertDuration'),
  3744.             zIndex: 2 // one above the .fc-view
  3745.         });
  3746.  
  3747.         // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents
  3748.         // of the view.
  3749.         var dragListener = new CellDragListener(view.coordMap, {
  3750.             distance: 5,
  3751.             scroll: view.opt('dragScroll'),
  3752.             subjectEl: el,
  3753.             subjectCenter: true,
  3754.             listenStart: function(ev) {
  3755.                 mouseFollower.hide(); // don't show until we know this is a real drag
  3756.                 mouseFollower.start(ev);
  3757.             },
  3758.             dragStart: function(ev) {
  3759.                 _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
  3760.                 _this.segDragStart(seg, ev);
  3761.                 view.hideEvent(event); // hide all event segments. our mouseFollower will take over
  3762.             },
  3763.             cellOver: function(cell, isOrig, origCell) {
  3764.  
  3765.                 // starting cell could be forced (DayGrid.limit)
  3766.                 if (seg.cell) {
  3767.                     origCell = seg.cell;
  3768.                 }
  3769.  
  3770.                 dropLocation = _this.computeEventDrop(origCell, cell, event);
  3771.  
  3772.                 if (dropLocation && !calendar.isEventRangeAllowed(dropLocation, event)) {
  3773.                     disableCursor();
  3774.                     dropLocation = null;
  3775.                 }
  3776.  
  3777.                 // if a valid drop location, have the subclass render a visual indication
  3778.                 if (dropLocation && view.renderDrag(dropLocation, seg)) {
  3779.                     mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own
  3780.                 }
  3781.                 else {
  3782.                     mouseFollower.show(); // otherwise, have the helper follow the mouse (no snapping)
  3783.                 }
  3784.  
  3785.                 if (isOrig) {
  3786.                     dropLocation = null; // needs to have moved cells to be a valid drop
  3787.                 }
  3788.             },
  3789.             cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells
  3790.                 view.unrenderDrag(); // unrender whatever was done in renderDrag
  3791.                 mouseFollower.show(); // show in case we are moving out of all cells
  3792.                 dropLocation = null;
  3793.             },
  3794.             cellDone: function() { // Called after a cellOut OR before a dragStop
  3795.                 enableCursor();
  3796.             },
  3797.             dragStop: function(ev) {
  3798.                 // do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
  3799.                 mouseFollower.stop(!dropLocation, function() {
  3800.                     view.unrenderDrag();
  3801.                     view.showEvent(event);
  3802.                     _this.segDragStop(seg, ev);
  3803.  
  3804.                     if (dropLocation) {
  3805.                         view.reportEventDrop(event, dropLocation, this.largeUnit, el, ev);
  3806.                     }
  3807.                 });
  3808.             },
  3809.             listenStop: function() {
  3810.                 mouseFollower.stop(); // put in listenStop in case there was a mousedown but the drag never started
  3811.             }
  3812.         });
  3813.  
  3814.         dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart
  3815.     },
  3816.  
  3817.  
  3818.     // Called before event segment dragging starts
  3819.     segDragStart: function(seg, ev) {
  3820.         this.isDraggingSeg = true;
  3821.         this.view.trigger('eventDragStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
  3822.     },
  3823.  
  3824.  
  3825.     // Called after event segment dragging stops
  3826.     segDragStop: function(seg, ev) {
  3827.         this.isDraggingSeg = false;
  3828.         this.view.trigger('eventDragStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
  3829.     },
  3830.  
  3831.  
  3832.     // Given the cell an event drag began, and the cell event was dropped, calculates the new start/end/allDay
  3833.     // values for the event. Subclasses may override and set additional properties to be used by renderDrag.
  3834.     // A falsy returned value indicates an invalid drop.
  3835.     computeEventDrop: function(startCell, endCell, event) {
  3836.         var calendar = this.view.calendar;
  3837.         var dragStart = startCell.start;
  3838.         var dragEnd = endCell.start;
  3839.         var delta;
  3840.         var dropLocation;
  3841.  
  3842.         if (dragStart.hasTime() === dragEnd.hasTime()) {
  3843.             delta = this.diffDates(dragEnd, dragStart);
  3844.  
  3845.             // if an all-day event was in a timed area and it was dragged to a different time,
  3846.             // guarantee an end and adjust start/end to have times
  3847.             if (event.allDay && durationHasTime(delta)) {
  3848.                 dropLocation = {
  3849.                     start: event.start.clone(),
  3850.                     end: calendar.getEventEnd(event), // will be an ambig day
  3851.                     allDay: false // for normalizeEventRangeTimes
  3852.                 };
  3853.                 calendar.normalizeEventRangeTimes(dropLocation);
  3854.             }
  3855.             // othewise, work off existing values
  3856.             else {
  3857.                 dropLocation = {
  3858.                     start: event.start.clone(),
  3859.                     end: event.end ? event.end.clone() : null,
  3860.                     allDay: event.allDay // keep it the same
  3861.                 };
  3862.             }
  3863.  
  3864.             dropLocation.start.add(delta);
  3865.             if (dropLocation.end) {
  3866.                 dropLocation.end.add(delta);
  3867.             }
  3868.         }
  3869.         else {
  3870.             // if switching from day <-> timed, start should be reset to the dropped date, and the end cleared
  3871.             dropLocation = {
  3872.                 start: dragEnd.clone(),
  3873.                 end: null, // end should be cleared
  3874.                 allDay: !dragEnd.hasTime()
  3875.             };
  3876.         }
  3877.  
  3878.         return dropLocation;
  3879.     },
  3880.  
  3881.  
  3882.     // Utility for apply dragOpacity to a jQuery set
  3883.     applyDragOpacity: function(els) {
  3884.         var opacity = this.view.opt('dragOpacity');
  3885.  
  3886.         if (opacity != null) {
  3887.             els.each(function(i, node) {
  3888.                 // Don't use jQuery (will set an IE filter), do it the old fashioned way.
  3889.                 // In IE8, a helper element will disappears if there's a filter.
  3890.                 node.style.opacity = opacity;
  3891.             });
  3892.         }
  3893.     },
  3894.  
  3895.  
  3896.     /* External Element Dragging
  3897.     ------------------------------------------------------------------------------------------------------------------*/
  3898.  
  3899.  
  3900.     // Called when a jQuery UI drag is initiated anywhere in the DOM
  3901.     externalDragStart: function(ev, ui) {
  3902.         var view = this.view;
  3903.         var el;
  3904.         var accept;
  3905.  
  3906.         if (view.opt('droppable')) { // only listen if this setting is on
  3907.             el = $((ui ? ui.item : null) || ev.target);
  3908.  
  3909.             // Test that the dragged element passes the dropAccept selector or filter function.
  3910.             // FYI, the default is "*" (matches all)
  3911.             accept = view.opt('dropAccept');
  3912.             if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) {
  3913.                 if (!this.isDraggingExternal) { // prevent double-listening if fired twice
  3914.                     this.listenToExternalDrag(el, ev, ui);
  3915.                 }
  3916.             }
  3917.         }
  3918.     },
  3919.  
  3920.  
  3921.     // Called when a jQuery UI drag starts and it needs to be monitored for cell dropping
  3922.     listenToExternalDrag: function(el, ev, ui) {
  3923.         var _this = this;
  3924.         var meta = getDraggedElMeta(el); // extra data about event drop, including possible event to create
  3925.         var dragListener;
  3926.         var dropLocation; // a null value signals an unsuccessful drag
  3927.  
  3928.         // listener that tracks mouse movement over date-associated pixel regions
  3929.         dragListener = new CellDragListener(this.coordMap, {
  3930.             listenStart: function() {
  3931.                 _this.isDraggingExternal = true;
  3932.             },
  3933.             cellOver: function(cell) {
  3934.                 dropLocation = _this.computeExternalDrop(cell, meta);
  3935.                 if (dropLocation) {
  3936.                     _this.renderDrag(dropLocation); // called without a seg parameter
  3937.                 }
  3938.                 else { // invalid drop cell
  3939.                     disableCursor();
  3940.                 }
  3941.             },
  3942.             cellOut: function() {
  3943.                 dropLocation = null; // signal unsuccessful
  3944.                 _this.unrenderDrag();
  3945.                 enableCursor();
  3946.             },
  3947.             dragStop: function() {
  3948.                 _this.unrenderDrag();
  3949.                 enableCursor();
  3950.  
  3951.                 if (dropLocation) { // element was dropped on a valid date/time cell
  3952.                     _this.view.reportExternalDrop(meta, dropLocation, el, ev, ui);
  3953.                 }
  3954.             },
  3955.             listenStop: function() {
  3956.                 _this.isDraggingExternal = false;
  3957.             }
  3958.         });
  3959.  
  3960.         dragListener.startDrag(ev); // start listening immediately
  3961.     },
  3962.  
  3963.  
  3964.     // Given a cell to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object),
  3965.     // returns start/end dates for the event that would result from the hypothetical drop. end might be null.
  3966.     // Returning a null value signals an invalid drop cell.
  3967.     computeExternalDrop: function(cell, meta) {
  3968.         var dropLocation = {
  3969.             start: cell.start.clone(),
  3970.             end: null
  3971.         };
  3972.  
  3973.         // if dropped on an all-day cell, and element's metadata specified a time, set it
  3974.         if (meta.startTime && !dropLocation.start.hasTime()) {
  3975.             dropLocation.start.time(meta.startTime);
  3976.         }
  3977.  
  3978.         if (meta.duration) {
  3979.             dropLocation.end = dropLocation.start.clone().add(meta.duration);
  3980.         }
  3981.  
  3982.         if (!this.view.calendar.isExternalDropRangeAllowed(dropLocation, meta.eventProps)) {
  3983.             return null;
  3984.         }
  3985.  
  3986.         return dropLocation;
  3987.     },
  3988.  
  3989.  
  3990.  
  3991.     /* Drag Rendering (for both events and an external elements)
  3992.     ------------------------------------------------------------------------------------------------------------------*/
  3993.  
  3994.  
  3995.     // Renders a visual indication of an event or external element being dragged.
  3996.     // `dropLocation` contains hypothetical start/end/allDay values the event would have if dropped. end can be null.
  3997.     // `seg` is the internal segment object that is being dragged. If dragging an external element, `seg` is null.
  3998.     // A truthy returned value indicates this method has rendered a helper element.
  3999.     renderDrag: function(dropLocation, seg) {
  4000.         // subclasses must implement
  4001.     },
  4002.  
  4003.  
  4004.     // Unrenders a visual indication of an event or external element being dragged
  4005.     unrenderDrag: function() {
  4006.         // subclasses must implement
  4007.     },
  4008.  
  4009.  
  4010.     /* Resizing
  4011.     ------------------------------------------------------------------------------------------------------------------*/
  4012.  
  4013.  
  4014.     // Called when the user does a mousedown on an event's resizer, which might lead to resizing.
  4015.     // Generic enough to work with any type of Grid.
  4016.     segResizeMousedown: function(seg, ev, isStart) {
  4017.         var _this = this;
  4018.         var view = this.view;
  4019.         var calendar = view.calendar;
  4020.         var el = seg.el;
  4021.         var event = seg.event;
  4022.         var eventEnd = calendar.getEventEnd(event);
  4023.         var dragListener;
  4024.         var resizeLocation; // falsy if invalid resize
  4025.  
  4026.         // Tracks mouse movement over the *grid's* coordinate map
  4027.         dragListener = new CellDragListener(this.coordMap, {
  4028.             distance: 5,
  4029.             scroll: view.opt('dragScroll'),
  4030.             subjectEl: el,
  4031.             dragStart: function(ev) {
  4032.                 _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
  4033.                 _this.segResizeStart(seg, ev);
  4034.             },
  4035.             cellOver: function(cell, isOrig, origCell) {
  4036.                 resizeLocation = isStart ?
  4037.                     _this.computeEventStartResize(origCell, cell, event) :
  4038.                     _this.computeEventEndResize(origCell, cell, event);
  4039.  
  4040.                 if (resizeLocation) {
  4041.                     if (!calendar.isEventRangeAllowed(resizeLocation, event)) {
  4042.                         disableCursor();
  4043.                         resizeLocation = null;
  4044.                     }
  4045.                     // no change? (TODO: how does this work with timezones?)
  4046.                     else if (resizeLocation.start.isSame(event.start) && resizeLocation.end.isSame(eventEnd)) {
  4047.                         resizeLocation = null;
  4048.                     }
  4049.                 }
  4050.  
  4051.                 if (resizeLocation) {
  4052.                     view.hideEvent(event);
  4053.                     _this.renderEventResize(resizeLocation, seg);
  4054.                 }
  4055.             },
  4056.             cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells
  4057.                 resizeLocation = null;
  4058.             },
  4059.             cellDone: function() { // resets the rendering to show the original event
  4060.                 _this.unrenderEventResize();
  4061.                 view.showEvent(event);
  4062.                 enableCursor();
  4063.             },
  4064.             dragStop: function(ev) {
  4065.                 _this.segResizeStop(seg, ev);
  4066.  
  4067.                 if (resizeLocation) { // valid date to resize to?
  4068.                     view.reportEventResize(event, resizeLocation, this.largeUnit, el, ev);
  4069.                 }
  4070.             }
  4071.         });
  4072.  
  4073.         dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart
  4074.     },
  4075.  
  4076.  
  4077.     // Called before event segment resizing starts
  4078.     segResizeStart: function(seg, ev) {
  4079.         this.isResizingSeg = true;
  4080.         this.view.trigger('eventResizeStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
  4081.     },
  4082.  
  4083.  
  4084.     // Called after event segment resizing stops
  4085.     segResizeStop: function(seg, ev) {
  4086.         this.isResizingSeg = false;
  4087.         this.view.trigger('eventResizeStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
  4088.     },
  4089.  
  4090.  
  4091.     // Returns new date-information for an event segment being resized from its start
  4092.     computeEventStartResize: function(startCell, endCell, event) {
  4093.         return this.computeEventResize('start', startCell, endCell, event);
  4094.     },
  4095.  
  4096.  
  4097.     // Returns new date-information for an event segment being resized from its end
  4098.     computeEventEndResize: function(startCell, endCell, event) {
  4099.         return this.computeEventResize('end', startCell, endCell, event);
  4100.     },
  4101.  
  4102.  
  4103.     // Returns new date-information for an event segment being resized from its start OR end
  4104.     // `type` is either 'start' or 'end'
  4105.     computeEventResize: function(type, startCell, endCell, event) {
  4106.         var calendar = this.view.calendar;
  4107.         var delta = this.diffDates(endCell[type], startCell[type]);
  4108.         var range;
  4109.         var defaultDuration;
  4110.  
  4111.         // build original values to work from, guaranteeing a start and end
  4112.         range = {
  4113.             start: event.start.clone(),
  4114.             end: calendar.getEventEnd(event),
  4115.             allDay: event.allDay
  4116.         };
  4117.  
  4118.         // if an all-day event was in a timed area and was resized to a time, adjust start/end to have times
  4119.         if (range.allDay && durationHasTime(delta)) {
  4120.             range.allDay = false;
  4121.             calendar.normalizeEventRangeTimes(range);
  4122.         }
  4123.  
  4124.         range[type].add(delta); // apply delta to start or end
  4125.  
  4126.         // if the event was compressed too small, find a new reasonable duration for it
  4127.         if (!range.start.isBefore(range.end)) {
  4128.  
  4129.             defaultDuration = event.allDay ?
  4130.                 calendar.defaultAllDayEventDuration :
  4131.                 calendar.defaultTimedEventDuration;
  4132.  
  4133.             // between the cell's duration and the event's default duration, use the smaller of the two.
  4134.             // example: if year-length slots, and compressed to one slot, we don't want the event to be a year long
  4135.             if (this.cellDuration && this.cellDuration < defaultDuration) {
  4136.                 defaultDuration = this.cellDuration;
  4137.             }
  4138.  
  4139.             if (type == 'start') { // resizing the start?
  4140.                 range.start = range.end.clone().subtract(defaultDuration);
  4141.             }
  4142.             else { // resizing the end?
  4143.                 range.end = range.start.clone().add(defaultDuration);
  4144.             }
  4145.         }
  4146.  
  4147.         return range;
  4148.     },
  4149.  
  4150.  
  4151.     // Renders a visual indication of an event being resized.
  4152.     // `range` has the updated dates of the event. `seg` is the original segment object involved in the drag.
  4153.     renderEventResize: function(range, seg) {
  4154.         // subclasses must implement
  4155.     },
  4156.  
  4157.  
  4158.     // Unrenders a visual indication of an event being resized.
  4159.     unrenderEventResize: function() {
  4160.         // subclasses must implement
  4161.     },
  4162.  
  4163.  
  4164.     /* Rendering Utils
  4165.     ------------------------------------------------------------------------------------------------------------------*/
  4166.  
  4167.  
  4168.     // Compute the text that should be displayed on an event's element.
  4169.     // `range` can be the Event object itself, or something range-like, with at least a `start`.
  4170.     // If event times are disabled, or the event has no time, will return a blank string.
  4171.     // If not specified, formatStr will default to the eventTimeFormat setting,
  4172.     // and displayEnd will default to the displayEventEnd setting.
  4173.     getEventTimeText: function(range, formatStr, displayEnd) {
  4174.  
  4175.         if (formatStr == null) {
  4176.             formatStr = this.eventTimeFormat;
  4177.         }
  4178.  
  4179.         if (displayEnd == null) {
  4180.             displayEnd = this.displayEventEnd;
  4181.         }
  4182.  
  4183.         if (this.displayEventTime && range.start.hasTime()) {
  4184.             if (displayEnd && range.end) {
  4185.                 return this.view.formatRange(range, formatStr);
  4186.             }
  4187.             else {
  4188.                 return range.start.format(formatStr);
  4189.             }
  4190.         }
  4191.  
  4192.         return '';
  4193.     },
  4194.  
  4195.  
  4196.     // Generic utility for generating the HTML classNames for an event segment's element
  4197.     getSegClasses: function(seg, isDraggable, isResizable) {
  4198.         var event = seg.event;
  4199.         var classes = [
  4200.             'fc-event',
  4201.             seg.isStart ? 'fc-start' : 'fc-not-start',
  4202.             seg.isEnd ? 'fc-end' : 'fc-not-end'
  4203.         ].concat(
  4204.             event.className,
  4205.             event.source ? event.source.className : []
  4206.         );
  4207.  
  4208.         if (isDraggable) {
  4209.             classes.push('fc-draggable');
  4210.         }
  4211.         if (isResizable) {
  4212.             classes.push('fc-resizable');
  4213.         }
  4214.  
  4215.         return classes;
  4216.     },
  4217.  
  4218.  
  4219.     // Utility for generating event skin-related CSS properties
  4220.     getEventSkinCss: function(event) {
  4221.         var view = this.view;
  4222.         var source = event.source || {};
  4223.         var eventColor = event.color;
  4224.         var sourceColor = source.color;
  4225.         var optionColor = view.opt('eventColor');
  4226.  
  4227.         return {
  4228.             'background-color':
  4229.                 event.backgroundColor ||
  4230.                 eventColor ||
  4231.                 source.backgroundColor ||
  4232.                 sourceColor ||
  4233.                 view.opt('eventBackgroundColor') ||
  4234.                 optionColor,
  4235.             'border-color':
  4236.                 event.borderColor ||
  4237.                 eventColor ||
  4238.                 source.borderColor ||
  4239.                 sourceColor ||
  4240.                 view.opt('eventBorderColor') ||
  4241.                 optionColor,
  4242.             color:
  4243.                 event.textColor ||
  4244.                 source.textColor ||
  4245.                 view.opt('eventTextColor')
  4246.         };
  4247.     },
  4248.  
  4249.  
  4250.     /* Converting events -> ranges -> segs
  4251.     ------------------------------------------------------------------------------------------------------------------*/
  4252.  
  4253.  
  4254.     // Converts an array of event objects into an array of event segment objects.
  4255.     // A custom `rangeToSegsFunc` may be given for arbitrarily slicing up events.
  4256.     // Doesn't guarantee an order for the resulting array.
  4257.     eventsToSegs: function(events, rangeToSegsFunc) {
  4258.         var eventRanges = this.eventsToRanges(events);
  4259.         var segs = [];
  4260.         var i;
  4261.  
  4262.         for (i = 0; i < eventRanges.length; i++) {
  4263.             segs.push.apply(
  4264.                 segs,
  4265.                 this.eventRangeToSegs(eventRanges[i], rangeToSegsFunc)
  4266.             );
  4267.         }
  4268.  
  4269.         return segs;
  4270.     },
  4271.  
  4272.  
  4273.     // Converts an array of events into an array of "range" objects.
  4274.     // A "range" object is a plain object with start/end properties denoting the time it covers. Also an event property.
  4275.     // For "normal" events, this will be identical to the event's start/end, but for "inverse-background" events,
  4276.     // will create an array of ranges that span the time *not* covered by the given event.
  4277.     // Doesn't guarantee an order for the resulting array.
  4278.     eventsToRanges: function(events) {
  4279.         var _this = this;
  4280.         var eventsById = groupEventsById(events);
  4281.         var ranges = [];
  4282.  
  4283.         // group by ID so that related inverse-background events can be rendered together
  4284.         $.each(eventsById, function(id, eventGroup) {
  4285.             if (eventGroup.length) {
  4286.                 ranges.push.apply(
  4287.                     ranges,
  4288.                     isInverseBgEvent(eventGroup[0]) ?
  4289.                         _this.eventsToInverseRanges(eventGroup) :
  4290.                         _this.eventsToNormalRanges(eventGroup)
  4291.                 );
  4292.             }
  4293.         });
  4294.  
  4295.         return ranges;
  4296.     },
  4297.  
  4298.  
  4299.     // Converts an array of "normal" events (not inverted rendering) into a parallel array of ranges
  4300.     eventsToNormalRanges: function(events) {
  4301.         var calendar = this.view.calendar;
  4302.         var ranges = [];
  4303.         var i, event;
  4304.         var eventStart, eventEnd;
  4305.  
  4306.         for (i = 0; i < events.length; i++) {
  4307.             event = events[i];
  4308.  
  4309.             // make copies and normalize by stripping timezone
  4310.             eventStart = event.start.clone().stripZone();
  4311.             eventEnd = calendar.getEventEnd(event).stripZone();
  4312.  
  4313.             ranges.push({
  4314.                 event: event,
  4315.                 start: eventStart,
  4316.                 end: eventEnd,
  4317.                 eventStartMS: +eventStart,
  4318.                 eventDurationMS: eventEnd - eventStart
  4319.             });
  4320.         }
  4321.  
  4322.         return ranges;
  4323.     },
  4324.  
  4325.  
  4326.     // Converts an array of events, with inverse-background rendering, into an array of range objects.
  4327.     // The range objects will cover all the time NOT covered by the events.
  4328.     eventsToInverseRanges: function(events) {
  4329.         var view = this.view;
  4330.         var viewStart = view.start.clone().stripZone(); // normalize timezone
  4331.         var viewEnd = view.end.clone().stripZone(); // normalize timezone
  4332.         var normalRanges = this.eventsToNormalRanges(events); // will give us normalized dates we can use w/o copies
  4333.         var inverseRanges = [];
  4334.         var event0 = events[0]; // assign this to each range's `.event`
  4335.         var start = viewStart; // the end of the previous range. the start of the new range
  4336.         var i, normalRange;
  4337.  
  4338.         // ranges need to be in order. required for our date-walking algorithm
  4339.         normalRanges.sort(compareNormalRanges);
  4340.  
  4341.         for (i = 0; i < normalRanges.length; i++) {
  4342.             normalRange = normalRanges[i];
  4343.  
  4344.             // add the span of time before the event (if there is any)
  4345.             if (normalRange.start > start) { // compare millisecond time (skip any ambig logic)
  4346.                 inverseRanges.push({
  4347.                     event: event0,
  4348.                     start: start,
  4349.                     end: normalRange.start
  4350.                 });
  4351.             }
  4352.  
  4353.             start = normalRange.end;
  4354.         }
  4355.  
  4356.         // add the span of time after the last event (if there is any)
  4357.         if (start < viewEnd) { // compare millisecond time (skip any ambig logic)
  4358.             inverseRanges.push({
  4359.                 event: event0,
  4360.                 start: start,
  4361.                 end: viewEnd
  4362.             });
  4363.         }
  4364.  
  4365.         return inverseRanges;
  4366.     },
  4367.  
  4368.  
  4369.     // Slices the given event range into one or more segment objects.
  4370.     // A `rangeToSegsFunc` custom slicing function can be given.
  4371.     eventRangeToSegs: function(eventRange, rangeToSegsFunc) {
  4372.         var segs;
  4373.         var i, seg;
  4374.  
  4375.         eventRange = this.view.calendar.ensureVisibleEventRange(eventRange);
  4376.  
  4377.         if (rangeToSegsFunc) {
  4378.             segs = rangeToSegsFunc(eventRange);
  4379.         }
  4380.         else {
  4381.             segs = this.rangeToSegs(eventRange); // defined by the subclass
  4382.         }
  4383.  
  4384.         for (i = 0; i < segs.length; i++) {
  4385.             seg = segs[i];
  4386.             seg.event = eventRange.event;
  4387.             seg.eventStartMS = eventRange.eventStartMS;
  4388.             seg.eventDurationMS = eventRange.eventDurationMS;
  4389.         }
  4390.  
  4391.         return segs;
  4392.     }
  4393.  
  4394. });
  4395.  
  4396.  
  4397. /* Utilities
  4398. ----------------------------------------------------------------------------------------------------------------------*/
  4399.  
  4400.  
  4401. function isBgEvent(event) { // returns true if background OR inverse-background
  4402.     var rendering = getEventRendering(event);
  4403.     return rendering === 'background' || rendering === 'inverse-background';
  4404. }
  4405.  
  4406.  
  4407. function isInverseBgEvent(event) {
  4408.     return getEventRendering(event) === 'inverse-background';
  4409. }
  4410.  
  4411.  
  4412. function getEventRendering(event) {
  4413.     return firstDefined((event.source || {}).rendering, event.rendering);
  4414. }
  4415.  
  4416.  
  4417. function groupEventsById(events) {
  4418.     var eventsById = {};
  4419.     var i, event;
  4420.  
  4421.     for (i = 0; i < events.length; i++) {
  4422.         event = events[i];
  4423.         (eventsById[event._id] || (eventsById[event._id] = [])).push(event);
  4424.     }
  4425.  
  4426.     return eventsById;
  4427. }
  4428.  
  4429.  
  4430. // A cmp function for determining which non-inverted "ranges" (see above) happen earlier
  4431. function compareNormalRanges(range1, range2) {
  4432.     return range1.eventStartMS - range2.eventStartMS; // earlier ranges go first
  4433. }
  4434.  
  4435.  
  4436. // A cmp function for determining which segments should take visual priority
  4437. // DOES NOT WORK ON INVERTED BACKGROUND EVENTS because they have no eventStartMS/eventDurationMS
  4438. function compareSegs(seg1, seg2) {
  4439.     return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first
  4440.         seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first
  4441.         seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1)
  4442.         (seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title
  4443. }
  4444.  
  4445. fc.compareSegs = compareSegs; // export
  4446.  
  4447.  
  4448. /* External-Dragging-Element Data
  4449. ----------------------------------------------------------------------------------------------------------------------*/
  4450.  
  4451. // Require all HTML5 data-* attributes used by FullCalendar to have this prefix.
  4452. // A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event.
  4453. fc.dataAttrPrefix = '';
  4454.  
  4455. // Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure
  4456. // to be used for Event Object creation.
  4457. // A defined `.eventProps`, even when empty, indicates that an event should be created.
  4458. function getDraggedElMeta(el) {
  4459.     var prefix = fc.dataAttrPrefix;
  4460.     var eventProps; // properties for creating the event, not related to date/time
  4461.     var startTime; // a Duration
  4462.     var duration;
  4463.     var stick;
  4464.  
  4465.     if (prefix) { prefix += '-'; }
  4466.     eventProps = el.data(prefix + 'event') || null;
  4467.  
  4468.     if (eventProps) {
  4469.         if (typeof eventProps === 'object') {
  4470.             eventProps = $.extend({}, eventProps); // make a copy
  4471.         }
  4472.         else { // something like 1 or true. still signal event creation
  4473.             eventProps = {};
  4474.         }
  4475.  
  4476.         // pluck special-cased date/time properties
  4477.         startTime = eventProps.start;
  4478.         if (startTime == null) { startTime = eventProps.time; } // accept 'time' as well
  4479.         duration = eventProps.duration;
  4480.         stick = eventProps.stick;
  4481.         delete eventProps.start;
  4482.         delete eventProps.time;
  4483.         delete eventProps.duration;
  4484.         delete eventProps.stick;
  4485.     }
  4486.  
  4487.     // fallback to standalone attribute values for each of the date/time properties
  4488.     if (startTime == null) { startTime = el.data(prefix + 'start'); }
  4489.     if (startTime == null) { startTime = el.data(prefix + 'time'); } // accept 'time' as well
  4490.     if (duration == null) { duration = el.data(prefix + 'duration'); }
  4491.     if (stick == null) { stick = el.data(prefix + 'stick'); }
  4492.  
  4493.     // massage into correct data types
  4494.     startTime = startTime != null ? moment.duration(startTime) : null;
  4495.     duration = duration != null ? moment.duration(duration) : null;
  4496.     stick = Boolean(stick);
  4497.  
  4498.     return { eventProps: eventProps, startTime: startTime, duration: duration, stick: stick };
  4499. }
  4500.  
  4501.  
  4502. ;;
  4503.  
  4504. /* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week.
  4505. ----------------------------------------------------------------------------------------------------------------------*/
  4506.  
  4507. var DayGrid = Grid.extend({
  4508.  
  4509.     numbersVisible: false, // should render a row for day/week numbers? set by outside view. TODO: make internal
  4510.     bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid
  4511.     breakOnWeeks: null, // should create a new row for each week? set by outside view
  4512.  
  4513.     cellDates: null, // flat chronological array of each cell's dates
  4514.     dayToCellOffsets: null, // maps days offsets from grid's start date, to cell offsets
  4515.  
  4516.     rowEls: null, // set of fake row elements
  4517.     dayEls: null, // set of whole-day elements comprising the row's background
  4518.     helperEls: null, // set of cell skeleton elements for rendering the mock event "helper"
  4519.  
  4520.  
  4521.     constructor: function() {
  4522.         Grid.apply(this, arguments);
  4523.  
  4524.         this.cellDuration = moment.duration(1, 'day'); // for Grid system
  4525.     },
  4526.  
  4527.  
  4528.     // Renders the rows and columns into the component's `this.el`, which should already be assigned.
  4529.     // isRigid determins whether the individual rows should ignore the contents and be a constant height.
  4530.     // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient.
  4531.     renderDates: function(isRigid) {
  4532.         var view = this.view;
  4533.         var rowCnt = this.rowCnt;
  4534.         var colCnt = this.colCnt;
  4535.         var cellCnt = rowCnt * colCnt;
  4536.         var html = '';
  4537.         var row;
  4538.         var i, cell;
  4539.  
  4540.         for (row = 0; row < rowCnt; row++) {
  4541.             html += this.dayRowHtml(row, isRigid);
  4542.         }
  4543.         this.el.html(html);
  4544.  
  4545.         this.rowEls = this.el.find('.fc-row');
  4546.         this.dayEls = this.el.find('.fc-day');
  4547.  
  4548.         // trigger dayRender with each cell's element
  4549.         for (i = 0; i < cellCnt; i++) {
  4550.             cell = this.getCell(i);
  4551.             view.trigger('dayRender', null, cell.start, this.dayEls.eq(i));
  4552.         }
  4553.     },
  4554.  
  4555.  
  4556.     unrenderDates: function() {
  4557.         this.removeSegPopover();
  4558.     },
  4559.  
  4560.  
  4561.     renderBusinessHours: function() {
  4562.         var events = this.view.calendar.getBusinessHoursEvents(true); // wholeDay=true
  4563.         var segs = this.eventsToSegs(events);
  4564.  
  4565.         this.renderFill('businessHours', segs, 'bgevent');
  4566.     },
  4567.  
  4568.  
  4569.     // Generates the HTML for a single row. `row` is the row number.
  4570.     dayRowHtml: function(row, isRigid) {
  4571.         var view = this.view;
  4572.         var classes = [ 'fc-row', 'fc-week', view.widgetContentClass ];
  4573.  
  4574.         if (isRigid) {
  4575.             classes.push('fc-rigid');
  4576.         }
  4577.  
  4578.         return '' +
  4579.             '<div class="' + classes.join(' ') + '">' +
  4580.                 '<div class="fc-bg">' +
  4581.                     '<table>' +
  4582.                         this.rowHtml('day', row) + // leverages RowRenderer. calls dayCellHtml()
  4583.                     '</table>' +
  4584.                 '</div>' +
  4585.                 '<div class="fc-content-skeleton">' +
  4586.                     '<table>' +
  4587.                         (this.numbersVisible ?
  4588.                             '<thead>' +
  4589.                                 this.rowHtml('number', row) + // leverages RowRenderer. View will define render method
  4590.                             '</thead>' :
  4591.                             ''
  4592.                             ) +
  4593.                     '</table>' +
  4594.                 '</div>' +
  4595.             '</div>';
  4596.     },
  4597.  
  4598.  
  4599.     // Renders the HTML for a whole-day cell. Will eventually end up in the day-row's background.
  4600.     // We go through a 'day' row type instead of just doing a 'bg' row type so that the View can do custom rendering
  4601.     // specifically for whole-day rows, whereas a 'bg' might also be used for other purposes (TimeGrid bg for example).
  4602.     dayCellHtml: function(cell) {
  4603.         return this.bgCellHtml(cell);
  4604.     },
  4605.  
  4606.  
  4607.     /* Options
  4608.     ------------------------------------------------------------------------------------------------------------------*/
  4609.  
  4610.  
  4611.     // Computes a default column header formatting string if `colFormat` is not explicitly defined
  4612.     computeColHeadFormat: function() {
  4613.         if (this.rowCnt > 1) { // more than one week row. day numbers will be in each cell
  4614.             return 'ddd'; // "Sat"
  4615.         }
  4616.         else if (this.colCnt > 1) { // multiple days, so full single date string WON'T be in title text
  4617.             return this.view.opt('dayOfMonthFormat'); // "Sat 12/10"
  4618.         }
  4619.         else { // single day, so full single date string will probably be in title text
  4620.             return 'dddd'; // "Saturday"
  4621.         }
  4622.     },
  4623.  
  4624.  
  4625.     // Computes a default event time formatting string if `timeFormat` is not explicitly defined
  4626.     computeEventTimeFormat: function() {
  4627.         return this.view.opt('noMeridiemTimeFormat'); // like "6p" or "6:30p"
  4628.     },
  4629.  
  4630.  
  4631.     // Computes a default `displayEventEnd` value if one is not expliclty defined
  4632.     computeDisplayEventEnd: function() {
  4633.         return this.colCnt == 1; // we'll likely have space if there's only one day
  4634.     },
  4635.  
  4636.  
  4637.     /* Cell System
  4638.     ------------------------------------------------------------------------------------------------------------------*/
  4639.  
  4640.  
  4641.     rangeUpdated: function() {
  4642.         var cellDates;
  4643.         var firstDay;
  4644.         var rowCnt;
  4645.         var colCnt;
  4646.  
  4647.         this.updateCellDates(); // populates cellDates and dayToCellOffsets
  4648.         cellDates = this.cellDates;
  4649.  
  4650.         if (this.breakOnWeeks) {
  4651.             // count columns until the day-of-week repeats
  4652.             firstDay = cellDates[0].day();
  4653.             for (colCnt = 1; colCnt < cellDates.length; colCnt++) {
  4654.                 if (cellDates[colCnt].day() == firstDay) {
  4655.                     break;
  4656.                 }
  4657.             }
  4658.             rowCnt = Math.ceil(cellDates.length / colCnt);
  4659.         }
  4660.         else {
  4661.             rowCnt = 1;
  4662.             colCnt = cellDates.length;
  4663.         }
  4664.  
  4665.         this.rowCnt = rowCnt;
  4666.         this.colCnt = colCnt;
  4667.     },
  4668.  
  4669.  
  4670.     // Populates cellDates and dayToCellOffsets
  4671.     updateCellDates: function() {
  4672.         var view = this.view;
  4673.         var date = this.start.clone();
  4674.         var dates = [];
  4675.         var offset = -1;
  4676.         var offsets = [];
  4677.  
  4678.         while (date.isBefore(this.end)) { // loop each day from start to end
  4679.             if (view.isHiddenDay(date)) {
  4680.                 offsets.push(offset + 0.5); // mark that it's between offsets
  4681.             }
  4682.             else {
  4683.                 offset++;
  4684.                 offsets.push(offset);
  4685.                 dates.push(date.clone());
  4686.             }
  4687.             date.add(1, 'days');
  4688.         }
  4689.  
  4690.         this.cellDates = dates;
  4691.         this.dayToCellOffsets = offsets;
  4692.     },
  4693.  
  4694.  
  4695.     // Given a cell object, generates its start date. Returns a reference-free copy.
  4696.     computeCellDate: function(cell) {
  4697.         var colCnt = this.colCnt;
  4698.         var index = cell.row * colCnt + (this.isRTL ? colCnt - cell.col - 1 : cell.col);
  4699.  
  4700.         return this.cellDates[index].clone();
  4701.     },
  4702.  
  4703.  
  4704.     // Retrieves the element representing the given row
  4705.     getRowEl: function(row) {
  4706.         return this.rowEls.eq(row);
  4707.     },
  4708.  
  4709.  
  4710.     // Retrieves the element representing the given column
  4711.     getColEl: function(col) {
  4712.         return this.dayEls.eq(col);
  4713.     },
  4714.  
  4715.  
  4716.     // Gets the whole-day element associated with the cell
  4717.     getCellDayEl: function(cell) {
  4718.         return this.dayEls.eq(cell.row * this.colCnt + cell.col);
  4719.     },
  4720.  
  4721.  
  4722.     // Overrides Grid's method for when row coordinates are computed
  4723.     computeRowCoords: function() {
  4724.         var rowCoords = Grid.prototype.computeRowCoords.call(this); // call the super-method
  4725.  
  4726.         // hack for extending last row (used by AgendaView)
  4727.         rowCoords[rowCoords.length - 1].bottom += this.bottomCoordPadding;
  4728.  
  4729.         return rowCoords;
  4730.     },
  4731.  
  4732.  
  4733.     /* Dates
  4734.     ------------------------------------------------------------------------------------------------------------------*/
  4735.  
  4736.  
  4737.     // Slices up a date range by row into an array of segments
  4738.     rangeToSegs: function(range) {
  4739.         var isRTL = this.isRTL;
  4740.         var rowCnt = this.rowCnt;
  4741.         var colCnt = this.colCnt;
  4742.         var segs = [];
  4743.         var first, last; // inclusive cell-offset range for given range
  4744.         var row;
  4745.         var rowFirst, rowLast; // inclusive cell-offset range for current row
  4746.         var isStart, isEnd;
  4747.         var segFirst, segLast; // inclusive cell-offset range for segment
  4748.         var seg;
  4749.  
  4750.         range = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold
  4751.         first = this.dateToCellOffset(range.start);
  4752.         last = this.dateToCellOffset(range.end.subtract(1, 'days')); // offset of inclusive end date
  4753.  
  4754.         for (row = 0; row < rowCnt; row++) {
  4755.             rowFirst = row * colCnt;
  4756.             rowLast = rowFirst + colCnt - 1;
  4757.  
  4758.             // intersect segment's offset range with the row's
  4759.             segFirst = Math.max(rowFirst, first);
  4760.             segLast = Math.min(rowLast, last);
  4761.  
  4762.             // deal with in-between indices
  4763.             segFirst = Math.ceil(segFirst); // in-between starts round to next cell
  4764.             segLast = Math.floor(segLast); // in-between ends round to prev cell
  4765.  
  4766.             if (segFirst <= segLast) { // was there any intersection with the current row?
  4767.  
  4768.                 // must be matching integers to be the segment's start/end
  4769.                 isStart = segFirst === first;
  4770.                 isEnd = segLast === last;
  4771.  
  4772.                 // translate offsets to be relative to start-of-row
  4773.                 segFirst -= rowFirst;
  4774.                 segLast -= rowFirst;
  4775.  
  4776.                 seg = { row: row, isStart: isStart, isEnd: isEnd };
  4777.                 if (isRTL) {
  4778.                     seg.leftCol = colCnt - segLast - 1;
  4779.                     seg.rightCol = colCnt - segFirst - 1;
  4780.                 }
  4781.                 else {
  4782.                     seg.leftCol = segFirst;
  4783.                     seg.rightCol = segLast;
  4784.                 }
  4785.                 segs.push(seg);
  4786.             }
  4787.         }
  4788.  
  4789.         return segs;
  4790.     },
  4791.  
  4792.  
  4793.     // Given a date, returns its chronolocial cell-offset from the first cell of the grid.
  4794.     // If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets.
  4795.     // If before the first offset, returns a negative number.
  4796.     // If after the last offset, returns an offset past the last cell offset.
  4797.     // Only works for *start* dates of cells. Will not work for exclusive end dates for cells.
  4798.     dateToCellOffset: function(date) {
  4799.         var offsets = this.dayToCellOffsets;
  4800.         var day = date.diff(this.start, 'days');
  4801.  
  4802.         if (day < 0) {
  4803.             return offsets[0] - 1;
  4804.         }
  4805.         else if (day >= offsets.length) {
  4806.             return offsets[offsets.length - 1] + 1;
  4807.         }
  4808.         else {
  4809.             return offsets[day];
  4810.         }
  4811.     },
  4812.  
  4813.  
  4814.     /* Event Drag Visualization
  4815.     ------------------------------------------------------------------------------------------------------------------*/
  4816.     // TODO: move to DayGrid.event, similar to what we did with Grid's drag methods
  4817.  
  4818.  
  4819.     // Renders a visual indication of an event or external element being dragged.
  4820.     // The dropLocation's end can be null. seg can be null. See Grid::renderDrag for more info.
  4821.     renderDrag: function(dropLocation, seg) {
  4822.  
  4823.         // always render a highlight underneath
  4824.         this.renderHighlight(this.eventRangeToSegs(dropLocation));
  4825.  
  4826.         // if a segment from the same calendar but another component is being dragged, render a helper event
  4827.         if (seg && !seg.el.closest(this.el).length) {
  4828.  
  4829.             this.renderRangeHelper(dropLocation, seg);
  4830.             this.applyDragOpacity(this.helperEls);
  4831.  
  4832.             return true; // a helper has been rendered
  4833.         }
  4834.     },
  4835.  
  4836.  
  4837.     // Unrenders any visual indication of a hovering event
  4838.     unrenderDrag: function() {
  4839.         this.unrenderHighlight();
  4840.         this.unrenderHelper();
  4841.     },
  4842.  
  4843.  
  4844.     /* Event Resize Visualization
  4845.     ------------------------------------------------------------------------------------------------------------------*/
  4846.  
  4847.  
  4848.     // Renders a visual indication of an event being resized
  4849.     renderEventResize: function(range, seg) {
  4850.         this.renderHighlight(this.eventRangeToSegs(range));
  4851.         this.renderRangeHelper(range, seg);
  4852.     },
  4853.  
  4854.  
  4855.     // Unrenders a visual indication of an event being resized
  4856.     unrenderEventResize: function() {
  4857.         this.unrenderHighlight();
  4858.         this.unrenderHelper();
  4859.     },
  4860.  
  4861.  
  4862.     /* Event Helper
  4863.     ------------------------------------------------------------------------------------------------------------------*/
  4864.  
  4865.  
  4866.     // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null.
  4867.     renderHelper: function(event, sourceSeg) {
  4868.         var helperNodes = [];
  4869.         var segs = this.eventsToSegs([ event ]);
  4870.         var rowStructs;
  4871.  
  4872.         segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered
  4873.         rowStructs = this.renderSegRows(segs);
  4874.  
  4875.         // inject each new event skeleton into each associated row
  4876.         this.rowEls.each(function(row, rowNode) {
  4877.             var rowEl = $(rowNode); // the .fc-row
  4878.             var skeletonEl = $('<div class="fc-helper-skeleton"><table/></div>'); // will be absolutely positioned
  4879.             var skeletonTop;
  4880.  
  4881.             // If there is an original segment, match the top position. Otherwise, put it at the row's top level
  4882.             if (sourceSeg && sourceSeg.row === row) {
  4883.                 skeletonTop = sourceSeg.el.position().top;
  4884.             }
  4885.             else {
  4886.                 skeletonTop = rowEl.find('.fc-content-skeleton tbody').position().top;
  4887.             }
  4888.  
  4889.             skeletonEl.css('top', skeletonTop)
  4890.                 .find('table')
  4891.                     .append(rowStructs[row].tbodyEl);
  4892.  
  4893.             rowEl.append(skeletonEl);
  4894.             helperNodes.push(skeletonEl[0]);
  4895.         });
  4896.  
  4897.         this.helperEls = $(helperNodes); // array -> jQuery set
  4898.     },
  4899.  
  4900.  
  4901.     // Unrenders any visual indication of a mock helper event
  4902.     unrenderHelper: function() {
  4903.         if (this.helperEls) {
  4904.             this.helperEls.remove();
  4905.             this.helperEls = null;
  4906.         }
  4907.     },
  4908.  
  4909.  
  4910.     /* Fill System (highlight, background events, business hours)
  4911.     ------------------------------------------------------------------------------------------------------------------*/
  4912.  
  4913.  
  4914.     fillSegTag: 'td', // override the default tag name
  4915.  
  4916.  
  4917.     // Renders a set of rectangles over the given segments of days.
  4918.     // Only returns segments that successfully rendered.
  4919.     renderFill: function(type, segs, className) {
  4920.         var nodes = [];
  4921.         var i, seg;
  4922.         var skeletonEl;
  4923.  
  4924.         segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs
  4925.  
  4926.         for (i = 0; i < segs.length; i++) {
  4927.             seg = segs[i];
  4928.             skeletonEl = this.renderFillRow(type, seg, className);
  4929.             this.rowEls.eq(seg.row).append(skeletonEl);
  4930.             nodes.push(skeletonEl[0]);
  4931.         }
  4932.  
  4933.         this.elsByFill[type] = $(nodes);
  4934.  
  4935.         return segs;
  4936.     },
  4937.  
  4938.  
  4939.     // Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered.
  4940.     renderFillRow: function(type, seg, className) {
  4941.         var colCnt = this.colCnt;
  4942.         var startCol = seg.leftCol;
  4943.         var endCol = seg.rightCol + 1;
  4944.         var skeletonEl;
  4945.         var trEl;
  4946.  
  4947.         className = className || type.toLowerCase();
  4948.  
  4949.         skeletonEl = $(
  4950.             '<div class="fc-' + className + '-skeleton">' +
  4951.                 '<table><tr/></table>' +
  4952.             '</div>'
  4953.         );
  4954.         trEl = skeletonEl.find('tr');
  4955.  
  4956.         if (startCol > 0) {
  4957.             trEl.append('<td colspan="' + startCol + '"/>');
  4958.         }
  4959.  
  4960.         trEl.append(
  4961.             seg.el.attr('colspan', endCol - startCol)
  4962.         );
  4963.  
  4964.         if (endCol < colCnt) {
  4965.             trEl.append('<td colspan="' + (colCnt - endCol) + '"/>');
  4966.         }
  4967.  
  4968.         this.bookendCells(trEl, type);
  4969.  
  4970.         return skeletonEl;
  4971.     }
  4972.  
  4973. });
  4974.  
  4975. ;;
  4976.  
  4977. /* Event-rendering methods for the DayGrid class
  4978. ----------------------------------------------------------------------------------------------------------------------*/
  4979.  
  4980. DayGrid.mixin({
  4981.  
  4982.     rowStructs: null, // an array of objects, each holding information about a row's foreground event-rendering
  4983.  
  4984.  
  4985.     // Unrenders all events currently rendered on the grid
  4986.     unrenderEvents: function() {
  4987.         this.removeSegPopover(); // removes the "more.." events popover
  4988.         Grid.prototype.unrenderEvents.apply(this, arguments); // calls the super-method
  4989.     },
  4990.  
  4991.  
  4992.     // Retrieves all rendered segment objects currently rendered on the grid
  4993.     getEventSegs: function() {
  4994.         return Grid.prototype.getEventSegs.call(this) // get the segments from the super-method
  4995.             .concat(this.popoverSegs || []); // append the segments from the "more..." popover
  4996.     },
  4997.  
  4998.  
  4999.     // Renders the given background event segments onto the grid
  5000.     renderBgSegs: function(segs) {
  5001.  
  5002.         // don't render timed background events
  5003.         var allDaySegs = $.grep(segs, function(seg) {
  5004.             return seg.event.allDay;
  5005.         });
  5006.  
  5007.         return Grid.prototype.renderBgSegs.call(this, allDaySegs); // call the super-method
  5008.     },
  5009.  
  5010.  
  5011.     // Renders the given foreground event segments onto the grid
  5012.     renderFgSegs: function(segs) {
  5013.         var rowStructs;
  5014.  
  5015.         // render an `.el` on each seg
  5016.         // returns a subset of the segs. segs that were actually rendered
  5017.         segs = this.renderFgSegEls(segs);
  5018.  
  5019.         rowStructs = this.rowStructs = this.renderSegRows(segs);
  5020.  
  5021.         // append to each row's content skeleton
  5022.         this.rowEls.each(function(i, rowNode) {
  5023.             $(rowNode).find('.fc-content-skeleton > table').append(
  5024.                 rowStructs[i].tbodyEl
  5025.             );
  5026.         });
  5027.  
  5028.         return segs; // return only the segs that were actually rendered
  5029.     },
  5030.  
  5031.  
  5032.     // Unrenders all currently rendered foreground event segments
  5033.     unrenderFgSegs: function() {
  5034.         var rowStructs = this.rowStructs || [];
  5035.         var rowStruct;
  5036.  
  5037.         while ((rowStruct = rowStructs.pop())) {
  5038.             rowStruct.tbodyEl.remove();
  5039.         }
  5040.  
  5041.         this.rowStructs = null;
  5042.     },
  5043.  
  5044.  
  5045.     // Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton.
  5046.     // Returns an array of rowStruct objects (see the bottom of `renderSegRow`).
  5047.     // PRECONDITION: each segment shoud already have a rendered and assigned `.el`
  5048.     renderSegRows: function(segs) {
  5049.         var rowStructs = [];
  5050.         var segRows;
  5051.         var row;
  5052.  
  5053.         segRows = this.groupSegRows(segs); // group into nested arrays
  5054.  
  5055.         // iterate each row of segment groupings
  5056.         for (row = 0; row < segRows.length; row++) {
  5057.             rowStructs.push(
  5058.                 this.renderSegRow(row, segRows[row])
  5059.             );
  5060.         }
  5061.  
  5062.         return rowStructs;
  5063.     },
  5064.  
  5065.  
  5066.     // Builds the HTML to be used for the default element for an individual segment
  5067.     fgSegHtml: function(seg, disableResizing) {
  5068.         var view = this.view;
  5069.         var event = seg.event;
  5070.         var isDraggable = view.isEventDraggable(event);
  5071.         var isResizableFromStart = !disableResizing && event.allDay &&
  5072.             seg.isStart && view.isEventResizableFromStart(event);
  5073.         var isResizableFromEnd = !disableResizing && event.allDay &&
  5074.             seg.isEnd && view.isEventResizableFromEnd(event);
  5075.         var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd);
  5076.         var skinCss = cssToStr(this.getEventSkinCss(event));
  5077.         var timeHtml = '';
  5078.         var timeText;
  5079.         var titleHtml;
  5080.  
  5081.         classes.unshift('fc-day-grid-event', 'fc-h-event');
  5082.  
  5083.         // Only display a timed events time if it is the starting segment
  5084.         if (seg.isStart) {
  5085.             timeText = this.getEventTimeText(event);
  5086.             if (timeText) {
  5087.                 timeHtml = '<span class="fc-time">' + htmlEscape(timeText) + '</span>';
  5088.             }
  5089.         }
  5090.  
  5091.         titleHtml =
  5092.             '<span class="fc-title">' +
  5093.                 (htmlEscape(event.title || '') || '&nbsp;') + // we always want one line of height
  5094.             '</span>';
  5095.        
  5096.         return '<a class="' + classes.join(' ') + '"' +
  5097.                 (event.url ?
  5098.                     ' href="' + htmlEscape(event.url)  + '"' :
  5099.                     ''
  5100.                     ) +
  5101.                 (skinCss ?
  5102.                     ' style="' + skinCss + '"' :
  5103.                     ''
  5104.                     ) +
  5105.             '>' +
  5106.                 '<div class="fc-content">' +
  5107.                     (this.isRTL ?
  5108.                         titleHtml + ' ' + timeHtml : // put a natural space in between
  5109.                         timeHtml + ' ' + titleHtml   //
  5110.                         ) +
  5111.                 '</div>' +
  5112.                 (isResizableFromStart ?
  5113.                     '<div class="fc-resizer fc-start-resizer" />' :
  5114.                     ''
  5115.                     ) +
  5116.                 (isResizableFromEnd ?
  5117.                     '<div class="fc-resizer fc-end-resizer" />' :
  5118.                     ''
  5119.                     ) +
  5120.             '</a>';
  5121.     },
  5122.  
  5123.  
  5124.     // Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains
  5125.     // the segments. Returns object with a bunch of internal data about how the render was calculated.
  5126.     // NOTE: modifies rowSegs
  5127.     renderSegRow: function(row, rowSegs) {
  5128.         var colCnt = this.colCnt;
  5129.         var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels
  5130.         var levelCnt = Math.max(1, segLevels.length); // ensure at least one level
  5131.         var tbody = $('<tbody/>');
  5132.         var segMatrix = []; // lookup for which segments are rendered into which level+col cells
  5133.         var cellMatrix = []; // lookup for all <td> elements of the level+col matrix
  5134.         var loneCellMatrix = []; // lookup for <td> elements that only take up a single column
  5135.         var i, levelSegs;
  5136.         var col;
  5137.         var tr;
  5138.         var j, seg;
  5139.         var td;
  5140.  
  5141.         // populates empty cells from the current column (`col`) to `endCol`
  5142.         function emptyCellsUntil(endCol) {
  5143.             while (col < endCol) {
  5144.                 // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell
  5145.                 td = (loneCellMatrix[i - 1] || [])[col];
  5146.                 if (td) {
  5147.                     td.attr(
  5148.                         'rowspan',
  5149.                         parseInt(td.attr('rowspan') || 1, 10) + 1
  5150.                     );
  5151.                 }
  5152.                 else {
  5153.                     td = $('<td/>');
  5154.                     tr.append(td);
  5155.                 }
  5156.                 cellMatrix[i][col] = td;
  5157.                 loneCellMatrix[i][col] = td;
  5158.                 col++;
  5159.             }
  5160.         }
  5161.  
  5162.         for (i = 0; i < levelCnt; i++) { // iterate through all levels
  5163.             levelSegs = segLevels[i];
  5164.             col = 0;
  5165.             tr = $('<tr/>');
  5166.  
  5167.             segMatrix.push([]);
  5168.             cellMatrix.push([]);
  5169.             loneCellMatrix.push([]);
  5170.  
  5171.             // levelCnt might be 1 even though there are no actual levels. protect against this.
  5172.             // this single empty row is useful for styling.
  5173.             if (levelSegs) {
  5174.                 for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level
  5175.                     seg = levelSegs[j];
  5176.  
  5177.                     emptyCellsUntil(seg.leftCol);
  5178.  
  5179.                     // create a container that occupies or more columns. append the event element.
  5180.                     td = $('<td class="fc-event-container"/>').append(seg.el);
  5181.                     if (seg.leftCol != seg.rightCol) {
  5182.                         td.attr('colspan', seg.rightCol - seg.leftCol + 1);
  5183.                     }
  5184.                     else { // a single-column segment
  5185.                         loneCellMatrix[i][col] = td;
  5186.                     }
  5187.  
  5188.                     while (col <= seg.rightCol) {
  5189.                         cellMatrix[i][col] = td;
  5190.                         segMatrix[i][col] = seg;
  5191.                         col++;
  5192.                     }
  5193.  
  5194.                     tr.append(td);
  5195.                 }
  5196.             }
  5197.  
  5198.             emptyCellsUntil(colCnt); // finish off the row
  5199.             this.bookendCells(tr, 'eventSkeleton');
  5200.             tbody.append(tr);
  5201.         }
  5202.  
  5203.         return { // a "rowStruct"
  5204.             row: row, // the row number
  5205.             tbodyEl: tbody,
  5206.             cellMatrix: cellMatrix,
  5207.             segMatrix: segMatrix,
  5208.             segLevels: segLevels,
  5209.             segs: rowSegs
  5210.         };
  5211.     },
  5212.  
  5213.  
  5214.     // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels.
  5215.     // NOTE: modifies segs
  5216.     buildSegLevels: function(segs) {
  5217.         var levels = [];
  5218.         var i, seg;
  5219.         var j;
  5220.  
  5221.         // Give preference to elements with certain criteria, so they have
  5222.         // a chance to be closer to the top.
  5223.         segs.sort(compareSegs);
  5224.        
  5225.         for (i = 0; i < segs.length; i++) {
  5226.             seg = segs[i];
  5227.  
  5228.             // loop through levels, starting with the topmost, until the segment doesn't collide with other segments
  5229.             for (j = 0; j < levels.length; j++) {
  5230.                 if (!isDaySegCollision(seg, levels[j])) {
  5231.                     break;
  5232.                 }
  5233.             }
  5234.             // `j` now holds the desired subrow index
  5235.             seg.level = j;
  5236.  
  5237.             // create new level array if needed and append segment
  5238.             (levels[j] || (levels[j] = [])).push(seg);
  5239.         }
  5240.  
  5241.         // order segments left-to-right. very important if calendar is RTL
  5242.         for (j = 0; j < levels.length; j++) {
  5243.             levels[j].sort(compareDaySegCols);
  5244.         }
  5245.  
  5246.         return levels;
  5247.     },
  5248.  
  5249.  
  5250.     // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row
  5251.     groupSegRows: function(segs) {
  5252.         var segRows = [];
  5253.         var i;
  5254.  
  5255.         for (i = 0; i < this.rowCnt; i++) {
  5256.             segRows.push([]);
  5257.         }
  5258.  
  5259.         for (i = 0; i < segs.length; i++) {
  5260.             segRows[segs[i].row].push(segs[i]);
  5261.         }
  5262.  
  5263.         return segRows;
  5264.     }
  5265.  
  5266. });
  5267.  
  5268.  
  5269. // Computes whether two segments' columns collide. They are assumed to be in the same row.
  5270. function isDaySegCollision(seg, otherSegs) {
  5271.     var i, otherSeg;
  5272.  
  5273.     for (i = 0; i < otherSegs.length; i++) {
  5274.         otherSeg = otherSegs[i];
  5275.  
  5276.         if (
  5277.             otherSeg.leftCol <= seg.rightCol &&
  5278.             otherSeg.rightCol >= seg.leftCol
  5279.         ) {
  5280.             return true;
  5281.         }
  5282.     }
  5283.  
  5284.     return false;
  5285. }
  5286.  
  5287.  
  5288. // A cmp function for determining the leftmost event
  5289. function compareDaySegCols(a, b) {
  5290.     return a.leftCol - b.leftCol;
  5291. }
  5292.  
  5293. ;;
  5294.  
  5295. /* Methods relate to limiting the number events for a given day on a DayGrid
  5296. ----------------------------------------------------------------------------------------------------------------------*/
  5297. // NOTE: all the segs being passed around in here are foreground segs
  5298.  
  5299. DayGrid.mixin({
  5300.  
  5301.     segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible
  5302.     popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible
  5303.  
  5304.  
  5305.     removeSegPopover: function() {
  5306.         if (this.segPopover) {
  5307.             this.segPopover.hide(); // in handler, will call segPopover's removeElement
  5308.         }
  5309.     },
  5310.  
  5311.  
  5312.     // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid.
  5313.     // `levelLimit` can be false (don't limit), a number, or true (should be computed).
  5314.     limitRows: function(levelLimit) {
  5315.         var rowStructs = this.rowStructs || [];
  5316.         var row; // row #
  5317.         var rowLevelLimit;
  5318.  
  5319.         for (row = 0; row < rowStructs.length; row++) {
  5320.             this.unlimitRow(row);
  5321.  
  5322.             if (!levelLimit) {
  5323.                 rowLevelLimit = false;
  5324.             }
  5325.             else if (typeof levelLimit === 'number') {
  5326.                 rowLevelLimit = levelLimit;
  5327.             }
  5328.             else {
  5329.                 rowLevelLimit = this.computeRowLevelLimit(row);
  5330.             }
  5331.  
  5332.             if (rowLevelLimit !== false) {
  5333.                 this.limitRow(row, rowLevelLimit);
  5334.             }
  5335.         }
  5336.     },
  5337.  
  5338.  
  5339.     // Computes the number of levels a row will accomodate without going outside its bounds.
  5340.     // Assumes the row is "rigid" (maintains a constant height regardless of what is inside).
  5341.     // `row` is the row number.
  5342.     computeRowLevelLimit: function(row) {
  5343.         var rowEl = this.rowEls.eq(row); // the containing "fake" row div
  5344.         var rowHeight = rowEl.height(); // TODO: cache somehow?
  5345.         var trEls = this.rowStructs[row].tbodyEl.children();
  5346.         var i, trEl;
  5347.         var trHeight;
  5348.  
  5349.         function iterInnerHeights(i, childNode) {
  5350.             trHeight = Math.max(trHeight, $(childNode).outerHeight());
  5351.         }
  5352.  
  5353.         // Reveal one level <tr> at a time and stop when we find one out of bounds
  5354.         for (i = 0; i < trEls.length; i++) {
  5355.             trEl = trEls.eq(i).removeClass('fc-limited'); // reset to original state (reveal)
  5356.  
  5357.             // with rowspans>1 and IE8, trEl.outerHeight() would return the height of the largest cell,
  5358.             // so instead, find the tallest inner content element.
  5359.             trHeight = 0;
  5360.             trEl.find('> td > :first-child').each(iterInnerHeights);
  5361.  
  5362.             if (trEl.position().top + trHeight > rowHeight) {
  5363.                 return i;
  5364.             }
  5365.         }
  5366.  
  5367.         return false; // should not limit at all
  5368.     },
  5369.  
  5370.  
  5371.     // Limits the given grid row to the maximum number of levels and injects "more" links if necessary.
  5372.     // `row` is the row number.
  5373.     // `levelLimit` is a number for the maximum (inclusive) number of levels allowed.
  5374.     limitRow: function(row, levelLimit) {
  5375.         var _this = this;
  5376.         var rowStruct = this.rowStructs[row];
  5377.         var moreNodes = []; // array of "more" <a> links and <td> DOM nodes
  5378.         var col = 0; // col #, left-to-right (not chronologically)
  5379.         var cell;
  5380.         var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right
  5381.         var cellMatrix; // a matrix (by level, then column) of all <td> jQuery elements in the row
  5382.         var limitedNodes; // array of temporarily hidden level <tr> and segment <td> DOM nodes
  5383.         var i, seg;
  5384.         var segsBelow; // array of segment objects below `seg` in the current `col`
  5385.         var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies
  5386.         var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column)
  5387.         var td, rowspan;
  5388.         var segMoreNodes; // array of "more" <td> cells that will stand-in for the current seg's cell
  5389.         var j;
  5390.         var moreTd, moreWrap, moreLink;
  5391.  
  5392.         // Iterates through empty level cells and places "more" links inside if need be
  5393.         function emptyCellsUntil(endCol) { // goes from current `col` to `endCol`
  5394.             while (col < endCol) {
  5395.                 cell = _this.getCell(row, col);
  5396.                 segsBelow = _this.getCellSegs(cell, levelLimit);
  5397.                 if (segsBelow.length) {
  5398.                     td = cellMatrix[levelLimit - 1][col];
  5399.                     moreLink = _this.renderMoreLink(cell, segsBelow);
  5400.                     moreWrap = $('<div/>').append(moreLink);
  5401.                     td.append(moreWrap);
  5402.                     moreNodes.push(moreWrap[0]);
  5403.                 }
  5404.                 col++;
  5405.             }
  5406.         }
  5407.  
  5408.         if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit?
  5409.             levelSegs = rowStruct.segLevels[levelLimit - 1];
  5410.             cellMatrix = rowStruct.cellMatrix;
  5411.  
  5412.             limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level <tr> elements past the limit
  5413.                 .addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array
  5414.  
  5415.             // iterate though segments in the last allowable level
  5416.             for (i = 0; i < levelSegs.length; i++) {
  5417.                 seg = levelSegs[i];
  5418.                 emptyCellsUntil(seg.leftCol); // process empty cells before the segment
  5419.  
  5420.                 // determine *all* segments below `seg` that occupy the same columns
  5421.                 colSegsBelow = [];
  5422.                 totalSegsBelow = 0;
  5423.                 while (col <= seg.rightCol) {
  5424.                     cell = this.getCell(row, col);
  5425.                     segsBelow = this.getCellSegs(cell, levelLimit);
  5426.                     colSegsBelow.push(segsBelow);
  5427.                     totalSegsBelow += segsBelow.length;
  5428.                     col++;
  5429.                 }
  5430.  
  5431.                 if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links?
  5432.                     td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell
  5433.                     rowspan = td.attr('rowspan') || 1;
  5434.                     segMoreNodes = [];
  5435.  
  5436.                     // make a replacement <td> for each column the segment occupies. will be one for each colspan
  5437.                     for (j = 0; j < colSegsBelow.length; j++) {
  5438.                         moreTd = $('<td class="fc-more-cell"/>').attr('rowspan', rowspan);
  5439.                         segsBelow = colSegsBelow[j];
  5440.                         cell = this.getCell(row, seg.leftCol + j);
  5441.                         moreLink = this.renderMoreLink(cell, [ seg ].concat(segsBelow)); // count seg as hidden too
  5442.                         moreWrap = $('<div/>').append(moreLink);
  5443.                         moreTd.append(moreWrap);
  5444.                         segMoreNodes.push(moreTd[0]);
  5445.                         moreNodes.push(moreTd[0]);
  5446.                     }
  5447.  
  5448.                     td.addClass('fc-limited').after($(segMoreNodes)); // hide original <td> and inject replacements
  5449.                     limitedNodes.push(td[0]);
  5450.                 }
  5451.             }
  5452.  
  5453.             emptyCellsUntil(this.colCnt); // finish off the level
  5454.             rowStruct.moreEls = $(moreNodes); // for easy undoing later
  5455.             rowStruct.limitedEls = $(limitedNodes); // for easy undoing later
  5456.         }
  5457.     },
  5458.  
  5459.  
  5460.     // Reveals all levels and removes all "more"-related elements for a grid's row.
  5461.     // `row` is a row number.
  5462.     unlimitRow: function(row) {
  5463.         var rowStruct = this.rowStructs[row];
  5464.  
  5465.         if (rowStruct.moreEls) {
  5466.             rowStruct.moreEls.remove();
  5467.             rowStruct.moreEls = null;
  5468.         }
  5469.  
  5470.         if (rowStruct.limitedEls) {
  5471.             rowStruct.limitedEls.removeClass('fc-limited');
  5472.             rowStruct.limitedEls = null;
  5473.         }
  5474.     },
  5475.  
  5476.  
  5477.     // Renders an <a> element that represents hidden event element for a cell.
  5478.     // Responsible for attaching click handler as well.
  5479.     renderMoreLink: function(cell, hiddenSegs) {
  5480.         var _this = this;
  5481.         var view = this.view;
  5482.  
  5483.         return $('<a class="fc-more"/>')
  5484.             .text(
  5485.                 this.getMoreLinkText(hiddenSegs.length)
  5486.             )
  5487.             .on('click', function(ev) {
  5488.                 var clickOption = view.opt('eventLimitClick');
  5489.                 var date = cell.start;
  5490.                 var moreEl = $(this);
  5491.                 var dayEl = _this.getCellDayEl(cell);
  5492.                 var allSegs = _this.getCellSegs(cell);
  5493.  
  5494.                 // rescope the segments to be within the cell's date
  5495.                 var reslicedAllSegs = _this.resliceDaySegs(allSegs, date);
  5496.                 var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date);
  5497.  
  5498.                 if (typeof clickOption === 'function') {
  5499.                     // the returned value can be an atomic option
  5500.                     clickOption = view.trigger('eventLimitClick', null, {
  5501.                         date: date,
  5502.                         dayEl: dayEl,
  5503.                         moreEl: moreEl,
  5504.                         segs: reslicedAllSegs,
  5505.                         hiddenSegs: reslicedHiddenSegs
  5506.                     }, ev);
  5507.                 }
  5508.  
  5509.                 if (clickOption === 'popover') {
  5510.                     _this.showSegPopover(cell, moreEl, reslicedAllSegs);
  5511.                 }
  5512.                 else if (typeof clickOption === 'string') { // a view name
  5513.                     view.calendar.zoomTo(date, clickOption);
  5514.                 }
  5515.             });
  5516.     },
  5517.  
  5518.  
  5519.     // Reveals the popover that displays all events within a cell
  5520.     showSegPopover: function(cell, moreLink, segs) {
  5521.         var _this = this;
  5522.         var view = this.view;
  5523.         var moreWrap = moreLink.parent(); // the <div> wrapper around the <a>
  5524.         var topEl; // the element we want to match the top coordinate of
  5525.         var options;
  5526.  
  5527.         if (this.rowCnt == 1) {
  5528.             topEl = view.el; // will cause the popover to cover any sort of header
  5529.         }
  5530.         else {
  5531.             topEl = this.rowEls.eq(cell.row); // will align with top of row
  5532.         }
  5533.  
  5534.         options = {
  5535.             className: 'fc-more-popover',
  5536.             content: this.renderSegPopoverContent(cell, segs),
  5537.             parentEl: this.el,
  5538.             top: topEl.offset().top,
  5539.             autoHide: true, // when the user clicks elsewhere, hide the popover
  5540.             viewportConstrain: view.opt('popoverViewportConstrain'),
  5541.             hide: function() {
  5542.                 // kill everything when the popover is hidden
  5543.                 _this.segPopover.removeElement();
  5544.                 _this.segPopover = null;
  5545.                 _this.popoverSegs = null;
  5546.             }
  5547.         };
  5548.  
  5549.         // Determine horizontal coordinate.
  5550.         // We use the moreWrap instead of the <td> to avoid border confusion.
  5551.         if (this.isRTL) {
  5552.             options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border
  5553.         }
  5554.         else {
  5555.             options.left = moreWrap.offset().left - 1; // -1 to be over cell border
  5556.         }
  5557.  
  5558.         this.segPopover = new Popover(options);
  5559.         this.segPopover.show();
  5560.     },
  5561.  
  5562.  
  5563.     // Builds the inner DOM contents of the segment popover
  5564.     renderSegPopoverContent: function(cell, segs) {
  5565.         var view = this.view;
  5566.         var isTheme = view.opt('theme');
  5567.         var title = cell.start.format(view.opt('dayPopoverFormat'));
  5568.         var content = $(
  5569.             '<div class="fc-header ' + view.widgetHeaderClass + '">' +
  5570.                 '<span class="fc-close ' +
  5571.                     (isTheme ? 'ui-icon ui-icon-closethick' : 'fc-icon fc-icon-x') +
  5572.                 '"></span>' +
  5573.                 '<span class="fc-title">' +
  5574.                     htmlEscape(title) +
  5575.                 '</span>' +
  5576.                 '<div class="fc-clear"/>' +
  5577.             '</div>' +
  5578.             '<div class="fc-body ' + view.widgetContentClass + '">' +
  5579.                 '<div class="fc-event-container"></div>' +
  5580.             '</div>'
  5581.         );
  5582.         var segContainer = content.find('.fc-event-container');
  5583.         var i;
  5584.  
  5585.         // render each seg's `el` and only return the visible segs
  5586.         segs = this.renderFgSegEls(segs, true); // disableResizing=true
  5587.         this.popoverSegs = segs;
  5588.  
  5589.         for (i = 0; i < segs.length; i++) {
  5590.  
  5591.             // because segments in the popover are not part of a grid coordinate system, provide a hint to any
  5592.             // grids that want to do drag-n-drop about which cell it came from
  5593.             segs[i].cell = cell;
  5594.  
  5595.             segContainer.append(segs[i].el);
  5596.         }
  5597.  
  5598.         return content;
  5599.     },
  5600.  
  5601.  
  5602.     // Given the events within an array of segment objects, reslice them to be in a single day
  5603.     resliceDaySegs: function(segs, dayDate) {
  5604.  
  5605.         // build an array of the original events
  5606.         var events = $.map(segs, function(seg) {
  5607.             return seg.event;
  5608.         });
  5609.  
  5610.         var dayStart = dayDate.clone().stripTime();
  5611.         var dayEnd = dayStart.clone().add(1, 'days');
  5612.         var dayRange = { start: dayStart, end: dayEnd };
  5613.  
  5614.         // slice the events with a custom slicing function
  5615.         segs = this.eventsToSegs(
  5616.             events,
  5617.             function(range) {
  5618.                 var seg = intersectionToSeg(range, dayRange); // undefind if no intersection
  5619.                 return seg ? [ seg ] : []; // must return an array of segments
  5620.             }
  5621.         );
  5622.  
  5623.         // force an order because eventsToSegs doesn't guarantee one
  5624.         segs.sort(compareSegs);
  5625.  
  5626.         return segs;
  5627.     },
  5628.  
  5629.  
  5630.     // Generates the text that should be inside a "more" link, given the number of events it represents
  5631.     getMoreLinkText: function(num) {
  5632.         var opt = this.view.opt('eventLimitText');
  5633.  
  5634.         if (typeof opt === 'function') {
  5635.             return opt(num);
  5636.         }
  5637.         else {
  5638.             return '+' + num + ' ' + opt;
  5639.         }
  5640.     },
  5641.  
  5642.  
  5643.     // Returns segments within a given cell.
  5644.     // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs.
  5645.     getCellSegs: function(cell, startLevel) {
  5646.         var segMatrix = this.rowStructs[cell.row].segMatrix;
  5647.         var level = startLevel || 0;
  5648.         var segs = [];
  5649.         var seg;
  5650.  
  5651.         while (level < segMatrix.length) {
  5652.             seg = segMatrix[level][cell.col];
  5653.             if (seg) {
  5654.                 segs.push(seg);
  5655.             }
  5656.             level++;
  5657.         }
  5658.  
  5659.         return segs;
  5660.     }
  5661.  
  5662. });
  5663.  
  5664. ;;
  5665.  
  5666. /* A component that renders one or more columns of vertical time slots
  5667. ----------------------------------------------------------------------------------------------------------------------*/
  5668.  
  5669. var TimeGrid = Grid.extend({
  5670.  
  5671.     slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines
  5672.     snapDuration: null, // granularity of time for dragging and selecting
  5673.     minTime: null, // Duration object that denotes the first visible time of any given day
  5674.     maxTime: null, // Duration object that denotes the exclusive visible end time of any given day
  5675.     colDates: null, // whole-day dates for each column. left to right
  5676.     axisFormat: null, // formatting string for times running along vertical axis
  5677.  
  5678.     dayEls: null, // cells elements in the day-row background
  5679.     slatEls: null, // elements running horizontally across all columns
  5680.  
  5681.     slatTops: null, // an array of top positions, relative to the container. last item holds bottom of last slot
  5682.  
  5683.     helperEl: null, // cell skeleton element for rendering the mock event "helper"
  5684.  
  5685.     businessHourSegs: null,
  5686.  
  5687.  
  5688.     constructor: function() {
  5689.         Grid.apply(this, arguments); // call the super-constructor
  5690.         this.processOptions();
  5691.     },
  5692.  
  5693.  
  5694.     // Renders the time grid into `this.el`, which should already be assigned.
  5695.     // Relies on the view's colCnt. In the future, this component should probably be self-sufficient.
  5696.     renderDates: function() {
  5697.         this.el.html(this.renderHtml());
  5698.         this.dayEls = this.el.find('.fc-day');
  5699.         this.slatEls = this.el.find('.fc-slats tr');
  5700.     },
  5701.  
  5702.  
  5703.     renderBusinessHours: function() {
  5704.         var events = this.view.calendar.getBusinessHoursEvents();
  5705.         this.businessHourSegs = this.renderFill('businessHours', this.eventsToSegs(events), 'bgevent');
  5706.     },
  5707.  
  5708.  
  5709.     // Renders the basic HTML skeleton for the grid
  5710.     renderHtml: function() {
  5711.         return '' +
  5712.             '<div class="fc-bg">' +
  5713.                 '<table>' +
  5714.                     this.rowHtml('slotBg') + // leverages RowRenderer, which will call slotBgCellHtml
  5715.                 '</table>' +
  5716.             '</div>' +
  5717.             '<div class="fc-slats">' +
  5718.                 '<table>' +
  5719.                     this.slatRowHtml() +
  5720.                 '</table>' +
  5721.             '</div>';
  5722.     },
  5723.  
  5724.  
  5725.     // Renders the HTML for a vertical background cell behind the slots.
  5726.     // This method is distinct from 'bg' because we wanted a new `rowType` so the View could customize the rendering.
  5727.     slotBgCellHtml: function(cell) {
  5728.         return this.bgCellHtml(cell);
  5729.     },
  5730.  
  5731.  
  5732.     // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL.
  5733.     slatRowHtml: function() {
  5734.         var view = this.view;
  5735.         var isRTL = this.isRTL;
  5736.         var html = '';
  5737.         var slotNormal = this.slotDuration.asMinutes() % 15 === 0;
  5738.         var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations
  5739.         var slotDate; // will be on the view's first day, but we only care about its time
  5740.         var minutes;
  5741.         var axisHtml;
  5742.  
  5743.         // Calculate the time for each slot
  5744.         while (slotTime < this.maxTime) {
  5745.             slotDate = this.start.clone().time(slotTime); // will be in UTC but that's good. to avoid DST issues
  5746.             minutes = slotDate.minutes();
  5747.  
  5748.             axisHtml =
  5749.                 '<td class="fc-axis fc-time ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' +
  5750.                     ((!slotNormal || !minutes) ? // if irregular slot duration, or on the hour, then display the time
  5751.                         '<span>' + // for matchCellWidths
  5752.                             htmlEscape(slotDate.format(this.axisFormat)) +
  5753.                         '</span>' :
  5754.                         ''
  5755.                         ) +
  5756.                 '</td>';
  5757.  
  5758.             html +=
  5759.                 '<tr ' + (!minutes ? '' : 'class="fc-minor"') + '>' +
  5760.                     (!isRTL ? axisHtml : '') +
  5761.                     '<td class="' + view.widgetContentClass + '"/>' +
  5762.                     (isRTL ? axisHtml : '') +
  5763.                 "</tr>";
  5764.  
  5765.             slotTime.add(this.slotDuration);
  5766.         }
  5767.  
  5768.         return html;
  5769.     },
  5770.  
  5771.  
  5772.     /* Options
  5773.     ------------------------------------------------------------------------------------------------------------------*/
  5774.  
  5775.  
  5776.     // Parses various options into properties of this object
  5777.     processOptions: function() {
  5778.         var view = this.view;
  5779.         var slotDuration = view.opt('slotDuration');
  5780.         var snapDuration = view.opt('snapDuration');
  5781.  
  5782.         slotDuration = moment.duration(slotDuration);
  5783.         snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration;
  5784.  
  5785.         this.slotDuration = slotDuration;
  5786.         this.snapDuration = snapDuration;
  5787.         this.cellDuration = snapDuration; // for Grid system
  5788.  
  5789.         this.minTime = moment.duration(view.opt('minTime'));
  5790.         this.maxTime = moment.duration(view.opt('maxTime'));
  5791.  
  5792.         this.axisFormat = view.opt('axisFormat') || view.opt('smallTimeFormat');
  5793.     },
  5794.  
  5795.  
  5796.     // Computes a default column header formatting string if `colFormat` is not explicitly defined
  5797.     computeColHeadFormat: function() {
  5798.         if (this.colCnt > 1) { // multiple days, so full single date string WON'T be in title text
  5799.             return this.view.opt('dayOfMonthFormat'); // "Sat 12/10"
  5800.         }
  5801.         else { // single day, so full single date string will probably be in title text
  5802.             return 'dddd'; // "Saturday"
  5803.         }
  5804.     },
  5805.  
  5806.  
  5807.     // Computes a default event time formatting string if `timeFormat` is not explicitly defined
  5808.     computeEventTimeFormat: function() {
  5809.         return this.view.opt('noMeridiemTimeFormat'); // like "6:30" (no AM/PM)
  5810.     },
  5811.  
  5812.  
  5813.     // Computes a default `displayEventEnd` value if one is not expliclty defined
  5814.     computeDisplayEventEnd: function() {
  5815.         return true;
  5816.     },
  5817.  
  5818.  
  5819.     /* Cell System
  5820.     ------------------------------------------------------------------------------------------------------------------*/
  5821.  
  5822.  
  5823.     rangeUpdated: function() {
  5824.         var view = this.view;
  5825.         var colDates = [];
  5826.         var date;
  5827.  
  5828.         date = this.start.clone();
  5829.         while (date.isBefore(this.end)) {
  5830.             colDates.push(date.clone());
  5831.             date.add(1, 'day');
  5832.             date = view.skipHiddenDays(date);
  5833.         }
  5834.  
  5835.         if (this.isRTL) {
  5836.             colDates.reverse();
  5837.         }
  5838.  
  5839.         this.colDates = colDates;
  5840.         this.colCnt = colDates.length;
  5841.         this.rowCnt = Math.ceil((this.maxTime - this.minTime) / this.snapDuration); // # of vertical snaps
  5842.     },
  5843.  
  5844.  
  5845.     // Given a cell object, generates its start date. Returns a reference-free copy.
  5846.     computeCellDate: function(cell) {
  5847.         var date = this.colDates[cell.col];
  5848.         var time = this.computeSnapTime(cell.row);
  5849.  
  5850.         date = this.view.calendar.rezoneDate(date); // give it a 00:00 time
  5851.         date.time(time);
  5852.  
  5853.         return date;
  5854.     },
  5855.  
  5856.  
  5857.     // Retrieves the element representing the given column
  5858.     getColEl: function(col) {
  5859.         return this.dayEls.eq(col);
  5860.     },
  5861.  
  5862.  
  5863.     /* Dates
  5864.     ------------------------------------------------------------------------------------------------------------------*/
  5865.  
  5866.  
  5867.     // Given a row number of the grid, representing a "snap", returns a time (Duration) from its start-of-day
  5868.     computeSnapTime: function(row) {
  5869.         return moment.duration(this.minTime + this.snapDuration * row);
  5870.     },
  5871.  
  5872.  
  5873.     // Slices up a date range by column into an array of segments
  5874.     rangeToSegs: function(range) {
  5875.         var colCnt = this.colCnt;
  5876.         var segs = [];
  5877.         var seg;
  5878.         var col;
  5879.         var colDate;
  5880.         var colRange;
  5881.  
  5882.         // normalize :(
  5883.         range = {
  5884.             start: range.start.clone().stripZone(),
  5885.             end: range.end.clone().stripZone()
  5886.         };
  5887.  
  5888.         for (col = 0; col < colCnt; col++) {
  5889.             colDate = this.colDates[col]; // will be ambig time/timezone
  5890.             colRange = {
  5891.                 start: colDate.clone().time(this.minTime),
  5892.                 end: colDate.clone().time(this.maxTime)
  5893.             };
  5894.             seg = intersectionToSeg(range, colRange); // both will be ambig timezone
  5895.             if (seg) {
  5896.                 seg.col = col;
  5897.                 segs.push(seg);
  5898.             }
  5899.         }
  5900.  
  5901.         return segs;
  5902.     },
  5903.  
  5904.  
  5905.     /* Coordinates
  5906.     ------------------------------------------------------------------------------------------------------------------*/
  5907.  
  5908.  
  5909.     updateSize: function(isResize) { // NOT a standard Grid method
  5910.         this.computeSlatTops();
  5911.  
  5912.         if (isResize) {
  5913.             this.updateSegVerticals();
  5914.         }
  5915.     },
  5916.  
  5917.  
  5918.     // Computes the top/bottom coordinates of each "snap" rows
  5919.     computeRowCoords: function() {
  5920.         var originTop = this.el.offset().top;
  5921.         var items = [];
  5922.         var i;
  5923.         var item;
  5924.  
  5925.         for (i = 0; i < this.rowCnt; i++) {
  5926.             item = {
  5927.                 top: originTop + this.computeTimeTop(this.computeSnapTime(i))
  5928.             };
  5929.             if (i > 0) {
  5930.                 items[i - 1].bottom = item.top;
  5931.             }
  5932.             items.push(item);
  5933.         }
  5934.         item.bottom = item.top + this.computeTimeTop(this.computeSnapTime(i));
  5935.  
  5936.         return items;
  5937.     },
  5938.  
  5939.  
  5940.     // Computes the top coordinate, relative to the bounds of the grid, of the given date.
  5941.     // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
  5942.     computeDateTop: function(date, startOfDayDate) {
  5943.         return this.computeTimeTop(
  5944.             moment.duration(
  5945.                 date.clone().stripZone() - startOfDayDate.clone().stripTime()
  5946.             )
  5947.         );
  5948.     },
  5949.  
  5950.  
  5951.     // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
  5952.     computeTimeTop: function(time) {
  5953.         var slatCoverage = (time - this.minTime) / this.slotDuration; // floating-point value of # of slots covered
  5954.         var slatIndex;
  5955.         var slatRemainder;
  5956.         var slatTop;
  5957.         var slatBottom;
  5958.  
  5959.         // constrain. because minTime/maxTime might be customized
  5960.         slatCoverage = Math.max(0, slatCoverage);
  5961.         slatCoverage = Math.min(this.slatEls.length, slatCoverage);
  5962.  
  5963.         slatIndex = Math.floor(slatCoverage); // an integer index of the furthest whole slot
  5964.         slatRemainder = slatCoverage - slatIndex;
  5965.         slatTop = this.slatTops[slatIndex]; // the top position of the furthest whole slot
  5966.  
  5967.         if (slatRemainder) { // time spans part-way into the slot
  5968.             slatBottom = this.slatTops[slatIndex + 1];
  5969.             return slatTop + (slatBottom - slatTop) * slatRemainder; // part-way between slots
  5970.         }
  5971.         else {
  5972.             return slatTop;
  5973.         }
  5974.     },
  5975.  
  5976.  
  5977.     // Queries each `slatEl` for its position relative to the grid's container and stores it in `slatTops`.
  5978.     // Includes the the bottom of the last slat as the last item in the array.
  5979.     computeSlatTops: function() {
  5980.         var tops = [];
  5981.         var top;
  5982.  
  5983.         this.slatEls.each(function(i, node) {
  5984.             top = $(node).position().top;
  5985.             tops.push(top);
  5986.         });
  5987.  
  5988.         tops.push(top + this.slatEls.last().outerHeight()); // bottom of the last slat
  5989.  
  5990.         this.slatTops = tops;
  5991.     },
  5992.  
  5993.  
  5994.     /* Event Drag Visualization
  5995.     ------------------------------------------------------------------------------------------------------------------*/
  5996.  
  5997.  
  5998.     // Renders a visual indication of an event being dragged over the specified date(s).
  5999.     // dropLocation's end might be null, as well as `seg`. See Grid::renderDrag for more info.
  6000.     // A returned value of `true` signals that a mock "helper" event has been rendered.
  6001.     renderDrag: function(dropLocation, seg) {
  6002.  
  6003.         if (seg) { // if there is event information for this drag, render a helper event
  6004.             this.renderRangeHelper(dropLocation, seg);
  6005.             this.applyDragOpacity(this.helperEl);
  6006.  
  6007.             return true; // signal that a helper has been rendered
  6008.         }
  6009.         else {
  6010.             // otherwise, just render a highlight
  6011.             this.renderHighlight(this.eventRangeToSegs(dropLocation));
  6012.         }
  6013.     },
  6014.  
  6015.  
  6016.     // Unrenders any visual indication of an event being dragged
  6017.     unrenderDrag: function() {
  6018.         this.unrenderHelper();
  6019.         this.unrenderHighlight();
  6020.     },
  6021.  
  6022.  
  6023.     /* Event Resize Visualization
  6024.     ------------------------------------------------------------------------------------------------------------------*/
  6025.  
  6026.  
  6027.     // Renders a visual indication of an event being resized
  6028.     renderEventResize: function(range, seg) {
  6029.         this.renderRangeHelper(range, seg);
  6030.     },
  6031.  
  6032.  
  6033.     // Unrenders any visual indication of an event being resized
  6034.     unrenderEventResize: function() {
  6035.         this.unrenderHelper();
  6036.     },
  6037.  
  6038.  
  6039.     /* Event Helper
  6040.     ------------------------------------------------------------------------------------------------------------------*/
  6041.  
  6042.  
  6043.     // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag)
  6044.     renderHelper: function(event, sourceSeg) {
  6045.         var segs = this.eventsToSegs([ event ]);
  6046.         var tableEl;
  6047.         var i, seg;
  6048.         var sourceEl;
  6049.  
  6050.         segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered
  6051.         tableEl = this.renderSegTable(segs);
  6052.  
  6053.         // Try to make the segment that is in the same row as sourceSeg look the same
  6054.         for (i = 0; i < segs.length; i++) {
  6055.             seg = segs[i];
  6056.             if (sourceSeg && sourceSeg.col === seg.col) {
  6057.                 sourceEl = sourceSeg.el;
  6058.                 seg.el.css({
  6059.                     left: sourceEl.css('left'),
  6060.                     right: sourceEl.css('right'),
  6061.                     'margin-left': sourceEl.css('margin-left'),
  6062.                     'margin-right': sourceEl.css('margin-right')
  6063.                 });
  6064.             }
  6065.         }
  6066.  
  6067.         this.helperEl = $('<div class="fc-helper-skeleton"/>')
  6068.             .append(tableEl)
  6069.                 .appendTo(this.el);
  6070.     },
  6071.  
  6072.  
  6073.     // Unrenders any mock helper event
  6074.     unrenderHelper: function() {
  6075.         if (this.helperEl) {
  6076.             this.helperEl.remove();
  6077.             this.helperEl = null;
  6078.         }
  6079.     },
  6080.  
  6081.  
  6082.     /* Selection
  6083.     ------------------------------------------------------------------------------------------------------------------*/
  6084.  
  6085.  
  6086.     // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight.
  6087.     renderSelection: function(range) {
  6088.         if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered
  6089.             this.renderRangeHelper(range);
  6090.         }
  6091.         else {
  6092.             this.renderHighlight(this.selectionRangeToSegs(range));
  6093.         }
  6094.     },
  6095.  
  6096.  
  6097.     // Unrenders any visual indication of a selection
  6098.     unrenderSelection: function() {
  6099.         this.unrenderHelper();
  6100.         this.unrenderHighlight();
  6101.     },
  6102.  
  6103.  
  6104.     /* Fill System (highlight, background events, business hours)
  6105.     ------------------------------------------------------------------------------------------------------------------*/
  6106.  
  6107.  
  6108.     // Renders a set of rectangles over the given time segments.
  6109.     // Only returns segments that successfully rendered.
  6110.     renderFill: function(type, segs, className) {
  6111.         var segCols;
  6112.         var skeletonEl;
  6113.         var trEl;
  6114.         var col, colSegs;
  6115.         var tdEl;
  6116.         var containerEl;
  6117.         var dayDate;
  6118.         var i, seg;
  6119.  
  6120.         if (segs.length) {
  6121.  
  6122.             segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs
  6123.             segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg
  6124.  
  6125.             className = className || type.toLowerCase();
  6126.             skeletonEl = $(
  6127.                 '<div class="fc-' + className + '-skeleton">' +
  6128.                     '<table><tr/></table>' +
  6129.                 '</div>'
  6130.             );
  6131.             trEl = skeletonEl.find('tr');
  6132.  
  6133.             for (col = 0; col < segCols.length; col++) {
  6134.                 colSegs = segCols[col];
  6135.                 tdEl = $('<td/>').appendTo(trEl);
  6136.  
  6137.                 if (colSegs.length) {
  6138.                     containerEl = $('<div class="fc-' + className + '-container"/>').appendTo(tdEl);
  6139.                     dayDate = this.colDates[col];
  6140.  
  6141.                     for (i = 0; i < colSegs.length; i++) {
  6142.                         seg = colSegs[i];
  6143.                         containerEl.append(
  6144.                             seg.el.css({
  6145.                                 top: this.computeDateTop(seg.start, dayDate),
  6146.                                 bottom: -this.computeDateTop(seg.end, dayDate) // the y position of the bottom edge
  6147.                             })
  6148.                         );
  6149.                     }
  6150.                 }
  6151.             }
  6152.  
  6153.             this.bookendCells(trEl, type);
  6154.  
  6155.             this.el.append(skeletonEl);
  6156.             this.elsByFill[type] = skeletonEl;
  6157.         }
  6158.  
  6159.         return segs;
  6160.     }
  6161.  
  6162. });
  6163.  
  6164. ;;
  6165.  
  6166. /* Event-rendering methods for the TimeGrid class
  6167. ----------------------------------------------------------------------------------------------------------------------*/
  6168.  
  6169. TimeGrid.mixin({
  6170.  
  6171.     eventSkeletonEl: null, // has cells with event-containers, which contain absolutely positioned event elements
  6172.  
  6173.  
  6174.     // Renders the given foreground event segments onto the grid
  6175.     renderFgSegs: function(segs) {
  6176.         segs = this.renderFgSegEls(segs); // returns a subset of the segs. segs that were actually rendered
  6177.  
  6178.         this.el.append(
  6179.             this.eventSkeletonEl = $('<div class="fc-content-skeleton"/>')
  6180.                 .append(this.renderSegTable(segs))
  6181.         );
  6182.  
  6183.         return segs; // return only the segs that were actually rendered
  6184.     },
  6185.  
  6186.  
  6187.     // Unrenders all currently rendered foreground event segments
  6188.     unrenderFgSegs: function(segs) {
  6189.         if (this.eventSkeletonEl) {
  6190.             this.eventSkeletonEl.remove();
  6191.             this.eventSkeletonEl = null;
  6192.         }
  6193.     },
  6194.  
  6195.  
  6196.     // Renders and returns the <table> portion of the event-skeleton.
  6197.     // Returns an object with properties 'tbodyEl' and 'segs'.
  6198.     renderSegTable: function(segs) {
  6199.         var tableEl = $('<table><tr/></table>');
  6200.         var trEl = tableEl.find('tr');
  6201.         var segCols;
  6202.         var i, seg;
  6203.         var col, colSegs;
  6204.         var containerEl;
  6205.  
  6206.         segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg
  6207.  
  6208.         this.computeSegVerticals(segs); // compute and assign top/bottom
  6209.  
  6210.         for (col = 0; col < segCols.length; col++) { // iterate each column grouping
  6211.             colSegs = segCols[col];
  6212.             placeSlotSegs(colSegs); // compute horizontal coordinates, z-index's, and reorder the array
  6213.  
  6214.             containerEl = $('<div class="fc-event-container"/>');
  6215.  
  6216.             // assign positioning CSS and insert into container
  6217.             for (i = 0; i < colSegs.length; i++) {
  6218.                 seg = colSegs[i];
  6219.                 seg.el.css(this.generateSegPositionCss(seg));
  6220.  
  6221.                 // if the height is short, add a className for alternate styling
  6222.                 if (seg.bottom - seg.top < 30) {
  6223.                     seg.el.addClass('fc-short');
  6224.                 }
  6225.  
  6226.                 containerEl.append(seg.el);
  6227.             }
  6228.  
  6229.             trEl.append($('<td/>').append(containerEl));
  6230.         }
  6231.  
  6232.         this.bookendCells(trEl, 'eventSkeleton');
  6233.  
  6234.         return tableEl;
  6235.     },
  6236.  
  6237.  
  6238.     // Refreshes the CSS top/bottom coordinates for each segment element. Probably after a window resize/zoom.
  6239.     // Repositions business hours segs too, so not just for events. Maybe shouldn't be here.
  6240.     updateSegVerticals: function() {
  6241.         var allSegs = (this.segs || []).concat(this.businessHourSegs || []);
  6242.         var i;
  6243.  
  6244.         this.computeSegVerticals(allSegs);
  6245.  
  6246.         for (i = 0; i < allSegs.length; i++) {
  6247.             allSegs[i].el.css(
  6248.                 this.generateSegVerticalCss(allSegs[i])
  6249.             );
  6250.         }
  6251.     },
  6252.  
  6253.  
  6254.     // For each segment in an array, computes and assigns its top and bottom properties
  6255.     computeSegVerticals: function(segs) {
  6256.         var i, seg;
  6257.  
  6258.         for (i = 0; i < segs.length; i++) {
  6259.             seg = segs[i];
  6260.             seg.top = this.computeDateTop(seg.start, seg.start);
  6261.             seg.bottom = this.computeDateTop(seg.end, seg.start);
  6262.         }
  6263.     },
  6264.  
  6265.  
  6266.     // Renders the HTML for a single event segment's default rendering
  6267.     fgSegHtml: function(seg, disableResizing) {
  6268.         var view = this.view;
  6269.         var event = seg.event;
  6270.         var isDraggable = view.isEventDraggable(event);
  6271.         var isResizableFromStart = !disableResizing && seg.isStart && view.isEventResizableFromStart(event);
  6272.         var isResizableFromEnd = !disableResizing && seg.isEnd && view.isEventResizableFromEnd(event);
  6273.         var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd);
  6274.         var skinCss = cssToStr(this.getEventSkinCss(event));
  6275.         var timeText;
  6276.         var fullTimeText; // more verbose time text. for the print stylesheet
  6277.         var startTimeText; // just the start time text
  6278.  
  6279.         classes.unshift('fc-time-grid-event', 'fc-v-event');
  6280.  
  6281.         if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day...
  6282.             // Don't display time text on segments that run entirely through a day.
  6283.             // That would appear as midnight-midnight and would look dumb.
  6284.             // Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am)
  6285.             if (seg.isStart || seg.isEnd) {
  6286.                 timeText = this.getEventTimeText(seg);
  6287.                 fullTimeText = this.getEventTimeText(seg, 'LT');
  6288.                 startTimeText = this.getEventTimeText(seg, null, false); // displayEnd=false
  6289.             }
  6290.         } else {
  6291.             // Display the normal time text for the *event's* times
  6292.             timeText = this.getEventTimeText(event);
  6293.             fullTimeText = this.getEventTimeText(event, 'LT');
  6294.             startTimeText = this.getEventTimeText(event, null, false); // displayEnd=false
  6295.         }
  6296.  
  6297.         return '<a class="' + classes.join(' ') + '"' +
  6298.             (event.url ?
  6299.                 ' href="' + htmlEscape(event.url) + '"' :
  6300.                 ''
  6301.                 ) +
  6302.             (skinCss ?
  6303.                 ' style="' + skinCss + '"' :
  6304.                 ''
  6305.                 ) +
  6306.             '>' +
  6307.                 '<div class="fc-content">' +
  6308.                     (timeText ?
  6309.                         '<div class="fc-time"' +
  6310.                         ' data-start="' + htmlEscape(startTimeText) + '"' +
  6311.                         ' data-full="' + htmlEscape(fullTimeText) + '"' +
  6312.                         '>' +
  6313.                             '<span>' + htmlEscape(timeText) + '</span>' +
  6314.                         '</div>' :
  6315.                         ''
  6316.                         ) +
  6317.                     (event.title ?
  6318.                         '<div class="fc-title">' +
  6319.                             htmlEscape(event.title) +
  6320.                         '</div>' :
  6321.                         ''
  6322.                         ) +
  6323.                 '</div>' +
  6324.                 '<div class="fc-bg"/>' +
  6325.                 /* TODO: write CSS for this
  6326.                 (isResizableFromStart ?
  6327.                     '<div class="fc-resizer fc-start-resizer" />' :
  6328.                     ''
  6329.                     ) +
  6330.                 */
  6331.                 (isResizableFromEnd ?
  6332.                     '<div class="fc-resizer fc-end-resizer" />' :
  6333.                     ''
  6334.                     ) +
  6335.             '</a>';
  6336.     },
  6337.  
  6338.  
  6339.     // Generates an object with CSS properties/values that should be applied to an event segment element.
  6340.     // Contains important positioning-related properties that should be applied to any event element, customized or not.
  6341.     generateSegPositionCss: function(seg) {
  6342.         var shouldOverlap = this.view.opt('slotEventOverlap');
  6343.         var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point
  6344.         var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point
  6345.         var props = this.generateSegVerticalCss(seg); // get top/bottom first
  6346.         var left; // amount of space from left edge, a fraction of the total width
  6347.         var right; // amount of space from right edge, a fraction of the total width
  6348.  
  6349.         if (shouldOverlap) {
  6350.             // double the width, but don't go beyond the maximum forward coordinate (1.0)
  6351.             forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2);
  6352.         }
  6353.  
  6354.         if (this.isRTL) {
  6355.             left = 1 - forwardCoord;
  6356.             right = backwardCoord;
  6357.         }
  6358.         else {
  6359.             left = backwardCoord;
  6360.             right = 1 - forwardCoord;
  6361.         }
  6362.  
  6363.         props.zIndex = seg.level + 1; // convert from 0-base to 1-based
  6364.         props.left = left * 100 + '%';
  6365.         props.right = right * 100 + '%';
  6366.  
  6367.         if (shouldOverlap && seg.forwardPressure) {
  6368.             // add padding to the edge so that forward stacked events don't cover the resizer's icon
  6369.             props[this.isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width
  6370.         }
  6371.  
  6372.         return props;
  6373.     },
  6374.  
  6375.  
  6376.     // Generates an object with CSS properties for the top/bottom coordinates of a segment element
  6377.     generateSegVerticalCss: function(seg) {
  6378.         return {
  6379.             top: seg.top,
  6380.             bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container
  6381.         };
  6382.     },
  6383.  
  6384.  
  6385.     // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col
  6386.     groupSegCols: function(segs) {
  6387.         var segCols = [];
  6388.         var i;
  6389.  
  6390.         for (i = 0; i < this.colCnt; i++) {
  6391.             segCols.push([]);
  6392.         }
  6393.  
  6394.         for (i = 0; i < segs.length; i++) {
  6395.             segCols[segs[i].col].push(segs[i]);
  6396.         }
  6397.  
  6398.         return segCols;
  6399.     }
  6400.  
  6401. });
  6402.  
  6403.  
  6404. // Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each.
  6405. // NOTE: Also reorders the given array by date!
  6406. function placeSlotSegs(segs) {
  6407.     var levels;
  6408.     var level0;
  6409.     var i;
  6410.  
  6411.     segs.sort(compareSegs); // order by date
  6412.     levels = buildSlotSegLevels(segs);
  6413.     computeForwardSlotSegs(levels);
  6414.  
  6415.     if ((level0 = levels[0])) {
  6416.  
  6417.         for (i = 0; i < level0.length; i++) {
  6418.             computeSlotSegPressures(level0[i]);
  6419.         }
  6420.  
  6421.         for (i = 0; i < level0.length; i++) {
  6422.             computeSlotSegCoords(level0[i], 0, 0);
  6423.         }
  6424.     }
  6425. }
  6426.  
  6427.  
  6428. // Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is
  6429. // left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date.
  6430. function buildSlotSegLevels(segs) {
  6431.     var levels = [];
  6432.     var i, seg;
  6433.     var j;
  6434.  
  6435.     for (i=0; i<segs.length; i++) {
  6436.         seg = segs[i];
  6437.  
  6438.         // go through all the levels and stop on the first level where there are no collisions
  6439.         for (j=0; j<levels.length; j++) {
  6440.             if (!computeSlotSegCollisions(seg, levels[j]).length) {
  6441.                 break;
  6442.             }
  6443.         }
  6444.  
  6445.         seg.level = j;
  6446.  
  6447.         (levels[j] || (levels[j] = [])).push(seg);
  6448.     }
  6449.  
  6450.     return levels;
  6451. }
  6452.  
  6453.  
  6454. // For every segment, figure out the other segments that are in subsequent
  6455. // levels that also occupy the same vertical space. Accumulate in seg.forwardSegs
  6456. function computeForwardSlotSegs(levels) {
  6457.     var i, level;
  6458.     var j, seg;
  6459.     var k;
  6460.  
  6461.     for (i=0; i<levels.length; i++) {
  6462.         level = levels[i];
  6463.  
  6464.         for (j=0; j<level.length; j++) {
  6465.             seg = level[j];
  6466.  
  6467.             seg.forwardSegs = [];
  6468.             for (k=i+1; k<levels.length; k++) {
  6469.                 computeSlotSegCollisions(seg, levels[k], seg.forwardSegs);
  6470.             }
  6471.         }
  6472.     }
  6473. }
  6474.  
  6475.  
  6476. // Figure out which path forward (via seg.forwardSegs) results in the longest path until
  6477. // the furthest edge is reached. The number of segments in this path will be seg.forwardPressure
  6478. function computeSlotSegPressures(seg) {
  6479.     var forwardSegs = seg.forwardSegs;
  6480.     var forwardPressure = 0;
  6481.     var i, forwardSeg;
  6482.  
  6483.     if (seg.forwardPressure === undefined) { // not already computed
  6484.  
  6485.         for (i=0; i<forwardSegs.length; i++) {
  6486.             forwardSeg = forwardSegs[i];
  6487.  
  6488.             // figure out the child's maximum forward path
  6489.             computeSlotSegPressures(forwardSeg);
  6490.  
  6491.             // either use the existing maximum, or use the child's forward pressure
  6492.             // plus one (for the forwardSeg itself)
  6493.             forwardPressure = Math.max(
  6494.                 forwardPressure,
  6495.                 1 + forwardSeg.forwardPressure
  6496.             );
  6497.         }
  6498.  
  6499.         seg.forwardPressure = forwardPressure;
  6500.     }
  6501. }
  6502.  
  6503.  
  6504. // Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
  6505. // from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
  6506. // seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
  6507. //
  6508. // The segment might be part of a "series", which means consecutive segments with the same pressure
  6509. // who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
  6510. // segments behind this one in the current series, and `seriesBackwardCoord` is the starting
  6511. // coordinate of the first segment in the series.
  6512. function computeSlotSegCoords(seg, seriesBackwardPressure, seriesBackwardCoord) {
  6513.     var forwardSegs = seg.forwardSegs;
  6514.     var i;
  6515.  
  6516.     if (seg.forwardCoord === undefined) { // not already computed
  6517.  
  6518.         if (!forwardSegs.length) {
  6519.  
  6520.             // if there are no forward segments, this segment should butt up against the edge
  6521.             seg.forwardCoord = 1;
  6522.         }
  6523.         else {
  6524.  
  6525.             // sort highest pressure first
  6526.             forwardSegs.sort(compareForwardSlotSegs);
  6527.  
  6528.             // this segment's forwardCoord will be calculated from the backwardCoord of the
  6529.             // highest-pressure forward segment.
  6530.             computeSlotSegCoords(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord);
  6531.             seg.forwardCoord = forwardSegs[0].backwardCoord;
  6532.         }
  6533.  
  6534.         // calculate the backwardCoord from the forwardCoord. consider the series
  6535.         seg.backwardCoord = seg.forwardCoord -
  6536.             (seg.forwardCoord - seriesBackwardCoord) / // available width for series
  6537.             (seriesBackwardPressure + 1); // # of segments in the series
  6538.  
  6539.         // use this segment's coordinates to computed the coordinates of the less-pressurized
  6540.         // forward segments
  6541.         for (i=0; i<forwardSegs.length; i++) {
  6542.             computeSlotSegCoords(forwardSegs[i], 0, seg.forwardCoord);
  6543.         }
  6544.     }
  6545. }
  6546.  
  6547.  
  6548. // Find all the segments in `otherSegs` that vertically collide with `seg`.
  6549. // Append into an optionally-supplied `results` array and return.
  6550. function computeSlotSegCollisions(seg, otherSegs, results) {
  6551.     results = results || [];
  6552.  
  6553.     for (var i=0; i<otherSegs.length; i++) {
  6554.         if (isSlotSegCollision(seg, otherSegs[i])) {
  6555.             results.push(otherSegs[i]);
  6556.         }
  6557.     }
  6558.  
  6559.     return results;
  6560. }
  6561.  
  6562.  
  6563. // Do these segments occupy the same vertical space?
  6564. function isSlotSegCollision(seg1, seg2) {
  6565.     return seg1.bottom > seg2.top && seg1.top < seg2.bottom;
  6566. }
  6567.  
  6568.  
  6569. // A cmp function for determining which forward segment to rely on more when computing coordinates.
  6570. function compareForwardSlotSegs(seg1, seg2) {
  6571.     // put higher-pressure first
  6572.     return seg2.forwardPressure - seg1.forwardPressure ||
  6573.         // put segments that are closer to initial edge first (and favor ones with no coords yet)
  6574.         (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) ||
  6575.         // do normal sorting...
  6576.         compareSegs(seg1, seg2);
  6577. }
  6578.  
  6579. ;;
  6580.  
  6581. /* An abstract class from which other views inherit from
  6582. ----------------------------------------------------------------------------------------------------------------------*/
  6583.  
  6584. var View = fc.View = Class.extend({
  6585.  
  6586.     type: null, // subclass' view name (string)
  6587.     name: null, // deprecated. use `type` instead
  6588.     title: null, // the text that will be displayed in the header's title
  6589.  
  6590.     calendar: null, // owner Calendar object
  6591.     options: null, // hash containing all options. already merged with view-specific-options
  6592.     coordMap: null, // a CoordMap object for converting pixel regions to dates
  6593.     el: null, // the view's containing element. set by Calendar
  6594.  
  6595.     displaying: null, // a promise representing the state of rendering. null if no render requested
  6596.     isSkeletonRendered: false,
  6597.     isEventsRendered: false,
  6598.  
  6599.     // range the view is actually displaying (moments)
  6600.     start: null,
  6601.     end: null, // exclusive
  6602.  
  6603.     // range the view is formally responsible for (moments)
  6604.     // may be different from start/end. for example, a month view might have 1st-31st, excluding padded dates
  6605.     intervalStart: null,
  6606.     intervalEnd: null, // exclusive
  6607.     intervalDuration: null,
  6608.     intervalUnit: null, // name of largest unit being displayed, like "month" or "week"
  6609.  
  6610.     isRTL: false,
  6611.     isSelected: false, // boolean whether a range of time is user-selected or not
  6612.  
  6613.     // subclasses can optionally use a scroll container
  6614.     scrollerEl: null, // the element that will most likely scroll when content is too tall
  6615.     scrollTop: null, // cached vertical scroll value
  6616.  
  6617.     // classNames styled by jqui themes
  6618.     widgetHeaderClass: null,
  6619.     widgetContentClass: null,
  6620.     highlightStateClass: null,
  6621.  
  6622.     // for date utils, computed from options
  6623.     nextDayThreshold: null,
  6624.     isHiddenDayHash: null,
  6625.  
  6626.     // document handlers, bound to `this` object
  6627.     documentMousedownProxy: null, // TODO: doesn't work with touch
  6628.  
  6629.  
  6630.     constructor: function(calendar, type, options, intervalDuration) {
  6631.  
  6632.         this.calendar = calendar;
  6633.         this.type = this.name = type; // .name is deprecated
  6634.         this.options = options;
  6635.         this.intervalDuration = intervalDuration || moment.duration(1, 'day');
  6636.  
  6637.         this.nextDayThreshold = moment.duration(this.opt('nextDayThreshold'));
  6638.         this.initThemingProps();
  6639.         this.initHiddenDays();
  6640.         this.isRTL = this.opt('isRTL');
  6641.  
  6642.         this.documentMousedownProxy = proxy(this, 'documentMousedown');
  6643.  
  6644.         this.initialize();
  6645.     },
  6646.  
  6647.  
  6648.     // A good place for subclasses to initialize member variables
  6649.     initialize: function() {
  6650.         // subclasses can implement
  6651.     },
  6652.  
  6653.  
  6654.     // Retrieves an option with the given name
  6655.     opt: function(name) {
  6656.         return this.options[name];
  6657.     },
  6658.  
  6659.  
  6660.     // Triggers handlers that are view-related. Modifies args before passing to calendar.
  6661.     trigger: function(name, thisObj) { // arguments beyond thisObj are passed along
  6662.         var calendar = this.calendar;
  6663.  
  6664.         return calendar.trigger.apply(
  6665.             calendar,
  6666.             [name, thisObj || this].concat(
  6667.                 Array.prototype.slice.call(arguments, 2), // arguments beyond thisObj
  6668.                 [ this ] // always make the last argument a reference to the view. TODO: deprecate
  6669.             )
  6670.         );
  6671.     },
  6672.  
  6673.  
  6674.     /* Dates
  6675.     ------------------------------------------------------------------------------------------------------------------*/
  6676.  
  6677.  
  6678.     // Updates all internal dates to center around the given current date
  6679.     setDate: function(date) {
  6680.         this.setRange(this.computeRange(date));
  6681.     },
  6682.  
  6683.  
  6684.     // Updates all internal dates for displaying the given range.
  6685.     // Expects all values to be normalized (like what computeRange does).
  6686.     setRange: function(range) {
  6687.         $.extend(this, range);
  6688.         this.updateTitle();
  6689.     },
  6690.  
  6691.  
  6692.     // Given a single current date, produce information about what range to display.
  6693.     // Subclasses can override. Must return all properties.
  6694.     computeRange: function(date) {
  6695.         var intervalUnit = computeIntervalUnit(this.intervalDuration);
  6696.         var intervalStart = date.clone().startOf(intervalUnit);
  6697.         var intervalEnd = intervalStart.clone().add(this.intervalDuration);
  6698.         var start, end;
  6699.  
  6700.         // normalize the range's time-ambiguity
  6701.         if (/year|month|week|day/.test(intervalUnit)) { // whole-days?
  6702.             intervalStart.stripTime();
  6703.             intervalEnd.stripTime();
  6704.         }
  6705.         else { // needs to have a time?
  6706.             if (!intervalStart.hasTime()) {
  6707.                 intervalStart = this.calendar.rezoneDate(intervalStart); // convert to current timezone, with 00:00
  6708.             }
  6709.             if (!intervalEnd.hasTime()) {
  6710.                 intervalEnd = this.calendar.rezoneDate(intervalEnd); // convert to current timezone, with 00:00
  6711.             }
  6712.         }
  6713.  
  6714.         start = intervalStart.clone();
  6715.         start = this.skipHiddenDays(start);
  6716.         end = intervalEnd.clone();
  6717.         end = this.skipHiddenDays(end, -1, true); // exclusively move backwards
  6718.  
  6719.         return {
  6720.             intervalUnit: intervalUnit,
  6721.             intervalStart: intervalStart,
  6722.             intervalEnd: intervalEnd,
  6723.             start: start,
  6724.             end: end
  6725.         };
  6726.     },
  6727.  
  6728.  
  6729.     // Computes the new date when the user hits the prev button, given the current date
  6730.     computePrevDate: function(date) {
  6731.         return this.massageCurrentDate(
  6732.             date.clone().startOf(this.intervalUnit).subtract(this.intervalDuration), -1
  6733.         );
  6734.     },
  6735.  
  6736.  
  6737.     // Computes the new date when the user hits the next button, given the current date
  6738.     computeNextDate: function(date) {
  6739.         return this.massageCurrentDate(
  6740.             date.clone().startOf(this.intervalUnit).add(this.intervalDuration)
  6741.         );
  6742.     },
  6743.  
  6744.  
  6745.     // Given an arbitrarily calculated current date of the calendar, returns a date that is ensured to be completely
  6746.     // visible. `direction` is optional and indicates which direction the current date was being
  6747.     // incremented or decremented (1 or -1).
  6748.     massageCurrentDate: function(date, direction) {
  6749.         if (this.intervalDuration.as('days') <= 1) { // if the view displays a single day or smaller
  6750.             if (this.isHiddenDay(date)) {
  6751.                 date = this.skipHiddenDays(date, direction);
  6752.                 date.startOf('day');
  6753.             }
  6754.         }
  6755.  
  6756.         return date;
  6757.     },
  6758.  
  6759.  
  6760.     /* Title and Date Formatting
  6761.     ------------------------------------------------------------------------------------------------------------------*/
  6762.  
  6763.  
  6764.     // Sets the view's title property to the most updated computed value
  6765.     updateTitle: function() {
  6766.         this.title = this.computeTitle();
  6767.     },
  6768.  
  6769.  
  6770.     // Computes what the title at the top of the calendar should be for this view
  6771.     computeTitle: function() {
  6772.         return this.formatRange(
  6773.             { start: this.intervalStart, end: this.intervalEnd },
  6774.             this.opt('titleFormat') || this.computeTitleFormat(),
  6775.             this.opt('titleRangeSeparator')
  6776.         );
  6777.     },
  6778.  
  6779.  
  6780.     // Generates the format string that should be used to generate the title for the current date range.
  6781.     // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
  6782.     computeTitleFormat: function() {
  6783.         if (this.intervalUnit == 'year') {
  6784.             return 'YYYY';
  6785.         }
  6786.         else if (this.intervalUnit == 'month') {
  6787.             return this.opt('monthYearFormat'); // like "September 2014"
  6788.         }
  6789.         else if (this.intervalDuration.as('days') > 1) {
  6790.             return 'll'; // multi-day range. shorter, like "Sep 9 - 10 2014"
  6791.         }
  6792.         else {
  6793.             return 'LL'; // one day. longer, like "September 9 2014"
  6794.         }
  6795.     },
  6796.  
  6797.  
  6798.     // Utility for formatting a range. Accepts a range object, formatting string, and optional separator.
  6799.     // Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account.
  6800.     formatRange: function(range, formatStr, separator) {
  6801.         var end = range.end;
  6802.  
  6803.         if (!end.hasTime()) { // all-day?
  6804.             end = end.clone().subtract(1); // convert to inclusive. last ms of previous day
  6805.         }
  6806.  
  6807.         return formatRange(range.start, end, formatStr, separator, this.opt('isRTL'));
  6808.     },
  6809.  
  6810.  
  6811.     /* Rendering
  6812.     ------------------------------------------------------------------------------------------------------------------*/
  6813.  
  6814.  
  6815.     // Sets the container element that the view should render inside of.
  6816.     // Does other DOM-related initializations.
  6817.     setElement: function(el) {
  6818.         this.el = el;
  6819.         this.bindGlobalHandlers();
  6820.     },
  6821.  
  6822.  
  6823.     // Removes the view's container element from the DOM, clearing any content beforehand.
  6824.     // Undoes any other DOM-related attachments.
  6825.     removeElement: function() {
  6826.         this.clear(); // clears all content
  6827.  
  6828.         // clean up the skeleton
  6829.         if (this.isSkeletonRendered) {
  6830.             this.unrenderSkeleton();
  6831.             this.isSkeletonRendered = false;
  6832.         }
  6833.  
  6834.         this.unbindGlobalHandlers();
  6835.  
  6836.         this.el.remove();
  6837.  
  6838.         // NOTE: don't null-out this.el in case the View was destroyed within an API callback.
  6839.         // We don't null-out the View's other jQuery element references upon destroy,
  6840.         //  so we shouldn't kill this.el either.
  6841.     },
  6842.  
  6843.  
  6844.     // Does everything necessary to display the view centered around the given date.
  6845.     // Does every type of rendering EXCEPT rendering events.
  6846.     // Is asychronous and returns a promise.
  6847.     display: function(date) {
  6848.         var _this = this;
  6849.         var scrollState = null;
  6850.  
  6851.         if (this.displaying) {
  6852.             scrollState = this.queryScroll();
  6853.         }
  6854.  
  6855.         return this.clear().then(function() { // clear the content first (async)
  6856.             return (
  6857.                 _this.displaying =
  6858.                     $.when(_this.displayView(date)) // displayView might return a promise
  6859.                         .then(function() {
  6860.                             _this.forceScroll(_this.computeInitialScroll(scrollState));
  6861.                             _this.triggerRender();
  6862.                         })
  6863.             );
  6864.         });
  6865.     },
  6866.  
  6867.  
  6868.     // Does everything necessary to clear the content of the view.
  6869.     // Clears dates and events. Does not clear the skeleton.
  6870.     // Is asychronous and returns a promise.
  6871.     clear: function() {
  6872.         var _this = this;
  6873.         var displaying = this.displaying;
  6874.  
  6875.         if (displaying) { // previously displayed, or in the process of being displayed?
  6876.             return displaying.then(function() { // wait for the display to finish
  6877.                 _this.displaying = null;
  6878.                 _this.clearEvents();
  6879.                 return _this.clearView(); // might return a promise. chain it
  6880.             });
  6881.         }
  6882.         else {
  6883.             return $.when(); // an immediately-resolved promise
  6884.         }
  6885.     },
  6886.  
  6887.  
  6888.     // Displays the view's non-event content, such as date-related content or anything required by events.
  6889.     // Renders the view's non-content skeleton if necessary.
  6890.     // Can be asynchronous and return a promise.
  6891.     displayView: function(date) {
  6892.         if (!this.isSkeletonRendered) {
  6893.             this.renderSkeleton();
  6894.             this.isSkeletonRendered = true;
  6895.         }
  6896.         this.setDate(date);
  6897.         if (this.render) {
  6898.             this.render(); // TODO: deprecate
  6899.         }
  6900.         this.renderDates();
  6901.         this.updateSize();
  6902.         this.renderBusinessHours(); // might need coordinates, so should go after updateSize()
  6903.     },
  6904.  
  6905.  
  6906.     // Unrenders the view content that was rendered in displayView.
  6907.     // Can be asynchronous and return a promise.
  6908.     clearView: function() {
  6909.         this.unselect();
  6910.         this.triggerUnrender();
  6911.         this.unrenderBusinessHours();
  6912.         this.unrenderDates();
  6913.         if (this.destroy) {
  6914.             this.destroy(); // TODO: deprecate
  6915.         }
  6916.     },
  6917.  
  6918.  
  6919.     // Renders the basic structure of the view before any content is rendered
  6920.     renderSkeleton: function() {
  6921.         // subclasses should implement
  6922.     },
  6923.  
  6924.  
  6925.     // Unrenders the basic structure of the view
  6926.     unrenderSkeleton: function() {
  6927.         // subclasses should implement
  6928.     },
  6929.  
  6930.  
  6931.     // Renders the view's date-related content (like cells that represent days/times).
  6932.     // Assumes setRange has already been called and the skeleton has already been rendered.
  6933.     renderDates: function() {
  6934.         // subclasses should implement
  6935.     },
  6936.  
  6937.  
  6938.     // Unrenders the view's date-related content
  6939.     unrenderDates: function() {
  6940.         // subclasses should override
  6941.     },
  6942.  
  6943.  
  6944.     // Renders business-hours onto the view. Assumes updateSize has already been called.
  6945.     renderBusinessHours: function() {
  6946.         // subclasses should implement
  6947.     },
  6948.  
  6949.  
  6950.     // Unrenders previously-rendered business-hours
  6951.     unrenderBusinessHours: function() {
  6952.         // subclasses should implement
  6953.     },
  6954.  
  6955.  
  6956.     // Signals that the view's content has been rendered
  6957.     triggerRender: function() {
  6958.         this.trigger('viewRender', this, this, this.el);
  6959.     },
  6960.  
  6961.  
  6962.     // Signals that the view's content is about to be unrendered
  6963.     triggerUnrender: function() {
  6964.         this.trigger('viewDestroy', this, this, this.el);
  6965.     },
  6966.  
  6967.  
  6968.     // Binds DOM handlers to elements that reside outside the view container, such as the document
  6969.     bindGlobalHandlers: function() {
  6970.         $(document).on('mousedown', this.documentMousedownProxy);
  6971.     },
  6972.  
  6973.  
  6974.     // Unbinds DOM handlers from elements that reside outside the view container
  6975.     unbindGlobalHandlers: function() {
  6976.         $(document).off('mousedown', this.documentMousedownProxy);
  6977.     },
  6978.  
  6979.  
  6980.     // Initializes internal variables related to theming
  6981.     initThemingProps: function() {
  6982.         var tm = this.opt('theme') ? 'ui' : 'fc';
  6983.  
  6984.         this.widgetHeaderClass = tm + '-widget-header';
  6985.         this.widgetContentClass = tm + '-widget-content';
  6986.         this.highlightStateClass = tm + '-state-highlight';
  6987.     },
  6988.  
  6989.  
  6990.     /* Dimensions
  6991.     ------------------------------------------------------------------------------------------------------------------*/
  6992.  
  6993.  
  6994.     // Refreshes anything dependant upon sizing of the container element of the grid
  6995.     updateSize: function(isResize) {
  6996.         var scrollState;
  6997.  
  6998.         if (isResize) {
  6999.             scrollState = this.queryScroll();
  7000.         }
  7001.  
  7002.         this.updateHeight(isResize);
  7003.         this.updateWidth(isResize);
  7004.  
  7005.         if (isResize) {
  7006.             this.setScroll(scrollState);
  7007.         }
  7008.     },
  7009.  
  7010.  
  7011.     // Refreshes the horizontal dimensions of the calendar
  7012.     updateWidth: function(isResize) {
  7013.         // subclasses should implement
  7014.     },
  7015.  
  7016.  
  7017.     // Refreshes the vertical dimensions of the calendar
  7018.     updateHeight: function(isResize) {
  7019.         var calendar = this.calendar; // we poll the calendar for height information
  7020.  
  7021.         this.setHeight(
  7022.             calendar.getSuggestedViewHeight(),
  7023.             calendar.isHeightAuto()
  7024.         );
  7025.     },
  7026.  
  7027.  
  7028.     // Updates the vertical dimensions of the calendar to the specified height.
  7029.     // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height.
  7030.     setHeight: function(height, isAuto) {
  7031.         // subclasses should implement
  7032.     },
  7033.  
  7034.  
  7035.     /* Scroller
  7036.     ------------------------------------------------------------------------------------------------------------------*/
  7037.  
  7038.  
  7039.     // Given the total height of the view, return the number of pixels that should be used for the scroller.
  7040.     // Utility for subclasses.
  7041.     computeScrollerHeight: function(totalHeight) {
  7042.         var scrollerEl = this.scrollerEl;
  7043.         var both;
  7044.         var otherHeight; // cumulative height of everything that is not the scrollerEl in the view (header+borders)
  7045.  
  7046.         both = this.el.add(scrollerEl);
  7047.  
  7048.         // fuckin IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked
  7049.         both.css({
  7050.             position: 'relative', // cause a reflow, which will force fresh dimension recalculation
  7051.             left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll
  7052.         });
  7053.         otherHeight = this.el.outerHeight() - scrollerEl.height(); // grab the dimensions
  7054.         both.css({ position: '', left: '' }); // undo hack
  7055.  
  7056.         return totalHeight - otherHeight;
  7057.     },
  7058.  
  7059.  
  7060.     // Computes the initial pre-configured scroll state prior to allowing the user to change it.
  7061.     // Given the scroll state from the previous rendering. If first time rendering, given null.
  7062.     computeInitialScroll: function(previousScrollState) {
  7063.         return 0;
  7064.     },
  7065.  
  7066.  
  7067.     // Retrieves the view's current natural scroll state. Can return an arbitrary format.
  7068.     queryScroll: function() {
  7069.         if (this.scrollerEl) {
  7070.             return this.scrollerEl.scrollTop(); // operates on scrollerEl by default
  7071.         }
  7072.     },
  7073.  
  7074.  
  7075.     // Sets the view's scroll state. Will accept the same format computeInitialScroll and queryScroll produce.
  7076.     setScroll: function(scrollState) {
  7077.         if (this.scrollerEl) {
  7078.             return this.scrollerEl.scrollTop(scrollState); // operates on scrollerEl by default
  7079.         }
  7080.     },
  7081.  
  7082.  
  7083.     // Sets the scroll state, making sure to overcome any predefined scroll value the browser has in mind
  7084.     forceScroll: function(scrollState) {
  7085.         var _this = this;
  7086.  
  7087.         this.setScroll(scrollState);
  7088.         setTimeout(function() {
  7089.             _this.setScroll(scrollState);
  7090.         }, 0);
  7091.     },
  7092.  
  7093.  
  7094.     /* Event Elements / Segments
  7095.     ------------------------------------------------------------------------------------------------------------------*/
  7096.  
  7097.  
  7098.     // Does everything necessary to display the given events onto the current view
  7099.     displayEvents: function(events) {
  7100.         var scrollState = this.queryScroll();
  7101.  
  7102.         this.clearEvents();
  7103.         this.renderEvents(events);
  7104.         this.isEventsRendered = true;
  7105.         this.setScroll(scrollState);
  7106.         this.triggerEventRender();
  7107.     },
  7108.  
  7109.  
  7110.     // Does everything necessary to clear the view's currently-rendered events
  7111.     clearEvents: function() {
  7112.         if (this.isEventsRendered) {
  7113.             this.triggerEventUnrender();
  7114.             if (this.destroyEvents) {
  7115.                 this.destroyEvents(); // TODO: deprecate
  7116.             }
  7117.             this.unrenderEvents();
  7118.             this.isEventsRendered = false;
  7119.         }
  7120.     },
  7121.  
  7122.  
  7123.     // Renders the events onto the view.
  7124.     renderEvents: function(events) {
  7125.         // subclasses should implement
  7126.     },
  7127.  
  7128.  
  7129.     // Removes event elements from the view.
  7130.     unrenderEvents: function() {
  7131.         // subclasses should implement
  7132.     },
  7133.  
  7134.  
  7135.     // Signals that all events have been rendered
  7136.     triggerEventRender: function() {
  7137.         this.renderedEventSegEach(function(seg) {
  7138.             this.trigger('eventAfterRender', seg.event, seg.event, seg.el);
  7139.         });
  7140.         this.trigger('eventAfterAllRender');
  7141.     },
  7142.  
  7143.  
  7144.     // Signals that all event elements are about to be removed
  7145.     triggerEventUnrender: function() {
  7146.         this.renderedEventSegEach(function(seg) {
  7147.             this.trigger('eventDestroy', seg.event, seg.event, seg.el);
  7148.         });
  7149.     },
  7150.  
  7151.  
  7152.     // Given an event and the default element used for rendering, returns the element that should actually be used.
  7153.     // Basically runs events and elements through the eventRender hook.
  7154.     resolveEventEl: function(event, el) {
  7155.         var custom = this.trigger('eventRender', event, event, el);
  7156.  
  7157.         if (custom === false) { // means don't render at all
  7158.             el = null;
  7159.         }
  7160.         else if (custom && custom !== true) {
  7161.             el = $(custom);
  7162.         }
  7163.  
  7164.         return el;
  7165.     },
  7166.  
  7167.  
  7168.     // Hides all rendered event segments linked to the given event
  7169.     showEvent: function(event) {
  7170.         this.renderedEventSegEach(function(seg) {
  7171.             seg.el.css('visibility', '');
  7172.         }, event);
  7173.     },
  7174.  
  7175.  
  7176.     // Shows all rendered event segments linked to the given event
  7177.     hideEvent: function(event) {
  7178.         this.renderedEventSegEach(function(seg) {
  7179.             seg.el.css('visibility', 'hidden');
  7180.         }, event);
  7181.     },
  7182.  
  7183.  
  7184.     // Iterates through event segments that have been rendered (have an el). Goes through all by default.
  7185.     // If the optional `event` argument is specified, only iterates through segments linked to that event.
  7186.     // The `this` value of the callback function will be the view.
  7187.     renderedEventSegEach: function(func, event) {
  7188.         var segs = this.getEventSegs();
  7189.         var i;
  7190.  
  7191.         for (i = 0; i < segs.length; i++) {
  7192.             if (!event || segs[i].event._id === event._id) {
  7193.                 if (segs[i].el) {
  7194.                     func.call(this, segs[i]);
  7195.                 }
  7196.             }
  7197.         }
  7198.     },
  7199.  
  7200.  
  7201.     // Retrieves all the rendered segment objects for the view
  7202.     getEventSegs: function() {
  7203.         // subclasses must implement
  7204.         return [];
  7205.     },
  7206.  
  7207.  
  7208.     /* Event Drag-n-Drop
  7209.     ------------------------------------------------------------------------------------------------------------------*/
  7210.  
  7211.  
  7212.     // Computes if the given event is allowed to be dragged by the user
  7213.     isEventDraggable: function(event) {
  7214.         var source = event.source || {};
  7215.  
  7216.         return firstDefined(
  7217.             event.startEditable,
  7218.             source.startEditable,
  7219.             this.opt('eventStartEditable'),
  7220.             event.editable,
  7221.             source.editable,
  7222.             this.opt('editable')
  7223.         );
  7224.     },
  7225.  
  7226.  
  7227.     // Must be called when an event in the view is dropped onto new location.
  7228.     // `dropLocation` is an object that contains the new start/end/allDay values for the event.
  7229.     reportEventDrop: function(event, dropLocation, largeUnit, el, ev) {
  7230.         var calendar = this.calendar;
  7231.         var mutateResult = calendar.mutateEvent(event, dropLocation, largeUnit);
  7232.         var undoFunc = function() {
  7233.             mutateResult.undo();
  7234.             calendar.reportEventChange();
  7235.         };
  7236.  
  7237.         this.triggerEventDrop(event, mutateResult.dateDelta, undoFunc, el, ev);
  7238.         calendar.reportEventChange(); // will rerender events
  7239.     },
  7240.  
  7241.  
  7242.     // Triggers event-drop handlers that have subscribed via the API
  7243.     triggerEventDrop: function(event, dateDelta, undoFunc, el, ev) {
  7244.         this.trigger('eventDrop', el[0], event, dateDelta, undoFunc, ev, {}); // {} = jqui dummy
  7245.     },
  7246.  
  7247.  
  7248.     /* External Element Drag-n-Drop
  7249.     ------------------------------------------------------------------------------------------------------------------*/
  7250.  
  7251.  
  7252.     // Must be called when an external element, via jQuery UI, has been dropped onto the calendar.
  7253.     // `meta` is the parsed data that has been embedded into the dragging event.
  7254.     // `dropLocation` is an object that contains the new start/end/allDay values for the event.
  7255.     reportExternalDrop: function(meta, dropLocation, el, ev, ui) {
  7256.         var eventProps = meta.eventProps;
  7257.         var eventInput;
  7258.         var event;
  7259.  
  7260.         // Try to build an event object and render it. TODO: decouple the two
  7261.         if (eventProps) {
  7262.             eventInput = $.extend({}, eventProps, dropLocation);
  7263.             event = this.calendar.renderEvent(eventInput, meta.stick)[0]; // renderEvent returns an array
  7264.         }
  7265.  
  7266.         this.triggerExternalDrop(event, dropLocation, el, ev, ui);
  7267.     },
  7268.  
  7269.  
  7270.     // Triggers external-drop handlers that have subscribed via the API
  7271.     triggerExternalDrop: function(event, dropLocation, el, ev, ui) {
  7272.  
  7273.         // trigger 'drop' regardless of whether element represents an event
  7274.         this.trigger('drop', el[0], dropLocation.start, ev, ui);
  7275.  
  7276.         if (event) {
  7277.             this.trigger('eventReceive', null, event); // signal an external event landed
  7278.         }
  7279.     },
  7280.  
  7281.  
  7282.     /* Drag-n-Drop Rendering (for both events and external elements)
  7283.     ------------------------------------------------------------------------------------------------------------------*/
  7284.  
  7285.  
  7286.     // Renders a visual indication of a event or external-element drag over the given drop zone.
  7287.     // If an external-element, seg will be `null`
  7288.     renderDrag: function(dropLocation, seg) {
  7289.         // subclasses must implement
  7290.     },
  7291.  
  7292.  
  7293.     // Unrenders a visual indication of an event or external-element being dragged.
  7294.     unrenderDrag: function() {
  7295.         // subclasses must implement
  7296.     },
  7297.  
  7298.  
  7299.     /* Event Resizing
  7300.     ------------------------------------------------------------------------------------------------------------------*/
  7301.  
  7302.  
  7303.     // Computes if the given event is allowed to be resized from its starting edge
  7304.     isEventResizableFromStart: function(event) {
  7305.         return this.opt('eventResizableFromStart') && this.isEventResizable(event);
  7306.     },
  7307.  
  7308.  
  7309.     // Computes if the given event is allowed to be resized from its ending edge
  7310.     isEventResizableFromEnd: function(event) {
  7311.         return this.isEventResizable(event);
  7312.     },
  7313.  
  7314.  
  7315.     // Computes if the given event is allowed to be resized by the user at all
  7316.     isEventResizable: function(event) {
  7317.         var source = event.source || {};
  7318.  
  7319.         return firstDefined(
  7320.             event.durationEditable,
  7321.             source.durationEditable,
  7322.             this.opt('eventDurationEditable'),
  7323.             event.editable,
  7324.             source.editable,
  7325.             this.opt('editable')
  7326.         );
  7327.     },
  7328.  
  7329.  
  7330.     // Must be called when an event in the view has been resized to a new length
  7331.     reportEventResize: function(event, resizeLocation, largeUnit, el, ev) {
  7332.         var calendar = this.calendar;
  7333.         var mutateResult = calendar.mutateEvent(event, resizeLocation, largeUnit);
  7334.         var undoFunc = function() {
  7335.             mutateResult.undo();
  7336.             calendar.reportEventChange();
  7337.         };
  7338.  
  7339.         this.triggerEventResize(event, mutateResult.durationDelta, undoFunc, el, ev);
  7340.         calendar.reportEventChange(); // will rerender events
  7341.     },
  7342.  
  7343.  
  7344.     // Triggers event-resize handlers that have subscribed via the API
  7345.     triggerEventResize: function(event, durationDelta, undoFunc, el, ev) {
  7346.         this.trigger('eventResize', el[0], event, durationDelta, undoFunc, ev, {}); // {} = jqui dummy
  7347.     },
  7348.  
  7349.  
  7350.     /* Selection
  7351.     ------------------------------------------------------------------------------------------------------------------*/
  7352.  
  7353.  
  7354.     // Selects a date range on the view. `start` and `end` are both Moments.
  7355.     // `ev` is the native mouse event that begin the interaction.
  7356.     select: function(range, ev) {
  7357.         this.unselect(ev);
  7358.         this.renderSelection(range);
  7359.         this.reportSelection(range, ev);
  7360.     },
  7361.  
  7362.  
  7363.     // Renders a visual indication of the selection
  7364.     renderSelection: function(range) {
  7365.         // subclasses should implement
  7366.     },
  7367.  
  7368.  
  7369.     // Called when a new selection is made. Updates internal state and triggers handlers.
  7370.     reportSelection: function(range, ev) {
  7371.         this.isSelected = true;
  7372.         this.triggerSelect(range, ev);
  7373.     },
  7374.  
  7375.  
  7376.     // Triggers handlers to 'select'
  7377.     triggerSelect: function(range, ev) {
  7378.         this.trigger('select', null, range.start, range.end, ev);
  7379.     },
  7380.  
  7381.  
  7382.     // Undoes a selection. updates in the internal state and triggers handlers.
  7383.     // `ev` is the native mouse event that began the interaction.
  7384.     unselect: function(ev) {
  7385.         if (this.isSelected) {
  7386.             this.isSelected = false;
  7387.             if (this.destroySelection) {
  7388.                 this.destroySelection(); // TODO: deprecate
  7389.             }
  7390.             this.unrenderSelection();
  7391.             this.trigger('unselect', null, ev);
  7392.         }
  7393.     },
  7394.  
  7395.  
  7396.     // Unrenders a visual indication of selection
  7397.     unrenderSelection: function() {
  7398.         // subclasses should implement
  7399.     },
  7400.  
  7401.  
  7402.     // Handler for unselecting when the user clicks something and the 'unselectAuto' setting is on
  7403.     documentMousedown: function(ev) {
  7404.         var ignore;
  7405.  
  7406.         // is there a selection, and has the user made a proper left click?
  7407.         if (this.isSelected && this.opt('unselectAuto') && isPrimaryMouseButton(ev)) {
  7408.  
  7409.             // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element
  7410.             ignore = this.opt('unselectCancel');
  7411.             if (!ignore || !$(ev.target).closest(ignore).length) {
  7412.                 this.unselect(ev);
  7413.             }
  7414.         }
  7415.     },
  7416.  
  7417.  
  7418.     /* Day Click
  7419.     ------------------------------------------------------------------------------------------------------------------*/
  7420.  
  7421.  
  7422.     // Triggers handlers to 'dayClick'
  7423.     triggerDayClick: function(cell, dayEl, ev) {
  7424.         this.trigger('dayClick', dayEl, cell.start, ev);
  7425.     },
  7426.  
  7427.  
  7428.     /* Date Utils
  7429.     ------------------------------------------------------------------------------------------------------------------*/
  7430.  
  7431.  
  7432.     // Initializes internal variables related to calculating hidden days-of-week
  7433.     initHiddenDays: function() {
  7434.         var hiddenDays = this.opt('hiddenDays') || []; // array of day-of-week indices that are hidden
  7435.         var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
  7436.         var dayCnt = 0;
  7437.         var i;
  7438.  
  7439.         if (this.opt('weekends') === false) {
  7440.             hiddenDays.push(0, 6); // 0=sunday, 6=saturday
  7441.         }
  7442.  
  7443.         for (i = 0; i < 7; i++) {
  7444.             if (
  7445.                 !(isHiddenDayHash[i] = $.inArray(i, hiddenDays) !== -1)
  7446.             ) {
  7447.                 dayCnt++;
  7448.             }
  7449.         }
  7450.  
  7451.         if (!dayCnt) {
  7452.             throw 'invalid hiddenDays'; // all days were hidden? bad.
  7453.         }
  7454.  
  7455.         this.isHiddenDayHash = isHiddenDayHash;
  7456.     },
  7457.  
  7458.  
  7459.     // Is the current day hidden?
  7460.     // `day` is a day-of-week index (0-6), or a Moment
  7461.     isHiddenDay: function(day) {
  7462.         if (moment.isMoment(day)) {
  7463.             day = day.day();
  7464.         }
  7465.         return this.isHiddenDayHash[day];
  7466.     },
  7467.  
  7468.  
  7469.     // Incrementing the current day until it is no longer a hidden day, returning a copy.
  7470.     // If the initial value of `date` is not a hidden day, don't do anything.
  7471.     // Pass `isExclusive` as `true` if you are dealing with an end date.
  7472.     // `inc` defaults to `1` (increment one day forward each time)
  7473.     skipHiddenDays: function(date, inc, isExclusive) {
  7474.         var out = date.clone();
  7475.         inc = inc || 1;
  7476.         while (
  7477.             this.isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7]
  7478.         ) {
  7479.             out.add(inc, 'days');
  7480.         }
  7481.         return out;
  7482.     },
  7483.  
  7484.  
  7485.     // Returns the date range of the full days the given range visually appears to occupy.
  7486.     // Returns a new range object.
  7487.     computeDayRange: function(range) {
  7488.         var startDay = range.start.clone().stripTime(); // the beginning of the day the range starts
  7489.         var end = range.end;
  7490.         var endDay = null;
  7491.         var endTimeMS;
  7492.  
  7493.         if (end) {
  7494.             endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends
  7495.             endTimeMS = +end.time(); // # of milliseconds into `endDay`
  7496.  
  7497.             // If the end time is actually inclusively part of the next day and is equal to or
  7498.             // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
  7499.             // Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
  7500.             if (endTimeMS && endTimeMS >= this.nextDayThreshold) {
  7501.                 endDay.add(1, 'days');
  7502.             }
  7503.         }
  7504.  
  7505.         // If no end was specified, or if it is within `startDay` but not past nextDayThreshold,
  7506.         // assign the default duration of one day.
  7507.         if (!end || endDay <= startDay) {
  7508.             endDay = startDay.clone().add(1, 'days');
  7509.         }
  7510.  
  7511.         return { start: startDay, end: endDay };
  7512.     },
  7513.  
  7514.  
  7515.     // Does the given event visually appear to occupy more than one day?
  7516.     isMultiDayEvent: function(event) {
  7517.         var range = this.computeDayRange(event); // event is range-ish
  7518.  
  7519.         return range.end.diff(range.start, 'days') > 1;
  7520.     }
  7521.  
  7522. });
  7523.  
  7524. ;;
  7525.  
  7526. var Calendar = fc.Calendar = Class.extend({
  7527.  
  7528.     dirDefaults: null, // option defaults related to LTR or RTL
  7529.     langDefaults: null, // option defaults related to current locale
  7530.     overrides: null, // option overrides given to the fullCalendar constructor
  7531.     options: null, // all defaults combined with overrides
  7532.     viewSpecCache: null, // cache of view definitions
  7533.     view: null, // current View object
  7534.     header: null,
  7535.     loadingLevel: 0, // number of simultaneous loading tasks
  7536.  
  7537.  
  7538.     // a lot of this class' OOP logic is scoped within this constructor function,
  7539.     // but in the future, write individual methods on the prototype.
  7540.     constructor: Calendar_constructor,
  7541.  
  7542.  
  7543.     // Subclasses can override this for initialization logic after the constructor has been called
  7544.     initialize: function() {
  7545.     },
  7546.  
  7547.  
  7548.     // Initializes `this.options` and other important options-related objects
  7549.     initOptions: function(overrides) {
  7550.         var lang, langDefaults;
  7551.         var isRTL, dirDefaults;
  7552.  
  7553.         // converts legacy options into non-legacy ones.
  7554.         // in the future, when this is removed, don't use `overrides` reference. make a copy.
  7555.         overrides = massageOverrides(overrides);
  7556.  
  7557.         lang = overrides.lang;
  7558.         langDefaults = langOptionHash[lang];
  7559.         if (!langDefaults) {
  7560.             lang = Calendar.defaults.lang;
  7561.             langDefaults = langOptionHash[lang] || {};
  7562.         }
  7563.  
  7564.         isRTL = firstDefined(
  7565.             overrides.isRTL,
  7566.             langDefaults.isRTL,
  7567.             Calendar.defaults.isRTL
  7568.         );
  7569.         dirDefaults = isRTL ? Calendar.rtlDefaults : {};
  7570.  
  7571.         this.dirDefaults = dirDefaults;
  7572.         this.langDefaults = langDefaults;
  7573.         this.overrides = overrides;
  7574.         this.options = mergeOptions([ // merge defaults and overrides. lowest to highest precedence
  7575.             Calendar.defaults, // global defaults
  7576.             dirDefaults,
  7577.             langDefaults,
  7578.             overrides
  7579.         ]);
  7580.         populateInstanceComputableOptions(this.options);
  7581.  
  7582.         this.viewSpecCache = {}; // somewhat unrelated
  7583.     },
  7584.  
  7585.  
  7586.     // Gets information about how to create a view. Will use a cache.
  7587.     getViewSpec: function(viewType) {
  7588.         var cache = this.viewSpecCache;
  7589.  
  7590.         return cache[viewType] || (cache[viewType] = this.buildViewSpec(viewType));
  7591.     },
  7592.  
  7593.  
  7594.     // Given a duration singular unit, like "week" or "day", finds a matching view spec.
  7595.     // Preference is given to views that have corresponding buttons.
  7596.     getUnitViewSpec: function(unit) {
  7597.         var viewTypes;
  7598.         var i;
  7599.         var spec;
  7600.  
  7601.         if ($.inArray(unit, intervalUnits) != -1) {
  7602.  
  7603.             // put views that have buttons first. there will be duplicates, but oh well
  7604.             viewTypes = this.header.getViewsWithButtons();
  7605.             $.each(fc.views, function(viewType) { // all views
  7606.                 viewTypes.push(viewType);
  7607.             });
  7608.  
  7609.             for (i = 0; i < viewTypes.length; i++) {
  7610.                 spec = this.getViewSpec(viewTypes[i]);
  7611.                 if (spec) {
  7612.                     if (spec.singleUnit == unit) {
  7613.                         return spec;
  7614.                     }
  7615.                 }
  7616.             }
  7617.         }
  7618.     },
  7619.  
  7620.  
  7621.     // Builds an object with information on how to create a given view
  7622.     buildViewSpec: function(requestedViewType) {
  7623.         var viewOverrides = this.overrides.views || {};
  7624.         var specChain = []; // for the view. lowest to highest priority
  7625.         var defaultsChain = []; // for the view. lowest to highest priority
  7626.         var overridesChain = []; // for the view. lowest to highest priority
  7627.         var viewType = requestedViewType;
  7628.         var spec; // for the view
  7629.         var overrides; // for the view
  7630.         var duration;
  7631.         var unit;
  7632.  
  7633.         // iterate from the specific view definition to a more general one until we hit an actual View class
  7634.         while (viewType) {
  7635.             spec = fcViews[viewType];
  7636.             overrides = viewOverrides[viewType];
  7637.             viewType = null; // clear. might repopulate for another iteration
  7638.  
  7639.             if (typeof spec === 'function') { // TODO: deprecate
  7640.                 spec = { 'class': spec };
  7641.             }
  7642.  
  7643.             if (spec) {
  7644.                 specChain.unshift(spec);
  7645.                 defaultsChain.unshift(spec.defaults || {});
  7646.                 duration = duration || spec.duration;
  7647.                 viewType = viewType || spec.type;
  7648.             }
  7649.  
  7650.             if (overrides) {
  7651.                 overridesChain.unshift(overrides); // view-specific option hashes have options at zero-level
  7652.                 duration = duration || overrides.duration;
  7653.                 viewType = viewType || overrides.type;
  7654.             }
  7655.         }
  7656.  
  7657.         spec = mergeProps(specChain);
  7658.         spec.type = requestedViewType;
  7659.         if (!spec['class']) {
  7660.             return false;
  7661.         }
  7662.  
  7663.         if (duration) {
  7664.             duration = moment.duration(duration);
  7665.             if (duration.valueOf()) { // valid?
  7666.                 spec.duration = duration;
  7667.                 unit = computeIntervalUnit(duration);
  7668.  
  7669.                 // view is a single-unit duration, like "week" or "day"
  7670.                 // incorporate options for this. lowest priority
  7671.                 if (duration.as(unit) === 1) {
  7672.                     spec.singleUnit = unit;
  7673.                     overridesChain.unshift(viewOverrides[unit] || {});
  7674.                 }
  7675.             }
  7676.         }
  7677.  
  7678.         spec.defaults = mergeOptions(defaultsChain);
  7679.         spec.overrides = mergeOptions(overridesChain);
  7680.  
  7681.         this.buildViewSpecOptions(spec);
  7682.         this.buildViewSpecButtonText(spec, requestedViewType);
  7683.  
  7684.         return spec;
  7685.     },
  7686.  
  7687.  
  7688.     // Builds and assigns a view spec's options object from its already-assigned defaults and overrides
  7689.     buildViewSpecOptions: function(spec) {
  7690.         spec.options = mergeOptions([ // lowest to highest priority
  7691.             Calendar.defaults, // global defaults
  7692.             spec.defaults, // view's defaults (from ViewSubclass.defaults)
  7693.             this.dirDefaults,
  7694.             this.langDefaults, // locale and dir take precedence over view's defaults!
  7695.             this.overrides, // calendar's overrides (options given to constructor)
  7696.             spec.overrides // view's overrides (view-specific options)
  7697.         ]);
  7698.         populateInstanceComputableOptions(spec.options);
  7699.     },
  7700.  
  7701.  
  7702.     // Computes and assigns a view spec's buttonText-related options
  7703.     buildViewSpecButtonText: function(spec, requestedViewType) {
  7704.  
  7705.         // given an options object with a possible `buttonText` hash, lookup the buttonText for the
  7706.         // requested view, falling back to a generic unit entry like "week" or "day"
  7707.         function queryButtonText(options) {
  7708.             var buttonText = options.buttonText || {};
  7709.             return buttonText[requestedViewType] ||
  7710.                 (spec.singleUnit ? buttonText[spec.singleUnit] : null);
  7711.         }
  7712.  
  7713.         // highest to lowest priority
  7714.         spec.buttonTextOverride =
  7715.             queryButtonText(this.overrides) || // constructor-specified buttonText lookup hash takes precedence
  7716.             spec.overrides.buttonText; // `buttonText` for view-specific options is a string
  7717.  
  7718.         // highest to lowest priority. mirrors buildViewSpecOptions
  7719.         spec.buttonTextDefault =
  7720.             queryButtonText(this.langDefaults) ||
  7721.             queryButtonText(this.dirDefaults) ||
  7722.             spec.defaults.buttonText || // a single string. from ViewSubclass.defaults
  7723.             queryButtonText(Calendar.defaults) ||
  7724.             (spec.duration ? this.humanizeDuration(spec.duration) : null) || // like "3 days"
  7725.             requestedViewType; // fall back to given view name
  7726.     },
  7727.  
  7728.  
  7729.     // Given a view name for a custom view or a standard view, creates a ready-to-go View object
  7730.     instantiateView: function(viewType) {
  7731.         var spec = this.getViewSpec(viewType);
  7732.  
  7733.         return new spec['class'](this, viewType, spec.options, spec.duration);
  7734.     },
  7735.  
  7736.  
  7737.     // Returns a boolean about whether the view is okay to instantiate at some point
  7738.     isValidViewType: function(viewType) {
  7739.         return Boolean(this.getViewSpec(viewType));
  7740.     },
  7741.  
  7742.  
  7743.     // Should be called when any type of async data fetching begins
  7744.     pushLoading: function() {
  7745.         if (!(this.loadingLevel++)) {
  7746.             this.trigger('loading', null, true, this.view);
  7747.         }
  7748.     },
  7749.  
  7750.  
  7751.     // Should be called when any type of async data fetching completes
  7752.     popLoading: function() {
  7753.         if (!(--this.loadingLevel)) {
  7754.             this.trigger('loading', null, false, this.view);
  7755.         }
  7756.     },
  7757.  
  7758.  
  7759.     // Given arguments to the select method in the API, returns a range
  7760.     buildSelectRange: function(start, end) {
  7761.  
  7762.         start = this.moment(start);
  7763.         if (end) {
  7764.             end = this.moment(end);
  7765.         }
  7766.         else if (start.hasTime()) {
  7767.             end = start.clone().add(this.defaultTimedEventDuration);
  7768.         }
  7769.         else {
  7770.             end = start.clone().add(this.defaultAllDayEventDuration);
  7771.         }
  7772.  
  7773.         return { start: start, end: end };
  7774.     }
  7775.  
  7776. });
  7777.  
  7778.  
  7779. function Calendar_constructor(element, overrides) {
  7780.     var t = this;
  7781.  
  7782.  
  7783.     t.initOptions(overrides || {});
  7784.     var options = this.options;
  7785.  
  7786.    
  7787.     // Exports
  7788.     // -----------------------------------------------------------------------------------
  7789.  
  7790.     t.render = render;
  7791.     t.destroy = destroy;
  7792.     t.refetchEvents = refetchEvents;
  7793.     t.reportEvents = reportEvents;
  7794.     t.reportEventChange = reportEventChange;
  7795.     t.rerenderEvents = renderEvents; // `renderEvents` serves as a rerender. an API method
  7796.     t.changeView = renderView; // `renderView` will switch to another view
  7797.     t.select = select;
  7798.     t.unselect = unselect;
  7799.     t.prev = prev;
  7800.     t.next = next;
  7801.     t.prevYear = prevYear;
  7802.     t.nextYear = nextYear;
  7803.     t.today = today;
  7804.     t.gotoDate = gotoDate;
  7805.     t.incrementDate = incrementDate;
  7806.     t.zoomTo = zoomTo;
  7807.     t.getDate = getDate;
  7808.     t.getCalendar = getCalendar;
  7809.     t.getView = getView;
  7810.     t.option = option;
  7811.     t.trigger = trigger;
  7812.  
  7813.  
  7814.  
  7815.     // Language-data Internals
  7816.     // -----------------------------------------------------------------------------------
  7817.     // Apply overrides to the current language's data
  7818.  
  7819.  
  7820.     var localeData = createObject( // make a cheap copy
  7821.         getMomentLocaleData(options.lang) // will fall back to en
  7822.     );
  7823.  
  7824.     if (options.monthNames) {
  7825.         localeData._months = options.monthNames;
  7826.     }
  7827.     if (options.monthNamesShort) {
  7828.         localeData._monthsShort = options.monthNamesShort;
  7829.     }
  7830.     if (options.dayNames) {
  7831.         localeData._weekdays = options.dayNames;
  7832.     }
  7833.     if (options.dayNamesShort) {
  7834.         localeData._weekdaysShort = options.dayNamesShort;
  7835.     }
  7836.     if (options.firstDay != null) {
  7837.         var _week = createObject(localeData._week); // _week: { dow: # }
  7838.         _week.dow = options.firstDay;
  7839.         localeData._week = _week;
  7840.     }
  7841.  
  7842.     // assign a normalized value, to be used by our .week() moment extension
  7843.     localeData._fullCalendar_weekCalc = (function(weekCalc) {
  7844.         if (typeof weekCalc === 'function') {
  7845.             return weekCalc;
  7846.         }
  7847.         else if (weekCalc === 'local') {
  7848.             return weekCalc;
  7849.         }
  7850.         else if (weekCalc === 'iso' || weekCalc === 'ISO') {
  7851.             return 'ISO';
  7852.         }
  7853.     })(options.weekNumberCalculation);
  7854.  
  7855.  
  7856.  
  7857.     // Calendar-specific Date Utilities
  7858.     // -----------------------------------------------------------------------------------
  7859.  
  7860.  
  7861.     t.defaultAllDayEventDuration = moment.duration(options.defaultAllDayEventDuration);
  7862.     t.defaultTimedEventDuration = moment.duration(options.defaultTimedEventDuration);
  7863.  
  7864.  
  7865.     // Builds a moment using the settings of the current calendar: timezone and language.
  7866.     // Accepts anything the vanilla moment() constructor accepts.
  7867.     t.moment = function() {
  7868.         var mom;
  7869.  
  7870.         if (options.timezone === 'local') {
  7871.             mom = fc.moment.apply(null, arguments);
  7872.  
  7873.             // Force the moment to be local, because fc.moment doesn't guarantee it.
  7874.             if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone
  7875.                 mom.local();
  7876.             }
  7877.         }
  7878.         else if (options.timezone === 'UTC') {
  7879.             mom = fc.moment.utc.apply(null, arguments); // process as UTC
  7880.         }
  7881.         else {
  7882.             mom = fc.moment.parseZone.apply(null, arguments); // let the input decide the zone
  7883.         }
  7884.  
  7885.         if ('_locale' in mom) { // moment 2.8 and above
  7886.             mom._locale = localeData;
  7887.         }
  7888.         else { // pre-moment-2.8
  7889.             mom._lang = localeData;
  7890.         }
  7891.  
  7892.         return mom;
  7893.     };
  7894.  
  7895.  
  7896.     // Returns a boolean about whether or not the calendar knows how to calculate
  7897.     // the timezone offset of arbitrary dates in the current timezone.
  7898.     t.getIsAmbigTimezone = function() {
  7899.         return options.timezone !== 'local' && options.timezone !== 'UTC';
  7900.     };
  7901.  
  7902.  
  7903.     // Returns a copy of the given date in the current timezone of it is ambiguously zoned.
  7904.     // This will also give the date an unambiguous time.
  7905.     t.rezoneDate = function(date) {
  7906.         return t.moment(date.toArray());
  7907.     };
  7908.  
  7909.  
  7910.     // Returns a moment for the current date, as defined by the client's computer,
  7911.     // or overridden by the `now` option.
  7912.     t.getNow = function() {
  7913.         var now = options.now;
  7914.         if (typeof now === 'function') {
  7915.             now = now();
  7916.         }
  7917.         return t.moment(now);
  7918.     };
  7919.  
  7920.  
  7921.     // Get an event's normalized end date. If not present, calculate it from the defaults.
  7922.     t.getEventEnd = function(event) {
  7923.         if (event.end) {
  7924.             return event.end.clone();
  7925.         }
  7926.         else {
  7927.             return t.getDefaultEventEnd(event.allDay, event.start);
  7928.         }
  7929.     };
  7930.  
  7931.  
  7932.     // Given an event's allDay status and start date, return swhat its fallback end date should be.
  7933.     t.getDefaultEventEnd = function(allDay, start) { // TODO: rename to computeDefaultEventEnd
  7934.         var end = start.clone();
  7935.  
  7936.         if (allDay) {
  7937.             end.stripTime().add(t.defaultAllDayEventDuration);
  7938.         }
  7939.         else {
  7940.             end.add(t.defaultTimedEventDuration);
  7941.         }
  7942.  
  7943.         if (t.getIsAmbigTimezone()) {
  7944.             end.stripZone(); // we don't know what the tzo should be
  7945.         }
  7946.  
  7947.         return end;
  7948.     };
  7949.  
  7950.  
  7951.     // Produces a human-readable string for the given duration.
  7952.     // Side-effect: changes the locale of the given duration.
  7953.     t.humanizeDuration = function(duration) {
  7954.         return (duration.locale || duration.lang).call(duration, options.lang) // works moment-pre-2.8
  7955.             .humanize();
  7956.     };
  7957.  
  7958.  
  7959.    
  7960.     // Imports
  7961.     // -----------------------------------------------------------------------------------
  7962.  
  7963.  
  7964.     EventManager.call(t, options);
  7965.     var isFetchNeeded = t.isFetchNeeded;
  7966.     var fetchEvents = t.fetchEvents;
  7967.  
  7968.  
  7969.  
  7970.     // Locals
  7971.     // -----------------------------------------------------------------------------------
  7972.  
  7973.  
  7974.     var _element = element[0];
  7975.     var header;
  7976.     var headerElement;
  7977.     var content;
  7978.     var tm; // for making theme classes
  7979.     var currentView; // NOTE: keep this in sync with this.view
  7980.     var viewsByType = {}; // holds all instantiated view instances, current or not
  7981.     var suggestedViewHeight;
  7982.     var windowResizeProxy; // wraps the windowResize function
  7983.     var ignoreWindowResize = 0;
  7984.     var date;
  7985.     var events = [];
  7986.    
  7987.    
  7988.    
  7989.     // Main Rendering
  7990.     // -----------------------------------------------------------------------------------
  7991.  
  7992.  
  7993.     if (options.defaultDate != null) {
  7994.         date = t.moment(options.defaultDate);
  7995.     }
  7996.     else {
  7997.         date = t.getNow();
  7998.     }
  7999.    
  8000.    
  8001.     function render() {
  8002.         if (!content) {
  8003.             initialRender();
  8004.         }
  8005.         else if (elementVisible()) {
  8006.             // mainly for the public API
  8007.             calcSize();
  8008.             renderView();
  8009.         }
  8010.     }
  8011.    
  8012.    
  8013.     function initialRender() {
  8014.         tm = options.theme ? 'ui' : 'fc';
  8015.         element.addClass('fc');
  8016.  
  8017.         if (options.isRTL) {
  8018.             element.addClass('fc-rtl');
  8019.         }
  8020.         else {
  8021.             element.addClass('fc-ltr');
  8022.         }
  8023.  
  8024.         if (options.theme) {
  8025.             element.addClass('ui-widget');
  8026.         }
  8027.         else {
  8028.             element.addClass('fc-unthemed');
  8029.         }
  8030.  
  8031.         content = $("<div class='fc-view-container'/>").prependTo(element);
  8032.  
  8033.         header = t.header = new Header(t, options);
  8034.         headerElement = header.render();
  8035.         if (headerElement) {
  8036.             element.prepend(headerElement);
  8037.         }
  8038.  
  8039.         renderView(options.defaultView);
  8040.  
  8041.         if (options.handleWindowResize) {
  8042.             windowResizeProxy = debounce(windowResize, options.windowResizeDelay); // prevents rapid calls
  8043.             $(window).resize(windowResizeProxy);
  8044.         }
  8045.     }
  8046.    
  8047.    
  8048.     function destroy() {
  8049.  
  8050.         if (currentView) {
  8051.             currentView.removeElement();
  8052.  
  8053.             // NOTE: don't null-out currentView/t.view in case API methods are called after destroy.
  8054.             // It is still the "current" view, just not rendered.
  8055.         }
  8056.  
  8057.         header.removeElement();
  8058.         content.remove();
  8059.         element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');
  8060.  
  8061.         if (windowResizeProxy) {
  8062.             $(window).unbind('resize', windowResizeProxy);
  8063.         }
  8064.     }
  8065.    
  8066.    
  8067.     function elementVisible() {
  8068.         return element.is(':visible');
  8069.     }
  8070.    
  8071.    
  8072.  
  8073.     // View Rendering
  8074.     // -----------------------------------------------------------------------------------
  8075.  
  8076.  
  8077.     // Renders a view because of a date change, view-type change, or for the first time.
  8078.     // If not given a viewType, keep the current view but render different dates.
  8079.     function renderView(viewType) {
  8080.         ignoreWindowResize++;
  8081.  
  8082.         // if viewType is changing, remove the old view's rendering
  8083.         if (currentView && viewType && currentView.type !== viewType) {
  8084.             header.deactivateButton(currentView.type);
  8085.             freezeContentHeight(); // prevent a scroll jump when view element is removed
  8086.             currentView.removeElement();
  8087.             currentView = t.view = null;
  8088.         }
  8089.  
  8090.         // if viewType changed, or the view was never created, create a fresh view
  8091.         if (!currentView && viewType) {
  8092.             currentView = t.view =
  8093.                 viewsByType[viewType] ||
  8094.                 (viewsByType[viewType] = t.instantiateView(viewType));
  8095.  
  8096.             currentView.setElement(
  8097.                 $("<div class='fc-view fc-" + viewType + "-view' />").appendTo(content)
  8098.             );
  8099.             header.activateButton(viewType);
  8100.         }
  8101.  
  8102.         if (currentView) {
  8103.  
  8104.             // in case the view should render a period of time that is completely hidden
  8105.             date = currentView.massageCurrentDate(date);
  8106.  
  8107.             // render or rerender the view
  8108.             if (
  8109.                 !currentView.displaying ||
  8110.                 !date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change
  8111.             ) {
  8112.                 if (elementVisible()) {
  8113.  
  8114.                     freezeContentHeight();
  8115.                     currentView.display(date);
  8116.                     unfreezeContentHeight(); // immediately unfreeze regardless of whether display is async
  8117.  
  8118.                     // need to do this after View::render, so dates are calculated
  8119.                     updateHeaderTitle();
  8120.                     updateTodayButton();
  8121.  
  8122.                     getAndRenderEvents();
  8123.                 }
  8124.             }
  8125.         }
  8126.  
  8127.         unfreezeContentHeight(); // undo any lone freezeContentHeight calls
  8128.         ignoreWindowResize--;
  8129.     }
  8130.  
  8131.    
  8132.  
  8133.     // Resizing
  8134.     // -----------------------------------------------------------------------------------
  8135.  
  8136.  
  8137.     t.getSuggestedViewHeight = function() {
  8138.         if (suggestedViewHeight === undefined) {
  8139.             calcSize();
  8140.         }
  8141.         return suggestedViewHeight;
  8142.     };
  8143.  
  8144.  
  8145.     t.isHeightAuto = function() {
  8146.         return options.contentHeight === 'auto' || options.height === 'auto';
  8147.     };
  8148.    
  8149.    
  8150.     function updateSize(shouldRecalc) {
  8151.         if (elementVisible()) {
  8152.  
  8153.             if (shouldRecalc) {
  8154.                 _calcSize();
  8155.             }
  8156.  
  8157.             ignoreWindowResize++;
  8158.             currentView.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto()
  8159.             ignoreWindowResize--;
  8160.  
  8161.             return true; // signal success
  8162.         }
  8163.     }
  8164.  
  8165.  
  8166.     function calcSize() {
  8167.         if (elementVisible()) {
  8168.             _calcSize();
  8169.         }
  8170.     }
  8171.    
  8172.    
  8173.     function _calcSize() { // assumes elementVisible
  8174.         if (typeof options.contentHeight === 'number') { // exists and not 'auto'
  8175.             suggestedViewHeight = options.contentHeight;
  8176.         }
  8177.         else if (typeof options.height === 'number') { // exists and not 'auto'
  8178.             suggestedViewHeight = options.height - (headerElement ? headerElement.outerHeight(true) : 0);
  8179.         }
  8180.         else {
  8181.             suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5));
  8182.         }
  8183.     }
  8184.    
  8185.    
  8186.     function windowResize(ev) {
  8187.         if (
  8188.             !ignoreWindowResize &&
  8189.             ev.target === window && // so we don't process jqui "resize" events that have bubbled up
  8190.             currentView.start // view has already been rendered
  8191.         ) {
  8192.             if (updateSize(true)) {
  8193.                 currentView.trigger('windowResize', _element);
  8194.             }
  8195.         }
  8196.     }
  8197.    
  8198.    
  8199.    
  8200.     /* Event Fetching/Rendering
  8201.     -----------------------------------------------------------------------------*/
  8202.     // TODO: going forward, most of this stuff should be directly handled by the view
  8203.  
  8204.  
  8205.     function refetchEvents() { // can be called as an API method
  8206.         destroyEvents(); // so that events are cleared before user starts waiting for AJAX
  8207.         fetchAndRenderEvents();
  8208.     }
  8209.  
  8210.  
  8211.     function renderEvents() { // destroys old events if previously rendered
  8212.         if (elementVisible()) {
  8213.             freezeContentHeight();
  8214.             currentView.displayEvents(events);
  8215.             unfreezeContentHeight();
  8216.         }
  8217.     }
  8218.  
  8219.  
  8220.     function destroyEvents() {
  8221.         freezeContentHeight();
  8222.         currentView.clearEvents();
  8223.         unfreezeContentHeight();
  8224.     }
  8225.    
  8226.  
  8227.     function getAndRenderEvents() {
  8228.         if (!options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) {
  8229.             fetchAndRenderEvents();
  8230.         }
  8231.         else {
  8232.             renderEvents();
  8233.         }
  8234.     }
  8235.  
  8236.  
  8237.     function fetchAndRenderEvents() {
  8238.         fetchEvents(currentView.start, currentView.end);
  8239.             // ... will call reportEvents
  8240.             // ... which will call renderEvents
  8241.     }
  8242.  
  8243.    
  8244.     // called when event data arrives
  8245.     function reportEvents(_events) {
  8246.         events = _events;
  8247.         renderEvents();
  8248.     }
  8249.  
  8250.  
  8251.     // called when a single event's data has been changed
  8252.     function reportEventChange() {
  8253.         renderEvents();
  8254.     }
  8255.  
  8256.  
  8257.  
  8258.     /* Header Updating
  8259.     -----------------------------------------------------------------------------*/
  8260.  
  8261.  
  8262.     function updateHeaderTitle() {
  8263.         header.updateTitle(currentView.title);
  8264.     }
  8265.  
  8266.  
  8267.     function updateTodayButton() {
  8268.         var now = t.getNow();
  8269.         if (now.isWithin(currentView.intervalStart, currentView.intervalEnd)) {
  8270.             header.disableButton('today');
  8271.         }
  8272.         else {
  8273.             header.enableButton('today');
  8274.         }
  8275.     }
  8276.    
  8277.  
  8278.  
  8279.     /* Selection
  8280.     -----------------------------------------------------------------------------*/
  8281.    
  8282.  
  8283.     function select(start, end) {
  8284.         currentView.select(
  8285.             t.buildSelectRange.apply(t, arguments)
  8286.         );
  8287.     }
  8288.    
  8289.  
  8290.     function unselect() { // safe to be called before renderView
  8291.         if (currentView) {
  8292.             currentView.unselect();
  8293.         }
  8294.     }
  8295.    
  8296.    
  8297.    
  8298.     /* Date
  8299.     -----------------------------------------------------------------------------*/
  8300.    
  8301.    
  8302.     function prev() {
  8303.         date = currentView.computePrevDate(date);
  8304.         renderView();
  8305.     }
  8306.    
  8307.    
  8308.     function next() {
  8309.         date = currentView.computeNextDate(date);
  8310.         renderView();
  8311.     }
  8312.    
  8313.    
  8314.     function prevYear() {
  8315.         date.add(-1, 'years');
  8316.         renderView();
  8317.     }
  8318.    
  8319.    
  8320.     function nextYear() {
  8321.         date.add(1, 'years');
  8322.         renderView();
  8323.     }
  8324.    
  8325.    
  8326.     function today() {
  8327.         date = t.getNow();
  8328.         renderView();
  8329.     }
  8330.    
  8331.    
  8332.     function gotoDate(dateInput) {
  8333.         date = t.moment(dateInput);
  8334.         renderView();
  8335.     }
  8336.    
  8337.    
  8338.     function incrementDate(delta) {
  8339.         date.add(moment.duration(delta));
  8340.         renderView();
  8341.     }
  8342.  
  8343.  
  8344.     // Forces navigation to a view for the given date.
  8345.     // `viewType` can be a specific view name or a generic one like "week" or "day".
  8346.     function zoomTo(newDate, viewType) {
  8347.         var spec;
  8348.  
  8349.         viewType = viewType || 'day'; // day is default zoom
  8350.         spec = t.getViewSpec(viewType) || t.getUnitViewSpec(viewType);
  8351.  
  8352.         date = newDate;
  8353.         renderView(spec ? spec.type : null);
  8354.     }
  8355.    
  8356.    
  8357.     function getDate() {
  8358.         return date.clone();
  8359.     }
  8360.  
  8361.  
  8362.  
  8363.     /* Height "Freezing"
  8364.     -----------------------------------------------------------------------------*/
  8365.     // TODO: move this into the view
  8366.  
  8367.  
  8368.     function freezeContentHeight() {
  8369.         content.css({
  8370.             width: '100%',
  8371.             height: content.height(),
  8372.             overflow: 'hidden'
  8373.         });
  8374.     }
  8375.  
  8376.  
  8377.     function unfreezeContentHeight() {
  8378.         content.css({
  8379.             width: '',
  8380.             height: '',
  8381.             overflow: ''
  8382.         });
  8383.     }
  8384.    
  8385.    
  8386.    
  8387.     /* Misc
  8388.     -----------------------------------------------------------------------------*/
  8389.    
  8390.  
  8391.     function getCalendar() {
  8392.         return t;
  8393.     }
  8394.  
  8395.    
  8396.     function getView() {
  8397.         return currentView;
  8398.     }
  8399.    
  8400.    
  8401.     function option(name, value) {
  8402.         if (value === undefined) {
  8403.             return options[name];
  8404.         }
  8405.         if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') {
  8406.             options[name] = value;
  8407.             updateSize(true); // true = allow recalculation of height
  8408.         }
  8409.     }
  8410.    
  8411.    
  8412.     function trigger(name, thisObj) {
  8413.         if (options[name]) {
  8414.             return options[name].apply(
  8415.                 thisObj || _element,
  8416.                 Array.prototype.slice.call(arguments, 2)
  8417.             );
  8418.         }
  8419.     }
  8420.  
  8421.     t.initialize();
  8422. }
  8423.  
  8424. ;;
  8425.  
  8426. Calendar.defaults = {
  8427.  
  8428.     titleRangeSeparator: ' \u2014 ', // emphasized dash
  8429.     monthYearFormat: 'MMMM YYYY', // required for en. other languages rely on datepicker computable option
  8430.  
  8431.     defaultTimedEventDuration: '02:00:00',
  8432.     defaultAllDayEventDuration: { days: 1 },
  8433.     forceEventDuration: false,
  8434.     nextDayThreshold: '09:00:00', // 9am
  8435.  
  8436.     // display
  8437.     defaultView: 'month',
  8438.     aspectRatio: 1.35,
  8439.     header: {
  8440.         left: 'title',
  8441.         center: '',
  8442.         right: 'today prev,next'
  8443.     },
  8444.     weekends: true,
  8445.     weekNumbers: true,
  8446.  
  8447.     weekNumberTitle: 'W',
  8448.     weekNumberCalculation: 'local',
  8449.    
  8450.     //editable: true,
  8451.  
  8452.     scrollTime: '06:00:00',
  8453.    
  8454.     // event ajax
  8455.     lazyFetching: true,
  8456.     startParam: 'start',
  8457.     endParam: 'end',
  8458.     timezoneParam: 'timezone',
  8459.  
  8460.     timezone: false,
  8461.  
  8462.     //allDayDefault: undefined,
  8463.  
  8464.     // locale
  8465.     isRTL: false,
  8466.     buttonText: {
  8467.         prev: "prev",
  8468.         next: "next",
  8469.         prevYear: "prev year",
  8470.         nextYear: "next year",
  8471.         year: 'rok', // TODO: locale files need to specify this
  8472.         today: 'teraz',
  8473.         month: 'miesiÄ…c',
  8474.         week: 'tydzieÅ„',
  8475.         day: 'dzieÅ„'
  8476.     },
  8477.  
  8478.     buttonIcons: {
  8479.         prev: 'left-single-arrow',
  8480.         next: 'right-single-arrow',
  8481.         prevYear: 'left-double-arrow',
  8482.         nextYear: 'right-double-arrow'
  8483.     },
  8484.    
  8485.     // jquery-ui theming
  8486.     theme: false,
  8487.     themeButtonIcons: {
  8488.         prev: 'circle-triangle-w',
  8489.         next: 'circle-triangle-e',
  8490.         prevYear: 'seek-prev',
  8491.         nextYear: 'seek-next'
  8492.     },
  8493.  
  8494.     //eventResizableFromStart: false,
  8495.     dragOpacity: .75,
  8496.     dragRevertDuration: 500,
  8497.     dragScroll: true,
  8498.    
  8499.     //selectable: false,
  8500.     unselectAuto: true,
  8501.    
  8502.     dropAccept: '*',
  8503.  
  8504.     eventLimit: false,
  8505.     eventLimitText: 'more',
  8506.     eventLimitClick: 'popover',
  8507.     dayPopoverFormat: 'LL',
  8508.    
  8509.     handleWindowResize: true,
  8510.     windowResizeDelay: 200 // milliseconds before an updateSize happens
  8511.    
  8512. };
  8513.  
  8514.  
  8515. Calendar.englishDefaults = { // used by lang.js
  8516.     dayPopoverFormat: 'dddd, MMMM D'
  8517. };
  8518.  
  8519.  
  8520. Calendar.rtlDefaults = { // right-to-left defaults
  8521.     header: { // TODO: smarter solution (first/center/last ?)
  8522.         left: 'next,prev today',
  8523.         center: '',
  8524.         right: 'title'
  8525.     },
  8526.     buttonIcons: {
  8527.         prev: 'right-single-arrow',
  8528.         next: 'left-single-arrow',
  8529.         prevYear: 'right-double-arrow',
  8530.         nextYear: 'left-double-arrow'
  8531.     },
  8532.     themeButtonIcons: {
  8533.         prev: 'circle-triangle-e',
  8534.         next: 'circle-triangle-w',
  8535.         nextYear: 'seek-prev',
  8536.         prevYear: 'seek-next'
  8537.     }
  8538. };
  8539.  
  8540. ;;
  8541.  
  8542. var langOptionHash = fc.langs = {}; // initialize and expose
  8543.  
  8544.  
  8545. // TODO: document the structure and ordering of a FullCalendar lang file
  8546. // TODO: rename everything "lang" to "locale", like what the moment project did
  8547.  
  8548.  
  8549. // Initialize jQuery UI datepicker translations while using some of the translations
  8550. // Will set this as the default language for datepicker.
  8551. fc.datepickerLang = function(langCode, dpLangCode, dpOptions) {
  8552.  
  8553.     // get the FullCalendar internal option hash for this language. create if necessary
  8554.     var fcOptions = langOptionHash[langCode] || (langOptionHash[langCode] = {});
  8555.  
  8556.     // transfer some simple options from datepicker to fc
  8557.     fcOptions.isRTL = dpOptions.isRTL;
  8558.     fcOptions.weekNumberTitle = dpOptions.weekHeader;
  8559.  
  8560.     // compute some more complex options from datepicker
  8561.     $.each(dpComputableOptions, function(name, func) {
  8562.         fcOptions[name] = func(dpOptions);
  8563.     });
  8564.  
  8565.     // is jQuery UI Datepicker is on the page?
  8566.     if ($.datepicker) {
  8567.  
  8568.         // Register the language data.
  8569.         // FullCalendar and MomentJS use language codes like "pt-br" but Datepicker
  8570.         // does it like "pt-BR" or if it doesn't have the language, maybe just "pt".
  8571.         // Make an alias so the language can be referenced either way.
  8572.         $.datepicker.regional[dpLangCode] =
  8573.             $.datepicker.regional[langCode] = // alias
  8574.                 dpOptions;
  8575.  
  8576.         // Alias 'en' to the default language data. Do this every time.
  8577.         $.datepicker.regional.en = $.datepicker.regional[''];
  8578.  
  8579.         // Set as Datepicker's global defaults.
  8580.         $.datepicker.setDefaults(dpOptions);
  8581.     }
  8582. };
  8583.  
  8584.  
  8585. // Sets FullCalendar-specific translations. Will set the language as the global default.
  8586. fc.lang = function(langCode, newFcOptions) {
  8587.     var fcOptions;
  8588.     var momOptions;
  8589.  
  8590.     // get the FullCalendar internal option hash for this language. create if necessary
  8591.     fcOptions = langOptionHash[langCode] || (langOptionHash[langCode] = {});
  8592.  
  8593.     // provided new options for this language? merge them in
  8594.     if (newFcOptions) {
  8595.         fcOptions = langOptionHash[langCode] = mergeOptions([ fcOptions, newFcOptions ]);
  8596.     }
  8597.  
  8598.     // compute language options that weren't defined.
  8599.     // always do this. newFcOptions can be undefined when initializing from i18n file,
  8600.     // so no way to tell if this is an initialization or a default-setting.
  8601.     momOptions = getMomentLocaleData(langCode); // will fall back to en
  8602.     $.each(momComputableOptions, function(name, func) {
  8603.         if (fcOptions[name] == null) {
  8604.             fcOptions[name] = func(momOptions, fcOptions);
  8605.         }
  8606.     });
  8607.  
  8608.     // set it as the default language for FullCalendar
  8609.     Calendar.defaults.lang = langCode;
  8610. };
  8611.  
  8612.  
  8613. // NOTE: can't guarantee any of these computations will run because not every language has datepicker
  8614. // configs, so make sure there are English fallbacks for these in the defaults file.
  8615. var dpComputableOptions = {
  8616.  
  8617.     buttonText: function(dpOptions) {
  8618.         return {
  8619.             // the translations sometimes wrongly contain HTML entities
  8620.             prev: stripHtmlEntities(dpOptions.prevText),
  8621.             next: stripHtmlEntities(dpOptions.nextText),
  8622.             today: stripHtmlEntities(dpOptions.currentText)
  8623.         };
  8624.     },
  8625.  
  8626.     // Produces format strings like "MMMM YYYY" -> "September 2014"
  8627.     monthYearFormat: function(dpOptions) {
  8628.         return dpOptions.showMonthAfterYear ?
  8629.             'YYYY[' + dpOptions.yearSuffix + '] MMMM' :
  8630.             'MMMM YYYY[' + dpOptions.yearSuffix + ']';
  8631.     }
  8632.  
  8633. };
  8634.  
  8635. var momComputableOptions = {
  8636.  
  8637.     // Produces format strings like "ddd M/D" -> "Fri 9/15"
  8638.     dayOfMonthFormat: function(momOptions, fcOptions) {
  8639.         var format = momOptions.longDateFormat('l'); // for the format like "M/D/YYYY"
  8640.  
  8641.         // strip the year off the edge, as well as other misc non-whitespace chars
  8642.         format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, '');
  8643.  
  8644.         if (fcOptions.isRTL) {
  8645.             format += ' ddd'; // for RTL, add day-of-week to end
  8646.         }
  8647.         else {
  8648.             format = 'ddd ' + format; // for LTR, add day-of-week to beginning
  8649.         }
  8650.         return format;
  8651.     },
  8652.  
  8653.     // Produces format strings like "h:mma" -> "6:00pm"
  8654.     mediumTimeFormat: function(momOptions) { // can't be called `timeFormat` because collides with option
  8655.         return momOptions.longDateFormat('LT')
  8656.             .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
  8657.     },
  8658.  
  8659.     // Produces format strings like "h(:mm)a" -> "6pm" / "6:30pm"
  8660.     smallTimeFormat: function(momOptions) {
  8661.         return momOptions.longDateFormat('LT')
  8662.             .replace(':mm', '(:mm)')
  8663.             .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs
  8664.             .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
  8665.     },
  8666.  
  8667.     // Produces format strings like "h(:mm)t" -> "6p" / "6:30p"
  8668.     extraSmallTimeFormat: function(momOptions) {
  8669.         return momOptions.longDateFormat('LT')
  8670.             .replace(':mm', '(:mm)')
  8671.             .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs
  8672.             .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand
  8673.     },
  8674.  
  8675.     // Produces format strings like "ha" / "H" -> "6pm" / "18"
  8676.     hourFormat: function(momOptions) {
  8677.         return momOptions.longDateFormat('LT')
  8678.             .replace(':mm', '')
  8679.             .replace(/(\Wmm)$/, '') // like above, but for foreign langs
  8680.             .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
  8681.     },
  8682.  
  8683.     // Produces format strings like "h:mm" -> "6:30" (with no AM/PM)
  8684.     noMeridiemTimeFormat: function(momOptions) {
  8685.         return momOptions.longDateFormat('LT')
  8686.             .replace(/\s*a$/i, ''); // remove trailing AM/PM
  8687.     }
  8688.  
  8689. };
  8690.  
  8691.  
  8692. // options that should be computed off live calendar options (considers override options)
  8693. var instanceComputableOptions = { // TODO: best place for this? related to lang?
  8694.  
  8695.     // Produces format strings for results like "Mo 16"
  8696.     smallDayDateFormat: function(options) {
  8697.         return options.isRTL ?
  8698.             'D dd' :
  8699.             'dd D';
  8700.     },
  8701.  
  8702.     // Produces format strings for results like "Wk 5"
  8703.     weekFormat: function(options) {
  8704.         return options.isRTL ?
  8705.             'w[ ' + options.weekNumberTitle + ']' :
  8706.             '[' + options.weekNumberTitle + ' ]w';
  8707.     },
  8708.  
  8709.     // Produces format strings for results like "Wk5"
  8710.     smallWeekFormat: function(options) {
  8711.         return options.isRTL ?
  8712.             'w[' + options.weekNumberTitle + ']' :
  8713.             '[' + options.weekNumberTitle + ']w';
  8714.     }
  8715.  
  8716. };
  8717.  
  8718. function populateInstanceComputableOptions(options) {
  8719.     $.each(instanceComputableOptions, function(name, func) {
  8720.         if (options[name] == null) {
  8721.             options[name] = func(options);
  8722.         }
  8723.     });
  8724. }
  8725.  
  8726.  
  8727. // Returns moment's internal locale data. If doesn't exist, returns English.
  8728. // Works with moment-pre-2.8
  8729. function getMomentLocaleData(langCode) {
  8730.     var func = moment.localeData || moment.langData;
  8731.     return func.call(moment, langCode) ||
  8732.         func.call(moment, 'en'); // the newer localData could return null, so fall back to en
  8733. }
  8734.  
  8735.  
  8736. // Initialize English by forcing computation of moment-derived options.
  8737. // Also, sets it as the default.
  8738. fc.lang('pl', Calendar.englishDefaults);
  8739.  
  8740. ;;
  8741.  
  8742. /* Top toolbar area with buttons and title
  8743. ----------------------------------------------------------------------------------------------------------------------*/
  8744. // TODO: rename all header-related things to "toolbar"
  8745.  
  8746. function Header(calendar, options) {
  8747.     var t = this;
  8748.    
  8749.     // exports
  8750.     t.render = render;
  8751.     t.removeElement = removeElement;
  8752.     t.updateTitle = updateTitle;
  8753.     t.activateButton = activateButton;
  8754.     t.deactivateButton = deactivateButton;
  8755.     t.disableButton = disableButton;
  8756.     t.enableButton = enableButton;
  8757.     t.getViewsWithButtons = getViewsWithButtons;
  8758.    
  8759.     // locals
  8760.     var el = $();
  8761.     var viewsWithButtons = [];
  8762.     var tm;
  8763.  
  8764.  
  8765.     function render() {
  8766.         var sections = options.header;
  8767.  
  8768.         tm = options.theme ? 'ui' : 'fc';
  8769.  
  8770.         if (sections) {
  8771.             el = $("<div class='fc-toolbar'/>")
  8772.                 .append(renderSection('left'))
  8773.                 .append(renderSection('right'))
  8774.                 .append(renderSection('center'))
  8775.                 .append('<div class="fc-clear"/>');
  8776.  
  8777.             return el;
  8778.         }
  8779.     }
  8780.    
  8781.    
  8782.     function removeElement() {
  8783.         el.remove();
  8784.         el = $();
  8785.     }
  8786.    
  8787.    
  8788.     function renderSection(position) {
  8789.         var sectionEl = $('<div class="fc-' + position + '"/>');
  8790.         var buttonStr = options.header[position];
  8791.  
  8792.         if (buttonStr) {
  8793.             $.each(buttonStr.split(' '), function(i) {
  8794.                 var groupChildren = $();
  8795.                 var isOnlyButtons = true;
  8796.                 var groupEl;
  8797.  
  8798.                 $.each(this.split(','), function(j, buttonName) {
  8799.                     var viewSpec;
  8800.                     var buttonClick;
  8801.                     var overrideText; // text explicitly set by calendar's constructor options. overcomes icons
  8802.                     var defaultText;
  8803.                     var themeIcon;
  8804.                     var normalIcon;
  8805.                     var innerHtml;
  8806.                     var classes;
  8807.                     var button;
  8808.  
  8809.                     if (buttonName == 'title') {
  8810.                         groupChildren = groupChildren.add($('<h2>&nbsp;</h2>')); // we always want it to take up height
  8811.                         isOnlyButtons = false;
  8812.                     }
  8813.                     else {
  8814.                         viewSpec = calendar.getViewSpec(buttonName);
  8815.  
  8816.                         if (viewSpec) {
  8817.                             buttonClick = function() {
  8818.                                 calendar.changeView(buttonName);
  8819.                             };
  8820.                             viewsWithButtons.push(buttonName);
  8821.                             overrideText = viewSpec.buttonTextOverride;
  8822.                             defaultText = viewSpec.buttonTextDefault;
  8823.                         }
  8824.                         else if (calendar[buttonName]) { // a calendar method
  8825.                             buttonClick = function() {
  8826.                                 calendar[buttonName]();
  8827.                             };
  8828.                             overrideText = (calendar.overrides.buttonText || {})[buttonName];
  8829.                             defaultText = options.buttonText[buttonName]; // everything else is considered default
  8830.                         }
  8831.  
  8832.                         if (buttonClick) {
  8833.  
  8834.                             themeIcon = options.themeButtonIcons[buttonName];
  8835.                             normalIcon = options.buttonIcons[buttonName];
  8836.  
  8837.                             if (overrideText) {
  8838.                                 innerHtml = htmlEscape(overrideText);
  8839.                             }
  8840.                             else if (themeIcon && options.theme) {
  8841.                                 innerHtml = "<span class='ui-icon ui-icon-" + themeIcon + "'></span>";
  8842.                             }
  8843.                             else if (normalIcon && !options.theme) {
  8844.                                 innerHtml = "<span class='fc-icon fc-icon-" + normalIcon + "'></span>";
  8845.                             }
  8846.                             else {
  8847.                                 innerHtml = htmlEscape(defaultText);
  8848.                             }
  8849.  
  8850.                             classes = [
  8851.                                 'fc-' + buttonName + '-button',
  8852.                                 tm + '-button',
  8853.                                 tm + '-state-default'
  8854.                             ];
  8855.  
  8856.                             button = $( // type="button" so that it doesn't submit a form
  8857.                                 '<button type="button" class="' + classes.join(' ') + '">' +
  8858.                                     innerHtml +
  8859.                                 '</button>'
  8860.                                 )
  8861.                                 .click(function() {
  8862.                                     // don't process clicks for disabled buttons
  8863.                                     if (!button.hasClass(tm + '-state-disabled')) {
  8864.  
  8865.                                         buttonClick();
  8866.  
  8867.                                         // after the click action, if the button becomes the "active" tab, or disabled,
  8868.                                         // it should never have a hover class, so remove it now.
  8869.                                         if (
  8870.                                             button.hasClass(tm + '-state-active') ||
  8871.                                             button.hasClass(tm + '-state-disabled')
  8872.                                         ) {
  8873.                                             button.removeClass(tm + '-state-hover');
  8874.                                         }
  8875.                                     }
  8876.                                 })
  8877.                                 .mousedown(function() {
  8878.                                     // the *down* effect (mouse pressed in).
  8879.                                     // only on buttons that are not the "active" tab, or disabled
  8880.                                     button
  8881.                                         .not('.' + tm + '-state-active')
  8882.                                         .not('.' + tm + '-state-disabled')
  8883.                                         .addClass(tm + '-state-down');
  8884.                                 })
  8885.                                 .mouseup(function() {
  8886.                                     // undo the *down* effect
  8887.                                     button.removeClass(tm + '-state-down');
  8888.                                 })
  8889.                                 .hover(
  8890.                                     function() {
  8891.                                         // the *hover* effect.
  8892.                                         // only on buttons that are not the "active" tab, or disabled
  8893.                                         button
  8894.                                             .not('.' + tm + '-state-active')
  8895.                                             .not('.' + tm + '-state-disabled')
  8896.                                             .addClass(tm + '-state-hover');
  8897.                                     },
  8898.                                     function() {
  8899.                                         // undo the *hover* effect
  8900.                                         button
  8901.                                             .removeClass(tm + '-state-hover')
  8902.                                             .removeClass(tm + '-state-down'); // if mouseleave happens before mouseup
  8903.                                     }
  8904.                                 );
  8905.  
  8906.                             groupChildren = groupChildren.add(button);
  8907.                         }
  8908.                     }
  8909.                 });
  8910.  
  8911.                 if (isOnlyButtons) {
  8912.                     groupChildren
  8913.                         .first().addClass(tm + '-corner-left').end()
  8914.                         .last().addClass(tm + '-corner-right').end();
  8915.                 }
  8916.  
  8917.                 if (groupChildren.length > 1) {
  8918.                     groupEl = $('<div/>');
  8919.                     if (isOnlyButtons) {
  8920.                         groupEl.addClass('fc-button-group');
  8921.                     }
  8922.                     groupEl.append(groupChildren);
  8923.                     sectionEl.append(groupEl);
  8924.                 }
  8925.                 else {
  8926.                     sectionEl.append(groupChildren); // 1 or 0 children
  8927.                 }
  8928.             });
  8929.         }
  8930.  
  8931.         return sectionEl;
  8932.     }
  8933.    
  8934.    
  8935.     function updateTitle(text) {
  8936.         el.find('h2').text(text);
  8937.     }
  8938.    
  8939.    
  8940.     function activateButton(buttonName) {
  8941.         el.find('.fc-' + buttonName + '-button')
  8942.             .addClass(tm + '-state-active');
  8943.     }
  8944.    
  8945.    
  8946.     function deactivateButton(buttonName) {
  8947.         el.find('.fc-' + buttonName + '-button')
  8948.             .removeClass(tm + '-state-active');
  8949.     }
  8950.    
  8951.    
  8952.     function disableButton(buttonName) {
  8953.         el.find('.fc-' + buttonName + '-button')
  8954.             .attr('disabled', 'disabled')
  8955.             .addClass(tm + '-state-disabled');
  8956.     }
  8957.    
  8958.    
  8959.     function enableButton(buttonName) {
  8960.         el.find('.fc-' + buttonName + '-button')
  8961.             .removeAttr('disabled')
  8962.             .removeClass(tm + '-state-disabled');
  8963.     }
  8964.  
  8965.  
  8966.     function getViewsWithButtons() {
  8967.         return viewsWithButtons;
  8968.     }
  8969.  
  8970. }
  8971.  
  8972. ;;
  8973.  
  8974. fc.sourceNormalizers = [];
  8975. fc.sourceFetchers = [];
  8976.  
  8977. var ajaxDefaults = {
  8978.     dataType: 'json',
  8979.     cache: false
  8980. };
  8981.  
  8982. var eventGUID = 1;
  8983.  
  8984.  
  8985. function EventManager(options) { // assumed to be a calendar
  8986.     var t = this;
  8987.    
  8988.    
  8989.     // exports
  8990.     t.isFetchNeeded = isFetchNeeded;
  8991.     t.fetchEvents = fetchEvents;
  8992.     t.addEventSource = addEventSource;
  8993.     t.removeEventSource = removeEventSource;
  8994.     t.updateEvent = updateEvent;
  8995.     t.renderEvent = renderEvent;
  8996.     t.removeEvents = removeEvents;
  8997.     t.clientEvents = clientEvents;
  8998.     t.mutateEvent = mutateEvent;
  8999.     t.normalizeEventRange = normalizeEventRange;
  9000.     t.normalizeEventRangeTimes = normalizeEventRangeTimes;
  9001.     t.ensureVisibleEventRange = ensureVisibleEventRange;
  9002.    
  9003.    
  9004.     // imports
  9005.     var reportEvents = t.reportEvents;
  9006.    
  9007.    
  9008.     // locals
  9009.     var stickySource = { events: [] };
  9010.     var sources = [ stickySource ];
  9011.     var rangeStart, rangeEnd;
  9012.     var currentFetchID = 0;
  9013.     var pendingSourceCnt = 0;
  9014.     var cache = []; // holds events that have already been expanded
  9015.  
  9016.  
  9017.     $.each(
  9018.         (options.events ? [ options.events ] : []).concat(options.eventSources || []),
  9019.         function(i, sourceInput) {
  9020.             var source = buildEventSource(sourceInput);
  9021.             if (source) {
  9022.                 sources.push(source);
  9023.             }
  9024.         }
  9025.     );
  9026.    
  9027.    
  9028.    
  9029.     /* Fetching
  9030.     -----------------------------------------------------------------------------*/
  9031.    
  9032.    
  9033.     function isFetchNeeded(start, end) {
  9034.         return !rangeStart || // nothing has been fetched yet?
  9035.             // or, a part of the new range is outside of the old range? (after normalizing)
  9036.             start.clone().stripZone() < rangeStart.clone().stripZone() ||
  9037.             end.clone().stripZone() > rangeEnd.clone().stripZone();
  9038.     }
  9039.    
  9040.    
  9041.     function fetchEvents(start, end) {
  9042.         rangeStart = start;
  9043.         rangeEnd = end;
  9044.         cache = [];
  9045.         var fetchID = ++currentFetchID;
  9046.         var len = sources.length;
  9047.         pendingSourceCnt = len;
  9048.         for (var i=0; i<len; i++) {
  9049.             fetchEventSource(sources[i], fetchID);
  9050.         }
  9051.     }
  9052.    
  9053.    
  9054.     function fetchEventSource(source, fetchID) {
  9055.         _fetchEventSource(source, function(eventInputs) {
  9056.             var isArraySource = $.isArray(source.events);
  9057.             var i, eventInput;
  9058.             var abstractEvent;
  9059.  
  9060.             if (fetchID == currentFetchID) {
  9061.  
  9062.                 if (eventInputs) {
  9063.                     for (i = 0; i < eventInputs.length; i++) {
  9064.                         eventInput = eventInputs[i];
  9065.  
  9066.                         if (isArraySource) { // array sources have already been convert to Event Objects
  9067.                             abstractEvent = eventInput;
  9068.                         }
  9069.                         else {
  9070.                             abstractEvent = buildEventFromInput(eventInput, source);
  9071.                         }
  9072.  
  9073.                         if (abstractEvent) { // not false (an invalid event)
  9074.                             cache.push.apply(
  9075.                                 cache,
  9076.                                 expandEvent(abstractEvent) // add individual expanded events to the cache
  9077.                             );
  9078.                         }
  9079.                     }
  9080.                 }
  9081.  
  9082.                 pendingSourceCnt--;
  9083.                 if (!pendingSourceCnt) {
  9084.                     reportEvents(cache);
  9085.                 }
  9086.             }
  9087.         });
  9088.     }
  9089.    
  9090.    
  9091.     function _fetchEventSource(source, callback) {
  9092.         var i;
  9093.         var fetchers = fc.sourceFetchers;
  9094.         var res;
  9095.  
  9096.         for (i=0; i<fetchers.length; i++) {
  9097.             res = fetchers[i].call(
  9098.                 t, // this, the Calendar object
  9099.                 source,
  9100.                 rangeStart.clone(),
  9101.                 rangeEnd.clone(),
  9102.                 options.timezone,
  9103.                 callback
  9104.             );
  9105.  
  9106.             if (res === true) {
  9107.                 // the fetcher is in charge. made its own async request
  9108.                 return;
  9109.             }
  9110.             else if (typeof res == 'object') {
  9111.                 // the fetcher returned a new source. process it
  9112.                 _fetchEventSource(res, callback);
  9113.                 return;
  9114.             }
  9115.         }
  9116.  
  9117.         var events = source.events;
  9118.         if (events) {
  9119.             if ($.isFunction(events)) {
  9120.                 t.pushLoading();
  9121.                 events.call(
  9122.                     t, // this, the Calendar object
  9123.                     rangeStart.clone(),
  9124.                     rangeEnd.clone(),
  9125.                     options.timezone,
  9126.                     function(events) {
  9127.                         callback(events);
  9128.                         t.popLoading();
  9129.                     }
  9130.                 );
  9131.             }
  9132.             else if ($.isArray(events)) {
  9133.                 callback(events);
  9134.             }
  9135.             else {
  9136.                 callback();
  9137.             }
  9138.         }else{
  9139.             var url = source.url;
  9140.             if (url) {
  9141.                 var success = source.success;
  9142.                 var error = source.error;
  9143.                 var complete = source.complete;
  9144.  
  9145.                 // retrieve any outbound GET/POST $.ajax data from the options
  9146.                 var customData;
  9147.                 if ($.isFunction(source.data)) {
  9148.                     // supplied as a function that returns a key/value object
  9149.                     customData = source.data();
  9150.                 }
  9151.                 else {
  9152.                     // supplied as a straight key/value object
  9153.                     customData = source.data;
  9154.                 }
  9155.  
  9156.                 // use a copy of the custom data so we can modify the parameters
  9157.                 // and not affect the passed-in object.
  9158.                 var data = $.extend({}, customData || {});
  9159.  
  9160.                 var startParam = firstDefined(source.startParam, options.startParam);
  9161.                 var endParam = firstDefined(source.endParam, options.endParam);
  9162.                 var timezoneParam = firstDefined(source.timezoneParam, options.timezoneParam);
  9163.  
  9164.                 if (startParam) {
  9165.                     data[startParam] = rangeStart.format();
  9166.                 }
  9167.                 if (endParam) {
  9168.                     data[endParam] = rangeEnd.format();
  9169.                 }
  9170.                 if (options.timezone && options.timezone != 'local') {
  9171.                     data[timezoneParam] = options.timezone;
  9172.                 }
  9173.  
  9174.                 t.pushLoading();
  9175.                 $.ajax($.extend({}, ajaxDefaults, source, {
  9176.                     data: data,
  9177.                     success: function(events) {
  9178.                         events = events || [];
  9179.                         var res = applyAll(success, this, arguments);
  9180.                         if ($.isArray(res)) {
  9181.                             events = res;
  9182.                         }
  9183.                         callback(events);
  9184.                     },
  9185.                     error: function() {
  9186.                         applyAll(error, this, arguments);
  9187.                         callback();
  9188.                     },
  9189.                     complete: function() {
  9190.                         applyAll(complete, this, arguments);
  9191.                         t.popLoading();
  9192.                     }
  9193.                 }));
  9194.             }else{
  9195.                 callback();
  9196.             }
  9197.         }
  9198.     }
  9199.    
  9200.    
  9201.    
  9202.     /* Sources
  9203.     -----------------------------------------------------------------------------*/
  9204.    
  9205.  
  9206.     function addEventSource(sourceInput) {
  9207.         var source = buildEventSource(sourceInput);
  9208.         if (source) {
  9209.             sources.push(source);
  9210.             pendingSourceCnt++;
  9211.             fetchEventSource(source, currentFetchID); // will eventually call reportEvents
  9212.         }
  9213.     }
  9214.  
  9215.  
  9216.     function buildEventSource(sourceInput) { // will return undefined if invalid source
  9217.         var normalizers = fc.sourceNormalizers;
  9218.         var source;
  9219.         var i;
  9220.  
  9221.         if ($.isFunction(sourceInput) || $.isArray(sourceInput)) {
  9222.             source = { events: sourceInput };
  9223.         }
  9224.         else if (typeof sourceInput === 'string') {
  9225.             source = { url: sourceInput };
  9226.         }
  9227.         else if (typeof sourceInput === 'object') {
  9228.             source = $.extend({}, sourceInput); // shallow copy
  9229.         }
  9230.  
  9231.         if (source) {
  9232.  
  9233.             // TODO: repeat code, same code for event classNames
  9234.             if (source.className) {
  9235.                 if (typeof source.className === 'string') {
  9236.                     source.className = source.className.split(/\s+/);
  9237.                 }
  9238.                 // otherwise, assumed to be an array
  9239.             }
  9240.             else {
  9241.                 source.className = [];
  9242.             }
  9243.  
  9244.             // for array sources, we convert to standard Event Objects up front
  9245.             if ($.isArray(source.events)) {
  9246.                 source.origArray = source.events; // for removeEventSource
  9247.                 source.events = $.map(source.events, function(eventInput) {
  9248.                     return buildEventFromInput(eventInput, source);
  9249.                 });
  9250.             }
  9251.  
  9252.             for (i=0; i<normalizers.length; i++) {
  9253.                 normalizers[i].call(t, source);
  9254.             }
  9255.  
  9256.             return source;
  9257.         }
  9258.     }
  9259.  
  9260.  
  9261.     function removeEventSource(source) {
  9262.         sources = $.grep(sources, function(src) {
  9263.             return !isSourcesEqual(src, source);
  9264.         });
  9265.         // remove all client events from that source
  9266.         cache = $.grep(cache, function(e) {
  9267.             return !isSourcesEqual(e.source, source);
  9268.         });
  9269.         reportEvents(cache);
  9270.     }
  9271.  
  9272.  
  9273.     function isSourcesEqual(source1, source2) {
  9274.         return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2);
  9275.     }
  9276.  
  9277.  
  9278.     function getSourcePrimitive(source) {
  9279.         return (
  9280.             (typeof source === 'object') ? // a normalized event source?
  9281.                 (source.origArray || source.googleCalendarId || source.url || source.events) : // get the primitive
  9282.                 null
  9283.         ) ||
  9284.         source; // the given argument *is* the primitive
  9285.     }
  9286.    
  9287.    
  9288.    
  9289.     /* Manipulation
  9290.     -----------------------------------------------------------------------------*/
  9291.  
  9292.  
  9293.     // Only ever called from the externally-facing API
  9294.     function updateEvent(event) {
  9295.  
  9296.         // massage start/end values, even if date string values
  9297.         event.start = t.moment(event.start);
  9298.         if (event.end) {
  9299.             event.end = t.moment(event.end);
  9300.         }
  9301.         else {
  9302.             event.end = null;
  9303.         }
  9304.  
  9305.         mutateEvent(event, getMiscEventProps(event)); // will handle start/end/allDay normalization
  9306.         reportEvents(cache); // reports event modifications (so we can redraw)
  9307.     }
  9308.  
  9309.  
  9310.     // Returns a hash of misc event properties that should be copied over to related events.
  9311.     function getMiscEventProps(event) {
  9312.         var props = {};
  9313.  
  9314.         $.each(event, function(name, val) {
  9315.             if (isMiscEventPropName(name)) {
  9316.                 if (val !== undefined && isAtomic(val)) { // a defined non-object
  9317.                     props[name] = val;
  9318.                 }
  9319.             }
  9320.         });
  9321.  
  9322.         return props;
  9323.     }
  9324.  
  9325.     // non-date-related, non-id-related, non-secret
  9326.     function isMiscEventPropName(name) {
  9327.         return !/^_|^(id|allDay|start|end)$/.test(name);
  9328.     }
  9329.  
  9330.    
  9331.     // returns the expanded events that were created
  9332.     function renderEvent(eventInput, stick) {
  9333.         var abstractEvent = buildEventFromInput(eventInput);
  9334.         var events;
  9335.         var i, event;
  9336.  
  9337.         if (abstractEvent) { // not false (a valid input)
  9338.             events = expandEvent(abstractEvent);
  9339.  
  9340.             for (i = 0; i < events.length; i++) {
  9341.                 event = events[i];
  9342.  
  9343.                 if (!event.source) {
  9344.                     if (stick) {
  9345.                         stickySource.events.push(event);
  9346.                         event.source = stickySource;
  9347.                     }
  9348.                     cache.push(event);
  9349.                 }
  9350.             }
  9351.  
  9352.             reportEvents(cache);
  9353.  
  9354.             return events;
  9355.         }
  9356.  
  9357.         return [];
  9358.     }
  9359.    
  9360.    
  9361.     function removeEvents(filter) {
  9362.         var eventID;
  9363.         var i;
  9364.  
  9365.         if (filter == null) { // null or undefined. remove all events
  9366.             filter = function() { return true; }; // will always match
  9367.         }
  9368.         else if (!$.isFunction(filter)) { // an event ID
  9369.             eventID = filter + '';
  9370.             filter = function(event) {
  9371.                 return event._id == eventID;
  9372.             };
  9373.         }
  9374.  
  9375.         // Purge event(s) from our local cache
  9376.         cache = $.grep(cache, filter, true); // inverse=true
  9377.  
  9378.         // Remove events from array sources.
  9379.         // This works because they have been converted to official Event Objects up front.
  9380.         // (and as a result, event._id has been calculated).
  9381.         for (i=0; i<sources.length; i++) {
  9382.             if ($.isArray(sources[i].events)) {
  9383.                 sources[i].events = $.grep(sources[i].events, filter, true);
  9384.             }
  9385.         }
  9386.  
  9387.         reportEvents(cache);
  9388.     }
  9389.    
  9390.    
  9391.     function clientEvents(filter) {
  9392.         if ($.isFunction(filter)) {
  9393.             return $.grep(cache, filter);
  9394.         }
  9395.         else if (filter != null) { // not null, not undefined. an event ID
  9396.             filter += '';
  9397.             return $.grep(cache, function(e) {
  9398.                 return e._id == filter;
  9399.             });
  9400.         }
  9401.         return cache; // else, return all
  9402.     }
  9403.    
  9404.    
  9405.    
  9406.     /* Event Normalization
  9407.     -----------------------------------------------------------------------------*/
  9408.  
  9409.  
  9410.     // Given a raw object with key/value properties, returns an "abstract" Event object.
  9411.     // An "abstract" event is an event that, if recurring, will not have been expanded yet.
  9412.     // Will return `false` when input is invalid.
  9413.     // `source` is optional
  9414.     function buildEventFromInput(input, source) {
  9415.         var out = {};
  9416.         var start, end;
  9417.         var allDay;
  9418.  
  9419.         if (options.eventDataTransform) {
  9420.             input = options.eventDataTransform(input);
  9421.         }
  9422.         if (source && source.eventDataTransform) {
  9423.             input = source.eventDataTransform(input);
  9424.         }
  9425.  
  9426.         // Copy all properties over to the resulting object.
  9427.         // The special-case properties will be copied over afterwards.
  9428.         $.extend(out, input);
  9429.  
  9430.         if (source) {
  9431.             out.source = source;
  9432.         }
  9433.  
  9434.         out._id = input._id || (input.id === undefined ? '_fc' + eventGUID++ : input.id + '');
  9435.  
  9436.         if (input.className) {
  9437.             if (typeof input.className == 'string') {
  9438.                 out.className = input.className.split(/\s+/);
  9439.             }
  9440.             else { // assumed to be an array
  9441.                 out.className = input.className;
  9442.             }
  9443.         }
  9444.         else {
  9445.             out.className = [];
  9446.         }
  9447.  
  9448.         start = input.start || input.date; // "date" is an alias for "start"
  9449.         end = input.end;
  9450.  
  9451.         // parse as a time (Duration) if applicable
  9452.         if (isTimeString(start)) {
  9453.             start = moment.duration(start);
  9454.         }
  9455.         if (isTimeString(end)) {
  9456.             end = moment.duration(end);
  9457.         }
  9458.  
  9459.         if (input.dow || moment.isDuration(start) || moment.isDuration(end)) {
  9460.  
  9461.             // the event is "abstract" (recurring) so don't calculate exact start/end dates just yet
  9462.             out.start = start ? moment.duration(start) : null; // will be a Duration or null
  9463.             out.end = end ? moment.duration(end) : null; // will be a Duration or null
  9464.             out._recurring = true; // our internal marker
  9465.         }
  9466.         else {
  9467.  
  9468.             if (start) {
  9469.                 start = t.moment(start);
  9470.                 if (!start.isValid()) {
  9471.                     return false;
  9472.                 }
  9473.             }
  9474.  
  9475.             if (end) {
  9476.                 end = t.moment(end);
  9477.                 if (!end.isValid()) {
  9478.                     end = null; // let defaults take over
  9479.                 }
  9480.             }
  9481.  
  9482.             allDay = input.allDay;
  9483.             if (allDay === undefined) { // still undefined? fallback to default
  9484.                 allDay = firstDefined(
  9485.                     source ? source.allDayDefault : undefined,
  9486.                     options.allDayDefault
  9487.                 );
  9488.                 // still undefined? normalizeEventRange will calculate it
  9489.             }
  9490.  
  9491.             assignDatesToEvent(start, end, allDay, out);
  9492.         }
  9493.  
  9494.         return out;
  9495.     }
  9496.  
  9497.  
  9498.     // Normalizes and assigns the given dates to the given partially-formed event object.
  9499.     // NOTE: mutates the given start/end moments. does not make a copy.
  9500.     function assignDatesToEvent(start, end, allDay, event) {
  9501.         event.start = start;
  9502.         event.end = end;
  9503.         event.allDay = allDay;
  9504.         normalizeEventRange(event);
  9505.         backupEventDates(event);
  9506.     }
  9507.  
  9508.  
  9509.     // Ensures proper values for allDay/start/end. Accepts an Event object, or a plain object with event-ish properties.
  9510.     // NOTE: Will modify the given object.
  9511.     function normalizeEventRange(props) {
  9512.  
  9513.         normalizeEventRangeTimes(props);
  9514.  
  9515.         if (props.end && !props.end.isAfter(props.start)) {
  9516.             props.end = null;
  9517.         }
  9518.  
  9519.         if (!props.end) {
  9520.             if (options.forceEventDuration) {
  9521.                 props.end = t.getDefaultEventEnd(props.allDay, props.start);
  9522.             }
  9523.             else {
  9524.                 props.end = null;
  9525.             }
  9526.         }
  9527.     }
  9528.  
  9529.  
  9530.     // Ensures the allDay property exists and the timeliness of the start/end dates are consistent
  9531.     function normalizeEventRangeTimes(range) {
  9532.         if (range.allDay == null) {
  9533.             range.allDay = !(range.start.hasTime() || (range.end && range.end.hasTime()));
  9534.         }
  9535.  
  9536.         if (range.allDay) {
  9537.             range.start.stripTime();
  9538.             if (range.end) {
  9539.                 // TODO: consider nextDayThreshold here? If so, will require a lot of testing and adjustment
  9540.                 range.end.stripTime();
  9541.             }
  9542.         }
  9543.         else {
  9544.             if (!range.start.hasTime()) {
  9545.                 range.start = t.rezoneDate(range.start); // will assign a 00:00 time
  9546.             }
  9547.             if (range.end && !range.end.hasTime()) {
  9548.                 range.end = t.rezoneDate(range.end); // will assign a 00:00 time
  9549.             }
  9550.         }
  9551.     }
  9552.  
  9553.  
  9554.     // If `range` is a proper range with a start and end, returns the original object.
  9555.     // If missing an end, computes a new range with an end, computing it as if it were an event.
  9556.     // TODO: make this a part of the event -> eventRange system
  9557.     function ensureVisibleEventRange(range) {
  9558.         var allDay;
  9559.  
  9560.         if (!range.end) {
  9561.  
  9562.             allDay = range.allDay; // range might be more event-ish than we think
  9563.             if (allDay == null) {
  9564.                 allDay = !range.start.hasTime();
  9565.             }
  9566.  
  9567.             range = $.extend({}, range); // make a copy, copying over other misc properties
  9568.             range.end = t.getDefaultEventEnd(allDay, range.start);
  9569.         }
  9570.         return range;
  9571.     }
  9572.  
  9573.  
  9574.     // If the given event is a recurring event, break it down into an array of individual instances.
  9575.     // If not a recurring event, return an array with the single original event.
  9576.     // If given a falsy input (probably because of a failed buildEventFromInput call), returns an empty array.
  9577.     // HACK: can override the recurring window by providing custom rangeStart/rangeEnd (for businessHours).
  9578.     function expandEvent(abstractEvent, _rangeStart, _rangeEnd) {
  9579.         var events = [];
  9580.         var dowHash;
  9581.         var dow;
  9582.         var i;
  9583.         var date;
  9584.         var startTime, endTime;
  9585.         var start, end;
  9586.         var event;
  9587.  
  9588.         _rangeStart = _rangeStart || rangeStart;
  9589.         _rangeEnd = _rangeEnd || rangeEnd;
  9590.  
  9591.         if (abstractEvent) {
  9592.             if (abstractEvent._recurring) {
  9593.  
  9594.                 // make a boolean hash as to whether the event occurs on each day-of-week
  9595.                 if ((dow = abstractEvent.dow)) {
  9596.                     dowHash = {};
  9597.                     for (i = 0; i < dow.length; i++) {
  9598.                         dowHash[dow[i]] = true;
  9599.                     }
  9600.                 }
  9601.  
  9602.                 // iterate through every day in the current range
  9603.                 date = _rangeStart.clone().stripTime(); // holds the date of the current day
  9604.                 while (date.isBefore(_rangeEnd)) {
  9605.  
  9606.                     if (!dowHash || dowHash[date.day()]) { // if everyday, or this particular day-of-week
  9607.  
  9608.                         startTime = abstractEvent.start; // the stored start and end properties are times (Durations)
  9609.                         endTime = abstractEvent.end; // "
  9610.                         start = date.clone();
  9611.                         end = null;
  9612.  
  9613.                         if (startTime) {
  9614.                             start = start.time(startTime);
  9615.                         }
  9616.                         if (endTime) {
  9617.                             end = date.clone().time(endTime);
  9618.                         }
  9619.  
  9620.                         event = $.extend({}, abstractEvent); // make a copy of the original
  9621.                         assignDatesToEvent(
  9622.                             start, end,
  9623.                             !startTime && !endTime, // allDay?
  9624.                             event
  9625.                         );
  9626.                         events.push(event);
  9627.                     }
  9628.  
  9629.                     date.add(1, 'days');
  9630.                 }
  9631.             }
  9632.             else {
  9633.                 events.push(abstractEvent); // return the original event. will be a one-item array
  9634.             }
  9635.         }
  9636.  
  9637.         return events;
  9638.     }
  9639.  
  9640.  
  9641.  
  9642.     /* Event Modification Math
  9643.     -----------------------------------------------------------------------------------------*/
  9644.  
  9645.  
  9646.     // Modifies an event and all related events by applying the given properties.
  9647.     // Special date-diffing logic is used for manipulation of dates.
  9648.     // If `props` does not contain start/end dates, the updated values are assumed to be the event's current start/end.
  9649.     // All date comparisons are done against the event's pristine _start and _end dates.
  9650.     // Returns an object with delta information and a function to undo all operations.
  9651.     // For making computations in a granularity greater than day/time, specify largeUnit.
  9652.     // NOTE: The given `newProps` might be mutated for normalization purposes.
  9653.     function mutateEvent(event, newProps, largeUnit) {
  9654.         var miscProps = {};
  9655.         var oldProps;
  9656.         var clearEnd;
  9657.         var startDelta;
  9658.         var endDelta;
  9659.         var durationDelta;
  9660.         var undoFunc;
  9661.  
  9662.         // diffs the dates in the appropriate way, returning a duration
  9663.         function diffDates(date1, date0) { // date1 - date0
  9664.             if (largeUnit) {
  9665.                 return diffByUnit(date1, date0, largeUnit);
  9666.             }
  9667.             else if (newProps.allDay) {
  9668.                 return diffDay(date1, date0);
  9669.             }
  9670.             else {
  9671.                 return diffDayTime(date1, date0);
  9672.             }
  9673.         }
  9674.  
  9675.         newProps = newProps || {};
  9676.  
  9677.         // normalize new date-related properties
  9678.         if (!newProps.start) {
  9679.             newProps.start = event.start.clone();
  9680.         }
  9681.         if (newProps.end === undefined) {
  9682.             newProps.end = event.end ? event.end.clone() : null;
  9683.         }
  9684.         if (newProps.allDay == null) { // is null or undefined?
  9685.             newProps.allDay = event.allDay;
  9686.         }
  9687.         normalizeEventRange(newProps);
  9688.  
  9689.         // create normalized versions of the original props to compare against
  9690.         // need a real end value, for diffing
  9691.         oldProps = {
  9692.             start: event._start.clone(),
  9693.             end: event._end ? event._end.clone() : t.getDefaultEventEnd(event._allDay, event._start),
  9694.             allDay: newProps.allDay // normalize the dates in the same regard as the new properties
  9695.         };
  9696.         normalizeEventRange(oldProps);
  9697.  
  9698.         // need to clear the end date if explicitly changed to null
  9699.         clearEnd = event._end !== null && newProps.end === null;
  9700.  
  9701.         // compute the delta for moving the start date
  9702.         startDelta = diffDates(newProps.start, oldProps.start);
  9703.  
  9704.         // compute the delta for moving the end date
  9705.         if (newProps.end) {
  9706.             endDelta = diffDates(newProps.end, oldProps.end);
  9707.             durationDelta = endDelta.subtract(startDelta);
  9708.         }
  9709.         else {
  9710.             durationDelta = null;
  9711.         }
  9712.  
  9713.         // gather all non-date-related properties
  9714.         $.each(newProps, function(name, val) {
  9715.             if (isMiscEventPropName(name)) {
  9716.                 if (val !== undefined) {
  9717.                     miscProps[name] = val;
  9718.                 }
  9719.             }
  9720.         });
  9721.  
  9722.         // apply the operations to the event and all related events
  9723.         undoFunc = mutateEvents(
  9724.             clientEvents(event._id), // get events with this ID
  9725.             clearEnd,
  9726.             newProps.allDay,
  9727.             startDelta,
  9728.             durationDelta,
  9729.             miscProps
  9730.         );
  9731.  
  9732.         return {
  9733.             dateDelta: startDelta,
  9734.             durationDelta: durationDelta,
  9735.             undo: undoFunc
  9736.         };
  9737.     }
  9738.  
  9739.  
  9740.     // Modifies an array of events in the following ways (operations are in order):
  9741.     // - clear the event's `end`
  9742.     // - convert the event to allDay
  9743.     // - add `dateDelta` to the start and end
  9744.     // - add `durationDelta` to the event's duration
  9745.     // - assign `miscProps` to the event
  9746.     //
  9747.     // Returns a function that can be called to undo all the operations.
  9748.     //
  9749.     // TODO: don't use so many closures. possible memory issues when lots of events with same ID.
  9750.     //
  9751.     function mutateEvents(events, clearEnd, allDay, dateDelta, durationDelta, miscProps) {
  9752.         var isAmbigTimezone = t.getIsAmbigTimezone();
  9753.         var undoFunctions = [];
  9754.  
  9755.         // normalize zero-length deltas to be null
  9756.         if (dateDelta && !dateDelta.valueOf()) { dateDelta = null; }
  9757.         if (durationDelta && !durationDelta.valueOf()) { durationDelta = null; }
  9758.  
  9759.         $.each(events, function(i, event) {
  9760.             var oldProps;
  9761.             var newProps;
  9762.  
  9763.             // build an object holding all the old values, both date-related and misc.
  9764.             // for the undo function.
  9765.             oldProps = {
  9766.                 start: event.start.clone(),
  9767.                 end: event.end ? event.end.clone() : null,
  9768.                 allDay: event.allDay
  9769.             };
  9770.             $.each(miscProps, function(name) {
  9771.                 oldProps[name] = event[name];
  9772.             });
  9773.  
  9774.             // new date-related properties. work off the original date snapshot.
  9775.             // ok to use references because they will be thrown away when backupEventDates is called.
  9776.             newProps = {
  9777.                 start: event._start,
  9778.                 end: event._end,
  9779.                 allDay: allDay // normalize the dates in the same regard as the new properties
  9780.             };
  9781.             normalizeEventRange(newProps); // massages start/end/allDay
  9782.  
  9783.             // strip or ensure the end date
  9784.             if (clearEnd) {
  9785.                 newProps.end = null;
  9786.             }
  9787.             else if (durationDelta && !newProps.end) { // the duration translation requires an end date
  9788.                 newProps.end = t.getDefaultEventEnd(newProps.allDay, newProps.start);
  9789.             }
  9790.  
  9791.             if (dateDelta) {
  9792.                 newProps.start.add(dateDelta);
  9793.                 if (newProps.end) {
  9794.                     newProps.end.add(dateDelta);
  9795.                 }
  9796.             }
  9797.  
  9798.             if (durationDelta) {
  9799.                 newProps.end.add(durationDelta); // end already ensured above
  9800.             }
  9801.  
  9802.             // if the dates have changed, and we know it is impossible to recompute the
  9803.             // timezone offsets, strip the zone.
  9804.             if (
  9805.                 isAmbigTimezone &&
  9806.                 !newProps.allDay &&
  9807.                 (dateDelta || durationDelta)
  9808.             ) {
  9809.                 newProps.start.stripZone();
  9810.                 if (newProps.end) {
  9811.                     newProps.end.stripZone();
  9812.                 }
  9813.             }
  9814.  
  9815.             $.extend(event, miscProps, newProps); // copy over misc props, then date-related props
  9816.             backupEventDates(event); // regenerate internal _start/_end/_allDay
  9817.  
  9818.             undoFunctions.push(function() {
  9819.                 $.extend(event, oldProps);
  9820.                 backupEventDates(event); // regenerate internal _start/_end/_allDay
  9821.             });
  9822.         });
  9823.  
  9824.         return function() {
  9825.             for (var i = 0; i < undoFunctions.length; i++) {
  9826.                 undoFunctions[i]();
  9827.             }
  9828.         };
  9829.     }
  9830.  
  9831.  
  9832.     /* Business Hours
  9833.     -----------------------------------------------------------------------------------------*/
  9834.  
  9835.     t.getBusinessHoursEvents = getBusinessHoursEvents;
  9836.  
  9837.  
  9838.     // Returns an array of events as to when the business hours occur in the given view.
  9839.     // Abuse of our event system :(
  9840.     function getBusinessHoursEvents(wholeDay) {
  9841.         var optionVal = options.businessHours;
  9842.         var defaultVal = {
  9843.             className: 'fc-nonbusiness',
  9844.             start: '09:00',
  9845.             end: '17:00',
  9846.             dow: [ 1, 2, 3, 4, 5 ], // monday - friday
  9847.             rendering: 'inverse-background'
  9848.         };
  9849.         var view = t.getView();
  9850.         var eventInput;
  9851.  
  9852.         if (optionVal) { // `true` (which means "use the defaults") or an override object
  9853.             eventInput = $.extend(
  9854.                 {}, // copy to a new object in either case
  9855.                 defaultVal,
  9856.                 typeof optionVal === 'object' ? optionVal : {} // override the defaults
  9857.             );
  9858.         }
  9859.  
  9860.         if (eventInput) {
  9861.  
  9862.             // if a whole-day series is requested, clear the start/end times
  9863.             if (wholeDay) {
  9864.                 eventInput.start = null;
  9865.                 eventInput.end = null;
  9866.             }
  9867.  
  9868.             return expandEvent(
  9869.                 buildEventFromInput(eventInput),
  9870.                 view.start,
  9871.                 view.end
  9872.             );
  9873.         }
  9874.  
  9875.         return [];
  9876.     }
  9877.  
  9878.  
  9879.     /* Overlapping / Constraining
  9880.     -----------------------------------------------------------------------------------------*/
  9881.  
  9882.     t.isEventRangeAllowed = isEventRangeAllowed;
  9883.     t.isSelectionRangeAllowed = isSelectionRangeAllowed;
  9884.     t.isExternalDropRangeAllowed = isExternalDropRangeAllowed;
  9885.  
  9886.  
  9887.     function isEventRangeAllowed(range, event) {
  9888.         var source = event.source || {};
  9889.         var constraint = firstDefined(
  9890.             event.constraint,
  9891.             source.constraint,
  9892.             options.eventConstraint
  9893.         );
  9894.         var overlap = firstDefined(
  9895.             event.overlap,
  9896.             source.overlap,
  9897.             options.eventOverlap
  9898.         );
  9899.  
  9900.         range = ensureVisibleEventRange(range); // ensure a proper range with an end for isRangeAllowed
  9901.  
  9902.         return isRangeAllowed(range, constraint, overlap, event);
  9903.     }
  9904.  
  9905.  
  9906.     function isSelectionRangeAllowed(range) {
  9907.         return isRangeAllowed(range, options.selectConstraint, options.selectOverlap);
  9908.     }
  9909.  
  9910.  
  9911.     // when `eventProps` is defined, consider this an event.
  9912.     // `eventProps` can contain misc non-date-related info about the event.
  9913.     function isExternalDropRangeAllowed(range, eventProps) {
  9914.         var eventInput;
  9915.         var event;
  9916.  
  9917.         // note: very similar logic is in View's reportExternalDrop
  9918.         if (eventProps) {
  9919.             eventInput = $.extend({}, eventProps, range);
  9920.             event = expandEvent(buildEventFromInput(eventInput))[0];
  9921.         }
  9922.  
  9923.         if (event) {
  9924.             return isEventRangeAllowed(range, event);
  9925.         }
  9926.         else { // treat it as a selection
  9927.  
  9928.             range = ensureVisibleEventRange(range); // ensure a proper range with an end for isSelectionRangeAllowed
  9929.  
  9930.             return isSelectionRangeAllowed(range);
  9931.         }
  9932.     }
  9933.  
  9934.  
  9935.     // Returns true if the given range (caused by an event drop/resize or a selection) is allowed to exist
  9936.     // according to the constraint/overlap settings.
  9937.     // `event` is not required if checking a selection.
  9938.     function isRangeAllowed(range, constraint, overlap, event) {
  9939.         var constraintEvents;
  9940.         var anyContainment;
  9941.         var peerEvents;
  9942.         var i, peerEvent;
  9943.         var peerOverlap;
  9944.  
  9945.         // normalize. fyi, we're normalizing in too many places :(
  9946.         range = $.extend({}, range); // copy all properties in case there are misc non-date properties
  9947.         range.start = range.start.clone().stripZone();
  9948.         range.end = range.end.clone().stripZone();
  9949.  
  9950.         // the range must be fully contained by at least one of produced constraint events
  9951.         if (constraint != null) {
  9952.  
  9953.             // not treated as an event! intermediate data structure
  9954.             // TODO: use ranges in the future
  9955.             constraintEvents = constraintToEvents(constraint);
  9956.  
  9957.             anyContainment = false;
  9958.             for (i = 0; i < constraintEvents.length; i++) {
  9959.                 if (eventContainsRange(constraintEvents[i], range)) {
  9960.                     anyContainment = true;
  9961.                     break;
  9962.                 }
  9963.             }
  9964.  
  9965.             if (!anyContainment) {
  9966.                 return false;
  9967.             }
  9968.         }
  9969.  
  9970.         peerEvents = t.getPeerEvents(event, range);
  9971.  
  9972.         for (i = 0; i < peerEvents.length; i++)  {
  9973.             peerEvent = peerEvents[i];
  9974.  
  9975.             // there needs to be an actual intersection before disallowing anything
  9976.             if (eventIntersectsRange(peerEvent, range)) {
  9977.  
  9978.                 // evaluate overlap for the given range and short-circuit if necessary
  9979.                 if (overlap === false) {
  9980.                     return false;
  9981.                 }
  9982.                 // if the event's overlap is a test function, pass the peer event in question as the first param
  9983.                 else if (typeof overlap === 'function' && !overlap(peerEvent, event)) {
  9984.                     return false;
  9985.                 }
  9986.  
  9987.                 // if we are computing if the given range is allowable for an event, consider the other event's
  9988.                 // EventObject-specific or Source-specific `overlap` property
  9989.                 if (event) {
  9990.                     peerOverlap = firstDefined(
  9991.                         peerEvent.overlap,
  9992.                         (peerEvent.source || {}).overlap
  9993.                         // we already considered the global `eventOverlap`
  9994.                     );
  9995.                     if (peerOverlap === false) {
  9996.                         return false;
  9997.                     }
  9998.                     // if the peer event's overlap is a test function, pass the subject event as the first param
  9999.                     if (typeof peerOverlap === 'function' && !peerOverlap(event, peerEvent)) {
  10000.                         return false;
  10001.                     }
  10002.                 }
  10003.             }
  10004.         }
  10005.  
  10006.         return true;
  10007.     }
  10008.  
  10009.  
  10010.     // Given an event input from the API, produces an array of event objects. Possible event inputs:
  10011.     // 'businessHours'
  10012.     // An event ID (number or string)
  10013.     // An object with specific start/end dates or a recurring event (like what businessHours accepts)
  10014.     function constraintToEvents(constraintInput) {
  10015.  
  10016.         if (constraintInput === 'businessHours') {
  10017.             return getBusinessHoursEvents();
  10018.         }
  10019.  
  10020.         if (typeof constraintInput === 'object') {
  10021.             return expandEvent(buildEventFromInput(constraintInput));
  10022.         }
  10023.  
  10024.         return clientEvents(constraintInput); // probably an ID
  10025.     }
  10026.  
  10027.  
  10028.     // Does the event's date range fully contain the given range?
  10029.     // start/end already assumed to have stripped zones :(
  10030.     function eventContainsRange(event, range) {
  10031.         var eventStart = event.start.clone().stripZone();
  10032.         var eventEnd = t.getEventEnd(event).stripZone();
  10033.  
  10034.         return range.start >= eventStart && range.end <= eventEnd;
  10035.     }
  10036.  
  10037.  
  10038.     // Does the event's date range intersect with the given range?
  10039.     // start/end already assumed to have stripped zones :(
  10040.     function eventIntersectsRange(event, range) {
  10041.         var eventStart = event.start.clone().stripZone();
  10042.         var eventEnd = t.getEventEnd(event).stripZone();
  10043.  
  10044.         return range.start < eventEnd && range.end > eventStart;
  10045.     }
  10046.  
  10047.  
  10048.     t.getEventCache = function() {
  10049.         return cache;
  10050.     };
  10051.  
  10052. }
  10053.  
  10054.  
  10055. // Returns a list of events that the given event should be compared against when being considered for a move to
  10056. // the specified range. Attached to the Calendar's prototype because EventManager is a mixin for a Calendar.
  10057. Calendar.prototype.getPeerEvents = function(event, range) {
  10058.     var cache = this.getEventCache();
  10059.     var peerEvents = [];
  10060.     var i, otherEvent;
  10061.  
  10062.     for (i = 0; i < cache.length; i++) {
  10063.         otherEvent = cache[i];
  10064.         if (
  10065.             !event ||
  10066.             event._id !== otherEvent._id // don't compare the event to itself or other related [repeating] events
  10067.         ) {
  10068.             peerEvents.push(otherEvent);
  10069.         }
  10070.     }
  10071.  
  10072.     return peerEvents;
  10073. };
  10074.  
  10075.  
  10076. // updates the "backup" properties, which are preserved in order to compute diffs later on.
  10077. function backupEventDates(event) {
  10078.     event._allDay = event.allDay;
  10079.     event._start = event.start.clone();
  10080.     event._end = event.end ? event.end.clone() : null;
  10081. }
  10082.  
  10083. ;;
  10084.  
  10085. /* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells.
  10086. ----------------------------------------------------------------------------------------------------------------------*/
  10087. // It is a manager for a DayGrid subcomponent, which does most of the heavy lifting.
  10088. // It is responsible for managing width/height.
  10089.  
  10090. var BasicView = View.extend({
  10091.  
  10092.     dayGrid: null, // the main subcomponent that does most of the heavy lifting
  10093.  
  10094.     dayNumbersVisible: false, // display day numbers on each day cell?
  10095.     weekNumbersVisible: false, // display week numbers along the side?
  10096.  
  10097.     weekNumberWidth: null, // width of all the week-number cells running down the side
  10098.  
  10099.     headRowEl: null, // the fake row element of the day-of-week header
  10100.  
  10101.  
  10102.     initialize: function() {
  10103.         this.dayGrid = new DayGrid(this);
  10104.         this.coordMap = this.dayGrid.coordMap; // the view's date-to-cell mapping is identical to the subcomponent's
  10105.     },
  10106.  
  10107.  
  10108.     // Sets the display range and computes all necessary dates
  10109.     setRange: function(range) {
  10110.         View.prototype.setRange.call(this, range); // call the super-method
  10111.  
  10112.         this.dayGrid.breakOnWeeks = /year|month|week/.test(this.intervalUnit); // do before setRange
  10113.         this.dayGrid.setRange(range);
  10114.     },
  10115.  
  10116.  
  10117.     // Compute the value to feed into setRange. Overrides superclass.
  10118.     computeRange: function(date) {
  10119.         var range = View.prototype.computeRange.call(this, date); // get value from the super-method
  10120.  
  10121.         // year and month views should be aligned with weeks. this is already done for week
  10122.         if (/year|month/.test(range.intervalUnit)) {
  10123.             range.start.startOf('week');
  10124.             range.start = this.skipHiddenDays(range.start);
  10125.  
  10126.             // make end-of-week if not already
  10127.             if (range.end.weekday()) {
  10128.                 range.end.add(1, 'week').startOf('week');
  10129.                 range.end = this.skipHiddenDays(range.end, -1, true); // exclusively move backwards
  10130.             }
  10131.         }
  10132.  
  10133.         return range;
  10134.     },
  10135.  
  10136.  
  10137.     // Renders the view into `this.el`, which should already be assigned
  10138.     renderDates: function() {
  10139.  
  10140.         this.dayNumbersVisible = this.dayGrid.rowCnt > 1; // TODO: make grid responsible
  10141.         this.weekNumbersVisible = this.opt('weekNumbers');
  10142.         this.dayGrid.numbersVisible = this.dayNumbersVisible || this.weekNumbersVisible;
  10143.  
  10144.         this.el.addClass('fc-basic-view').html(this.renderHtml());
  10145.  
  10146.         this.headRowEl = this.el.find('thead .fc-row');
  10147.  
  10148.         this.scrollerEl = this.el.find('.fc-day-grid-container');
  10149.         this.dayGrid.coordMap.containerEl = this.scrollerEl; // constrain clicks/etc to the dimensions of the scroller
  10150.  
  10151.         this.dayGrid.setElement(this.el.find('.fc-day-grid'));
  10152.         this.dayGrid.renderDates(this.hasRigidRows());
  10153.     },
  10154.  
  10155.  
  10156.     // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering,
  10157.     // always completely kill the dayGrid's rendering.
  10158.     unrenderDates: function() {
  10159.         this.dayGrid.unrenderDates();
  10160.         this.dayGrid.removeElement();
  10161.     },
  10162.  
  10163.  
  10164.     renderBusinessHours: function() {
  10165.         this.dayGrid.renderBusinessHours();
  10166.     },
  10167.  
  10168.  
  10169.     // Builds the HTML skeleton for the view.
  10170.     // The day-grid component will render inside of a container defined by this HTML.
  10171.     renderHtml: function() {
  10172.         return '' +
  10173.             '<table>' +
  10174.                 '<thead class="fc-head">' +
  10175.                     '<tr>' +
  10176.                         '<td class="' + this.widgetHeaderClass + '">' +
  10177.                             this.dayGrid.headHtml() + // render the day-of-week headers
  10178.                         '</td>' +
  10179.                     '</tr>' +
  10180.                 '</thead>' +
  10181.                 '<tbody class="fc-body">' +
  10182.                     '<tr>' +
  10183.                         '<td class="' + this.widgetContentClass + '">' +
  10184.                             '<div class="fc-day-grid-container">' +
  10185.                                 '<div class="fc-day-grid"/>' +
  10186.                             '</div>' +
  10187.                         '</td>' +
  10188.                     '</tr>' +
  10189.                 '</tbody>' +
  10190.             '</table>';
  10191.     },
  10192.  
  10193.  
  10194.     // Generates the HTML that will go before the day-of week header cells.
  10195.     // Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL.
  10196.     headIntroHtml: function() {
  10197.         if (this.weekNumbersVisible) {
  10198.             return '' +
  10199.                 '<th class="fc-week-number ' + this.widgetHeaderClass + '" ' + this.weekNumberStyleAttr() + '>' +
  10200.                     '<span>' + // needed for matchCellWidths
  10201.                         htmlEscape(this.opt('weekNumberTitle')) +
  10202.                     '</span>' +
  10203.                 '</th>';
  10204.         }
  10205.     },
  10206.  
  10207.  
  10208.     // Generates the HTML that will go before content-skeleton cells that display the day/week numbers.
  10209.     // Queried by the DayGrid subcomponent. Ordering depends on isRTL.
  10210.     numberIntroHtml: function(row) {
  10211.         if (this.weekNumbersVisible) {
  10212.             return '' +
  10213.                 '<td class="fc-week-number" ' + this.weekNumberStyleAttr() + '>' +
  10214.                     '<span>' + // needed for matchCellWidths
  10215.                         this.dayGrid.getCell(row, 0).start.format('w') +
  10216.                     '</span>' +
  10217.                 '</td>';
  10218.         }
  10219.     },
  10220.  
  10221.  
  10222.     // Generates the HTML that goes before the day bg cells for each day-row.
  10223.     // Queried by the DayGrid subcomponent. Ordering depends on isRTL.
  10224.     dayIntroHtml: function() {
  10225.         if (this.weekNumbersVisible) {
  10226.             return '<td class="fc-week-number ' + this.widgetContentClass + '" ' +
  10227.                 this.weekNumberStyleAttr() + '></td>';
  10228.         }
  10229.     },
  10230.  
  10231.  
  10232.     // Generates the HTML that goes before every other type of row generated by DayGrid. Ordering depends on isRTL.
  10233.     // Affects helper-skeleton and highlight-skeleton rows.
  10234.     introHtml: function() {
  10235.         if (this.weekNumbersVisible) {
  10236.             return '<td class="fc-week-number" ' + this.weekNumberStyleAttr() + '></td>';
  10237.         }
  10238.     },
  10239.  
  10240.  
  10241.     // Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton.
  10242.     // The number row will only exist if either day numbers or week numbers are turned on.
  10243.     numberCellHtml: function(cell) {
  10244.         var date = cell.start;
  10245.         var classes;
  10246.  
  10247.         if (!this.dayNumbersVisible) { // if there are week numbers but not day numbers
  10248.             return '<td/>'; //  will create an empty space above events :(
  10249.         }
  10250.  
  10251.         classes = this.dayGrid.getDayClasses(date);
  10252.         classes.unshift('fc-day-number');
  10253.  
  10254.         return '' +
  10255.             '<td class="' + classes.join(' ') + '" data-date="' + date.format() + '">' +
  10256.                 date.date() +
  10257.             '</td>';
  10258.     },
  10259.  
  10260.  
  10261.     // Generates an HTML attribute string for setting the width of the week number column, if it is known
  10262.     weekNumberStyleAttr: function() {
  10263.         if (this.weekNumberWidth !== null) {
  10264.             return 'style="width:' + this.weekNumberWidth + 'px"';
  10265.         }
  10266.         return '';
  10267.     },
  10268.  
  10269.  
  10270.     // Determines whether each row should have a constant height
  10271.     hasRigidRows: function() {
  10272.         var eventLimit = this.opt('eventLimit');
  10273.         return eventLimit && typeof eventLimit !== 'number';
  10274.     },
  10275.  
  10276.  
  10277.     /* Dimensions
  10278.     ------------------------------------------------------------------------------------------------------------------*/
  10279.  
  10280.  
  10281.     // Refreshes the horizontal dimensions of the view
  10282.     updateWidth: function() {
  10283.         if (this.weekNumbersVisible) {
  10284.             // Make sure all week number cells running down the side have the same width.
  10285.             // Record the width for cells created later.
  10286.             this.weekNumberWidth = matchCellWidths(
  10287.                 this.el.find('.fc-week-number')
  10288.             );
  10289.         }
  10290.     },
  10291.  
  10292.  
  10293.     // Adjusts the vertical dimensions of the view to the specified values
  10294.     setHeight: function(totalHeight, isAuto) {
  10295.         var eventLimit = this.opt('eventLimit');
  10296.         var scrollerHeight;
  10297.  
  10298.         // reset all heights to be natural
  10299.         unsetScroller(this.scrollerEl);
  10300.         uncompensateScroll(this.headRowEl);
  10301.  
  10302.         this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed
  10303.  
  10304.         // is the event limit a constant level number?
  10305.         if (eventLimit && typeof eventLimit === 'number') {
  10306.             this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after
  10307.         }
  10308.  
  10309.         scrollerHeight = this.computeScrollerHeight(totalHeight);
  10310.         this.setGridHeight(scrollerHeight, isAuto);
  10311.  
  10312.         // is the event limit dynamically calculated?
  10313.         if (eventLimit && typeof eventLimit !== 'number') {
  10314.             this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set
  10315.         }
  10316.  
  10317.         if (!isAuto && setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars?
  10318.  
  10319.             compensateScroll(this.headRowEl, getScrollbarWidths(this.scrollerEl));
  10320.  
  10321.             // doing the scrollbar compensation might have created text overflow which created more height. redo
  10322.             scrollerHeight = this.computeScrollerHeight(totalHeight);
  10323.             this.scrollerEl.height(scrollerHeight);
  10324.         }
  10325.     },
  10326.  
  10327.  
  10328.     // Sets the height of just the DayGrid component in this view
  10329.     setGridHeight: function(height, isAuto) {
  10330.         if (isAuto) {
  10331.             undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding
  10332.         }
  10333.         else {
  10334.             distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows
  10335.         }
  10336.     },
  10337.  
  10338.  
  10339.     /* Events
  10340.     ------------------------------------------------------------------------------------------------------------------*/
  10341.  
  10342.  
  10343.     // Renders the given events onto the view and populates the segments array
  10344.     renderEvents: function(events) {
  10345.         this.dayGrid.renderEvents(events);
  10346.  
  10347.         this.updateHeight(); // must compensate for events that overflow the row
  10348.     },
  10349.  
  10350.  
  10351.     // Retrieves all segment objects that are rendered in the view
  10352.     getEventSegs: function() {
  10353.         return this.dayGrid.getEventSegs();
  10354.     },
  10355.  
  10356.  
  10357.     // Unrenders all event elements and clears internal segment data
  10358.     unrenderEvents: function() {
  10359.         this.dayGrid.unrenderEvents();
  10360.  
  10361.         // we DON'T need to call updateHeight() because:
  10362.         // A) a renderEvents() call always happens after this, which will eventually call updateHeight()
  10363.         // B) in IE8, this causes a flash whenever events are rerendered
  10364.     },
  10365.  
  10366.  
  10367.     /* Dragging (for both events and external elements)
  10368.     ------------------------------------------------------------------------------------------------------------------*/
  10369.  
  10370.  
  10371.     // A returned value of `true` signals that a mock "helper" event has been rendered.
  10372.     renderDrag: function(dropLocation, seg) {
  10373.         return this.dayGrid.renderDrag(dropLocation, seg);
  10374.     },
  10375.  
  10376.  
  10377.     unrenderDrag: function() {
  10378.         this.dayGrid.unrenderDrag();
  10379.     },
  10380.  
  10381.  
  10382.     /* Selection
  10383.     ------------------------------------------------------------------------------------------------------------------*/
  10384.  
  10385.  
  10386.     // Renders a visual indication of a selection
  10387.     renderSelection: function(range) {
  10388.         this.dayGrid.renderSelection(range);
  10389.     },
  10390.  
  10391.  
  10392.     // Unrenders a visual indications of a selection
  10393.     unrenderSelection: function() {
  10394.         this.dayGrid.unrenderSelection();
  10395.     }
  10396.  
  10397. });
  10398.  
  10399. ;;
  10400.  
  10401. /* A month view with day cells running in rows (one-per-week) and columns
  10402. ----------------------------------------------------------------------------------------------------------------------*/
  10403.  
  10404. var MonthView = BasicView.extend({
  10405.  
  10406.     // Produces information about what range to display
  10407.     computeRange: function(date) {
  10408.         var range = BasicView.prototype.computeRange.call(this, date); // get value from super-method
  10409.         var rowCnt;
  10410.  
  10411.         // ensure 6 weeks
  10412.         if (this.isFixedWeeks()) {
  10413.             rowCnt = Math.ceil(range.end.diff(range.start, 'weeks', true)); // could be partial weeks due to hiddenDays
  10414.             range.end.add(6 - rowCnt, 'weeks');
  10415.         }
  10416.  
  10417.         return range;
  10418.     },
  10419.  
  10420.  
  10421.     // Overrides the default BasicView behavior to have special multi-week auto-height logic
  10422.     setGridHeight: function(height, isAuto) {
  10423.  
  10424.         isAuto = isAuto || this.opt('weekMode') === 'variable'; // LEGACY: weekMode is deprecated
  10425.  
  10426.         // if auto, make the height of each row the height that it would be if there were 6 weeks
  10427.         if (isAuto) {
  10428.             height *= this.rowCnt / 6;
  10429.         }
  10430.  
  10431.         distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows
  10432.     },
  10433.  
  10434.  
  10435.     isFixedWeeks: function() {
  10436.         var weekMode = this.opt('weekMode'); // LEGACY: weekMode is deprecated
  10437.         if (weekMode) {
  10438.             return weekMode === 'fixed'; // if any other type of weekMode, assume NOT fixed
  10439.         }
  10440.  
  10441.         return this.opt('fixedWeekCount');
  10442.     }
  10443.  
  10444. });
  10445.  
  10446. ;;
  10447.  
  10448. fcViews.basic = {
  10449.     'class': BasicView
  10450. };
  10451.  
  10452. fcViews.basicDay = {
  10453.     type: 'basic',
  10454.     duration: { days: 1 }
  10455. };
  10456.  
  10457. fcViews.basicWeek = {
  10458.     type: 'basic',
  10459.     duration: { weeks: 1 }
  10460. };
  10461.  
  10462. fcViews.month = {
  10463.     'class': MonthView,
  10464.     duration: { months: 1 }, // important for prev/next
  10465.     defaults: {
  10466.         fixedWeekCount: true
  10467.     }
  10468. };
  10469. ;;
  10470.  
  10471. /* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically.
  10472. ----------------------------------------------------------------------------------------------------------------------*/
  10473. // Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on).
  10474. // Responsible for managing width/height.
  10475.  
  10476. var AgendaView = View.extend({
  10477.  
  10478.     timeGrid: null, // the main time-grid subcomponent of this view
  10479.     dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null
  10480.  
  10481.     axisWidth: null, // the width of the time axis running down the side
  10482.  
  10483.     noScrollRowEls: null, // set of fake row elements that must compensate when scrollerEl has scrollbars
  10484.  
  10485.     // when the time-grid isn't tall enough to occupy the given height, we render an <hr> underneath
  10486.     bottomRuleEl: null,
  10487.     bottomRuleHeight: null,
  10488.  
  10489.  
  10490.     initialize: function() {
  10491.         this.timeGrid = new TimeGrid(this);
  10492.  
  10493.         if (this.opt('allDaySlot')) { // should we display the "all-day" area?
  10494.             this.dayGrid = new DayGrid(this); // the all-day subcomponent of this view
  10495.  
  10496.             // the coordinate grid will be a combination of both subcomponents' grids
  10497.             this.coordMap = new ComboCoordMap([
  10498.                 this.dayGrid.coordMap,
  10499.                 this.timeGrid.coordMap
  10500.             ]);
  10501.         }
  10502.         else {
  10503.             this.coordMap = this.timeGrid.coordMap;
  10504.         }
  10505.     },
  10506.  
  10507.  
  10508.     /* Rendering
  10509.     ------------------------------------------------------------------------------------------------------------------*/
  10510.  
  10511.  
  10512.     // Sets the display range and computes all necessary dates
  10513.     setRange: function(range) {
  10514.         View.prototype.setRange.call(this, range); // call the super-method
  10515.  
  10516.         this.timeGrid.setRange(range);
  10517.         if (this.dayGrid) {
  10518.             this.dayGrid.setRange(range);
  10519.         }
  10520.     },
  10521.  
  10522.  
  10523.     // Renders the view into `this.el`, which has already been assigned
  10524.     renderDates: function() {
  10525.  
  10526.         this.el.addClass('fc-agenda-view').html(this.renderHtml());
  10527.  
  10528.         // the element that wraps the time-grid that will probably scroll
  10529.         this.scrollerEl = this.el.find('.fc-time-grid-container');
  10530.         this.timeGrid.coordMap.containerEl = this.scrollerEl; // don't accept clicks/etc outside of this
  10531.  
  10532.         this.timeGrid.setElement(this.el.find('.fc-time-grid'));
  10533.         this.timeGrid.renderDates();
  10534.  
  10535.         // the <hr> that sometimes displays under the time-grid
  10536.         this.bottomRuleEl = $('<hr class="fc-divider ' + this.widgetHeaderClass + '"/>')
  10537.             .appendTo(this.timeGrid.el); // inject it into the time-grid
  10538.  
  10539.         if (this.dayGrid) {
  10540.             this.dayGrid.setElement(this.el.find('.fc-day-grid'));
  10541.             this.dayGrid.renderDates();
  10542.  
  10543.             // have the day-grid extend it's coordinate area over the <hr> dividing the two grids
  10544.             this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight();
  10545.         }
  10546.  
  10547.         this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller
  10548.     },
  10549.  
  10550.  
  10551.     // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering,
  10552.     // always completely kill each grid's rendering.
  10553.     unrenderDates: function() {
  10554.         this.timeGrid.unrenderDates();
  10555.         this.timeGrid.removeElement();
  10556.  
  10557.         if (this.dayGrid) {
  10558.             this.dayGrid.unrenderDates();
  10559.             this.dayGrid.removeElement();
  10560.         }
  10561.     },
  10562.  
  10563.  
  10564.     renderBusinessHours: function() {
  10565.         this.timeGrid.renderBusinessHours();
  10566.  
  10567.         if (this.dayGrid) {
  10568.             this.dayGrid.renderBusinessHours();
  10569.         }
  10570.     },
  10571.  
  10572.  
  10573.     // Builds the HTML skeleton for the view.
  10574.     // The day-grid and time-grid components will render inside containers defined by this HTML.
  10575.     renderHtml: function() {
  10576.         return '' +
  10577.             '<table>' +
  10578.                 '<thead class="fc-head">' +
  10579.                     '<tr>' +
  10580.                         '<td class="' + this.widgetHeaderClass + '">' +
  10581.                             this.timeGrid.headHtml() + // render the day-of-week headers
  10582.                         '</td>' +
  10583.                     '</tr>' +
  10584.                 '</thead>' +
  10585.                 '<tbody class="fc-body">' +
  10586.                     '<tr>' +
  10587.                         '<td class="' + this.widgetContentClass + '">' +
  10588.                             (this.dayGrid ?
  10589.                                 '<div class="fc-day-grid"/>' +
  10590.                                 '<hr class="fc-divider ' + this.widgetHeaderClass + '"/>' :
  10591.                                 ''
  10592.                                 ) +
  10593.                             '<div class="fc-time-grid-container">' +
  10594.                                 '<div class="fc-time-grid"/>' +
  10595.                             '</div>' +
  10596.                         '</td>' +
  10597.                     '</tr>' +
  10598.                 '</tbody>' +
  10599.             '</table>';
  10600.     },
  10601.  
  10602.  
  10603.     // Generates the HTML that will go before the day-of week header cells.
  10604.     // Queried by the TimeGrid subcomponent when generating rows. Ordering depends on isRTL.
  10605.     headIntroHtml: function() {
  10606.         var date;
  10607.         var weekText;
  10608.  
  10609.         if (this.opt('weekNumbers')) {
  10610.             date = this.timeGrid.getCell(0).start;
  10611.             weekText = date.format(this.opt('smallWeekFormat'));
  10612.  
  10613.             return '' +
  10614.                 '<th class="fc-axis fc-week-number ' + this.widgetHeaderClass + '" ' + this.axisStyleAttr() + '>' +
  10615.                     '<span>' + // needed for matchCellWidths
  10616.                         htmlEscape(weekText) +
  10617.                     '</span>' +
  10618.                 '</th>';
  10619.         }
  10620.         else {
  10621.             return '<th class="fc-axis ' + this.widgetHeaderClass + '" ' + this.axisStyleAttr() + '></th>';
  10622.         }
  10623.     },
  10624.  
  10625.  
  10626.     // Generates the HTML that goes before the all-day cells.
  10627.     // Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL.
  10628.     dayIntroHtml: function() {
  10629.         return '' +
  10630.             '<td class="fc-axis ' + this.widgetContentClass + '" ' + this.axisStyleAttr() + '>' +
  10631.                 '<span>' + // needed for matchCellWidths
  10632.                     (this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'))) +
  10633.                 '</span>' +
  10634.             '</td>';
  10635.     },
  10636.  
  10637.  
  10638.     // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column.
  10639.     slotBgIntroHtml: function() {
  10640.         return '<td class="fc-axis ' + this.widgetContentClass + '" ' + this.axisStyleAttr() + '></td>';
  10641.     },
  10642.  
  10643.  
  10644.     // Generates the HTML that goes before all other types of cells.
  10645.     // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
  10646.     // Queried by the TimeGrid and DayGrid subcomponents when generating rows. Ordering depends on isRTL.
  10647.     introHtml: function() {
  10648.         return '<td class="fc-axis" ' + this.axisStyleAttr() + '></td>';
  10649.     },
  10650.  
  10651.  
  10652.     // Generates an HTML attribute string for setting the width of the axis, if it is known
  10653.     axisStyleAttr: function() {
  10654.         if (this.axisWidth !== null) {
  10655.              return 'style="width:' + this.axisWidth + 'px"';
  10656.         }
  10657.         return '';
  10658.     },
  10659.  
  10660.  
  10661.     /* Dimensions
  10662.     ------------------------------------------------------------------------------------------------------------------*/
  10663.  
  10664.  
  10665.     updateSize: function(isResize) {
  10666.         this.timeGrid.updateSize(isResize);
  10667.  
  10668.         View.prototype.updateSize.call(this, isResize); // call the super-method
  10669.     },
  10670.  
  10671.  
  10672.     // Refreshes the horizontal dimensions of the view
  10673.     updateWidth: function() {
  10674.         // make all axis cells line up, and record the width so newly created axis cells will have it
  10675.         this.axisWidth = matchCellWidths(this.el.find('.fc-axis'));
  10676.     },
  10677.  
  10678.  
  10679.     // Adjusts the vertical dimensions of the view to the specified values
  10680.     setHeight: function(totalHeight, isAuto) {
  10681.         var eventLimit;
  10682.         var scrollerHeight;
  10683.  
  10684.         if (this.bottomRuleHeight === null) {
  10685.             // calculate the height of the rule the very first time
  10686.             this.bottomRuleHeight = this.bottomRuleEl.outerHeight();
  10687.         }
  10688.         this.bottomRuleEl.hide(); // .show() will be called later if this <hr> is necessary
  10689.  
  10690.         // reset all dimensions back to the original state
  10691.         this.scrollerEl.css('overflow', '');
  10692.         unsetScroller(this.scrollerEl);
  10693.         uncompensateScroll(this.noScrollRowEls);
  10694.  
  10695.         // limit number of events in the all-day area
  10696.         if (this.dayGrid) {
  10697.             this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed
  10698.  
  10699.             eventLimit = this.opt('eventLimit');
  10700.             if (eventLimit && typeof eventLimit !== 'number') {
  10701.                 eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number
  10702.             }
  10703.             if (eventLimit) {
  10704.                 this.dayGrid.limitRows(eventLimit);
  10705.             }
  10706.         }
  10707.  
  10708.         if (!isAuto) { // should we force dimensions of the scroll container, or let the contents be natural height?
  10709.  
  10710.             scrollerHeight = this.computeScrollerHeight(totalHeight);
  10711.             if (setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars?
  10712.  
  10713.                 // make the all-day and header rows lines up
  10714.                 compensateScroll(this.noScrollRowEls, getScrollbarWidths(this.scrollerEl));
  10715.  
  10716.                 // the scrollbar compensation might have changed text flow, which might affect height, so recalculate
  10717.                 // and reapply the desired height to the scroller.
  10718.                 scrollerHeight = this.computeScrollerHeight(totalHeight);
  10719.                 this.scrollerEl.height(scrollerHeight);
  10720.             }
  10721.             else { // no scrollbars
  10722.                 // still, force a height and display the bottom rule (marks the end of day)
  10723.                 this.scrollerEl.height(scrollerHeight).css('overflow', 'hidden'); // in case <hr> goes outside
  10724.                 this.bottomRuleEl.show();
  10725.             }
  10726.         }
  10727.     },
  10728.  
  10729.  
  10730.     // Computes the initial pre-configured scroll state prior to allowing the user to change it
  10731.     computeInitialScroll: function() {
  10732.         var scrollTime = moment.duration(this.opt('scrollTime'));
  10733.         var top = this.timeGrid.computeTimeTop(scrollTime);
  10734.  
  10735.         // zoom can give weird floating-point values. rather scroll a little bit further
  10736.         top = Math.ceil(top);
  10737.  
  10738.         if (top) {
  10739.             top++; // to overcome top border that slots beyond the first have. looks better
  10740.         }
  10741.  
  10742.         return top;
  10743.     },
  10744.  
  10745.  
  10746.     /* Events
  10747.     ------------------------------------------------------------------------------------------------------------------*/
  10748.  
  10749.  
  10750.     // Renders events onto the view and populates the View's segment array
  10751.     renderEvents: function(events) {
  10752.         var dayEvents = [];
  10753.         var timedEvents = [];
  10754.         var daySegs = [];
  10755.         var timedSegs;
  10756.         var i;
  10757.  
  10758.         // separate the events into all-day and timed
  10759.         for (i = 0; i < events.length; i++) {
  10760.             if (events[i].allDay) {
  10761.                 dayEvents.push(events[i]);
  10762.             }
  10763.             else {
  10764.                 timedEvents.push(events[i]);
  10765.             }
  10766.         }
  10767.  
  10768.         // render the events in the subcomponents
  10769.         timedSegs = this.timeGrid.renderEvents(timedEvents);
  10770.         if (this.dayGrid) {
  10771.             daySegs = this.dayGrid.renderEvents(dayEvents);
  10772.         }
  10773.  
  10774.         // the all-day area is flexible and might have a lot of events, so shift the height
  10775.         this.updateHeight();
  10776.     },
  10777.  
  10778.  
  10779.     // Retrieves all segment objects that are rendered in the view
  10780.     getEventSegs: function() {
  10781.         return this.timeGrid.getEventSegs().concat(
  10782.             this.dayGrid ? this.dayGrid.getEventSegs() : []
  10783.         );
  10784.     },
  10785.  
  10786.  
  10787.     // Unrenders all event elements and clears internal segment data
  10788.     unrenderEvents: function() {
  10789.  
  10790.         // unrender the events in the subcomponents
  10791.         this.timeGrid.unrenderEvents();
  10792.         if (this.dayGrid) {
  10793.             this.dayGrid.unrenderEvents();
  10794.         }
  10795.  
  10796.         // we DON'T need to call updateHeight() because:
  10797.         // A) a renderEvents() call always happens after this, which will eventually call updateHeight()
  10798.         // B) in IE8, this causes a flash whenever events are rerendered
  10799.     },
  10800.  
  10801.  
  10802.     /* Dragging (for events and external elements)
  10803.     ------------------------------------------------------------------------------------------------------------------*/
  10804.  
  10805.  
  10806.     // A returned value of `true` signals that a mock "helper" event has been rendered.
  10807.     renderDrag: function(dropLocation, seg) {
  10808.         if (dropLocation.start.hasTime()) {
  10809.             return this.timeGrid.renderDrag(dropLocation, seg);
  10810.         }
  10811.         else if (this.dayGrid) {
  10812.             return this.dayGrid.renderDrag(dropLocation, seg);
  10813.         }
  10814.     },
  10815.  
  10816.  
  10817.     unrenderDrag: function() {
  10818.         this.timeGrid.unrenderDrag();
  10819.         if (this.dayGrid) {
  10820.             this.dayGrid.unrenderDrag();
  10821.         }
  10822.     },
  10823.  
  10824.  
  10825.     /* Selection
  10826.     ------------------------------------------------------------------------------------------------------------------*/
  10827.  
  10828.  
  10829.     // Renders a visual indication of a selection
  10830.     renderSelection: function(range) {
  10831.         if (range.start.hasTime() || range.end.hasTime()) {
  10832.             this.timeGrid.renderSelection(range);
  10833.         }
  10834.         else if (this.dayGrid) {
  10835.             this.dayGrid.renderSelection(range);
  10836.         }
  10837.     },
  10838.  
  10839.  
  10840.     // Unrenders a visual indications of a selection
  10841.     unrenderSelection: function() {
  10842.         this.timeGrid.unrenderSelection();
  10843.         if (this.dayGrid) {
  10844.             this.dayGrid.unrenderSelection();
  10845.         }
  10846.     }
  10847.  
  10848. });
  10849.  
  10850. ;;
  10851.  
  10852. var AGENDA_ALL_DAY_EVENT_LIMIT = 5;
  10853.  
  10854. fcViews.agenda = {
  10855.     'class': AgendaView,
  10856.     defaults: {
  10857.         allDaySlot: true,
  10858.         allDayText: 'all-day',
  10859.         slotDuration: '00:30:00',
  10860.         minTime: '00:00:00',
  10861.         maxTime: '24:00:00',
  10862.         slotEventOverlap: true // a bad name. confused with overlap/constraint system
  10863.     }
  10864. };
  10865.  
  10866. fcViews.agendaDay = {
  10867.     type: 'agenda',
  10868.     duration: { days: 1 }
  10869. };
  10870.  
  10871. fcViews.agendaWeek = {
  10872.     type: 'agenda',
  10873.     duration: { weeks: 1 }
  10874. };
  10875. ;;
  10876.  
  10877. return fc; // export for Node/CommonJS
  10878. });
Add Comment
Please, Sign In to add comment