Subversion-Projekte lars-tiefland.cienc

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

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