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