PageSourceSearch

https://www.bilbaorkestra.eus/wp-content/themes/bos-2016/js/fullcalendar.js

js bilbaorkestra.eus collected 2026-10-02 10:29:26 UTC 319,309 bytes, 11,170 lines download raw bytes

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
21var fc = $.fullCalendar = { version: "2.4.0" };
22var 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
58var 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
67function 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.
74function 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
124fc.intersectionToSeg = intersectionToSeg;
125fc.applyAll = applyAll;
126fc.debounce = debounce;
127fc.isInt = isInt;
128fc.htmlEscape = htmlEscape;
129fc.cssToStr = cssToStr;
130fc.proxy = proxy;
131fc.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.
140function 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
157function 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
168function disableCursor() {
169	$('body').addClass('fc-not-allowed');
170}
171
172
173// Returns the mouse cursor to its original look
174function 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.
183function 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
236function 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
244function 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
265function 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.
279function unsetScroller(containerEl) {
280	containerEl.height('').removeClass('fc-scroller');
281}
282
283
284/* General DOM Utilities
285----------------------------------------------------------------------------------------------------------------------*/
286
287fc.getClientRect = getClientRect;
288fc.getContentRect = getContentRect;
289fc.getScrollbarWidths = getScrollbarWidths;
290
291
292// borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51
293function 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).
308function 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.
323function 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).
340function 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.
356function 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
378var _isLeftRtlScrollbars = null;
379
380function getIsLeftRtlScrollbars() { // responsible for caching the computation
381	if (_isLeftRtlScrollbars === null) {
382		_isLeftRtlScrollbars = computeIsLeftRtlScrollbars();
383	}
384	return _isLeftRtlScrollbars;
385}
386
387function 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.
408function 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)
414function isPrimaryMouseButton(ev) {
415	return ev.which == 1 && !ev.ctrlKey;
416}
417
418
419/* Geometry
420----------------------------------------------------------------------------------------------------------------------*/
421
422fc.intersectRects = intersectRects;
423
424// Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false
425function 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
441function 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
450function 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
459function 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
470fc.parseFieldSpecs = parseFieldSpecs;
471fc.compareByFieldSpecs = compareByFieldSpecs;
472fc.compareByFieldSpec = compareByFieldSpec;
473fc.flexibleCompare = flexibleCompare;
474
475
476function 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
510function 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
525function 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
534function 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?
558function 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
599fc.computeIntervalUnit = computeIntervalUnit;
600fc.divideRangeByDuration = divideRangeByDuration;
601fc.divideDurationByDuration = divideDurationByDuration;
602fc.multiplyDuration = multiplyDuration;
603fc.durationHasTime = durationHasTime;
604
605var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ];
606var 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.
611function 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.
620function 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.
628function 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.
639function 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).
660function 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
675function 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
690function 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
709function 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)
724function durationHasTime(dur) {
725	return Boolean(dur.hours() || dur.minutes() || dur.seconds() || dur.milliseconds());
726}
727
728
729function 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"
735function isTimeString(str) {
736	return /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str);
737}
738
739
740/* Logging and Debug
741----------------------------------------------------------------------------------------------------------------------*/
742
743fc.log = function() {
744	var console = window.console;
745
746	if (console && console.log) {
747		return console.log.apply(console, arguments);
748	}
749};
750
751fc.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
766var 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.
771function 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
819function createObject(proto) {
820	var f = function() {};
821	f.prototype = proto;
822	return new f();
823}
824
825
826function 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
837function 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
851function hasOwnProp(obj, name) {
852	return hasOwnPropMethod.call(obj, name);
853}
854
855
856// Is the given value a non-object non-function value?
857function isAtomic(val) {
858	return /undefined|null|boolean|number|string/.test($.type(val));
859}
860
861
862function 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
877function firstDefined() {
878	for (var i=0; i<arguments.length; i++) {
879		if (arguments[i] !== undefined) {
880			return arguments[i];
881		}
882	}
883}
884
885
886function 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
896function 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.
903function 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
916function capitaliseFirstLetter(str) {
917	return str.charAt(0).toUpperCase() + str.slice(1);
918}
919
920
921function compareNumbers(a, b) { // for .sort()
922	return a - b;
923}
924
925
926function 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.
934function 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
947function 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
978var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/;
979var 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+)?)?)?)?)?$/;
981var newMomentProto = moment.fn; // where we will attach our new methods
982var oldMomentProto = $.extend({}, newMomentProto); // copy of original moment methods
983var allowValueOptimization;
984var setUTCValues; // function defined below
985var 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.
995fc.moment = function() {
996	return makeMoment(arguments);
997};
998
999// Sames as fc.moment, but forces the resulting moment to be in the UTC timezone.
1000fc.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.
1014fc.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?
1023function 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
1095newMomentProto.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`
1114newMomentProto.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.
1139newMomentProto.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.
1182newMomentProto.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)
1204newMomentProto.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
1216newMomentProto.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)
1240newMomentProto.hasZone = function() {
1241	return !this._ambigZone;
1242};
1243
1244
1245// this method implicitly marks a zone
1246newMomentProto.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
1269newMomentProto.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
1308newMomentProto.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
1321newMomentProto.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.
1337newMomentProto.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.
1344newMomentProto.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.
1390function 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.
1425function 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.
1444function 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?
1455allowValueOptimization = '_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.
1459setUTCValues = 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.
1467setLocalValues = 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
1488function 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.
1495function formatDate(date, formatStr) {
1496	return formatDateWithChunks(date, getFormatStringChunks(formatStr));
1497}
1498
1499
1500function 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
1513var 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
1523function 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 str
1551ing, 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.
1555function 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}
1578fc.formatRange = formatRange; // expose
1579
1580
1581function 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
1631var 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`.
1651function 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
1676var formatStringChunkCache = {};
1677
1678
1679function 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
1688function 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
1713fc.Class = Class; // export
1714
1715// class that all other classes will inherit from
1716function Class() { }
1717
1718// called upon a class to create a subclass
1719Class.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.
1750Class.mixin = function(members) {
1751	copyOwnProps(members.prototype || members, this.prototype); // TODO: copyNativeMethods?
1752};
1753;;
1754
1755var 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------------------------------------------------------------------------------------------------------------------------
1810Options:
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
1822var 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------------------------------------------------------------------------------------------------------------------------
1979Common 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
1991var 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
2093var 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
2145var 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------------------------------------------------------------------------------------------------------------------------
2532options:
2533- subjectEl
2534- subjectCenter
2535*/
2536
2537var 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.
2705function 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
2725var 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
2914var 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
3016var 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
3701Grid.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
4629function isBgEvent(event) { // returns true if background OR inverse-background
4630	var rendering = getEventRendering(event);
4631	return rendering === 'background' || rendering === 'inverse-background';
4632}
4633
4634
4635function isInverseBgEvent(event) {
4636	return getEventRendering(event) === 'inverse-background';
4637}
4638
4639
4640function getEventRendering(event) {
4641	return firstDefined((event.source || {}).rendering, event.rendering);
4642}
4643
4644
4645function 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
4659function 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.
4669fc.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.
4674function 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
4723var 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
5196DayGrid.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.
5486function 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
5505function 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
5515DayGrid.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
5885var 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"') + '>
5975' +
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
6420TimeGrid.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.
6741function 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
6767function 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
6789function 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.
6817function 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?
6831function 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
6840var 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
7786var 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
8039Calendar.mixin(Emitter);
8040
8041
8042function 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
8691Calendar.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
8782Calendar.englishDefaults = { // used by lang.js
8783	dayPopoverFormat: 'dddd, MMMM D'
8784};
8785
8786
8787Calendar.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
8809var 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.
8818fc.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.
8853fc.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.
8882var 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
8902var 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)
8960var 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
8985function 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
8996function 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.
9005fc.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
9013function 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
9256fc.sourceNormalizers = [];
9257fc.sourceFetchers = [];
9258
9259var ajaxDefaults = {
9260	dataType: 'json',
9261	cache: false
9262};
9263
9264var eventGUID = 1;
9265
9266
9267function 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.
10339Calendar.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.
10359function 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
10372var 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
10686var 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
10730fcViews.basic = {
10731	'class': BasicView
10732};
10733
10734fcViews.basicDay = {
10735	type: 'basic',
10736	duration: { days: 1 }
10737};
10738
10739fcViews.basicWeek = {
10740	type: 'basic',
10741	duration: { weeks: 1 }
10742};
10743
10744fcViews.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
10758var 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
11134var AGENDA_ALL_DAY_EVENT_LIMIT = 5;
11135
11136// potential nice values for the slot-duration and interval-duration
11137// from largest to smallest
11138var AGENDA_STOCK_SUB_DURATIONS = [
11139	{ hours: 1 },
11140	{ minutes: 30 },
11141	{ minutes: 15 },
11142	{ seconds: 30 },
11143	{ seconds: 15 }
11144];
11145
11146fcViews.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
11158fcViews.agendaDay = {
11159	type: 'agenda',
11160	duration: { days: 1 }
11161};
11162
11163fcViews.agendaWeek = {
11164	type: 'agenda',
11165	duration: { weeks: 1 }
11166};
11167;;
11168
11169return fc; // export for Node/CommonJS
11170});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.