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