Subversion Repositories Integrator Subversion

Rev

Details | Last modification | View Log | RSS feed

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