Subversion-Projekte lars-tiefland.cienc

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

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