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