PageSourceSearch

https://climateactionlewisham.org/wp/wp-content/plugins/dans-gcal/js/fullcalendar/fullcalendar.js?ver=6.4.12

js climateactionlewisham.org collected 2026-10-01 23:53:25 UTC 373,552 bytes, 13,200 lines download raw bytes

1/*!
2 * <%= meta.title %> v<%= meta.version %>
3 * Docs & License: <%= meta.homepage %>
4 * (c) <%= meta.copyright %>
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 = {
22	version: "<%= meta.version %>",
23	internalApiVersion: 5
24};
25var fcViews = FC.views = {};
26
27
28$.fn.fullCalendar = function(options) {
29	var args = Array.prototype.slice.call(arguments, 1); // for a possible method call
30	var res = this; // what this function will return (this jQuery object by default)
31
32	this.each(function(i, _element) { // loop each DOM element involved
33		var element = $(_element);
34		var calendar = element.data('fullCalendar'); // get the existing calendar object (if any)
35		var singleRes; // the returned value of this single method call
36
37		// a method call
38		if (typeof options === 'string') {
39			if (calendar && $.isFunction(calendar[options])) {
40				singleRes = calendar[options].apply(calendar, args);
41				if (!i) {
42					res = singleRes; // record the first method call result
43				}
44				if (options === 'destroy') { // for the destroy method, must remove Calendar object data
45					element.removeData('fullCalendar');
46				}
47			}
48		}
49		// a new calendar initialization
50		else if (!calendar) { // don't initialize twice
51			calendar = new Calendar(element, options);
52			element.data('fullCalendar', calendar);
53			calendar.render();
54		}
55	});
56	
57	return res;
58};
59
60
61var complexOptions = [ // names of options that are objects whose properties should be combined
62	'header',
63	'buttonText',
64	'buttonIcons',
65	'themeButtonIcons'
66];
67
68
69// Merges an array of option objects into a single object
70function mergeOptions(optionObjs) {
71	return mergeProps(optionObjs, complexOptions);
72}
73
74
75// Given options specified for the calendar's constructor, massages any legacy options into a non-legacy form.
76// Converts View-Option-Hashes into the View-Specific-Options format.
77function massageOverrides(input) {
78	var overrides = { views: input.views || {} }; // the output. ensure a `views` hash
79	var subObj;
80
81	// iterate through all option override properties (except `views`)
82	$.each(input, function(name, val) {
83		if (name != 'views') {
84
85			// could the value be a legacy View-Option-Hash?
86			if (
87				$.isPlainObject(val) &&
88				!/(time|duration|interval)$/i.test(name) && // exclude duration options. might be given as objects
89				$.inArray(name, complexOptions) == -1 // complex options aren't allowed to be View-Option-Hashes
90			) {
91				subObj = null;
92
93				// iterate through the properties of this possible View-Option-Hash value
94				$.each(val, function(subName, subVal) {
95
96					// is the property targeting a view?
97					if (/^(month|week|day|default|basic(Week|Day)?|agenda(Week|Day)?)$/.test(subName)) {
98						if (!overrides.views[subName]) { // ensure the view-target entry exists
99							overrides.views[subName] = {};
100						}
101						overrides.views[subName][name] = subVal; // record the value in the `views` object
102					}
103					else { // a non-View-Option-Hash property
104						if (!subObj) {
105							subObj = {};
106						}
107						subObj[subName] = subVal; // accumulate these unrelated values for later
108					}
109				});
110
111				if (subObj) { // non-View-Option-Hash properties? transfer them as-is
112					overrides[name] = subObj;
113				}
114			}
115			else {
116				overrides[name] = val; // transfer normal options as-is
117			}
118		}
119	});
120
121	return overrides;
122}
123
124;;
125
126// exports
127FC.intersectRanges = intersectRanges;
128FC.applyAll = applyAll;
129FC.debounce = debounce;
130FC.isInt = isInt;
131FC.htmlEscape = htmlEscape;
132FC.cssToStr = cssToStr;
133FC.proxy = proxy;
134FC.capitaliseFirstLetter = capitaliseFirstLetter;
135
136
137/* FullCalendar-specific DOM Utilities
138----------------------------------------------------------------------------------------------------------------------*/
139
140
141// Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left
142// and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that.
143function compensateScroll(rowEls, scrollbarWidths) {
144	if (scrollbarWidths.left) {
145		rowEls.css({
146			'border-left-width': 1,
147			'margin-left': scrollbarWidths.left - 1
148		});
149	}
150	if (scrollbarWidths.right) {
151		rowEls.css({
152			'border-right-width': 1,
153			'margin-right': scrollbarWidths.right - 1
154		});
155	}
156}
157
158
159// Undoes compensateScroll and restores all borders/margins
160function uncompensateScroll(rowEls) {
161	rowEls.css({
162		'margin-left': '',
163		'margin-right': '',
164		'border-left-width': '',
165		'border-right-width': ''
166	});
167}
168
169
170// Make the mouse cursor express that an event is not allowed in the current area
171function disableCursor() {
172	$('body').addClass('fc-not-allowed');
173}
174
175
176// Returns the mouse cursor to its original look
177function enableCursor() {
178	$('body').removeClass('fc-not-allowed');
179}
180
181
182// Given a total available height to fill, have `els` (essentially child rows) expand to accomodate.
183// By default, all elements that are shorter than the recommended height are expanded uniformly, not considering
184// any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and 
185// reduces the available height.
186function distributeHeight(els, availableHeight, shouldRedistribute) {
187
188	// *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions,
189	// and it is better to be shorter than taller, to avoid creating unnecessary scrollbars.
190
191	var minOffset1 = Math.floor(availableHeight / els.length); // for non-last element
192	var minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)); // for last element *FLOORING NOTE*
193	var flexEls = []; // elements that are allowed to expand. array of DOM nodes
194	var flexOffsets = []; // amount of vertical space it takes up
195	var flexHeights = []; // actual css height
196	var usedHeight = 0;
197
198	undistributeHeight(els); // give all elements their natural height
199
200	// find elements that are below the recommended height (expandable).
201	// important to query for heights in a single first pass (to avoid reflow oscillation).
202	els.each(function(i, el) {
203		var minOffset = i === els.length - 1 ? minOffset2 : minOffset1;
204		var naturalOffset = $(el).outerHeight(true);
205
206		if (naturalOffset < minOffset) {
207			flexEls.push(el);
208			flexOffsets.push(naturalOffset);
209			flexHeights.push($(el).height());
210		}
211		else {
212			// this element stretches past recommended height (non-expandable). mark the space as occupied.
213			usedHeight += naturalOffset;
214		}
215	});
216
217	// readjust the recommended height to only consider the height available to non-maxed-out rows.
218	if (shouldRedistribute) {
219		availableHeight -= usedHeight;
220		minOffset1 = Math.floor(availableHeight / flexEls.length);
221		minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)); // *FLOORING NOTE*
222	}
223
224	// assign heights to all expandable elements
225	$(flexEls).each(function(i, el) {
226		var minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1;
227		var naturalOffset = flexOffsets[i];
228		var naturalHeight = flexHeights[i];
229		var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding
230
231		if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things
232			$(el).height(newHeight);
233		}
234	});
235}
236
237
238// Undoes distrubuteHeight, restoring all els to their natural height
239function undistributeHeight(els) {
240	els.height('');
241}
242
243
244// Given `els`, a jQuery set of <td> cells, find the cell with the largest natural width and set the widths of all the
245// cells to be that width.
246// PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline
247function matchCellWidths(els) {
248	var maxInnerWidth = 0;
249
250	els.find('> span').each(function(i, innerEl) {
251		var innerWidth = $(innerEl).outerWidth();
252		if (innerWidth > maxInnerWidth) {
253			maxInnerWidth = innerWidth;
254		}
255	});
256
257	maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance
258
259	els.width(maxInnerWidth);
260
261	return maxInnerWidth;
262}
263
264
265// Given one element that resides inside another,
266// Subtracts the height of the inner element from the outer element.
267function subtractInnerElHeight(outerEl, innerEl) {
268	var both = outerEl.add(innerEl);
269	var diff;
270
271	// effin' IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked
272	both.css({
273		position: 'relative', // cause a reflow, which will force fresh dimension recalculation
274		left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll
275	});
276	diff = outerEl.outerHeight() - innerEl.outerHeight(); // grab the dimensions
277	both.css({ position: '', left: '' }); // undo hack
278
279	return diff;
280}
281
282
283/* Element Geom Utilities
284----------------------------------------------------------------------------------------------------------------------*/
285
286FC.getOuterRect = getOuterRect;
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).
308// Origin is optional.
309function getOuterRect(el, origin) {
310	var offset = el.offset();
311	var left = offset.left - (origin ? origin.left : 0);
312	var top = offset.top - (origin ? origin.top : 0);
313
314	return {
315		left: left,
316		right: left + el.outerWidth(),
317		top: top,
318		bottom: top + el.outerHeight()
319	};
320}
321
322
323// Queries the area within the margin/border/scrollbars of a jQuery element. Does not go within the padding.
324// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
325// Origin is optional.
326// NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
327function getClientRect(el, origin) {
328	var offset = el.offset();
329	var scrollbarWidths = getScrollbarWidths(el);
330	var left = offset.left + getCssFloat(el, 'border-left-width') + scrollbarWidths.left - (origin ? origin.left : 0);
331	var top = offset.top + getCssFloat(el, 'border-top-width') + scrollbarWidths.top - (origin ? origin.top : 0);
332
333	return {
334		left: left,
335		right: left + el[0].clientWidth, // clientWidth includes padding but NOT scrollbars
336		top: top,
337		bottom: top + el[0].clientHeight // clientHeight includes padding but NOT scrollbars
338	};
339}
340
341
342// Queries the area within the margin/border/padding of a jQuery element. Assumed not to have scrollbars.
343// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
344// Origin is optional.
345function getContentRect(el, origin) {
346	var offset = el.offset(); // just outside of border, margin not included
347	var left = offset.left + getCssFloat(el, 'border-left-width') + getCssFloat(el, 'padding-left') -
348		(origin ? origin.left : 0);
349	var top = offset.top + getCssFloat(el, 'border-top-width') + getCssFloat(el, 'padding-top') -
350		(origin ? origin.top : 0);
351
352	return {
353		left: left,
354		right: left + el.width(),
355		top: top,
356		bottom: top + el.height()
357	};
358}
359
360
361// Returns the computed left/right/top/bottom scrollbar widths for the given jQuery element.
362// NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
363function getScrollbarWidths(el) {
364	var leftRightWidth = el.innerWidth() - el[0].clientWidth; // the paddings cancel out, leaving the scrollbars
365	var widths = {
366		left: 0,
367		right: 0,
368		top: 0,
369		bottom: el.innerHeight() - el[0].clientHeight // the paddings cancel out, leaving the bottom scrollbar
370	};
371
372	if (getIsLeftRtlScrollbars() && el.css('direction') == 'rtl') { // is the scrollbar on the left side?
373		widths.left = leftRightWidth;
374	}
375	else {
376		widths.right = leftRightWidth;
377	}
378
379	return widths;
380}
381
382
383// Logic for determining if, when the element is right-to-left, the scrollbar appears on the left side
384
385var _isLeftRtlScrollbars = null;
386
387function getIsLeftRtlScrollbars() { // responsible for caching the computation
388	if (_isLeftRtlScrollbars === null) {
389		_isLeftRtlScrollbars = computeIsLeftRtlScrollbars();
390	}
391	return _isLeftRtlScrollbars;
392}
393
394function computeIsLeftRtlScrollbars() { // creates an offscreen test element, then removes it
395	var el = $('<div><div/></div>')
396		.css({
397			position: 'absolute',
398			top: -1000,
399			left: 0,
400			border: 0,
401			padding: 0,
402			overflow: 'scroll',
403			direction: 'rtl'
404		})
405		.appendTo('body');
406	var innerEl = el.children();
407	var res = innerEl.offset().left > el.offset().left; // is the inner div shifted to accommodate a left scrollbar?
408	el.remove();
409	return res;
410}
411
412
413// Retrieves a jQuery element's computed CSS value as a floating-point number.
414// If the queried value is non-numeric (ex: IE can return "medium" for border width), will just return zero.
415function getCssFloat(el, prop) {
416	return parseFloat(el.css(prop)) || 0;
417}
418
419
420/* Mouse / Touch Utilities
421----------------------------------------------------------------------------------------------------------------------*/
422
423FC.preventDefault = preventDefault;
424
425
426// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
427function isPrimaryMouseButton(ev) {
428	return ev.which == 1 && !ev.ctrlKey;
429}
430
431
432function getEvX(ev) {
433	if (ev.pageX !== undefined) {
434		return ev.pageX;
435	}
436	var touches = ev.originalEvent.touches;
437	if (touches) {
438		return touches[0].pageX;
439	}
440}
441
442
443function getEvY(ev) {
444	if (ev.pageY !== undefined) {
445		return ev.pageY;
446	}
447	var touches = ev.originalEvent.touches;
448	if (touches) {
449		return touches[0].pageY;
450	}
451}
452
453
454function getEvIsTouch(ev) {
455	return /^touch/.test(ev.type);
456}
457
458
459function preventSelection(el) {
460	el.addClass('fc-unselectable')
461		.on('selectstart', preventDefault);
462}
463
464
465// Stops a mouse/touch event from doing it's native browser action
466function preventDefault(ev) {
467	ev.preventDefault();
468}
469
470
471// attach a handler to get called when ANY scroll action happens on the page.
472// this was impossible to do with normal on/off because 'scroll' doesn't bubble.
473// http://stackoverflow.com/a/32954565/96342
474// returns `true` on success.
475function bindAnyScroll(handler) {
476	if (window.addEventListener) {
477		window.addEventListener('scroll', handler, true); // useCapture=true
478		return true;
479	}
480	return false;
481}
482
483
484// undoes bindAnyScroll. must pass in the original function.
485// returns `true` on success.
486function unbindAnyScroll(handler) {
487	if (window.removeEventListener) {
488		window.removeEventListener('scroll', handler, true); // useCapture=true
489		return true;
490	}
491	return false;
492}
493
494
495/* General Geometry Utils
496----------------------------------------------------------------------------------------------------------------------*/
497
498FC.intersectRects = intersectRects;
499
500// Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false
501function intersectRects(rect1, rect2) {
502	var res = {
503		left: Math.max(rect1.left, rect2.left),
504		right: Math.min(rect1.right, rect2.right),
505		top: Math.max(rect1.top, rect2.top),
506		bottom: Math.min(rect1.bottom, rect2.bottom)
507	};
508
509	if (res.left < res.right && res.top < res.bottom) {
510		return res;
511	}
512	return false;
513}
514
515
516// Returns a new point that will have been moved to reside within the given rectangle
517function constrainPoint(point, rect) {
518	return {
519		left: Math.min(Math.max(point.left, rect.left), rect.right),
520		top: Math.min(Math.max(point.top, rect.top), rect.bottom)
521	};
522}
523
524
525// Returns a point that is the center of the given rectangle
526function getRectCenter(rect) {
527	return {
528		left: (rect.left + rect.right) / 2,
529		top: (rect.top + rect.bottom) / 2
530	};
531}
532
533
534// Subtracts point2's coordinates from point1's coordinates, returning a delta
535function diffPoints(point1, point2) {
536	return {
537		left: point1.left - point2.left,
538		top: point1.top - point2.top
539	};
540}
541
542
543/* Object Ordering by Field
544----------------------------------------------------------------------------------------------------------------------*/
545
546FC.parseFieldSpecs = parseFieldSpecs;
547FC.compareByFieldSpecs = compareByFieldSpecs;
548FC.compareByFieldSpec = compareByFieldSpec;
549FC.flexibleCompare = flexibleCompare;
550
551
552function parseFieldSpecs(input) {
553	var specs = [];
554	var tokens = [];
555	var i, token;
556
557	if (typeof input === 'string') {
558		tokens = input.split(/\s*,\s*/);
559	}
560	else if (typeof input === 'function') {
561		tokens = [ input ];
562	}
563	else if ($.isArray(input)) {
564		tokens = input;
565	}
566
567	for (i = 0; i < tokens.length; i++) {
568		token = tokens[i];
569
570		if (typeof token === 'string') {
571			specs.push(
572				token.charAt(0) == '-' ?
573					{ field: token.substring(1), order: -1 } :
574					{ field: token, order: 1 }
575			);
576		}
577		else if (typeof token === 'function') {
578			specs.push({ func: token });
579		}
580	}
581
582	return specs;
583}
584
585
586function compareByFieldSpecs(obj1, obj2, fieldSpecs) {
587	var i;
588	var cmp;
589
590	for (i = 0; i < fieldSpecs.length; i++) {
591		cmp = compareByFieldSpec(obj1, obj2, fieldSpecs[i]);
592		if (cmp) {
593			return cmp;
594		}
595	}
596
597	return 0;
598}
599
600
601function compareByFieldSpec(obj1, obj2, fieldSpec) {
602	if (fieldSpec.func) {
603		return fieldSpec.func(obj1, obj2);
604	}
605	return flexibleCompare(obj1[fieldSpec.field], obj2[fieldSpec.field]) *
606		(fieldSpec.order || 1);
607}
608
609
610function flexibleCompare(a, b) {
611	if (!a && !b) {
612		return 0;
613	}
614	if (b == null) {
615		return -1;
616	}
617	if (a == null) {
618		return 1;
619	}
620	if ($.type(a) === 'string' || $.type(b) === 'string') {
621		return String(a).localeCompare(String(b));
622	}
623	return a - b;
624}
625
626
627/* FullCalendar-specific Misc Utilities
628----------------------------------------------------------------------------------------------------------------------*/
629
630
631// Computes the intersection of the two ranges. Returns undefined if no intersection.
632// Expects all dates to be normalized to the same timezone beforehand.
633// TODO: move to date section?
634function intersectRanges(subjectRange, constraintRange) {
635	var subjectStart = subjectRange.start;
636	var subjectEnd = subjectRange.end;
637	var constraintStart = constraintRange.start;
638	var constraintEnd = constraintRange.end;
639	var segStart, segEnd;
640	var isStart, isEnd;
641
642	if (subjectEnd > constraintStart && subjectStart < constraintEnd) { // in bounds at all?
643
644		if (subjectStart >= constraintStart) {
645			segStart = subjectStart.clone();
646			isStart = true;
647		}
648		else {
649			segStart = constraintStart.clone();
650			isStart =  false;
651		}
652
653		if (subjectEnd <= constraintEnd) {
654			segEnd = subjectEnd.clone();
655			isEnd = true;
656		}
657		else {
658			segEnd = constraintEnd.clone();
659			isEnd = false;
660		}
661
662		return {
663			start: segStart,
664			end: segEnd,
665			isStart: isStart,
666			isEnd: isEnd
667		};
668	}
669}
670
671
672/* Date Utilities
673----------------------------------------------------------------------------------------------------------------------*/
674
675FC.computeIntervalUnit = computeIntervalUnit;
676FC.divideRangeByDuration = divideRangeByDuration;
677FC.divideDurationByDuration = divideDurationByDuration;
678FC.multiplyDuration = multiplyDuration;
679FC.durationHasTime = durationHasTime;
680
681var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ];
682var intervalUnits = [ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' ];
683
684
685// Diffs the two moments into a Duration where full-days are recorded first, then the remaining time.
686// Moments will have their timezones normalized.
687function diffDayTime(a, b) {
688	return moment.duration({
689		days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'),
690		ms: a.time() - b.time() // time-of-day from day start. disregards timezone
691	});
692}
693
694
695// Diffs the two moments via their start-of-day (regardless of timezone). Produces whole-day durations.
696function diffDay(a, b) {
697	return moment.duration({
698		days: a.clone().stripTime().diff(b.clone().stripTime(), 'days')
699	});
700}
701
702
703// Diffs two moments, producing a duration, made of a whole-unit-increment of the given unit. Uses rounding.
704function diffByUnit(a, b, unit) {
705	return moment.duration(
706		Math.round(a.diff(b, unit, true)), // returnFloat=true
707		unit
708	);
709}
710
711
712// Computes the unit name of the largest whole-unit period of time.
713// For example, 48 hours will be "days" whereas 49 hours will be "hours".
714// Accepts start/end, a range object, or an original duration object.
715function computeIntervalUnit(start, end) {
716	var i, unit;
717	var val;
718
719	for (i = 0; i < intervalUnits.length; i++) {
720		unit = intervalUnits[i];
721		val = computeRangeAs(unit, start, end);
722
723		if (val >= 1 && isInt(val)) {
724			break;
725		}
726	}
727
728	return unit; // will be "milliseconds" if nothing else matches
729}
730
731
732// Computes the number of units (like "hours") in the given range.
733// Range can be a {start,end} object, separate start/end args, or a Duration.
734// Results are based on Moment's .as() and .diff() methods, so results can depend on internal handling
735// of month-diffing logic (which tends to vary from version to version).
736function computeRangeAs(unit, start, end) {
737
738	if (end != null) { // given start, end
739		return end.diff(start, unit, true);
740	}
741	else if (moment.isDuration(start)) { // given duration
742		return start.as(unit);
743	}
744	else { // given { start, end } range object
745		return start.end.diff(start.start, unit, true);
746	}
747}
748
749
750// Intelligently divides a range (specified by a start/end params) by a duration
751function divideRangeByDuration(start, end, dur) {
752	var months;
753
754	if (durationHasTime(dur)) {
755		return (end - start) / dur;
756	}
757	months = dur.asMonths();
758	if (Math.abs(months) >= 1 && isInt(months)) {
759		return end.diff(start, 'months', true) / months;
760	}
761	return end.diff(start, 'days', true) / dur.asDays();
762}
763
764
765// Intelligently divides one duration by another
766function divideDurationByDuration(dur1, dur2) {
767	var months1, months2;
768
769	if (durationHasTime(dur1) || durationHasTime(dur2)) {
770		return dur1 / dur2;
771	}
772	months1 = dur1.asMonths();
773	months2 = dur2.asMonths();
774	if (
775		Math.abs(months1) >= 1 && isInt(months1) &&
776		Math.abs(months2) >= 1 && isInt(months2)
777	) {
778		return months1 / months2;
779	}
780	return dur1.asDays() / dur2.asDays();
781}
782
783
784// Intelligently multiplies a duration by a number
785function multiplyDuration(dur, n) {
786	var months;
787
788	if (durationHasTime(dur)) {
789		return moment.duration(dur * n);
790	}
791	months = dur.asMonths();
792	if (Math.abs(months) >= 1 && isInt(months)) {
793		return moment.duration({ months: months * n });
794	}
795	return moment.duration({ days: dur.asDays() * n });
796}
797
798
799// Returns a boolean about whether the given duration has any time parts (hours/minutes/seconds/ms)
800function durationHasTime(dur) {
801	return Boolean(dur.hours() || dur.minutes() || dur.seconds() || dur.milliseconds());
802}
803
804
805function isNativeDate(input) {
806	return  Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date;
807}
808
809
810// Returns a boolean about whether the given input is a time string, like "06:40:00" or "06:00"
811function isTimeString(str) {
812	return /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str);
813}
814
815
816/* Logging and Debug
817----------------------------------------------------------------------------------------------------------------------*/
818
819FC.log = function() {
820	var console = window.console;
821
822	if (console && console.log) {
823		return console.log.apply(console, arguments);
824	}
825};
826
827FC.warn = function() {
828	var console = window.console;
829
830	if (console && console.warn) {
831		return console.warn.apply(console, arguments);
832	}
833	else {
834		return FC.log.apply(FC, arguments);
835	}
836};
837
838
839/* General Utilities
840----------------------------------------------------------------------------------------------------------------------*/
841
842var hasOwnPropMethod = {}.hasOwnProperty;
843
844
845// Merges an array of objects into a single object.
846// The second argument allows for an array of property names who's object values will be merged together.
847function mergeProps(propObjs, complexProps) {
848	var dest = {};
849	var i, name;
850	var complexObjs;
851	var j, val;
852	var props;
853
854	if (complexProps) {
855		for (i = 0; i < complexProps.length; i++) {
856			name = complexProps[i];
857			complexObjs = [];
858
859			// collect the trailing object values, stopping when a non-object is discovered
860			for (j = propObjs.length - 1; j >= 0; j--) {
861				val = propObjs[j][name];
862
863				if (typeof val === 'object') {
864					complexObjs.unshift(val);
865				}
866				else if (val !== undefined) {
867					dest[name] = val; // if there were no objects, this value will be used
868					break;
869				}
870			}
871
872			// if the trailing values were objects, use the merged value
873			if (complexObjs.length) {
874				dest[name] = mergeProps(complexObjs);
875			}
876		}
877	}
878
879	// copy values into the destination, going from last to first
880	for (i = propObjs.length - 1; i >= 0; i--) {
881		props = propObjs[i];
882
883		for (name in props) {
884			if (!(name in dest)) { // if already assigned by previous props or complex props, don't reassign
885				dest[name] = props[name];
886			}
887		}
888	}
889
890	return dest;
891}
892
893
894// Create an object that has the given prototype. Just like Object.create
895function createObject(proto) {
896	var f = function() {};
897	f.prototype = proto;
898	return new f();
899}
900
901
902function copyOwnProps(src, dest) {
903	for (var name in src) {
904		if (hasOwnProp(src, name)) {
905			dest[name] = src[name];
906		}
907	}
908}
909
910
911// Copies over certain methods with the same names as Object.prototype methods. Overcomes an IE<=8 bug:
912// https://developer.mozilla.org/en-US/docs/ECMAScript_DontEnum_attribute#JScript_DontEnum_Bug
913function copyNativeMethods(src, dest) {
914	var names = [ 'constructor', 'toString', 'valueOf' ];
915	var i, name;
916
917	for (i = 0; i < names.length; i++) {
918		name = names[i];
919
920		if (src[name] !== Object.prototype[name]) {
921			dest[name] = src[name];
922		}
923	}
924}
925
926
927function hasOwnProp(obj, name) {
928	return hasOwnPropMethod.call(obj, name);
929}
930
931
932// Is the given value a non-object non-function value?
933function isAtomic(val) {
934	return /undefined|null|boolean|number|string/.test($.type(val));
935}
936
937
938function applyAll(functions, thisObj, args) {
939	if ($.isFunction(functions)) {
940		functions = [ functions ];
941	}
942	if (functions) {
943		var i;
944		var ret;
945		for (i=0; i<functions.length; i++) {
946			ret = functions[i].apply(thisObj, args) || ret;
947		}
948		return ret;
949	}
950}
951
952
953function firstDefined() {
954	for (var i=0; i<arguments.length; i++) {
955		if (arguments[i] !== undefined) {
956			return arguments[i];
957		}
958	}
959}
960
961
962function htmlEscape(s) {
963	return (s + '').replace(/&/g, '&amp;')
964		.replace(/</g, '&lt;')
965		.replace(/>/g, '&gt;')
966		.replace(/'/g, '&#039;')
967		.replace(/"/g, '&quot;')
968		.replace(/\n/g, '<br />');
969}
970
971
972function stripHtmlEntities(text) {
973	return text.replace(/&.*?;/g, '');
974}
975
976
977// Given a hash of CSS properties, returns a string of CSS.
978// Uses property names as-is (no camel-case conversion). Will not make statements for null/undefined values.
979function cssToStr(cssProps) {
980	var statements = [];
981
982	$.each(cssProps, function(name, val) {
983		if (val != null) {
984			statements.push(name + ':' + val);
985		}
986	});
987
988	return statements.join(';');
989}
990
991
992function capitaliseFirstLetter(str) {
993	return str.charAt(0).toUpperCase() + str.slice(1);
994}
995
996
997function compareNumbers(a, b) { // for .sort()
998	return a - b;
999}
1000
1001
1002function isInt(n) {
1003	return n % 1 === 0;
1004}
1005
1006
1007// Returns a method bound to the given object context.
1008// Just like one of the jQuery.proxy signatures, but without the undesired behavior of treating the same method with
1009// different contexts as identical when binding/unbinding events.
1010function proxy(obj, methodName) {
1011	var method = obj[methodName];
1012
1013	return function() {
1014		return method.apply(obj, arguments);
1015	};
1016}
1017
1018
1019// Returns a function, that, as long as it continues to be invoked, will not
1020// be triggered. The function will be called after it stops being called for
1021// N milliseconds. If `immediate` is passed, trigger the function on the
1022// leading edge, instead of the trailing.
1023// https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714
1024function debounce(func, wait, immediate) {
1025	var timeout, args, context, timestamp, result;
1026
1027	var later = function() {
1028		var last = +new Date() - timestamp;
1029		if (last < wait) {
1030			timeout = setTimeout(later, wait - last);
1031		}
1032		else {
1033			timeout = null;
1034			if (!immediate) {
1035				result = func.apply(context, args);
1036				context = args = null;
1037			}
1038		}
1039	};
1040
1041	return function() {
1042		context = this;
1043		args = arguments;
1044		timestamp = +new Date();
1045		var callNow = immediate && !timeout;
1046		if (!timeout) {
1047			timeout = setTimeout(later, wait);
1048		}
1049		if (callNow) {
1050			result = func.apply(context, args);
1051			context = args = null;
1052		}
1053		return result;
1054	};
1055}
1056
1057
1058// HACK around jQuery's now A+ promises: execute callback synchronously if already resolved.
1059// thenFunc shouldn't accept args.
1060// similar to whenResources in Scheduler plugin.
1061function syncThen(promise, thenFunc) {
1062	// not a promise, or an already-resolved promise?
1063	if (!promise || !promise.then || promise.state() === 'resolved') {
1064		return $.when(thenFunc()); // resolve immediately
1065	}
1066	else if (thenFunc) {
1067		return promise.then(thenFunc);
1068	}
1069}
1070
1071;;
1072
1073var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/;
1074var ambigTimeOrZoneRegex =
1075	/^\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+)?)?)?)?)?$/;
1076var newMomentProto = moment.fn; // where we will attach our new methods
1077var oldMomentProto = $.extend({}, newMomentProto); // copy of original moment methods
1078var allowValueOptimization;
1079var setUTCValues; // function defined below
1080var setLocalValues; // function defined below
1081
1082
1083// Creating
1084// -------------------------------------------------------------------------------------------------
1085
1086// Creates a new moment, similar to the vanilla moment(...) constructor, but with
1087// extra features (ambiguous time, enhanced formatting). When given an existing moment,
1088// it will function as a clone (and retain the zone of the moment). Anything else will
1089// result in a moment in the local zone.
1090FC.moment = function() {
1091	return makeMoment(arguments);
1092};
1093
1094// Sames as FC.moment, but forces the resulting moment to be in the UTC timezone.
1095FC.moment.utc = function() {
1096	var mom = makeMoment(arguments, true);
1097
1098	// Force it into UTC because makeMoment doesn't guarantee it
1099	// (if given a pre-existing moment for example)
1100	if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone
1101		mom.utc();
1102	}
1103
1104	return mom;
1105};
1106
1107// Same as FC.moment, but when given an ISO8601 string, the timezone offset is preserved.
1108// ISO8601 strings with no timezone offset will become ambiguously zoned.
1109FC.moment.parseZone = function() {
1110	return makeMoment(arguments, true, true);
1111};
1112
1113// Builds an enhanced moment from args. When given an existing moment, it clones. When given a
1114// native Date, or called with no arguments (the current time), the resulting moment will be local.
1115// Anything else needs to be "parsed" (a string or an array), and will be affected by:
1116//    parseAsUTC - if there is no zone information, should we parse the input in UTC?
1117//    parseZone - if there is zone information, should we force the zone of the moment?
1118function makeMoment(args, parseAsUTC, parseZone) {
1119	var input = args[0];
1120	var isSingleString = args.length == 1 && typeof input === 'string';
1121	var isAmbigTime;
1122	var isAmbigZone;
1123	var ambigMatch;
1124	var mom;
1125
1126	if (moment.isMoment(input)) {
1127		mom = moment.apply(null, args); // clone it
1128		transferAmbigs(input, mom); // the ambig flags weren't transfered with the clone
1129	}
1130	else if (isNativeDate(input) || input === undefined) {
1131		mom = moment.apply(null, args); // will be local
1132	}
1133	else { // "parsing" is required
1134		isAmbigTime = false;
1135		isAmbigZone = false;
1136
1137		if (isSingleString) {
1138			if (ambigDateOfMonthRegex.test(input)) {
1139				// accept strings like '2014-05', but convert to the first of the month
1140				input += '-01';
1141				args = [ input ]; // for when we pass it on to moment's constructor
1142				isAmbigTime = true;
1143				isAmbigZone = true;
1144			}
1145			else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) {
1146				isAmbigTime = !ambigMatch[5]; // no time part?
1147				isAmbigZone = true;
1148			}
1149		}
1150		else if ($.isArray(input)) {
1151			// arrays have no timezone information, so assume ambiguous zone
1152			isAmbigZone = true;
1153		}
1154		// otherwise, probably a string with a format
1155
1156		if (parseAsUTC || isAmbigTime) {
1157			mom = moment.utc.apply(moment, args);
1158		}
1159		else {
1160			mom = moment.apply(null, args);
1161		}
1162
1163		if (isAmbigTime) {
1164			mom._ambigTime = true;
1165			mom._ambigZone = true; // ambiguous time always means ambiguous zone
1166		}
1167		else if (parseZone) { // let's record the inputted zone somehow
1168			if (isAmbigZone) {
1169				mom._ambigZone = true;
1170			}
1171			else if (isSingleString) {
1172				if (mom.utcOffset) {
1173					mom.utcOffset(input); // if not a valid zone, will assign UTC
1174				}
1175				else {
1176					mom.zone(input); // for moment-pre-2.9
1177				}
1178			}
1179		}
1180	}
1181
1182	mom._fullCalendar = true; // flag for extended functionality
1183
1184	return mom;
1185}
1186
1187
1188// A clone method that works with the flags related to our enhanced functionality.
1189// In the future, use moment.momentProperties
1190newMomentProto.clone = function() {
1191	var mom = oldMomentProto.clone.apply(this, arguments);
1192
1193	// these flags weren't transfered with the clone
1194	transferAmbigs(this, mom);
1195	if (this._fullCalendar) {
1196		mom._fullCalendar = true;
1197	}
1198
1199	return mom;
1200};
1201
1202
1203// Week Number
1204// -------------------------------------------------------------------------------------------------
1205
1206
1207// Returns the week number, considering the locale's custom week number calcuation
1208// `weeks` is an alias for `week`
1209newMomentProto.week = newMomentProto.weeks = function(input) {
1210	var weekCalc = (this._locale || this._lang) // works pre-moment-2.8
1211		._fullCalendar_weekCalc;
1212
1213	if (input == null && typeof weekCalc === 'function') { // custom function only works for getter
1214		return weekCalc(this);
1215	}
1216	else if (weekCalc === 'ISO') {
1217		return oldMomentProto.isoWeek.apply(this, arguments); // ISO getter/setter
1218	}
1219
1220	return oldMomentProto.week.apply(this, arguments); // local getter/setter
1221};
1222
1223
1224// Time-of-day
1225// -------------------------------------------------------------------------------------------------
1226
1227// GETTER
1228// Returns a Duration with the hours/minutes/seconds/ms values of the moment.
1229// If the moment has an ambiguous time, a duration of 00:00 will be returned.
1230//
1231// SETTER
1232// You can supply a Duration, a Moment, or a Duration-like argument.
1233// When setting the time, and the moment has an ambiguous time, it then becomes unambiguous.
1234newMomentProto.time = function(time) {
1235
1236	// Fallback to the original method (if there is one) if this moment wasn't created via FullCalendar.
1237	// `time` is a generic enough method name where this precaution is necessary to avoid collisions w/ other plugins.
1238	if (!this._fullCalendar) {
1239		return oldMomentProto.time.apply(this, arguments);
1240	}
1241
1242	if (time == null) { // getter
1243		return moment.duration({
1244			hours: this.hours(),
1245			minutes: this.minutes(),
1246			seconds: this.seconds(),
1247			milliseconds: this.milliseconds()
1248		});
1249	}
1250	else { // setter
1251
1252		this._ambigTime = false; // mark that the moment now has a time
1253
1254		if (!moment.isDuration(time) && !moment.isMoment(time)) {
1255			time = moment.duration(time);
1256		}
1257
1258		// The day value should cause overflow (so 24 hours becomes 00:00:00 of next day).
1259		// Only for Duration times, not Moment times.
1260		var dayHours = 0;
1261		if (moment.isDuration(time)) {
1262			dayHours = Math.floor(time.asDays()) * 24;
1263		}
1264
1265		// We need to set the individual fields.
1266		// Can't use startOf('day') then add duration. In case of DST at start of day.
1267		return this.hours(dayHours + time.hours())
1268			.minutes(time.minutes())
1269			.seconds(time.seconds())
1270			.milliseconds(time.milliseconds());
1271	}
1272};
1273
1274// Converts the moment to UTC, stripping out its time-of-day and timezone offset,
1275// but preserving its YMD. A moment with a stripped time will display no time
1276// nor timezone offset when .format() is called.
1277newMomentProto.stripTime = function() {
1278	var a;
1279
1280	if (!this._ambigTime) {
1281
1282		// get the values before any conversion happens
1283		a = this.toArray(); // array of y/m/d/h/m/s/ms
1284
1285		// TODO: use keepLocalTime in the future
1286		this.utc(); // set the internal UTC flag (will clear the ambig flags)
1287		setUTCValues(this, a.slice(0, 3)); // set the year/month/date. time will be zero
1288
1289		// Mark the time as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(),
1290		// which clears all ambig flags. Same with setUTCValues with moment-timezone.
1291		this._ambigTime = true;
1292		this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset
1293	}
1294
1295	return this; // for chaining
1296};
1297
1298// Returns if the moment has a non-ambiguous time (boolean)
1299newMomentProto.hasTime = function() {
1300	return !this._ambigTime;
1301};
1302
1303
1304// Timezone
1305// -------------------------------------------------------------------------------------------------
1306
1307// Converts the moment to UTC, stripping out its timezone offset, but preserving its
1308// YMD and time-of-day. A moment with a stripped timezone offset will display no
1309// timezone offset when .format() is called.
1310// TODO: look into Moment's keepLocalTime functionality
1311newMomentProto.stripZone = function() {
1312	var a, wasAmbigTime;
1313
1314	if (!this._ambigZone) {
1315
1316		// get the values before any conversion happens
1317		a = this.toArray(); // array of y/m/d/h/m/s/ms
1318		wasAmbigTime = this._ambigTime;
1319
1320		this.utc(); // set the internal UTC flag (might clear the ambig flags, depending on Moment internals)
1321		setUTCValues(this, a); // will set the year/month/date/hours/minutes/seconds/ms
1322
1323		// the above call to .utc()/.utcOffset() unfortunately might clear the ambig flags, so restore
1324		this._ambigTime = wasAmbigTime || false;
1325
1326		// Mark the zone as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(),
1327		// which clears the ambig flags. Same with setUTCValues with moment-timezone.
1328		this._ambigZone = true;
1329	}
1330
1331	return this; // for chaining
1332};
1333
1334// Returns of the moment has a non-ambiguous timezone offset (boolean)
1335newMomentProto.hasZone = function() {
1336	return !this._ambigZone;
1337};
1338
1339
1340// this method implicitly marks a zone
1341newMomentProto.local = function() {
1342	var a = this.toArray(); // year,month,date,hours,minutes,seconds,ms as an array
1343	var wasAmbigZone = this._ambigZone;
1344
1345	oldMomentProto.local.apply(this, arguments);
1346
1347	// ensure non-ambiguous
1348	// this probably already happened via local() -> utcOffset(), but don't rely on Moment's internals
1349	this._ambigTime = false;
1350	this._ambigZone = false;
1351
1352	if (wasAmbigZone) {
1353		// If the moment was ambiguously zoned, the date fields were stored as UTC.
1354		// We want to preserve these, but in local time.
1355		// TODO: look into Moment's keepLocalTime functionality
1356		setLocalValues(this, a);
1357	}
1358
1359	return this; // for chaining
1360};
1361
1362
1363// implicitly marks a zone
1364newMomentProto.utc = function() {
1365	oldMomentProto.utc.apply(this, arguments);
1366
1367	// ensure non-ambiguous
1368	// this probably already happened via utc() -> utcOffset(), but don't rely on Moment's internals
1369	this._ambigTime = false;
1370	this._ambigZone = false;
1371
1372	return this;
1373};
1374
1375
1376// methods for arbitrarily manipulating timezone offset.
1377// should clear time/zone ambiguity when called.
1378$.each([
1379	'zone', // only in moment-pre-2.9. deprecated afterwards
1380	'utcOffset'
1381], function(i, name) {
1382	if (oldMomentProto[name]) { // original method exists?
1383
1384		// this method implicitly marks a zone (will probably get called upon .utc() and .local())
1385		newMomentProto[name] = function(tzo) {
1386
1387			if (tzo != null) { // setter
1388				// these assignments needs to happen before the original zone method is called.
1389				// I forget why, something to do with a browser crash.
1390				this._ambigTime = false;
1391				this._ambigZone = false;
1392			}
1393
1394			return oldMomentProto[name].apply(this, arguments);
1395		};
1396	}
1397});
1398
1399
1400// Formatting
1401// -------------------------------------------------------------------------------------------------
1402
1403newMomentProto.format = function() {
1404	if (this._fullCalendar && arguments[0]) { // an enhanced moment? and a format string provided?
1405		return formatDate(this, arguments[0]); // our extended formatting
1406	}
1407	if (this._ambigTime) {
1408		return oldMomentFormat(this, 'YYYY-MM-DD');
1409	}
1410	if (this._ambigZone) {
1411		return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
1412	}
1413	return oldMomentProto.format.apply(this, arguments);
1414};
1415
1416newMomentProto.toISOString = function() {
1417	if (this._ambigTime) {
1418		return oldMomentFormat(this, 'YYYY-MM-DD');
1419	}
1420	if (this._ambigZone) {
1421		return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
1422	}
1423	return oldMomentProto.toISOString.apply(this, arguments);
1424};
1425
1426
1427// Querying
1428// -------------------------------------------------------------------------------------------------
1429
1430// Is the moment within the specified range? `end` is exclusive.
1431// FYI, this method is not a standard Moment method, so always do our enhanced logic.
1432newMomentProto.isWithin = function(start, end) {
1433	var a = commonlyAmbiguate([ this, start, end ]);
1434	return a[0] >= a[1] && a[0] < a[2];
1435};
1436
1437// When isSame is called with units, timezone ambiguity is normalized before the comparison happens.
1438// If no units specified, the two moments must be identically the same, with matching ambig flags.
1439newMomentProto.isSame = function(input, units) {
1440	var a;
1441
1442	// only do custom logic if this is an enhanced moment
1443	if (!this._fullCalendar) {
1444		return oldMomentProto.isSame.apply(this, arguments);
1445	}
1446
1447	if (units) {
1448		a = commonlyAmbiguate([ this, input ], true); // normalize timezones but don't erase times
1449		return oldMomentProto.isSame.call(a[0], a[1], units);
1450	}
1451	else {
1452		input = FC.moment.parseZone(input); // normalize input
1453		return oldMomentProto.isSame.call(this, input) &&
1454			Boolean(this._ambigTime) === Boolean(input._ambigTime) &&
1455			Boolean(this._ambigZone) === Boolean(input._ambigZone);
1456	}
1457};
1458
1459// Make these query methods work with ambiguous moments
1460$.each([
1461	'isBefore',
1462	'isAfter'
1463], function(i, methodName) {
1464	newMomentProto[methodName] = function(input, units) {
1465		var a;
1466
1467		// only do custom logic if this is an enhanced moment
1468		if (!this._fullCalendar) {
1469			return oldMomentProto[methodName].apply(this, arguments);
1470		}
1471
1472		a = commonlyAmbiguate([ this, input ]);
1473		return oldMomentProto[methodName].call(a[0], a[1], units);
1474	};
1475});
1476
1477
1478// Misc Internals
1479// -------------------------------------------------------------------------------------------------
1480
1481// given an array of moment-like inputs, return a parallel array w/ moments similarly ambiguated.
1482// for example, of one moment has ambig time, but not others, all moments will have their time stripped.
1483// set `preserveTime` to `true` to keep times, but only normalize zone ambiguity.
1484// returns the original moments if no modifications are necessary.
1485function commonlyAmbiguate(inputs, preserveTime) {
1486	var anyAmbigTime = false;
1487	var anyAmbigZone = false;
1488	var len = inputs.length;
1489	var moms = [];
1490	var i, mom;
1491
1492	// parse inputs into real moments and query their ambig flags
1493	for (i = 0; i < len; i++) {
1494		mom = inputs[i];
1495		if (!moment.isMoment(mom)) {
1496			mom = FC.moment.parseZone(mom);
1497		}
1498		anyAmbigTime = anyAmbigTime || mom._ambigTime;
1499		anyAmbigZone = anyAmbigZone || mom._ambigZone;
1500		moms.push(mom);
1501	}
1502
1503	// strip each moment down to lowest common ambiguity
1504	// use clones to avoid modifying the original moments
1505	for (i = 0; i < len; i++) {
1506		mom = moms[i];
1507		if (!preserveTime && anyAmbigTime && !mom._ambigTime) {
1508			moms[i] = mom.clone().stripTime();
1509		}
1510		else if (anyAmbigZone && !mom._ambigZone) {
1511			moms[i] = mom.clone().stripZone();
1512		}
1513	}
1514
1515	return moms;
1516}
1517
1518// Transfers all the flags related to ambiguous time/zone from the `src` moment to the `dest` moment
1519// TODO: look into moment.momentProperties for this.
1520function transferAmbigs(src, dest) {
1521	if (src._ambigTime) {
1522		dest._ambigTime = true;
1523	}
1524	else if (dest._ambigTime) {
1525		dest._ambigTime = false;
1526	}
1527
1528	if (src._ambigZone) {
1529		dest._ambigZone = true;
1530	}
1531	else if (dest._ambigZone) {
1532		dest._ambigZone = false;
1533	}
1534}
1535
1536
1537// Sets the year/month/date/etc values of the moment from the given array.
1538// Inefficient because it calls each individual setter.
1539function setMomentValues(mom, a) {
1540	mom.year(a[0] || 0)
1541		.month(a[1] || 0)
1542		.date(a[2] || 0)
1543		.hours(a[3] || 0)
1544		.minutes(a[4] || 0)
1545		.seconds(a[5] || 0)
1546		.milliseconds(a[6] || 0);
1547}
1548
1549// Can we set the moment's internal date directly?
1550allowValueOptimization = '_d' in moment() && 'updateOffset' in moment;
1551
1552// Utility function. Accepts a moment and an array of the UTC year/month/date/etc values to set.
1553// Assumes the given moment is already in UTC mode.
1554setUTCValues = allowValueOptimization ? function(mom, a) {
1555	// simlate what moment's accessors do
1556	mom._d.setTime(Date.UTC.apply(Date, a));
1557	moment.updateOffset(mom, false); // keepTime=false
1558} : setMomentValues;
1559
1560// Utility function. Accepts a moment and an array of the local year/month/date/etc values to set.
1561// Assumes the given moment is already in local mode.
1562setLocalValues = allowValueOptimization ? function(mom, a) {
1563	// simlate what moment's accessors do
1564	mom._d.setTime(+new Date( // FYI, there is now way to apply an array of args to a constructor
1565		a[0] || 0,
1566		a[1] || 0,
1567		a[2] || 0,
1568		a[3] || 0,
1569		a[4] || 0,
1570		a[5] || 0,
1571		a[6] || 0
1572	));
1573	moment.updateOffset(mom, false); // keepTime=false
1574} : setMomentValues;
1575
1576;;
1577
1578// Single Date Formatting
1579// -------------------------------------------------------------------------------------------------
1580
1581
1582// call this if you want Moment's original format method to be used
1583function oldMomentFormat(mom, formatStr) {
1584	return oldMomentProto.format.call(mom, formatStr); // oldMomentProto defined in moment-ext.js
1585}
1586
1587
1588// Formats `date` with a Moment formatting string, but allow our non-zero areas and
1589// additional token.
1590function formatDate(date, formatStr) {
1591	return formatDateWithChunks(date, getFormatStringChunks(formatStr));
1592}
1593
1594
1595function formatDateWithChunks(date, chunks) {
1596	var s = '';
1597	var i;
1598
1599	for (i=0; i<chunks.length; i++) {
1600		s += formatDateWithChunk(date, chunks[i]);
1601	}
1602
1603	return s;
1604}
1605
1606
1607// addition formatting tokens we want recognized
1608var tokenOverrides = {
1609	t: function(date) { // "a" or "p"
1610		return oldMomentFormat(date, 'a').charAt(0);
1611	},
1612	T: function(date) { // "A" or "P"
1613		return oldMomentFormat(date, 'A').charAt(0);
1614	}
1615};
1616
1617
1618function formatDateWithChunk(date, chunk) {
1619	var token;
1620	var maybeStr;
1621
1622	if (typeof chunk === 'string') { // a literal string
1623		return chunk;
1624	}
1625	else if ((token = chunk.token)) { // a token, like "YYYY"
1626		if (tokenOverrides[token]) {
1627			return tokenOverrides[token](date); // use our custom token
1628		}
1629		return oldMomentFormat(date, token);
1630	}
1631	else if (chunk.maybe) { // a grouping of other chunks that must be non-zero
1632		maybeStr = formatDateWithChunks(date, chunk.maybe);
1633		if (maybeStr.match(/[1-9]/)) {
1634			return maybeStr;
1635		}
1636	}
1637
1638	return '';
1639}
1640
1641
1642// Date Range Formatting
1643// -------------------------------------------------------------------------------------------------
1644// TODO: make it work with timezone offset
1645
1646// Using a formatting string meant for a single date, generate a range str
1646ing, like
1647// "Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ.
1648// If the dates are the same as far as the format string is concerned, just return a single
1649// rendering of one date, without any separator.
1650function formatRange(date1, date2, formatStr, separator, isRTL) {
1651	var localeData;
1652
1653	date1 = FC.moment.parseZone(date1);
1654	date2 = FC.moment.parseZone(date2);
1655
1656	localeData = (date1.localeData || date1.lang).call(date1); // works with moment-pre-2.8
1657
1658	// Expand localized format strings, like "LL" -> "MMMM D YYYY"
1659	formatStr = localeData.longDateFormat(formatStr) || formatStr;
1660	// BTW, this is not important for `formatDate` because it is impossible to put custom tokens
1661	// or non-zero areas in Moment's localized format strings.
1662
1663	separator = separator || ' - ';
1664
1665	return formatRangeWithChunks(
1666		date1,
1667		date2,
1668		getFormatStringChunks(formatStr),
1669		separator,
1670		isRTL
1671	);
1672}
1673FC.formatRange = formatRange; // expose
1674
1675
1676function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) {
1677	var unzonedDate1 = date1.clone().stripZone(); // for formatSimilarChunk
1678	var unzonedDate2 = date2.clone().stripZone(); // "
1679	var chunkStr; // the rendering of the chunk
1680	var leftI;
1681	var leftStr = '';
1682	var rightI;
1683	var rightStr = '';
1684	var middleI;
1685	var middleStr1 = '';
1686	var middleStr2 = '';
1687	var middleStr = '';
1688
1689	// Start at the leftmost side of the formatting string and continue until you hit a token
1690	// that is not the same between dates.
1691	for (leftI=0; leftI<chunks.length; leftI++) {
1692		chunkStr = formatSimilarChunk(date1, date2, unzonedDate1, unzonedDate2, chunks[leftI]);
1693		if (chunkStr === false) {
1694			break;
1695		}
1696		leftStr += chunkStr;
1697	}
1698
1699	// Similarly, start at the rightmost side of the formatting string and move left
1700	for (rightI=chunks.length-1; rightI>leftI; rightI--) {
1701		chunkStr = formatSimilarChunk(date1, date2, unzonedDate1, unzonedDate2,  chunks[rightI]);
1702		if (chunkStr === false) {
1703			break;
1704		}
1705		rightStr = chunkStr + rightStr;
1706	}
1707
1708	// The area in the middle is different for both of the dates.
1709	// Collect them distinctly so we can jam them together later.
1710	for (middleI=leftI; middleI<=rightI; middleI++) {
1711		middleStr1 += formatDateWithChunk(date1, chunks[middleI]);
1712		middleStr2 += formatDateWithChunk(date2, chunks[middleI]);
1713	}
1714
1715	if (middleStr1 || middleStr2) {
1716		if (isRTL) {
1717			middleStr = middleStr2 + separator + middleStr1;
1718		}
1719		else {
1720			middleStr = middleStr1 + separator + middleStr2;
1721		}
1722	}
1723
1724	return leftStr + middleStr + rightStr;
1725}
1726
1727
1728var similarUnitMap = {
1729	Y: 'year',
1730	M: 'month',
1731	D: 'day', // day of month
1732	d: 'day', // day of week
1733	// prevents a separator between anything time-related...
1734	A: 'second', // AM/PM
1735	a: 'second', // am/pm
1736	T: 'second', // A/P
1737	t: 'second', // a/p
1738	H: 'second', // hour (24)
1739	h: 'second', // hour (12)
1740	m: 'second', // minute
1741	s: 'second' // second
1742};
1743// TODO: week maybe?
1744
1745
1746// Given a formatting chunk, and given that both dates are similar in the regard the
1747// formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`.
1748function formatSimilarChunk(date1, date2, unzonedDate1, unzonedDate2, chunk) {
1749	var token;
1750	var unit;
1751
1752	if (typeof chunk === 'string') { // a literal string
1753		return chunk;
1754	}
1755	else if ((token = chunk.token)) {
1756		unit = similarUnitMap[token.charAt(0)];
1757
1758		// are the dates the same for this unit of measurement?
1759		// use the unzoned dates for this calculation because unreliable when near DST (bug #2396)
1760		if (unit && unzonedDate1.isSame(unzonedDate2, unit)) {
1761			return oldMomentFormat(date1, token); // would be the same if we used `date2`
1762			// BTW, don't support custom tokens
1763		}
1764	}
1765
1766	return false; // the chunk is NOT the same for the two dates
1767	// BTW, don't support splitting on non-zero areas
1768}
1769
1770
1771// Chunking Utils
1772// -------------------------------------------------------------------------------------------------
1773
1774
1775var formatStringChunkCache = {};
1776
1777
1778function getFormatStringChunks(formatStr) {
1779	if (formatStr in formatStringChunkCache) {
1780		return formatStringChunkCache[formatStr];
1781	}
1782	return (formatStringChunkCache[formatStr] = chunkFormatString(formatStr));
1783}
1784
1785
1786// Break the formatting string into an array of chunks
1787function chunkFormatString(formatStr) {
1788	var chunks = [];
1789	var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g; // TODO: more descrimination
1790	var match;
1791
1792	while ((match = chunker.exec(formatStr))) {
1793		if (match[1]) { // a literal string inside [ ... ]
1794			chunks.push(match[1]);
1795		}
1796		else if (match[2]) { // non-zero formatting inside ( ... )
1797			chunks.push({ maybe: chunkFormatString(match[2]) });
1798		}
1799		else if (match[3]) { // a formatting token
1800			chunks.push({ token: match[3] });
1801		}
1802		else if (match[5]) { // an unenclosed literal string
1803			chunks.push(match[5]);
1804		}
1805	}
1806
1807	return chunks;
1808}
1809
1810;;
1811
1812FC.Class = Class; // export
1813
1814// Class that all other classes will inherit from
1815function Class() { }
1816
1817
1818// Called on a class to create a subclass.
1819// Last argument contains instance methods. Any argument before the last are considered mixins.
1820Class.extend = function() {
1821	var len = arguments.length;
1822	var i;
1823	var members;
1824
1825	for (i = 0; i < len; i++) {
1826		members = arguments[i];
1827		if (i < len - 1) { // not the last argument?
1828			mixIntoClass(this, members);
1829		}
1830	}
1831
1832	return extendClass(this, members || {}); // members will be undefined if no arguments
1833};
1834
1835
1836// Adds new member variables/methods to the class's prototype.
1837// Can be called with another class, or a plain object hash containing new members.
1838Class.mixin = function(members) {
1839	mixIntoClass(this, members);
1840};
1841
1842
1843function extendClass(superClass, members) {
1844	var subClass;
1845
1846	// ensure a constructor for the subclass, forwarding all arguments to the super-constructor if it doesn't exist
1847	if (hasOwnProp(members, 'constructor')) {
1848		subClass = members.constructor;
1849	}
1850	if (typeof subClass !== 'function') {
1851		subClass = members.constructor = function() {
1852			superClass.apply(this, arguments);
1853		};
1854	}
1855
1856	// build the base prototype for the subclass, which is an new object chained to the superclass's prototype
1857	subClass.prototype = createObject(superClass.prototype);
1858
1859	// copy each member variable/method onto the the subclass's prototype
1860	copyOwnProps(members, subClass.prototype);
1861	copyNativeMethods(members, subClass.prototype); // hack for IE8
1862
1863	// copy over all class variables/methods to the subclass, such as `extend` and `mixin`
1864	copyOwnProps(superClass, subClass);
1865
1866	return subClass;
1867}
1868
1869
1870function mixIntoClass(theClass, members) {
1871	copyOwnProps(members, theClass.prototype); // TODO: copyNativeMethods?
1872}
1873;;
1874
1875var EmitterMixin = FC.EmitterMixin = {
1876
1877	// jQuery-ification via $(this) allows a non-DOM object to have
1878	// the same event handling capabilities (including namespaces).
1879
1880
1881	on: function(types, handler) {
1882
1883		// handlers are always called with an "event" object as their first param.
1884		// sneak the `this` context and arguments into the extra parameter object
1885		// and forward them on to the original handler.
1886		var intercept = function(ev, extra) {
1887			return handler.apply(
1888				extra.context || this,
1889				extra.args || []
1890			);
1891		};
1892
1893		// mimick jQuery's internal "proxy" system (risky, I know)
1894		// causing all functions with the same .guid to appear to be the same.
1895		// https://github.com/jquery/jquery/blob/2.2.4/src/core.js#L448
1896		// this is needed for calling .off with the original non-intercept handler.
1897		if (!handler.guid) {
1898			handler.guid = $.guid++;
1899		}
1900		intercept.guid = handler.guid;
1901
1902		$(this).on(types, intercept);
1903
1904		return this; // for chaining
1905	},
1906
1907
1908	off: function(types, handler) {
1909		$(this).off(types, handler);
1910
1911		return this; // for chaining
1912	},
1913
1914
1915	trigger: function(types) {
1916		var args = Array.prototype.slice.call(arguments, 1); // arguments after the first
1917
1918		// pass in "extra" info to the intercept
1919		$(this).triggerHandler(types, { args: args });
1920
1921		return this; // for chaining
1922	},
1923
1924
1925	triggerWith: function(types, context, args) {
1926
1927		// `triggerHandler` is less reliant on the DOM compared to `trigger`.
1928		// pass in "extra" info to the intercept.
1929		$(this).triggerHandler(types, { context: context, args: args });
1930
1931		return this; // for chaining
1932	}
1933
1934};
1935
1936;;
1937
1938/*
1939Utility methods for easily listening to events on another object,
1940and more importantly, easily unlistening from them.
1941*/
1942var ListenerMixin = FC.ListenerMixin = (function() {
1943	var guid = 0;
1944	var ListenerMixin = {
1945
1946		listenerId: null,
1947
1948		/*
1949		Given an `other` object that has on/off methods, bind the given `callback` to an event by the given name.
1950		The `callback` will be called with the `this` context of the object that .listenTo is being called on.
1951		Can be called:
1952			.listenTo(other, eventName, callback)
1953		OR
1954			.listenTo(other, {
1955				eventName1: callback1,
1956				eventName2: callback2
1957			})
1958		*/
1959		listenTo: function(other, arg, callback) {
1960			if (typeof arg === 'object') { // given dictionary of callbacks
1961				for (var eventName in arg) {
1962					if (arg.hasOwnProperty(eventName)) {
1963						this.listenTo(other, eventName, arg[eventName]);
1964					}
1965				}
1966			}
1967			else if (typeof arg === 'string') {
1968				other.on(
1969					arg + '.' + this.getListenerNamespace(), // use event namespacing to identify this object
1970					$.proxy(callback, this) // always use `this` context
1971						// the usually-undesired jQuery guid behavior doesn't matter,
1972						// because we always unbind via namespace
1973				);
1974			}
1975		},
1976
1977		/*
1978		Causes the current object to stop listening to events on the `other` object.
1979		`eventName` is optional. If omitted, will stop listening to ALL events on `other`.
1980		*/
1981		stopListeningTo: function(other, eventName) {
1982			other.off((eventName || '') + '.' + this.getListenerNamespace());
1983		},
1984
1985		/*
1986		Returns a string, unique to this object, to be used for event namespacing
1987		*/
1988		getListenerNamespace: function() {
1989			if (this.listenerId == null) {
1990				this.listenerId = guid++;
1991			}
1992			return '_listener' + this.listenerId;
1993		}
1994
1995	};
1996	return ListenerMixin;
1997})();
1998;;
1999
2000// simple class for toggle a `isIgnoringMouse` flag on delay
2001// initMouseIgnoring must first be called, with a millisecond delay setting.
2002var MouseIgnorerMixin = {
2003
2004	isIgnoringMouse: false, // bool
2005	delayUnignoreMouse: null, // method
2006
2007
2008	initMouseIgnoring: function(delay) {
2009		this.delayUnignoreMouse = debounce(proxy(this, 'unignoreMouse'), delay || 1000);
2010	},
2011
2012
2013	// temporarily ignore mouse actions on segments
2014	tempIgnoreMouse: function() {
2015		this.isIgnoringMouse = true;
2016		this.delayUnignoreMouse();
2017	},
2018
2019
2020	// delayUnignoreMouse eventually calls this
2021	unignoreMouse: function() {
2022		this.isIgnoringMouse = false;
2023	}
2024
2025};
2026
2027;;
2028
2029/* A rectangular panel that is absolutely positioned over other content
2030------------------------------------------------------------------------------------------------------------------------
2031Options:
2032	- className (string)
2033	- content (HTML string or jQuery element set)
2034	- parentEl
2035	- top
2036	- left
2037	- right (the x coord of where the right edge should be. not a "CSS" right)
2038	- autoHide (boolean)
2039	- show (callback)
2040	- hide (callback)
2041*/
2042
2043var Popover = Class.extend(ListenerMixin, {
2044
2045	isHidden: true,
2046	options: null,
2047	el: null, // the container element for the popover. generated by this object
2048	margin: 10, // the space required between the popover and the edges of the scroll container
2049
2050
2051	constructor: function(options) {
2052		this.options = options || {};
2053	},
2054
2055
2056	// Shows the popover on the specified position. Renders it if not already
2057	show: function() {
2058		if (this.isHidden) {
2059			if (!this.el) {
2060				this.render();
2061			}
2062			this.el.show();
2063			this.position();
2064			this.isHidden = false;
2065			this.trigger('show');
2066		}
2067	},
2068
2069
2070	// Hides the popover, through CSS, but does not remove it from the DOM
2071	hide: function() {
2072		if (!this.isHidden) {
2073			this.el.hide();
2074			this.isHidden = true;
2075			this.trigger('hide');
2076		}
2077	},
2078
2079
2080	// Creates `this.el` and renders content inside of it
2081	render: function() {
2082		var _this = this;
2083		var options = this.options;
2084
2085		this.el = $('<div class="fc-popover"/>')
2086			.addClass(options.className || '')
2087			.css({
2088				// position initially to the top left to avoid creating scrollbars
2089				top: 0,
2090				left: 0
2091			})
2092			.append(options.content)
2093			.appendTo(options.parentEl);
2094
2095		// when a click happens on anything inside with a 'fc-close' className, hide the popover
2096		this.el.on('click', '.fc-close', function() {
2097			_this.hide();
2098		});
2099
2100		if (options.autoHide) {
2101			this.listenTo($(document), 'mousedown', this.documentMousedown);
2102		}
2103	},
2104
2105
2106	// Triggered when the user clicks *anywhere* in the document, for the autoHide feature
2107	documentMousedown: function(ev) {
2108		// only hide the popover if the click happened outside the popover
2109		if (this.el && !$(ev.target).closest(this.el).length) {
2110			this.hide();
2111		}
2112	},
2113
2114
2115	// Hides and unregisters any handlers
2116	removeElement: function() {
2117		this.hide();
2118
2119		if (this.el) {
2120			this.el.remove();
2121			this.el = null;
2122		}
2123
2124		this.stopListeningTo($(document), 'mousedown');
2125	},
2126
2127
2128	// Positions the popover optimally, using the top/left/right options
2129	position: function() {
2130		var options = this.options;
2131		var origin = this.el.offsetParent().offset();
2132		var width = this.el.outerWidth();
2133		var height = this.el.outerHeight();
2134		var windowEl = $(window);
2135		var viewportEl = getScrollParent(this.el);
2136		var viewportTop;
2137		var viewportLeft;
2138		var viewportOffset;
2139		var top; // the "position" (not "offset") values for the popover
2140		var left; //
2141
2142		// compute top and left
2143		top = options.top || 0;
2144		if (options.left !== undefined) {
2145			left = options.left;
2146		}
2147		else if (options.right !== undefined) {
2148			left = options.right - width; // derive the left value from the right value
2149		}
2150		else {
2151			left = 0;
2152		}
2153
2154		if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result
2155			viewportEl = windowEl;
2156			viewportTop = 0; // the window is always at the top left
2157			viewportLeft = 0; // (and .offset() won't work if called here)
2158		}
2159		else {
2160			viewportOffset = viewportEl.offset();
2161			viewportTop = viewportOffset.top;
2162			viewportLeft = viewportOffset.left;
2163		}
2164
2165		// if the window is scrolled, it causes the visible area to be further down
2166		viewportTop += windowEl.scrollTop();
2167		viewportLeft += windowEl.scrollLeft();
2168
2169		// constrain to the view port. if constrained by two edges, give precedence to top/left
2170		if (options.viewportConstrain !== false) {
2171			top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin);
2172			top = Math.max(top, viewportTop + this.margin);
2173			left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin);
2174			left = Math.max(left, viewportLeft + this.margin);
2175		}
2176
2177		this.el.css({
2178			top: top - origin.top,
2179			left: left - origin.left
2180		});
2181	},
2182
2183
2184	// Triggers a callback. Calls a function in the option hash of the same name.
2185	// Arguments beyond the first `name` are forwarded on.
2186	// TODO: better code reuse for this. Repeat code
2187	trigger: function(name) {
2188		if (this.options[name]) {
2189			this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
2190		}
2191	}
2192
2193});
2194
2195;;
2196
2197/*
2198A cache for the left/right/top/bottom/width/height values for one or more elements.
2199Works with both offset (from topleft document) and position (from offsetParent).
2200
2201options:
2202- els
2203- isHorizontal
2204- isVertical
2205*/
2206var CoordCache = FC.CoordCache = Class.extend({
2207
2208	els: null, // jQuery set (assumed to be siblings)
2209	forcedOffsetParentEl: null, // options can override the natural offsetParent
2210	origin: null, // {left,top} position of offsetParent of els
2211	boundingRect: null, // constrain cordinates to this rectangle. {left,right,top,bottom} or null
2212	isHorizontal: false, // whether to query for left/right/width
2213	isVertical: false, // whether to query for top/bottom/height
2214
2215	// arrays of coordinates (offsets from topleft of document)
2216	lefts: null,
2217	rights: null,
2218	tops: null,
2219	bottoms: null,
2220
2221
2222	constructor: function(options) {
2223		this.els = $(options.els);
2224		this.isHorizontal = options.isHorizontal;
2225		this.isVertical = options.isVertical;
2226		this.forcedOffsetParentEl = options.offsetParent ? $(options.offsetParent) : null;
2227	},
2228
2229
2230	// Queries the els for coordinates and stores them.
2231	// Call this method before using and of the get* methods below.
2232	build: function() {
2233		var offsetParentEl = this.forcedOffsetParentEl || this.els.eq(0).offsetParent();
2234
2235		this.origin = offsetParentEl.offset();
2236		this.boundingRect = this.queryBoundingRect();
2237
2238		if (this.isHorizontal) {
2239			this.buildElHorizontals();
2240		}
2241		if (this.isVertical) {
2242			this.buildElVerticals();
2243		}
2244	},
2245
2246
2247	// Destroys all internal data about coordinates, freeing memory
2248	clear: function() {
2249		this.origin = null;
2250		this.boundingRect = null;
2251		this.lefts = null;
2252		this.rights = null;
2253		this.tops = null;
2254		this.bottoms = null;
2255	},
2256
2257
2258	// When called, if coord caches aren't built, builds them
2259	ensureBuilt: function() {
2260		if (!this.origin) {
2261			this.build();
2262		}
2263	},
2264
2265
2266	// Populates the left/right internal coordinate arrays
2267	buildElHorizontals: function() {
2268		var lefts = [];
2269		var rights = [];
2270
2271		this.els.each(function(i, node) {
2272			var el = $(node);
2273			var left = el.offset().left;
2274			var width = el.outerWidth();
2275
2276			lefts.push(left);
2277			rights.push(left + width);
2278		});
2279
2280		this.lefts = lefts;
2281		this.rights = rights;
2282	},
2283
2284
2285	// Populates the top/bottom internal coordinate arrays
2286	buildElVerticals: function() {
2287		var tops = [];
2288		var bottoms = [];
2289
2290		this.els.each(function(i, node) {
2291			var el = $(node);
2292			var top = el.offset().top;
2293			var height = el.outerHeight();
2294
2295			tops.push(top);
2296			bottoms.push(top + height);
2297		});
2298
2299		this.tops = tops;
2300		this.bottoms = bottoms;
2301	},
2302
2303
2304	// Given a left offset (from document left), returns the index of the el that it horizontally intersects.
2305	// If no intersection is made, returns undefined.
2306	getHorizontalIndex: function(leftOffset) {
2307		this.ensureBuilt();
2308
2309		var lefts = this.lefts;
2310		var rights = this.rights;
2311		var len = lefts.length;
2312		var i;
2313
2314		for (i = 0; i < len; i++) {
2315			if (leftOffset >= lefts[i] && leftOffset < rights[i]) {
2316				return i;
2317			}
2318		}
2319	},
2320
2321
2322	// Given a top offset (from document top), returns the index of the el that it vertically intersects.
2323	// If no intersection is made, returns undefined.
2324	getVerticalIndex: function(topOffset) {
2325		this.ensureBuilt();
2326
2327		var tops = this.tops;
2328		var bottoms = this.bottoms;
2329		var len = tops.length;
2330		var i;
2331
2332		for (i = 0; i < len; i++) {
2333			if (topOffset >= tops[i] && topOffset < bottoms[i]) {
2334				return i;
2335			}
2336		}
2337	},
2338
2339
2340	// Gets the left offset (from document left) of the element at the given index
2341	getLeftOffset: function(leftIndex) {
2342		this.ensureBuilt();
2343		return this.lefts[leftIndex];
2344	},
2345
2346
2347	// Gets the left position (from offsetParent left) of the element at the given index
2348	getLeftPosition: function(leftIndex) {
2349		this.ensureBuilt();
2350		return this.lefts[leftIndex] - this.origin.left;
2351	},
2352
2353
2354	// Gets the right offset (from document left) of the element at the given index.
2355	// This value is NOT relative to the document's right edge, like the CSS concept of "right" would be.
2356	getRightOffset: function(leftIndex) {
2357		this.ensureBuilt();
2358		return this.rights[leftIndex];
2359	},
2360
2361
2362	// Gets the right position (from offsetParent left) of the element at the given index.
2363	// This value is NOT relative to the offsetParent's right edge, like the CSS concept of "right" would be.
2364	getRightPosition: function(leftIndex) {
2365		this.ensureBuilt();
2366		return this.rights[leftIndex] - this.origin.left;
2367	},
2368
2369
2370	// Gets the width of the element at the given index
2371	getWidth: function(leftIndex) {
2372		this.ensureBuilt();
2373		return this.rights[leftIndex] - this.lefts[leftIndex];
2374	},
2375
2376
2377	// Gets the top offset (from document top) of the element at the given index
2378	getTopOffset: function(topIndex) {
2379		this.ensureBuilt();
2380		return this.tops[topIndex];
2381	},
2382
2383
2384	// Gets the top position (from offsetParent top) of the element at the given position
2385	getTopPosition: function(topIndex) {
2386		this.ensureBuilt();
2387		return this.tops[topIndex] - this.origin.top;
2388	},
2389
2390	// Gets the bottom offset (from the document top) of the element at the given index.
2391	// This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be.
2392	getBottomOffset: function(topIndex) {
2393		this.ensureBuilt();
2394		return this.bottoms[topIndex];
2395	},
2396
2397
2398	// Gets the bottom position (from the offsetParent top) of the element at the given index.
2399	// This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be.
2400	getBottomPosition: function(topIndex) {
2401		this.ensureBuilt();
2402		return this.bottoms[topIndex] - this.origin.top;
2403	},
2404
2405
2406	// Gets the height of the element at the given index
2407	getHeight: function(topIndex) {
2408		this.ensureBuilt();
2409		return this.bottoms[topIndex] - this.tops[topIndex];
2410	},
2411
2412
2413	// Bounding Rect
2414	// TODO: decouple this from CoordCache
2415
2416	// Compute and return what the elements' bounding rectangle is, from the user's perspective.
2417	// Right now, only returns a rectangle if constrained by an overflow:scroll element.
2418	queryBoundingRect: function() {
2419		var scrollParentEl = getScrollParent(this.els.eq(0));
2420
2421		if (!scrollParentEl.is(document)) {
2422			return getClientRect(scrollParentEl);
2423		}
2424	},
2425
2426	isPointInBounds: function(leftOffset, topOffset) {
2427		return this.isLeftInBounds(leftOffset) && this.isTopInBounds(topOffset);
2428	},
2429
2430	isLeftInBounds: function(leftOffset) {
2431		return !this.boundingRect || (leftOffset >= this.boundingRect.left && leftOffset < this.boundingRect.right);
2432	},
2433
2434	isTopInBounds: function(topOffset) {
2435		return !this.boundingRect || (topOffset >= this.boundingRect.top && topOffset < this.boundingRect.bottom);
2436	}
2437
2438});
2439
2440;;
2441
2442/* Tracks a drag's mouse movement, firing various handlers
2443----------------------------------------------------------------------------------------------------------------------*/
2444// TODO: use Emitter
2445
2446var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMixin, {
2447
2448	options: null,
2449
2450	// for IE8 bug-fighting behavior
2451	subjectEl: null,
2452	subjectHref: null,
2453
2454	// coordinates of the initial mousedown
2455	originX: null,
2456	originY: null,
2457
2458	// the wrapping element that scrolls, or MIGHT scroll if there's overflow.
2459	// TODO: do this for wrappers that have overflow:hidden as well.
2460	scrollEl: null,
2461
2462	isInteracting: false,
2463	isDistanceSurpassed: false,
2464	isDelayEnded: false,
2465	isDragging: false,
2466	isTouch: false,
2467
2468	delay: null,
2469	delayTimeoutId: null,
2470	minDistance: null,
2471
2472	handleTouchScrollProxy: null, // calls handleTouchScroll, always bound to `this`
2473
2474
2475	constructor: function(options) {
2476		this.options = options || {};
2477		this.handleTouchScrollProxy = proxy(this, 'handleTouchScroll');
2478		this.initMouseIgnoring(500);
2479	},
2480
2481
2482	// Interaction (high-level)
2483	// -----------------------------------------------------------------------------------------------------------------
2484
2485
2486	startInteraction: function(ev, extraOptions) {
2487		var isTouch = getEvIsTouch(ev);
2488
2489		if (ev.type === 'mousedown') {
2490			if (this.isIgnoringMouse) {
2491				return;
2492			}
2493			else if (!isPrimaryMouseButton(ev)) {
2494				return;
2495			}
2496			else {
2497				ev.preventDefault(); // prevents native selection in most browsers
2498			}
2499		}
2500
2501		if (!this.isInteracting) {
2502
2503			// process options
2504			extraOptions = extraOptions || {};
2505			this.delay = firstDefined(extraOptions.delay, this.options.delay, 0);
2506			this.minDistance = firstDefined(extraOptions.distance, this.options.distance, 0);
2507			this.subjectEl = this.options.subjectEl;
2508
2509			this.isInteracting = true;
2510			this.isTouch = isTouch;
2511			this.isDelayEnded = false;
2512			this.isDistanceSurpassed = false;
2513
2514			this.originX = getEvX(ev);
2515			this.originY = getEvY(ev);
2516			this.scrollEl = getScrollParent($(ev.target));
2517
2518			this.bindHandlers();
2519			this.initAutoScroll();
2520			this.handleInteractionStart(ev);
2521			this.startDelay(ev);
2522
2523			if (!this.minDistance) {
2524				this.handleDistanceSurpassed(ev);
2525			}
2526		}
2527	},
2528
2529
2530	handleInteractionStart: function(ev) {
2531		this.trigger('interactionStart', ev);
2532	},
2533
2534
2535	endInteraction: function(ev, isCancelled) {
2536		if (this.isInteracting) {
2537			this.endDrag(ev);
2538
2539			if (this.delayTimeoutId) {
2540				clearTimeout(this.delayTimeoutId);
2541				this.delayTimeoutId = null;
2542			}
2543
2544			this.destroyAutoScroll();
2545			this.unbindHandlers();
2546
2547			this.isInteracting = false;
2548			this.handleInteractionEnd(ev, isCancelled);
2549
2550			// a touchstart+touchend on the same element will result in the following addition simulated events:
2551			// mouseover + mouseout + click
2552			// let's ignore these bogus events
2553			if (this.isTouch) {
2554				this.tempIgnoreMouse();
2555			}
2556		}
2557	},
2558
2559
2560	handleInteractionEnd: function(ev, isCancelled) {
2561		this.trigger('interactionEnd', ev, isCancelled || false);
2562	},
2563
2564
2565	// Binding To DOM
2566	// -----------------------------------------------------------------------------------------------------------------
2567
2568
2569	bindHandlers: function() {
2570		var _this = this;
2571		var touchStartIgnores = 1;
2572
2573		if (this.isTouch) {
2574			this.listenTo($(document), {
2575				touchmove: this.handleTouchMove,
2576				touchend: this.endInteraction,
2577				touchcancel: this.endInteraction,
2578
2579				// Sometimes touchend doesn't fire
2580				// (can't figure out why. touchcancel doesn't fire either. has to do with scrolling?)
2581				// If another touchstart happens, we know it's bogus, so cancel the drag.
2582				// touchend will continue to be broken until user does a shorttap/scroll, but this is best we can do.
2583				touchstart: function(ev) {
2584					if (touchStartIgnores) { // bindHandlers is called from within a touchstart,
2585						touchStartIgnores--; // and we don't want this to fire immediately, so ignore.
2586					}
2587					else {
2588						_this.endInteraction(ev, true); // isCancelled=true
2589					}
2590				}
2591			});
2592
2593			// listen to ALL scroll actions on the page
2594			if (
2595				!bindAnyScroll(this.handleTouchScrollProxy) && // hopefully this works and short-circuits the rest
2596				this.scrollEl // otherwise, attach a single handler to this
2597			) {
2598				this.listenTo(this.scrollEl, 'scroll', this.handleTouchScroll);
2599			}
2600		}
2601		else {
2602			this.listenTo($(document), {
2603				mousemove: this.handleMouseMove,
2604				mouseup: this.endInteraction
2605			});
2606		}
2607
2608		this.listenTo($(document), {
2609			selectstart: preventDefault, // don't allow selection while dragging
2610			contextmenu: preventDefault // long taps would open menu on Chrome dev tools
2611		});
2612	},
2613
2614
2615	unbindHandlers: function() {
2616		this.stopListeningTo($(document));
2617
2618		// unbind scroll listening
2619		unbindAnyScroll(this.handleTouchScrollProxy);
2620		if (this.scrollEl) {
2621			this.stopListeningTo(this.scrollEl, 'scroll');
2622		}
2623	},
2624
2625
2626	// Drag (high-level)
2627	// -----------------------------------------------------------------------------------------------------------------
2628
2629
2630	// extraOptions ignored if drag already started
2631	startDrag: function(ev, extraOptions) {
2632		this.startInteraction(ev, extraOptions); // ensure interaction began
2633
2634		if (!this.isDragging) {
2635			this.isDragging = true;
2636			this.handleDragStart(ev);
2637		}
2638	},
2639
2640
2641	handleDragStart: function(ev) {
2642		this.trigger('dragStart', ev);
2643		this.initHrefHack();
2644	},
2645
2646
2647	handleMove: function(ev) {
2648		var dx = getEvX(ev) - this.originX;
2649		var dy = getEvY(ev) - this.originY;
2650		var minDistance = this.minDistance;
2651		var distanceSq; // current distance from the origin, squared
2652
2653		if (!this.isDistanceSurpassed) {
2654			distanceSq = dx * dx + dy * dy;
2655			if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem
2656				this.handleDistanceSurpassed(ev);
2657			}
2658		}
2659
2660		if (this.isDragging) {
2661			this.handleDrag(dx, dy, ev);
2662		}
2663	},
2664
2665
2666	// Called while the mouse is being moved and when we know a legitimate drag is taking place
2667	handleDrag: function(dx, dy, ev) {
2668		this.trigger('drag', dx, dy, ev);
2669		this.updateAutoScroll(ev); // will possibly cause scrolling
2670	},
2671
2672
2673	endDrag: function(ev) {
2674		if (this.isDragging) {
2675			this.isDragging = false;
2676			this.handleDragEnd(ev);
2677		}
2678	},
2679
2680
2681	handleDragEnd: function(ev) {
2682		this.trigger('dragEnd', ev);
2683		this.destroyHrefHack();
2684	},
2685
2686
2687	// Delay
2688	// -----------------------------------------------------------------------------------------------------------------
2689
2690
2691	startDelay: function(initialEv) {
2692		var _this = this;
2693
2694		if (this.delay) {
2695			this.delayTimeoutId = setTimeout(function() {
2696				_this.handleDelayEnd(initialEv);
2697			}, this.delay);
2698		}
2699		else {
2700			this.handleDelayEnd(initialEv);
2701		}
2702	},
2703
2704
2705	handleDelayEnd: function(initialEv) {
2706		this.isDelayEnded = true;
2707
2708		if (this.isDistanceSurpassed) {
2709			this.startDrag(initialEv);
2710		}
2711	},
2712
2713
2714	// Distance
2715	// -----------------------------------------------------------------------------------------------------------------
2716
2717
2718	handleDistanceSurpassed: function(ev) {
2719		this.isDistanceSurpassed = true;
2720
2721		if (this.isDelayEnded) {
2722			this.startDrag(ev);
2723		}
2724	},
2725
2726
2727	// Mouse / Touch
2728	// -----------------------------------------------------------------------------------------------------------------
2729
2730
2731	handleTouchMove: function(ev) {
2732		// prevent inertia and touchmove-scrolling while dragging
2733		if (this.isDragging) {
2734			ev.preventDefault();
2735		}
2736
2737		this.handleMove(ev);
2738	},
2739
2740
2741	handleMouseMove: function(ev) {
2742		this.handleMove(ev);
2743	},
2744
2745
2746	// Scrolling (unrelated to auto-scroll)
2747	// -----------------------------------------------------------------------------------------------------------------
2748
2749
2750	handleTouchScroll: function(ev) {
2751		// if the drag is being initiated by touch, but a scroll happens before
2752		// the drag-initiating delay is over, cancel the drag
2753		if (!this.isDragging) {
2754			this.endInteraction(ev, true); // isCancelled=true
2755		}
2756	},
2757
2758
2759	// <A> HREF Hack
2760	// -----------------------------------------------------------------------------------------------------------------
2761
2762
2763	initHrefHack: function() {
2764		var subjectEl = this.subjectEl;
2765
2766		// remove a mousedown'd <a>'s href so it is not visited (IE8 bug)
2767		if ((this.subjectHref = subjectEl ? subjectEl.attr('href') : null)) {
2768			subjectEl.removeAttr('href');
2769		}
2770	},
2771
2772
2773	destroyHrefHack: function() {
2774		var subjectEl = this.subjectEl;
2775		var subjectHref = this.subjectHref;
2776
2777		// restore a mousedown'd <a>'s href (for IE8 bug)
2778		setTimeout(function() { // must be outside of the click's execution
2779			if (subjectHref) {
2780				subjectEl.attr('href', subjectHref);
2781			}
2782		}, 0);
2783	},
2784
2785
2786	// Utils
2787	// -----------------------------------------------------------------------------------------------------------------
2788
2789
2790	// Triggers a callback. Calls a function in the option hash of the same name.
2791	// Arguments beyond the first `name` are forwarded on.
2792	trigger: function(name) {
2793		if (this.options[name]) {
2794			this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
2795		}
2796		// makes _methods callable by event name. TODO: kill this
2797		if (this['_' + name]) {
2798			this['_' + name].apply(this, Array.prototype.slice.call(arguments, 1));
2799		}
2800	}
2801
2802
2803});
2804
2805;;
2806/*
2807this.scrollEl is set in DragListener
2808*/
2809DragListener.mixin({
2810
2811	isAutoScroll: false,
2812
2813	scrollBounds: null, // { top, bottom, left, right }
2814	scrollTopVel: null, // pixels per second
2815	scrollLeftVel: null, // pixels per second
2816	scrollIntervalId: null, // ID of setTimeout for scrolling animation loop
2817
2818	// defaults
2819	scrollSensitivity: 30, // pixels from edge for scrolling to start
2820	scrollSpeed: 200, // pixels per second, at maximum speed
2821	scrollIntervalMs: 50, // millisecond wait between scroll increment
2822
2823
2824	initAutoScroll: function() {
2825		var scrollEl = this.scrollEl;
2826
2827		this.isAutoScroll =
2828			this.options.scroll &&
2829			scrollEl &&
2830			!scrollEl.is(window) &&
2831			!scrollEl.is(document);
2832
2833		if (this.isAutoScroll) {
2834			// debounce makes sure rapid calls don't happen
2835			this.listenTo(scrollEl, 'scroll', debounce(this.handleDebouncedScroll, 100));
2836		}
2837	},
2838
2839
2840	destroyAutoScroll: function() {
2841		this.endAutoScroll(); // kill any animation loop
2842
2843		// remove the scroll handler if there is a scrollEl
2844		if (this.isAutoScroll) {
2845			this.stopListeningTo(this.scrollEl, 'scroll'); // will probably get removed by unbindHandlers too :(
2846		}
2847	},
2848
2849
2850	// Computes and stores the bounding rectangle of scrollEl
2851	computeScrollBounds: function() {
2852		if (this.isAutoScroll) {
2853			this.scrollBounds = getOuterRect(this.scrollEl);
2854			// TODO: use getClientRect in future. but prevents auto scrolling when on top of scrollbars
2855		}
2856	},
2857
2858
2859	// Called when the dragging is in progress and scrolling should be updated
2860	updateAutoScroll: function(ev) {
2861		var sensitivity = this.scrollSensitivity;
2862		var bounds = this.scrollBounds;
2863		var topCloseness, bottomCloseness;
2864		var leftCloseness, rightCloseness;
2865		var topVel = 0;
2866		var leftVel = 0;
2867
2868		if (bounds) { // only scroll if scrollEl exists
2869
2870			// compute closeness to edges. valid range is from 0.0 - 1.0
2871			topCloseness = (sensitivity - (getEvY(ev) - bounds.top)) / sensitivity;
2872			bottomCloseness = (sensitivity - (bounds.bottom - getEvY(ev))) / sensitivity;
2873			leftCloseness = (sensitivity - (getEvX(ev) - bounds.left)) / sensitivity;
2874			rightCloseness = (sensitivity - (bounds.right - getEvX(ev))) / sensitivity;
2875
2876			// translate vertical closeness into velocity.
2877			// mouse must be completely in bounds for velocity to happen.
2878			if (topCloseness >= 0 && topCloseness <= 1) {
2879				topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up
2880			}
2881			else if (bottomCloseness >= 0 && bottomCloseness <= 1) {
2882				topVel = bottomCloseness * this.scrollSpeed;
2883			}
2884
2885			// translate horizontal closeness into velocity
2886			if (leftCloseness >= 0 && leftCloseness <= 1) {
2887				leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left
2888			}
2889			else if (rightCloseness >= 0 && rightCloseness <= 1) {
2890				leftVel = rightCloseness * this.scrollSpeed;
2891			}
2892		}
2893
2894		this.setScrollVel(topVel, leftVel);
2895	},
2896
2897
2898	// Sets the speed-of-scrolling for the scrollEl
2899	setScrollVel: function(topVel, leftVel) {
2900
2901		this.scrollTopVel = topVel;
2902		this.scrollLeftVel = leftVel;
2903
2904		this.constrainScrollVel(); // massages into realistic values
2905
2906		// if there is non-zero velocity, and an animation loop hasn't already started, then START
2907		if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) {
2908			this.scrollIntervalId = setInterval(
2909				proxy(this, 'scrollIntervalFunc'), // scope to `this`
2910				this.scrollIntervalMs
2911			);
2912		}
2913	},
2914
2915
2916	// Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way
2917	constrainScrollVel: function() {
2918		var el = this.scrollEl;
2919
2920		if (this.scrollTopVel < 0) { // scrolling up?
2921			if (el.scrollTop() <= 0) { // already scrolled all the way up?
2922				this.scrollTopVel = 0;
2923			}
2924		}
2925		else if (this.scrollTopVel > 0) { // scrolling down?
2926			if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) { // already scrolled all the way down?
2927				this.scrollTopVel = 0;
2928			}
2929		}
2930
2931		if (this.scrollLeftVel < 0) { // scrolling left?
2932			if (el.scrollLeft() <= 0) { // already scrolled all the left?
2933				this.scrollLeftVel = 0;
2934			}
2935		}
2936		else if (this.scrollLeftVel > 0) { // scrolling right?
2937			if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) { // already scrolled all the way right?
2938				this.scrollLeftVel = 0;
2939			}
2940		}
2941	},
2942
2943
2944	// This function gets called during every iteration of the scrolling animation loop
2945	scrollIntervalFunc: function() {
2946		var el = this.scrollEl;
2947		var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by
2948
2949		// change the value of scrollEl's scroll
2950		if (this.scrollTopVel) {
2951			el.scrollTop(el.scrollTop() + this.scrollTopVel * frac);
2952		}
2953		if (this.scrollLeftVel) {
2954			el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac);
2955		}
2956
2957		this.constrainScrollVel(); // since the scroll values changed, recompute the velocities
2958
2959		// if scrolled all the way, which causes the vels to be zero, stop the animation loop
2960		if (!this.scrollTopVel && !this.scrollLeftVel) {
2961			this.endAutoScroll();
2962		}
2963	},
2964
2965
2966	// Kills any existing scrolling animation loop
2967	endAutoScroll: function() {
2968		if (this.scrollIntervalId) {
2969			clearInterval(this.scrollIntervalId);
2970			this.scrollIntervalId = null;
2971
2972			this.handleScrollEnd();
2973		}
2974	},
2975
2976
2977	// Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce)
2978	handleDebouncedScroll: function() {
2979		// recompute all coordinates, but *only* if this is *not* part of our scrolling animation
2980		if (!this.scrollIntervalId) {
2981			this.handleScrollEnd();
2982		}
2983	},
2984
2985
2986	// Called when scrolling has stopped, whether through auto scroll, or the user scrolling
2987	handleScrollEnd: function() {
2988	}
2989
2990});
2991;;
2992
2993/* Tracks mouse movements over a component and raises events about which hit the mouse is over.
2994------------------------------------------------------------------------------------------------------------------------
2995options:
2996- subjectEl
2997- subjectCenter
2998*/
2999
3000var HitDragListener = DragListener.extend({
3001
3002	component: null, // converts coordinates to hits
3003		// methods: prepareHits, releaseHits, queryHit
3004
3005	origHit: null, // the hit the mouse was over when listening started
3006	hit: null, // the hit the mouse is over
3007	coordAdjust: null, // delta that will be added to the mouse coordinates when computing collisions
3008
3009
3010	constructor: function(component, options) {
3011		DragListener.call(this, options); // call the super-constructor
3012
3013		this.component = component;
3014	},
3015
3016
3017	// Called when drag listening starts (but a real drag has not necessarily began).
3018	// ev might be undefined if dragging was started manually.
3019	handleInteractionStart: function(ev) {
3020		var subjectEl = this.subjectEl;
3021		var subjectRect;
3022		var origPoint;
3023		var point;
3024
3025		this.computeCoords();
3026
3027		if (ev) {
3028			origPoint = { left: getEvX(ev), top: getEvY(ev) };
3029			point = origPoint;
3030
3031			// constrain the point to bounds of the element being dragged
3032			if (subjectEl) {
3033				subjectRect = getOuterRect(subjectEl); // used for centering as well
3034				point = constrainPoint(point, subjectRect);
3035			}
3036
3037			this.origHit = this.queryHit(point.left, point.top);
3038
3039			// treat the center of the subject as the collision point?
3040			if (subjectEl && this.options.subjectCenter) {
3041
3042				// only consider the area the subject overlaps the hit. best for large subjects.
3043				// TODO: skip this if hit didn't supply left/right/top/bottom
3044				if (this.origHit) {
3045					subjectRect = intersectRects(this.origHit, subjectRect) ||
3046						subjectRect; // in case there is no intersection
3047				}
3048
3049				point = getRectCenter(subjectRect);
3050			}
3051
3052			this.coordAdjust = diffPoints(point, origPoint); // point - origPoint
3053		}
3054		else {
3055			this.origHit = null;
3056			this.coordAdjust = null;
3057		}
3058
3059		// call the super-method. do it after origHit has been computed
3060		DragListener.prototype.handleInteractionStart.apply(this, arguments);
3061	},
3062
3063
3064	// Recomputes the drag-critical positions of elements
3065	computeCoords: function() {
3066		this.component.prepareHits();
3067		this.computeScrollBounds(); // why is this here??????
3068	},
3069
3070
3071	// Called when the actual drag has started
3072	handleDragStart: function(ev) {
3073		var hit;
3074
3075		DragListener.prototype.handleDragStart.apply(this, arguments); // call the super-method
3076
3077		// might be different from this.origHit if the min-distance is large
3078		hit = this.queryHit(getEvX(ev), getEvY(ev));
3079
3080		// report the initial hit the mouse is over
3081		// especially important if no min-distance and drag starts immediately
3082		if (hit) {
3083			this.handleHitOver(hit);
3084		}
3085	},
3086
3087
3088	// Called when the drag moves
3089	handleDrag: function(dx, dy, ev) {
3090		var hit;
3091
3092		DragListener.prototype.handleDrag.apply(this, arguments); // call the super-method
3093
3094		hit = this.queryHit(getEvX(ev), getEvY(ev));
3095
3096		if (!isHitsEqual(hit, this.hit)) { // a different hit than before?
3097			if (this.hit) {
3098				this.handleHitOut();
3099			}
3100			if (hit) {
3101				this.handleHitOver(hit);
3102			}
3103		}
3104	},
3105
3106
3107	// Called when dragging has been stopped
3108	handleDragEnd: function() {
3109		this.handleHitDone();
3110		DragListener.prototype.handleDragEnd.apply(this, arguments); // call the super-method
3111	},
3112
3113
3114	// Called when a the mouse has just moved over a new hit
3115	handleHitOver: function(hit) {
3116		var isOrig = isHitsEqual(hit, this.origHit);
3117
3118		this.hit = hit;
3119
3120		this.trigger('hitOver', this.hit, isOrig, this.origHit);
3121	},
3122
3123
3124	// Called when the mouse has just moved out of a hit
3125	handleHitOut: function() {
3126		if (this.hit) {
3127			this.trigger('hitOut', this.hit);
3128			this.handleHitDone();
3129			this.hit = null;
3130		}
3131	},
3132
3133
3134	// Called after a hitOut. Also called before a dragStop
3135	handleHitDone: function() {
3136		if (this.hit) {
3137			this.trigger('hitDone', this.hit);
3138		}
3139	},
3140
3141
3142	// Called when the interaction ends, whether there was a real drag or not
3143	handleInteractionEnd: function() {
3144		DragListener.prototype.handleInteractionEnd.apply(this, arguments); // call the super-method
3145
3146		this.origHit = null;
3147		this.hit = null;
3148
3149		this.component.releaseHits();
3150	},
3151
3152
3153	// Called when scrolling has stopped, whether through auto scroll, or the user scrolling
3154	handleScrollEnd: function() {
3155		DragListener.prototype.handleScrollEnd.apply(this, arguments); // call the super-method
3156
3157		this.computeCoords(); // hits' absolute positions will be in new places. recompute
3158	},
3159
3160
3161	// Gets the hit underneath the coordinates for the given mouse event
3162	queryHit: function(left, top) {
3163
3164		if (this.coordAdjust) {
3165			left += this.coordAdjust.left;
3166			top += this.coordAdjust.top;
3167		}
3168
3169		return this.component.queryHit(left, top);
3170	}
3171
3172});
3173
3174
3175// Returns `true` if the hits are identically equal. `false` otherwise. Must be from the same component.
3176// Two null values will be considered equal, as two "out of the component" states are the same.
3177function isHitsEqual(hit0, hit1) {
3178
3179	if (!hit0 && !hit1) {
3180		return true;
3181	}
3182
3183	if (hit0 && hit1) {
3184		return hit0.component === hit1.component &&
3185			isHitPropsWithin(hit0, hit1) &&
3186			isHitPropsWithin(hit1, hit0); // ensures all props are identical
3187	}
3188
3189	return false;
3190}
3191
3192
3193// Returns true if all of subHit's non-standard properties are within superHit
3194function isHitPropsWithin(subHit, superHit) {
3195	for (var propName in subHit) {
3196		if (!/^(component|left|right|top|bottom)$/.test(propName)) {
3197			if (subHit[propName] !== superHit[propName]) {
3198				return false;
3199			}
3200		}
3201	}
3202	return true;
3203}
3204
3205;;
3206
3207/* Creates a clone of an element and lets it track the mouse as it moves
3208----------------------------------------------------------------------------------------------------------------------*/
3209
3210var MouseFollower = Class.extend(ListenerMixin, {
3211
3212	options: null,
3213
3214	sourceEl: null, // the element that will be cloned and made to look like it is dragging
3215	el: null, // the clone of `sourceEl` that will track the mouse
3216	parentEl: null, // the element that `el` (the clone) will be attached to
3217
3218	// the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl
3219	top0: null,
3220	left0: null,
3221
3222	// the absolute coordinates of the initiating touch/mouse action
3223	y0: null,
3224	x0: null,
3225
3226	// the number of pixels the mouse has moved from its initial position
3227	topDelta: null,
3228	leftDelta: null,
3229
3230	isFollowing: false,
3231	isHidden: false,
3232	isAnimating: false, // doing the revert animation?
3233
3234	constructor: function(sourceEl, options) {
3235		this.options = options = options || {};
3236		this.sourceEl = sourceEl;
3237		this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent
3238	},
3239
3240
3241	// Causes the element to start following the mouse
3242	start: function(ev) {
3243		if (!this.isFollowing) {
3244			this.isFollowing = true;
3245
3246			this.y0 = getEvY(ev);
3247			this.x0 = getEvX(ev);
3248			this.topDelta = 0;
3249			this.leftDelta = 0;
3250
3251			if (!this.isHidden) {
3252				this.updatePosition();
3253			}
3254
3255			if (getEvIsTouch(ev)) {
3256				this.listenTo($(document), 'touchmove', this.handleMove);
3257			}
3258			else {
3259				this.listenTo($(document), 'mousemove', this.handleMove);
3260			}
3261		}
3262	},
3263
3264
3265	// Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position.
3266	// `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately.
3267	stop: function(shouldRevert, callback) {
3268		var _this = this;
3269		var revertDuration = this.options.revertDuration;
3270
3271		function complete() { // might be called by .animate(), which might change `this` context
3272			_this.isAnimating = false;
3273			_this.removeElement();
3274
3275			_this.top0 = _this.left0 = null; // reset state for future updatePosition calls
3276
3277			if (callback) {
3278				callback();
3279			}
3280		}
3281
3282		if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time
3283			this.isFollowing = false;
3284
3285			this.stopListeningTo($(document));
3286
3287			if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation?
3288				this.isAnimating = true;
3289				this.el.animate({
3290					top: this.top0,
3291					left: this.left0
3292				}, {
3293					duration: revertDuration,
3294					complete: complete
3295				});
3296			}
3297			else {
3298				complete();
3299			}
3300		}
3301	},
3302
3303
3304	// Gets the tracking element. Create it if necessary
3305	getEl: function() {
3306		var el = this.el;
3307
3308		if (!el) {
3309			this.sourceEl.width(); // hack to force IE8 to compute correct bounding box
3310			el = this.el = this.sourceEl.clone()
3311				.addClass(this.options.additionalClass || '')
3312				.css({
3313					position: 'absolute',
3314					visibility: '', // in case original element was hidden (commonly through hideEvents())
3315					display: this.isHidden ? 'none' : '', // for when initially hidden
3316					margin: 0,
3317					right: 'auto', // erase and set width instead
3318					bottom: 'auto', // erase and set height instead
3319					width: this.sourceEl.width(), // explicit height in case there was a 'right' value
3320					height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value
3321					opacity: this.options.opacity || '',
3322					zIndex: this.options.zIndex
3323				});
3324
3325			// we don't want long taps or any mouse interaction causing selection/menus.
3326			// would use preventSelection(), but that prevents selectstart, causing problems.
3327			el.addClass('fc-unselectable');
3328
3329			el.appendTo(this.parentEl);
3330		}
3331
3332		return el;
3333	},
3334
3335
3336	// Removes the tracking element if it has already been created
3337	removeElement: function() {
3338		if (this.el) {
3339			this.el.remove();
3340			this.el = null;
3341		}
3342	},
3343
3344
3345	// Update the CSS position of the tracking element
3346	updatePosition: function() {
3347		var sourceOffset;
3348		var origin;
3349
3350		this.getEl(); // ensure this.el
3351
3352		// make sure origin info was computed
3353		if (this.top0 === null) {
3354			this.sourceEl.width(); // hack to force IE8 to compute correct bounding box
3355			sourceOffset = this.sourceEl.offset();
3356			origin = this.el.offsetParent().offset();
3357			this.top0 = sourceOffset.top - origin.top;
3358			this.left0 = sourceOffset.left - origin.left;
3359		}
3360
3361		this.el.css({
3362			top: this.top0 + this.topDelta,
3363			left: this.left0 + this.leftDelta
3364		});
3365	},
3366
3367
3368	// Gets called when the user moves the mouse
3369	handleMove: function(ev) {
3370		this.topDelta = getEvY(ev) - this.y0;
3371		this.leftDelta = getEvX(ev) - this.x0;
3372
3373		if (!this.isHidden) {
3374			this.updatePosition();
3375		}
3376	},
3377
3378
3379	// Temporarily makes the tracking element invisible. Can be called before following starts
3380	hide: function() {
3381		if (!this.isHidden) {
3382			this.isHidden = true;
3383			if (this.el) {
3384				this.el.hide();
3385			}
3386		}
3387	},
3388
3389
3390	// Show the tracking element after it has been temporarily hidden
3391	show: function() {
3392		if (this.isHidden) {
3393			this.isHidden = false;
3394			this.updatePosition();
3395			this.getEl().show();
3396		}
3397	}
3398
3399});
3400
3401;;
3402
3403/* An abstract class comprised of a "grid" of areas that each represent a specific datetime
3404----------------------------------------------------------------------------------------------------------------------*/
3405
3406var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
3407
3408	view: null, // a View object
3409	isRTL: null, // shortcut to the view's isRTL option
3410
3411	start: null,
3412	end: null,
3413
3414	el: null, // the containing element
3415	elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.
3416
3417	// derived from options
3418	eventTimeFormat: null,
3419	displayEventTime: null,
3420	displayEventEnd: null,
3421
3422	minResizeDuration: null, // TODO: hack. set by subclasses. minumum event resize duration
3423
3424	// if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity
3425	// of the date areas. if not defined, assumes to be day and time granularity.
3426	// TODO: port isTimeScale into same system?
3427	largeUnit: null,
3428
3429	dayDragListener: null,
3430	segDragListener: null,
3431	segResizeListener: null,
3432	externalDragListener: null,
3433
3434
3435	constructor: function(view) {
3436		this.view = view;
3437		this.isRTL = view.opt('isRTL');
3438		this.elsByFill = {};
3439
3440		this.dayDragListener = this.buildDayDragListener();
3441		this.initMouseIgnoring();
3442	},
3443
3444
3445	/* Options
3446	------------------------------------------------------------------------------------------------------------------*/
3447
3448
3449	// Generates the format string used for event time text, if not explicitly defined by 'timeFormat'
3450	computeEventTimeFormat: function() {
3451		return this.view.opt('smallTimeFormat');
3452	},
3453
3454
3455	// Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventTime'.
3456	// Only applies to non-all-day events.
3457	computeDisplayEventTime: function() {
3458		return true;
3459	},
3460
3461
3462	// Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd'
3463	computeDisplayEventEnd: function() {
3464		return true;
3465	},
3466
3467
3468	/* Dates
3469	------------------------------------------------------------------------------------------------------------------*/
3470
3471
3472	// Tells the grid about what period of time to display.
3473	// Any date-related internal data should be generated.
3474	setRange: function(range) {
3475		this.start = range.start.clone();
3476		this.end = range.end.clone();
3477
3478		this.rangeUpdated();
3479		this.processRangeOptions();
3480	},
3481
3482
3483	// Called when internal variables that rely on the range should be updated
3484	rangeUpdated: function() {
3485	},
3486
3487
3488	// Updates values that rely on options and also relate to range
3489	processRangeOptions: function() {
3490		var view = this.view;
3491		var displayEventTime;
3492		var displayEventEnd;
3493
3494		this.eventTimeFormat =
3495			view.opt('eventTimeFormat') ||
3496			view.opt('timeFormat') || // deprecated
3497			this.computeEventTimeFormat();
3498
3499		displayEventTime = view.opt('displayEventTime');
3500		if (displayEventTime == null) {
3501			displayEventTime = this.computeDisplayEventTime(); // might be based off of range
3502		}
3503
3504		displayEventEnd = view.opt('displayEventEnd');
3505		if (displayEventEnd == null) {
3506			displayEventEnd = this.computeDisplayEventEnd(); // might be based off of range
3507		}
3508
3509		this.displayEventTime = displayEventTime;
3510		this.displayEventEnd = displayEventEnd;
3511	},
3512
3513
3514	// Converts a span (has unzoned start/end and any other grid-specific location information)
3515	// into an array of segments (pieces of events whose format is decided by the grid).
3516	spanToSegs: function(span) {
3517		// subclasses must implement
3518	},
3519
3520
3521	// Diffs the two dates, returning a duration, based on granularity of the grid
3522	// TODO: port isTimeScale into this system?
3523	diffDates: function(a, b) {
3524		if (this.largeUnit) {
3525			return diffByUnit(a, b, this.largeUnit);
3526		}
3527		else {
3528			return diffDayTime(a, b);
3529		}
3530	},
3531
3532
3533	/* Hit Area
3534	------------------------------------------------------------------------------------------------------------------*/
3535
3536
3537	// Called before one or more queryHit calls might happen. Should prepare any cached coordinates for queryHit
3538	prepareHits: function() {
3539	},
3540
3541
3542	// Called when queryHit calls have subsided. Good place to clear any coordinate caches.
3543	releaseHits: function() {
3544	},
3545
3546
3547	// Given coordinates from the topleft of the document, return data about the date-related area underneath.
3548	// Can return an object with arbitrary properties (although top/right/left/bottom are encouraged).
3549	// Must have a `grid` property, a reference to this current grid. TODO: avoid this
3550	// The returned object will be processed by getHitSpan and getHitEl.
3551	queryHit: function(leftOffset, topOffset) {
3552	},
3553
3554
3555	// Given position-level information about a date-related area within the grid,
3556	// should return an object with at least a start/end date. Can provide other information as well.
3557	getHitSpan: function(hit) {
3558	},
3559
3560
3561	// Given position-level information about a date-related area within the grid,
3562	// should return a jQuery element that best represents it. passed to dayClick callback.
3563	getHitEl: function(hit) {
3564	},
3565
3566
3567	/* Rendering
3568	------------------------------------------------------------------------------------------------------------------*/
3569
3570
3571	// Sets the container element that the grid should render inside of.
3572	// Does other DOM-related initializations.
3573	setElement: function(el) {
3574		this.el = el;
3575		preventSelection(el);
3576
3577		this.bindDayHandler('touchstart', this.dayTouchStart);
3578		this.bindDayHandler('mousedown', this.dayMousedown);
3579
3580		// attach event-element-related handlers. in Grid.events
3581		// same garbage collection note as above.
3582		this.bindSegHandlers();
3583
3584		this.bindGlobalHandlers();
3585	},
3586
3587
3588	bindDayHandler: function(name, handler) {
3589		var _this = this;
3590
3591		// attach a handler to the grid's root element.
3592		// jQuery will take care of unregistering them when removeElement gets called.
3593		this.el.on(name, function(ev) {
3594			if (
3595				!$(ev.target).is('.fc-event-container *, .fc-more') // not an an event element, or "more.." link
3596			) {
3597				return handler.call(_this, ev);
3598			}
3599		});
3600	},
3601
3602
3603	// Removes the grid's container element from the DOM. Undoes any other DOM-related attachments.
3604	// DOES NOT remove any content beforehand (doesn't clear events or call unrenderDates), unlike View
3605	removeElement: function() {
3606		this.unbindGlobalHandlers();
3607		this.clearDragListeners();
3608
3609		this.el.remove();
3610
3611		// NOTE: we don't null-out this.el for the same reasons we don't do it within View::removeElement
3612	},
3613
3614
3615	// Renders the basic structure of grid view before any content is rendered
3616	renderSkeleton: function() {
3617		// subclasses should implement
3618	},
3619
3620
3621	// Renders the grid's date-related content (like areas that represent days/times).
3622	// Assumes setRange has already been called and the skeleton has already been rendered.
3623	renderDates: function() {
3624		// subclasses should implement
3625	},
3626
3627
3628	// Unrenders the grid's date-related content
3629	unrenderDates: function() {
3630		// subclasses should implement
3631	},
3632
3633
3634	/* Handlers
3635	------------------------------------------------------------------------------------------------------------------*/
3636
3637
3638	// Binds DOM handlers to elements that reside outside the grid, such as the document
3639	bindGlobalHandlers: function() {
3640		this.listenTo($(document), {
3641			dragstart: this.externalDragStart, // jqui
3642			sortstart: this.externalDragStart // jqui
3643		});
3644	},
3645
3646
3647	// Unbinds DOM handlers from elements that reside outside the grid
3648	unbindGlobalHandlers: function() {
3649		this.stopListeningTo($(document));
3650	},
3651
3652
3653	// Process a mousedown on an element that represents a day. For day clicking and selecting.
3654	dayMousedown: function(ev) {
3655		if (!this.isIgnoringMouse) {
3656			this.dayDragListener.startInteraction(ev, {
3657				//distance: 5, // needs more work if we want dayClick to fire correctly
3658			});
3659		}
3660	},
3661
3662
3663	dayTouchStart: function(ev) {
3664		var view = this.view;
3665
3666		// HACK to prevent a user's clickaway for unselecting a range or an event
3667		// from causing a dayClick.
3668		if (view.isSelected || view.selectedEvent) {
3669			this.tempIgnoreMouse();
3670		}
3671
3672		this.dayDragListener.startInteraction(ev, {
3673			delay: this.view.opt('longPressDelay')
3674		});
3675	},
3676
3677
3678	// Creates a listener that tracks the user's drag across day elements.
3679	// For day clicking and selecting.
3680	buildDayDragListener: function() {
3681		var _this = this;
3682		var view = this.view;
3683		var isSelectable = view.opt('selectable');
3684		var dayClickHit; // null if invalid dayClick
3685		var selectionSpan; // null if invalid selection
3686
3687		// this listener tracks a mousedown on a day element, and a subsequent drag.
3688		// if the drag ends on the same day, it is a 'dayClick'.
3689		// if 'selectable' is enabled, this listener also detects selections.
3690		var dragListener = new HitDragListener(this, {
3691			scroll: view.opt('dragScroll'),
3692			interactionStart: function() {
3693				dayClickHit = dragListener.origHit; // for dayClick, where no dragging happens
3694				selectionSpan = null;
3695			},
3696			dragStart: function() {
3697				view.unselect(); // since we could be rendering a new selection, we want to clear any old one
3698			},
3699			hitOver: function(hit, isOrig, origHit) {
3700				if (origHit) { // click needs to have started on a hit
3701
3702					// if user dragged to another cell at any point, it can no longer be a dayClick
3703					if (!isOrig) {
3704						dayClickHit = null;
3705					}
3706
3707					if (isSelectable) {
3708						selectionSpan = _this.computeSelection(
3709							_this.getHitSpan(origHit),
3710							_this.getHitSpan(hit)
3711						);
3712						if (selectionSpan) {
3713							_this.renderSelection(selectionSpan);
3714						}
3715						else if (selectionSpan === false) {
3716							disableCursor();
3717						}
3718					}
3719				}
3720			},
3721			hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
3722				dayClickHit = null;
3723				selectionSpan = null;
3724				_this.unrenderSelection();
3725			},
3726			hitDone: function() { // called after a hitOut OR before a dragEnd
3727				enableCursor();
3728			},
3729			interactionEnd: function(ev, isCancelled) {
3730				if (!isCancelled) {
3731					if (
3732						dayClickHit &&
3733						!_this.isIgnoringMouse // see hack in dayTouchStart
3734					) {
3735						view.triggerDayClick(
3736							_this.getHitSpan(dayClickHit),
3737							_this.getHitEl(dayClickHit),
3738							ev
3739						);
3740					}
3741					if (selectionSpan) {
3742						// the selection will already have been rendered. just report it
3743						view.reportSelection(selectionSpan, ev);
3744					}
3745				}
3746			}
3747		});
3748
3749		return dragListener;
3750	},
3751
3752
3753	// Kills all in-progress dragging.
3754	// Useful for when public API methods that result in re-rendering are invoked during a drag.
3755	// Also useful for when touch devices misbehave and don't fire their touchend.
3756	clearDragListeners: function() {
3757		this.dayDragListener.endInteraction();
3758
3759		if (this.segDragListener) {
3760			this.segDragListener.endInteraction(); // will clear this.segDragListener
3761		}
3762		if (this.segResizeListener) {
3763			this.segResizeListener.endInteraction(); // will clear this.segResizeListener
3764		}
3765		if (this.externalDragListener) {
3766			this.externalDragListener.endInteraction(); // will clear this.externalDragListener
3767		}
3768	},
3769
3770
3771	/* Event Helper
3772	------------------------------------------------------------------------------------------------------------------*/
3773	// TODO: should probably move this to Grid.events, like we did event dragging / resizing
3774
3775
3776	// Renders a mock event at the given event location, which contains zoned start/end properties.
3777	// Returns all mock event elements.
3778	renderEventLocationHelper: function(eventLocation, sourceSeg) {
3779		var fakeEvent = this.fabricateHelperEvent(eventLocation, sourceSeg);
3780
3781		return this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering
3782	},
3783
3784
3785	// Builds a fake event given zoned event date properties and a segment is should be inspired from.
3786	// The range's end can be null, in which case the mock event that is rendered will have a null end time.
3787	// `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging.
3788	fabricateHelperEvent: function(eventLocation, sourceSeg) {
3789		var fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible
3790
3791		fakeEvent.start = eventLocation.start.clone();
3792		fakeEvent.end = eventLocation.end ? eventLocation.end.clone() : null;
3793		fakeEvent.allDay = null; // force it to be freshly computed by normalizeEventDates
3794		this.view.calendar.normalizeEventDates(fakeEvent);
3795
3796		// this extra className will be useful for differentiating real events from mock events in CSS
3797		fakeEvent.className = (fakeEvent.className || []).concat('fc-helper');
3798
3799		// if something external is being dragged in, don't render a resizer
3800		if (!sourceSeg) {
3801			fakeEvent.editable = false;
3802		}
3803
3804		return fakeEvent;
3805	},
3806
3807
3808	// Renders a mock event. Given zoned event date properties.
3809	// Must return all mock event elements.
3810	renderHelper: function(eventLocation, sourceSeg) {
3811		// subclasses must implement
3812	},
3813
3814
3815	// Unrenders a mock event
3816	unrenderHelper: function() {
3817		// subclasses must implement
3818	},
3819
3820
3821	/* Selection
3822	------------------------------------------------------------------------------------------------------------------*/
3823
3824
3825	// Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses.
3826	// Given a span (unzoned start/end and other misc data)
3827	renderSelection: function(span) {
3828		this.renderHighlight(span);
3829	},
3830
3831
3832	// Unrenders any visual indications of a selection. Will unrender a highlight by default.
3833	unrenderSelection: function() {
3834		this.unrenderHighlight();
3835	},
3836
3837
3838	// Given the first and last date-spans of a selection, returns another date-span object.
3839	// Subclasses can override and provide additional data in the span object. Will be passed to render
3839Selection().
3840	// Will return false if the selection is invalid and this should be indicated to the user.
3841	// Will return null/undefined if a selection invalid but no error should be reported.
3842	computeSelection: function(span0, span1) {
3843		var span = this.computeSelectionSpan(span0, span1);
3844
3845		if (span && !this.view.calendar.isSelectionSpanAllowed(span)) {
3846			return false;
3847		}
3848
3849		return span;
3850	},
3851
3852
3853	// Given two spans, must return the combination of the two.
3854	// TODO: do this separation of concerns (combining VS validation) for event dnd/resize too.
3855	computeSelectionSpan: function(span0, span1) {
3856		var dates = [ span0.start, span0.end, span1.start, span1.end ];
3857
3858		dates.sort(compareNumbers); // sorts chronologically. works with Moments
3859
3860		return { start: dates[0].clone(), end: dates[3].clone() };
3861	},
3862
3863
3864	/* Highlight
3865	------------------------------------------------------------------------------------------------------------------*/
3866
3867
3868	// Renders an emphasis on the given date range. Given a span (unzoned start/end and other misc data)
3869	renderHighlight: function(span) {
3870		this.renderFill('highlight', this.spanToSegs(span));
3871	},
3872
3873
3874	// Unrenders the emphasis on a date range
3875	unrenderHighlight: function() {
3876		this.unrenderFill('highlight');
3877	},
3878
3879
3880	// Generates an array of classNames for rendering the highlight. Used by the fill system.
3881	highlightSegClasses: function() {
3882		return [ 'fc-highlight' ];
3883	},
3884
3885
3886	/* Business Hours
3887	------------------------------------------------------------------------------------------------------------------*/
3888
3889
3890	renderBusinessHours: function() {
3891	},
3892
3893
3894	unrenderBusinessHours: function() {
3895	},
3896
3897
3898	/* Now Indicator
3899	------------------------------------------------------------------------------------------------------------------*/
3900
3901
3902	getNowIndicatorUnit: function() {
3903	},
3904
3905
3906	renderNowIndicator: function(date) {
3907	},
3908
3909
3910	unrenderNowIndicator: function() {
3911	},
3912
3913
3914	/* Fill System (highlight, background events, business hours)
3915	--------------------------------------------------------------------------------------------------------------------
3916	TODO: remove this system. like we did in TimeGrid
3917	*/
3918
3919
3920	// Renders a set of rectangles over the given segments of time.
3921	// MUST RETURN a subset of segs, the segs that were actually rendered.
3922	// Responsible for populating this.elsByFill. TODO: better API for expressing this requirement
3923	renderFill: function(type, segs) {
3924		// subclasses must implement
3925	},
3926
3927
3928	// Unrenders a specific type of fill that is currently rendered on the grid
3929	unrenderFill: function(type) {
3930		var el = this.elsByFill[type];
3931
3932		if (el) {
3933			el.remove();
3934			delete this.elsByFill[type];
3935		}
3936	},
3937
3938
3939	// Renders and assigns an `el` property for each fill segment. Generic enough to work with different types.
3940	// Only returns segments that successfully rendered.
3941	// To be harnessed by renderFill (implemented by subclasses).
3942	// Analagous to renderFgSegEls.
3943	renderFillSegEls: function(type, segs) {
3944		var _this = this;
3945		var segElMethod = this[type + 'SegEl'];
3946		var html = '';
3947		var renderedSegs = [];
3948		var i;
3949
3950		if (segs.length) {
3951
3952			// build a large concatenation of segment HTML
3953			for (i = 0; i < segs.length; i++) {
3954				html += this.fillSegHtml(type, segs[i]);
3955			}
3956
3957			// Grab individual elements from the combined HTML string. Use each as the default rendering.
3958			// Then, compute the 'el' for each segment.
3959			$(html).each(function(i, node) {
3960				var seg = segs[i];
3961				var el = $(node);
3962
3963				// allow custom filter methods per-type
3964				if (segElMethod) {
3965					el = segElMethod.call(_this, seg, el);
3966				}
3967
3968				if (el) { // custom filters did not cancel the render
3969					el = $(el); // allow custom filter to return raw DOM node
3970
3971					// correct element type? (would be bad if a non-TD were inserted into a table for example)
3972					if (el.is(_this.fillSegTag)) {
3973						seg.el = el;
3974						renderedSegs.push(seg);
3975					}
3976				}
3977			});
3978		}
3979
3980		return renderedSegs;
3981	},
3982
3983
3984	fillSegTag: 'div', // subclasses can override
3985
3986
3987	// Builds the HTML needed for one fill segment. Generic enough to work with different types.
3988	fillSegHtml: function(type, seg) {
3989
3990		// custom hooks per-type
3991		var classesMethod = this[type + 'SegClasses'];
3992		var cssMethod = this[type + 'SegCss'];
3993
3994		var classes = classesMethod ? classesMethod.call(this, seg) : [];
3995		var css = cssToStr(cssMethod ? cssMethod.call(this, seg) : {});
3996
3997		return '<' + this.fillSegTag +
3998			(classes.length ? ' class="' + classes.join(' ') + '"' : '') +
3999			(css ? ' style="' + css + '"' : '') +
4000			' />';
4001	},
4002
4003
4004
4005	/* Generic rendering utilities for subclasses
4006	------------------------------------------------------------------------------------------------------------------*/
4007
4008
4009	// Computes HTML classNames for a single-day element
4010	getDayClasses: function(date) {
4011		var view = this.view;
4012		var today = view.calendar.getNow();
4013		var classes = [ 'fc-' + dayIDs[date.day()] ];
4014
4015		if (
4016			view.intervalDuration.as('months') == 1 &&
4017			date.month() != view.intervalStart.month()
4018		) {
4019			classes.push('fc-other-month');
4020		}
4021
4022		if (date.isSame(today, 'day')) {
4023			classes.push(
4024				'fc-today',
4025				view.highlightStateClass
4026			);
4027		}
4028		else if (date < today) {
4029			classes.push('fc-past');
4030		}
4031		else {
4032			classes.push('fc-future');
4033		}
4034
4035		return classes;
4036	}
4037
4038});
4039
4040;;
4041
4042/* Event-rendering and event-interaction methods for the abstract Grid class
4043----------------------------------------------------------------------------------------------------------------------*/
4044
4045Grid.mixin({
4046
4047	mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing
4048	isDraggingSeg: false, // is a segment being dragged? boolean
4049	isResizingSeg: false, // is a segment being resized? boolean
4050	isDraggingExternal: false, // jqui-dragging an external element? boolean
4051	segs: null, // the *event* segments currently rendered in the grid. TODO: rename to `eventSegs`
4052
4053
4054	// Renders the given events onto the grid
4055	renderEvents: function(events) {
4056		var bgEvents = [];
4057		var fgEvents = [];
4058		var i;
4059
4060		for (i = 0; i < events.length; i++) {
4061			(isBgEvent(events[i]) ? bgEvents : fgEvents).push(events[i]);
4062		}
4063
4064		this.segs = [].concat( // record all segs
4065			this.renderBgEvents(bgEvents),
4066			this.renderFgEvents(fgEvents)
4067		);
4068	},
4069
4070
4071	renderBgEvents: function(events) {
4072		var segs = this.eventsToSegs(events);
4073
4074		// renderBgSegs might return a subset of segs, segs that were actually rendered
4075		return this.renderBgSegs(segs) || segs;
4076	},
4077
4078
4079	renderFgEvents: function(events) {
4080		var segs = this.eventsToSegs(events);
4081
4082		// renderFgSegs might return a subset of segs, segs that were actually rendered
4083		return this.renderFgSegs(segs) || segs;
4084	},
4085
4086
4087	// Unrenders all events currently rendered on the grid
4088	unrenderEvents: function() {
4089		this.handleSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
4090		this.clearDragListeners();
4091
4092		this.unrenderFgSegs();
4093		this.unrenderBgSegs();
4094
4095		this.segs = null;
4096	},
4097
4098
4099	// Retrieves all rendered segment objects currently rendered on the grid
4100	getEventSegs: function() {
4101		return this.segs || [];
4102	},
4103
4104
4105	/* Foreground Segment Rendering
4106	------------------------------------------------------------------------------------------------------------------*/
4107
4108
4109	// Renders foreground event segments onto the grid. May return a subset of segs that were rendered.
4110	renderFgSegs: function(segs) {
4111		// subclasses must implement
4112	},
4113
4114
4115	// Unrenders all currently rendered foreground segments
4116	unrenderFgSegs: function() {
4117		// subclasses must implement
4118	},
4119
4120
4121	// Renders and assigns an `el` property for each foreground event segment.
4122	// Only returns segments that successfully rendered.
4123	// A utility that subclasses may use.
4124	renderFgSegEls: function(segs, disableResizing) {
4125		var view = this.view;
4126		var html = '';
4127		var renderedSegs = [];
4128		var i;
4129
4130		if (segs.length) { // don't build an empty html string
4131
4132			// build a large concatenation of event segment HTML
4133			for (i = 0; i < segs.length; i++) {
4134				html += this.fgSegHtml(segs[i], disableResizing);
4135			}
4136
4137			// Grab individual elements from the combined HTML string. Use each as the default rendering.
4138			// Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
4139			$(html).each(function(i, node) {
4140				var seg = segs[i];
4141				var el = view.resolveEventEl(seg.event, $(node));
4142
4143				if (el) {
4144					el.data('fc-seg', seg); // used by handlers
4145					seg.el = el;
4146					renderedSegs.push(seg);
4147				}
4148			});
4149		}
4150
4151		return renderedSegs;
4152	},
4153
4154
4155	// Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls()
4156	fgSegHtml: function(seg, disableResizing) {
4157		// subclasses should implement
4158	},
4159
4160
4161	/* Background Segment Rendering
4162	------------------------------------------------------------------------------------------------------------------*/
4163
4164
4165	// Renders the given background event segments onto the grid.
4166	// Returns a subset of the segs that were actually rendered.
4167	renderBgSegs: function(segs) {
4168		return this.renderFill('bgEvent', segs);
4169	},
4170
4171
4172	// Unrenders all the currently rendered background event segments
4173	unrenderBgSegs: function() {
4174		this.unrenderFill('bgEvent');
4175	},
4176
4177
4178	// Renders a background event element, given the default rendering. Called by the fill system.
4179	bgEventSegEl: function(seg, el) {
4180		return this.view.resolveEventEl(seg.event, el); // will filter through eventRender
4181	},
4182
4183
4184	// Generates an array of classNames to be used for the default rendering of a background event.
4185	// Called by fillSegHtml.
4186	bgEventSegClasses: function(seg) {
4187		var event = seg.event;
4188		var source = event.source || {};
4189
4190		return [ 'fc-bgevent' ].concat(
4191			event.className,
4192			source.className || []
4193		);
4194	},
4195
4196
4197	// Generates a semicolon-separated CSS string to be used for the default rendering of a background event.
4198	// Called by fillSegHtml.
4199	bgEventSegCss: function(seg) {
4200		return {
4201			'background-color': this.getSegSkinCss(seg)['background-color']
4202		};
4203	},
4204
4205
4206	// Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system.
4207	// Called by fillSegHtml.
4208	businessHoursSegClasses: function(seg) {
4209		return [ 'fc-nonbusiness', 'fc-bgevent' ];
4210	},
4211
4212
4213	/* Business Hours
4214	------------------------------------------------------------------------------------------------------------------*/
4215
4216
4217	// Compute business hour segs for the grid's current date range.
4218	// Caller must ask if whole-day business hours are needed.
4219	buildBusinessHourSegs: function(wholeDay) {
4220		var events = this.view.calendar.getCurrentBusinessHourEvents(wholeDay);
4221
4222		// HACK. Eventually refactor business hours "events" system.
4223		// If no events are given, but businessHours is activated, this means the entire visible range should be
4224		// marked as *not* business-hours, via inverse-background rendering.
4225		if (
4226			!events.length &&
4227			this.view.calendar.options.businessHours // don't access view option. doesn't update with dynamic options
4228		) {
4229			events = [
4230				$.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, {
4231					start: this.view.end, // guaranteed out-of-range
4232					end: this.view.end,   // "
4233					dow: null
4234				})
4235			];
4236		}
4237
4238		return this.eventsToSegs(events);
4239	},
4240
4241
4242	/* Handlers
4243	------------------------------------------------------------------------------------------------------------------*/
4244
4245
4246	// Attaches event-element-related handlers for *all* rendered event segments of the view.
4247	bindSegHandlers: function() {
4248		this.bindSegHandlersToEl(this.el);
4249	},
4250
4251
4252	// Attaches event-element-related handlers to an arbitrary container element. leverages bubbling.
4253	bindSegHandlersToEl: function(el) {
4254		this.bindSegHandlerToEl(el, 'touchstart', this.handleSegTouchStart);
4255		this.bindSegHandlerToEl(el, 'touchend', this.handleSegTouchEnd);
4256		this.bindSegHandlerToEl(el, 'mouseenter', this.handleSegMouseover);
4257		this.bindSegHandlerToEl(el, 'mouseleave', this.handleSegMouseout);
4258		this.bindSegHandlerToEl(el, 'mousedown', this.handleSegMousedown);
4259		this.bindSegHandlerToEl(el, 'click', this.handleSegClick);
4260	},
4261
4262
4263	// Executes a handler for any a user-interaction on a segment.
4264	// Handler gets called with (seg, ev), and with the `this` context of the Grid
4265	bindSegHandlerToEl: function(el, name, handler) {
4266		var _this = this;
4267
4268		el.on(name, '.fc-event-container > *', function(ev) {
4269			var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents
4270
4271			// only call the handlers if there is not a drag/resize in progress
4272			if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) {
4273				return handler.call(_this, seg, ev); // context will be the Grid
4274			}
4275		});
4276	},
4277
4278
4279	handleSegClick: function(seg, ev) {
4280		return this.view.trigger('eventClick', seg.el[0], seg.event, ev); // can return `false` to cancel
4281	},
4282
4283
4284	// Updates internal state and triggers handlers for when an event element is moused over
4285	handleSegMouseover: function(seg, ev) {
4286		if (
4287			!this.isIgnoringMouse &&
4288			!this.mousedOverSeg
4289		) {
4290			this.mousedOverSeg = seg;
4291			seg.el.addClass('fc-allow-mouse-resize');
4292			this.view.trigger('eventMouseover', seg.el[0], seg.event, ev);
4293		}
4294	},
4295
4296
4297	// Updates internal state and triggers handlers for when an event element is moused out.
4298	// Can be given no arguments, in which case it will mouseout the segment that was previously moused over.
4299	handleSegMouseout: function(seg, ev) {
4300		ev = ev || {}; // if given no args, make a mock mouse event
4301
4302		if (this.mousedOverSeg) {
4303			seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment
4304			this.mousedOverSeg = null;
4305			seg.el.removeClass('fc-allow-mouse-resize');
4306			this.view.trigger('eventMouseout', seg.el[0], seg.event, ev);
4307		}
4308	},
4309
4310
4311	handleSegMousedown: function(seg, ev) {
4312		var isResizing = this.startSegResize(seg, ev, { distance: 5 });
4313
4314		if (!isResizing && this.view.isEventDraggable(seg.event)) {
4315			this.buildSegDragListener(seg)
4316				.startInteraction(ev, {
4317					distance: 5
4318				});
4319		}
4320	},
4321
4322
4323	handleSegTouchStart: function(seg, ev) {
4324		var view = this.view;
4325		var event = seg.event;
4326		var isSelected = view.isEventSelected(event);
4327		var isDraggable = view.isEventDraggable(event);
4328		var isResizable = view.isEventResizable(event);
4329		var isResizing = false;
4330		var dragListener;
4331
4332		if (isSelected && isResizable) {
4333			// only allow resizing of the event is selected
4334			isResizing = this.startSegResize(seg, ev);
4335		}
4336
4337		if (!isResizing && (isDraggable || isResizable)) { // allowed to be selected?
4338
4339			dragListener = isDraggable ?
4340				this.buildSegDragListener(seg) :
4341				this.buildSegSelectListener(seg); // seg isn't draggable, but still needs to be selected
4342
4343			dragListener.startInteraction(ev, { // won't start if already started
4344				delay: isSelected ? 0 : this.view.opt('longPressDelay') // do delay if not already selected
4345			});
4346		}
4347
4348		// a long tap simulates a mouseover. ignore this bogus mouseover.
4349		this.tempIgnoreMouse();
4350	},
4351
4352
4353	handleSegTouchEnd: function(seg, ev) {
4354		// touchstart+touchend = click, which simulates a mouseover.
4355		// ignore this bogus mouseover.
4356		this.tempIgnoreMouse();
4357	},
4358
4359
4360	// returns boolean whether resizing actually started or not.
4361	// assumes the seg allows resizing.
4362	// `dragOptions` are optional.
4363	startSegResize: function(seg, ev, dragOptions) {
4364		if ($(ev.target).is('.fc-resizer')) {
4365			this.buildSegResizeListener(seg, $(ev.target).is('.fc-start-resizer'))
4366				.startInteraction(ev, dragOptions);
4367			return true;
4368		}
4369		return false;
4370	},
4371
4372
4373
4374	/* Event Dragging
4375	------------------------------------------------------------------------------------------------------------------*/
4376
4377
4378	// Builds a listener that will track user-dragging on an event segment.
4379	// Generic enough to work with any type of Grid.
4380	// Has side effect of setting/unsetting `segDragListener`
4381	buildSegDragListener: function(seg) {
4382		var _this = this;
4383		var view = this.view;
4384		var calendar = view.calendar;
4385		var el = seg.el;
4386		var event = seg.event;
4387		var isDragging;
4388		var mouseFollower; // A clone of the original element that will move with the mouse
4389		var dropLocation; // zoned event date properties
4390
4391		if (this.segDragListener) {
4392			return this.segDragListener;
4393		}
4394
4395		// Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents
4396		// of the view.
4397		var dragListener = this.segDragListener = new HitDragListener(view, {
4398			scroll: view.opt('dragScroll'),
4399			subjectEl: el,
4400			subjectCenter: true,
4401			interactionStart: function(ev) {
4402				seg.component = _this; // for renderDrag
4403				isDragging = false;
4404				mouseFollower = new MouseFollower(seg.el, {
4405					additionalClass: 'fc-dragging',
4406					parentEl: view.el,
4407					opacity: dragListener.isTouch ? null : view.opt('dragOpacity'),
4408					revertDuration: view.opt('dragRevertDuration'),
4409					zIndex: 2 // one above the .fc-view
4410				});
4411				mouseFollower.hide(); // don't show until we know this is a real drag
4412				mouseFollower.start(ev);
4413			},
4414			dragStart: function(ev) {
4415				if (dragListener.isTouch && !view.isEventSelected(event)) {
4416					// if not previously selected, will fire after a delay. then, select the event
4417					view.selectEvent(event);
4418				}
4419				isDragging = true;
4420				_this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
4421				_this.segDragStart(seg, ev);
4422				view.hideEvent(event); // hide all event segments. our mouseFollower will take over
4423			},
4424			hitOver: function(hit, isOrig, origHit) {
4425				var dragHelperEls;
4426
4427				// starting hit could be forced (DayGrid.limit)
4428				if (seg.hit) {
4429					origHit = seg.hit;
4430				}
4431
4432				// since we are querying the parent view, might not belong to this grid
4433				dropLocation = _this.computeEventDrop(
4434					origHit.component.getHitSpan(origHit),
4435					hit.component.getHitSpan(hit),
4436					event
4437				);
4438
4439				if (dropLocation && !calendar.isEventSpanAllowed(_this.eventToSpan(dropLocation), event)) {
4440					disableCursor();
4441					dropLocation = null;
4442				}
4443
4444				// if a valid drop location, have the subclass render a visual indication
4445				if (dropLocation && (dragHelperEls = view.renderDrag(dropLocation, seg))) {
4446
4447					dragHelperEls.addClass('fc-dragging');
4448					if (!dragListener.isTouch) {
4449						_this.applyDragOpacity(dragHelperEls);
4450					}
4451
4452					mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own
4453				}
4454				else {
4455					mouseFollower.show(); // otherwise, have the helper follow the mouse (no snapping)
4456				}
4457
4458				if (isOrig) {
4459					dropLocation = null; // needs to have moved hits to be a valid drop
4460				}
4461			},
4462			hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
4463				view.unrenderDrag(); // unrender whatever was done in renderDrag
4464				mouseFollower.show(); // show in case we are moving out of all hits
4465				dropLocation = null;
4466			},
4467			hitDone: function() { // Called after a hitOut OR before a dragEnd
4468				enableCursor();
4469			},
4470			interactionEnd: function(ev) {
4471				delete seg.component; // prevent side effects
4472
4473				// do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
4474				mouseFollower.stop(!dropLocation, function() {
4475					if (isDragging) {
4476						view.unrenderDrag();
4477						view.showEvent(event);
4478						_this.segDragStop(seg, ev);
4479					}
4480					if (dropLocation) {
4481						view.reportEventDrop(event, dropLocation, this.largeUnit, el, ev);
4482					}
4483				});
4484				_this.segDragListener = null;
4485			}
4486		});
4487
4488		return dragListener;
4489	},
4490
4491
4492	// seg isn't draggable, but let's use a generic DragListener
4493	// simply for the delay, so it can be selected.
4494	// Has side effect of setting/unsetting `segDragListener`
4495	buildSegSelectListener: function(seg) {
4496		var _this = this;
4497		var view = this.view;
4498		var event = seg.event;
4499
4500		if (this.segDragListener) {
4501			return this.segDragListener;
4502		}
4503
4504		var dragListener = this.segDragListener = new DragListener({
4505			dragStart: function(ev) {
4506				if (dragListener.isTouch && !view.isEventSelected(event)) {
4507					// if not previously selected, will fire after a delay. then, select the event
4508					view.selectEvent(event);
4509				}
4510			},
4511			interactionEnd: function(ev) {
4512				_this.segDragListener = null;
4513			}
4514		});
4515
4516		return dragListener;
4517	},
4518
4519
4520	// Called before event segment dragging starts
4521	segDragStart: function(seg, ev) {
4522		this.isDraggingSeg = true;
4523		this.view.trigger('eventDragStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
4524	},
4525
4526
4527	// Called after event segment dragging stops
4528	segDragStop: function(seg, ev) {
4529		this.isDraggingSeg = false;
4530		this.view.trigger('eventDragStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
4531	},
4532
4533
4534	// Given the spans an event drag began, and the span event was dropped, calculates the new zoned start/end/allDay
4535	// values for the event. Subclasses may override and set additional properties to be used by renderDrag.
4536	// A falsy returned value indicates an invalid drop.
4537	// DOES NOT consider overlap/constraint.
4538	computeEventDrop: function(startSpan, endSpan, event) {
4539		var calendar = this.view.calendar;
4540		var dragStart = startSpan.start;
4541		var dragEnd = endSpan.start;
4542		var delta;
4543		var dropLocation; // zoned event date properties
4544
4545		if (dragStart.hasTime() === dragEnd.hasTime()) {
4546			delta = this.diffDates(dragEnd, dragStart);
4547
4548			// if an all-day event was in a timed area and it was dragged to a different time,
4549			// guarantee an end and adjust start/end to have times
4550			if (event.allDay && durationHasTime(delta)) {
4551				dropLocation = {
4552					start: event.start.clone(),
4553					end: calendar.getEventEnd(event), // will be an ambig day
4554					allDay: false // for normalizeEventTimes
4555				};
4556				calendar.normalizeEventTimes(dropLocation);
4557			}
4558			// othewise, work off existing values
4559			else {
4560				dropLocation = {
4561					start: event.start.clone(),
4562					end: event.end ? event.end.clone() : null,
4563					allDay: event.allDay // keep it the same
4564				};
4565			}
4566
4567			dropLocation.start.add(delta);
4568			if (dropLocation.end) {
4569				dropLocation.end.add(delta);
4570			}
4571		}
4572		else {
4573			// if switching from day <-> timed, start should be reset to the dropped date, and the end cleared
4574			dropLocation = {
4575				start: dragEnd.clone(),
4576				end: null, // end should be cleared
4577				allDay: !dragEnd.hasTime()
4578			};
4579		}
4580
4581		return dropLocation;
4582	},
4583
4584
4585	// Utility for apply dragOpacity to a jQuery set
4586	applyDragOpacity: function(els) {
4587		var opacity = this.view.opt('dragOpacity');
4588
4589		if (opacity != null) {
4590			els.each(function(i, node) {
4591				// Don't use jQuery (will set an IE filter), do it the old fashioned way.
4592				// In IE8, a helper element will disappears if there's a filter.
4593				node.style.opacity = opacity;
4594			});
4595		}
4596	},
4597
4598
4599	/* External Element Dragging
4600	------------------------------------------------------------------------------------------------------------------*/
4601
4602
4603	// Called when a jQuery UI drag is initiated anywhere in the DOM
4604	externalDragStart: function(ev, ui) {
4605		var view = this.view;
4606		var el;
4607		var accept;
4608
4609		if (view.opt('droppable')) { // only listen if this setting is on
4610			el = $((ui ? ui.item : null) || ev.target);
4611
4612			// Test that the dragged element passes the dropAccept selector or filter function.
4613			// FYI, the default is "*" (matches all)
4614			accept = view.opt('dropAccept');
4615			if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) {
4616				if (!this.isDraggingExternal) { // prevent double-listening if fired twice
4617					this.listenToExternalDrag(el, ev, ui);
4618				}
4619			}
4620		}
4621	},
4622
4623
4624	// Called when a jQuery UI drag starts and it needs to be monitored for dropping
4625	listenToExternalDrag: function(el, ev, ui) {
4626		var _this = this;
4627		var calendar = this.view.calendar;
4628		var meta = getDraggedElMeta(el); // extra data about event drop, including possible event to create
4629		var dropLocation; // a null value signals an unsuccessful drag
4630
4631		// listener that tracks mouse movement over date-associated pixel regions
4632		var dragListener = _this.externalDragListener = new HitDragListener(this, {
4633			interactionStart: function() {
4634				_this.isDraggingExternal = true;
4635			},
4636			hitOver: function(hit) {
4637				dropLocation = _this.computeExternalDrop(
4638					hit.component.getHitSpan(hit), // since we are querying the parent view, might not belong to this grid
4639					meta
4640				);
4641
4642				if ( // invalid hit?
4643					dropLocation &&
4644					!calendar.isExternalSpanAllowed(_this.eventToSpan(dropLocation), dropLocation, meta.eventProps)
4645				) {
4646					disableCursor();
4647					dropLocation = null;
4648				}
4649
4650				if (dropLocation) {
4651					_this.renderDrag(dropLocation); // called without a seg parameter
4652				}
4653			},
4654			hitOut: function() {
4655				dropLocation = null; // signal unsuccessful
4656			},
4657			hitDone: function() { // Called after a hitOut OR before a dragEnd
4658				enableCursor();
4659				_this.unrenderDrag();
4660			},
4661			interactionEnd: function(ev) {
4662				if (dropLocation) { // element was dropped on a valid hit
4663					_this.view.reportExternalDrop(meta, dropLocation, el, ev, ui);
4664				}
4665				_this.isDraggingExternal = false;
4666				_this.externalDragListener = null;
4667			}
4668		});
4669
4670		dragListener.startDrag(ev); // start listening immediately
4671	},
4672
4673
4674	// Given a hit to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object),
4675	// returns the zoned start/end dates for the event that would result from the hypothetical drop. end might be null.
4676	// Returning a null value signals an invalid drop hit.
4677	// DOES NOT consider overlap/constraint.
4678	computeExternalDrop: function(span, meta) {
4679		var calendar = this.view.calendar;
4680		var dropLocation = {
4681			start: calendar.applyTimezone(span.start), // simulate a zoned event start date
4682			end: null
4683		};
4684
4685		// if dropped on an all-day span, and element's metadata specified a time, set it
4686		if (meta.startTime && !dropLocation.start.hasTime()) {
4687			dropLocation.start.time(meta.startTime);
4688		}
4689
4690		if (meta.duration) {
4691			dropLocation.end = dropLocation.start.clone().add(meta.duration);
4692		}
4693
4694		return dropLocation;
4695	},
4696
4697
4698
4699	/* Drag Rendering (for both events and an external elements)
4700	------------------------------------------------------------------------------------------------------------------*/
4701
4702
4703	// Renders a visual indication of an event or external element being dragged.
4704	// `dropLocation` contains hypothetical start/end/allDay values the event would have if dropped. end can be null.
4705	// `seg` is the internal segment object that is being dragged. If dragging an external element, `seg` is null.
4706	// A truthy returned value indicates this method has rendered a helper element.
4707	// Must return elements used for any mock events.
4708	renderDrag: function(dropLocation, seg) {
4709		// subclasses must implement
4710	},
4711
4712
4713	// Unrenders a visual indication of an event or external element being dragged
4714	unrenderDrag: function() {
4715		// subclasses must implement
4716	},
4717
4718
4719	/* Resizing
4720	------------------------------------------------------------------------------------------------------------------*/
4721
4722
4723	// Creates a listener that tracks the user as they resize an event segment.
4724	// Generic enough to work with any type of Grid.
4725	buildSegResizeListener: function(seg, isStart) {
4726		var _this = this;
4727		var view = this.view;
4728		var calendar = view.calendar;
4729		var el = seg.el;
4730		var event = seg.event;
4731		var eventEnd = calendar.getEventEnd(event);
4732		var isDragging;
4733		var resizeLocation; // zoned event date properties. falsy if invalid resize
4734
4735		// Tracks mouse movement over the *grid's* coordinate map
4736		var dragListener = this.segResizeListener = new HitDragListener(this, {
4737			scroll: view.opt('dragScroll'),
4738			subjectEl: el,
4739			interactionStart: function() {
4740				isDragging = false;
4741			},
4742			dragStart: function(ev) {
4743				isDragging = true;
4744				_this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
4745				_this.segResizeStart(seg, ev);
4746			},
4747			hitOver: function(hit, isOrig, origHit) {
4748				var origHitSpan = _this.getHitSpan(origHit);
4749				var hitSpan = _this.getHitSpan(hit);
4750
4751				resizeLocation = isStart ?
4752					_this.computeEventStartResize(origHitSpan, hitSpan, event) :
4753					_this.computeEventEndResize(origHitSpan, hitSpan, event);
4754
4755				if (resizeLocation) {
4756					if (!calendar.isEventSpanAllowed(_this.eventToSpan(resizeLocation), event)) {
4757						disableCursor();
4758						resizeLocation = null;
4759					}
4760					// no change? (TODO: how does this work with timezones?)
4761					else if (resizeLocation.start.isSame(event.start) && resizeLocation.end.isSame(eventEnd)) {
4762						resizeLocation = null;
4763					}
4764				}
4765
4766				if (resizeLocation) {
4767					view.hideEvent(event);
4768					_this.renderEventResize(resizeLocation, seg);
4769				}
4770			},
4771			hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
4772				resizeLocation = null;
4773			},
4774			hitDone: function() { // resets the rendering to show the original event
4775				_this.unrenderEventResize();
4776				view.showEvent(event);
4777				enableCursor();
4778			},
4779			interactionEnd: function(ev) {
4780				if (isDragging) {
4781					_this.segResizeStop(seg, ev);
4782				}
4783				if (resizeLocation) { // valid date to resize to?
4784					view.reportEventResize(event, resizeLocation, this.largeUnit, el, ev);
4785				}
4786				_this.segResizeListener = null;
4787			}
4788		});
4789
4790		return dragListener;
4791	},
4792
4793
4794	// Called before event segment resizing starts
4795	segResizeStart: function(seg, ev) {
4796		this.isResizingSeg = true;
4797		this.view.trigger('eventResizeStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
4798	},
4799
4800
4801	// Called after event segment resizing stops
4802	segResizeStop: function(seg, ev) {
4803		this.isResizingSeg = false;
4804		this.view.trigger('eventResizeStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
4805	},
4806
4807
4808	// Returns new date-information for an event segment being resized from its start
4809	computeEventStartResize: function(startSpan, endSpan, event) {
4810		return this.computeEventResize('start', startSpan, endSpan, event);
4811	},
4812
4813
4814	// Returns new date-information for an event segment being resized from its end
4815	computeEventEndResize: function(startSpan, endSpan, event) {
4816		return this.computeEventResize('end', startSpan, endSpan, event);
4817	},
4818
4819
4820	// Returns new zoned date information for an event segment being resized from its start OR end
4821	// `type` is either 'start' or 'end'.
4822	// DOES NOT consider overlap/constraint.
4823	computeEventResize: function(type, startSpan, endSpan, event) {
4824		var calendar = this.view.calendar;
4825		var delta = this.diffDates(endSpan[type], startSpan[type]);
4826		var resizeLocation; // zoned event date properties
4827		var defaultDuration;
4828
4829		// build original values to work from, guaranteeing a start and end
4830		resizeLocation = {
4831			start: event.start.clone(),
4832			end: calendar.getEventEnd(event),
4833			allDay: event.allDay
4834		};
4835
4836		// if an all-day event was in a timed area and was resized to a time, adjust start/end to have times
4837		if (resizeLocation.allDay && durationHasTime(delta)) {
4838			resizeLocation.allDay = false;
4839			calendar.normalizeEventTimes(resizeLocation);
4840		}
4841
4842		resizeLocation[type].add(delta); // apply delta to start or end
4843
4844		// if the event was compressed too small, find a new reasonable duration for it
4845		if (!resizeLocation.start.isBefore(resizeLocation.end)) {
4846
4847			defaultDuration =
4848				this.minResizeDuration || // TODO: hack
4849				(event.allDay ?
4850					calendar.defaultAllDayEventDuration :
4851					calendar.defaultTimedEventDuration);
4852
4853			if (type == 'start') { // resizing the start?
4854				resizeLocation.start = resizeLocation.end.clone().subtract(defaultDuration);
4855			}
4856			else { // resizing the end?
4857				resizeLocation.end = resizeLocation.start.clone().add(defaultDuration);
4858			}
4859		}
4860
4861		return resizeLocation;
4862	},
4863
4864
4865	// Renders a visual indication of an event being resized.
4866	// `range` has the updated dates of the event. `seg` is the original segment object involved in the drag.
4867	// Must return elements used for any mock events.
4868	renderEventResize: function(range, seg) {
4869		// subclasses must implement
4870	},
4871
4872
4873	// Unrenders a visual indication of an event being resized.
4874	unrenderEventResize: function() {
4875		// subclasses must implement
4876	},
4877
4878
4879	/* Rendering Utils
4880	------------------------------------------------------------------------------------------------------------------*/
4881
4882
4883	// Compute the text that should be displayed on an event's element.
4884	// `range` can be the Event object itself, or something range-like, with at least a `start`.
4885	// If event times are disabled, or the event has no time, will return a blank string.
4886	// If not specified, formatStr will default to the eventTimeFormat setting,
4887	// and displayEnd will default to the displayEventEnd setting.
4888	getEventTimeText: function(range, formatStr, displayEnd) {
4889
4890		if (formatStr == null) {
4891			formatStr = this.eventTimeFormat;
4892		}
4893
4894		if (displayEnd == null) {
4895			displayEnd = this.displayEventEnd;
4896		}
4897
4898		if (this.displayEventTime && range.start.hasTime()) {
4899			if (displayEnd && range.end) {
4900				return this.view.formatRange(range, formatStr);
4901			}
4902			else {
4903				return range.start.format(formatStr);
4904			}
4905		}
4906
4907		return '';
4908	},
4909
4910
4911	// Generic utility for generating the HTML classNames for an event segment's element
4912	getSegClasses: function(seg, isDraggable, isResizable) {
4913		var view = this.view;
4914		var event = seg.event;
4915		var classes = [
4916			'fc-event',
4917			seg.isStart ? 'fc-start' : 'fc-not-start',
4918			seg.isEnd ? 'fc-end' : 'fc-not-end'
4919		].concat(
4920			event.className,
4921			event.source ? event.source.className : []
4922		);
4923
4924		if (isDraggable) {
4925			classes.push('fc-draggable');
4926		}
4927		if (isResizable) {
4928			classes.push('fc-resizable');
4929		}
4930
4931		// event is currently selected? attach a className.
4932		if (view.isEventSelected(event)) {
4933			classes.push('fc-selected');
4934		}
4935
4936		return classes;
4937	},
4938
4939
4940	// Utility for generating event skin-related CSS properties
4941	getSegSkinCss: function(seg) {
4942		var event = seg.event;
4943		var view = this.view;
4944		var source = event.source || {};
4945		var eventColor = event.color;
4946		var sourceColor = source.color;
4947		var optionColor = view.opt('eventColor');
4948
4949		return {
4950			'background-color':
4951				event.backgroundColor ||
4952				eventColor ||
4953				source.backgroundColor ||
4954				sourceColor ||
4955				view.opt('eventBackgroundColor') ||
4956				optionColor,
4957			'border-color':
4958				event.borderColor ||
4959				eventColor ||
4960				source.borderColor ||
4961				sourceColor ||
4962				view.opt('eventBorderColor') ||
4963				optionColor,
4964			color:
4965				event.textColor ||
4966				source.textColor ||
4967				view.opt('eventTextColor')
4968		};
4969	},
4970
4971
4972	/* Converting events -> eventRange -> eventSpan -> eventSegs
4973	------------------------------------------------------------------------------------------------------------------*/
4974
4975
4976	// Generates an array of segments for the given single event
4977	// Can accept an event "location" as well (which only has start/end and no allDay)
4978	eventToSegs: function(event) {
4979		return this.eventsToSegs([ event ]);
4980	},
4981
4982
4983	eventToSpan: function(event) {
4984		return this.eventToSpans(event)[0];
4985	},
4986
4987
4988	// Generates spans (always unzoned) for the given event.
4989	// Does not do any inverting for inverse-background events.
4990	// Can accept an event "location" as well (which only has start/end and no allDay)
4991	eventToSpans: function(event) {
4992		var range = this.eventToRange(event);
4993		return this.eventRangeToSpans(range, event);
4994	},
4995
4996
4997
4998	// Converts an array of event objects into an array of event segment objects.
4999	// A custom `segSliceFunc` may be given for arbitrarily slicing up events.
5000	// Doesn't guarantee an order for the resulting array.
5001	eventsToSegs: function(allEvents, segSliceFunc) {
5002		var _this = this;
5003		var eventsById = groupEventsById(allEvents);
5004		var segs = [];
5005
5006		$.each(eventsById, function(id, events) {
5007			var ranges = [];
5008			var i;
5009
5010			for (i = 0; i < events.length; i++) {
5011				ranges.push(_this.eventToRange(events[i]));
5012			}
5013
5014			// inverse-background events (utilize only the first event in calculations)
5015			if (isInverseBgEvent(events[0])) {
5016				ranges = _this.invertRanges(ranges);
5017
5018				for (i = 0; i < ranges.length; i++) {
5019					segs.push.apply(segs, // append to
5020						_this.eventRangeToSegs(ranges[i], events[0], segSliceFunc));
5021				}
5022			}
5023			// normal event ranges
5024			else {
5025				for (i = 0; i < ranges.length; i++) {
5026					segs.push.apply(segs, // append to
5027						_this.eventRangeToSegs(ranges[i], events[i], segSliceFunc));
5028				}
5029			}
5030		});
5031
5032		return segs;
5033	},
5034
5035
5036	// Generates the unzoned start/end dates an event appears to occupy
5037	// Can accept an event "location" as well (which only has start/end and no allDay)
5038	eventToRange: function(event) {
5039		return {
5040			start: event.start.clone().stripZone(),
5041			end: (
5042				event.end ?
5043					event.end.clone() :
5044					// derive the end from the start and allDay. compute allDay if necessary
5045					this.view.calendar.getDefaultEventEnd(
5046						event.allDay != null ?
5047							event.allDay :
5048							!event.start.hasTime(),
5049						event.start
5050					)
5051			).stripZone()
5052		};
5053	},
5054
5055
5056	// Given an event's range (unzoned start/end), and the event itself,
5057	// slice into segments (using the segSliceFunc function if specified)
5058	eventRangeToSegs: function(range, event, segSliceFunc) {
5059		var spans = this.eventRangeToSpans(range, event);
5060		var segs = [];
5061		var i;
5062
5063		for (i = 0; i < spans.length; i++) {
5064			segs.push.apply(segs, // append to
5065				this.eventSpanToSegs(spans[i], event, segSliceFunc));
5066		}
5067
5068		return segs;
5069	},
5070
5071
5072	// Given an event's unzoned date range, return an array of "span" objects.
5073	// Subclasses can override.
5074	eventRangeToSpans: function(range, event) {
5075		return [ $.extend({}, range) ]; // copy into a single-item array
5076	},
5077
5078
5079	// Given an event's span (unzoned start/end and other misc data), and the event itself,
5080	// slices into segments and attaches event-derived properties to them.
5081	eventSpanToSegs: function(span, event, segSliceFunc) {
5082		var segs = segSliceFunc ? segSliceFunc(span) : this.spanToSegs(span);
5083		var i, seg;
5084
5085		for (i = 0; i < segs.length; i++) {
5086			seg = segs[i];
5087			seg.event = event;
5088			seg.eventStartMS = +span.start; // TODO: not the best name after making spans unzoned
5089			seg.eventDurationMS = span.end - span.start;
5090		}
5091
5092		return segs;
5093	},
5094
5095
5096	// Produces a new array of range objects that will cover all the time NOT covered by the given ranges.
5097	// SIDE EFFECT: will mutate the given array and will use its date references.
5098	invertRanges: function(ranges) {
5099		var view = this.view;
5100		var viewStart = view.start.clone(); // need a copy
5101		var viewEnd = view.end.clone(); // need a copy
5102		var inverseRanges = [];
5103		var start = viewStart; // the end of the previous range. the start of the new range
5104		var i, range;
5105
5106		// ranges need to be in order. required for our date-walking algorithm
5107		ranges.sort(compareRanges);
5108
5109		for (i = 0; i < ranges.length; i++) {
5110			range = ranges[i];
5111
5112			// add the span of time before the event (if there is any)
5113			if (range.start > start) { // compare millisecond time (skip any ambig logic)
5114				inverseRanges.push({
5115					start: start,
5116					end: range.start
5117				});
5118			}
5119
5120			start = range.end;
5121		}
5122
5123		// add the span of time after the last event (if there is any)
5124		if (start < viewEnd) { // compare millisecond time (skip any ambig logic)
5125			inverseRanges.push({
5126				start: start,
5127				end: viewEnd
5128			});
5129		}
5130
5131		return inverseRanges;
5132	},
5133
5134
5135	sortEventSegs: function(segs) {
5136		segs.sort(proxy(this, 'compareEventSegs'));
5137	},
5138
5139
5140	// A cmp function for determining which segments should take visual priority
5141	compareEventSegs: function(seg1, seg2) {
5142		return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first
5143			seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first
5144			seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1)
5145			compareByFieldSpecs(seg1.event, seg2.event, this.view.eventOrderSpecs);
5146	}
5147
5148});
5149
5150
5151/* Utilities
5152----------------------------------------------------------------------------------------------------------------------*/
5153
5154
5155function isBgEvent(event) { // returns true if background OR inverse-background
5156	var rendering = getEventRendering(event);
5157	return rendering === 'background' || rendering === 'inverse-background';
5158}
5159FC.isBgEvent = isBgEvent; // export
5160
5161
5162function isInverseBgEvent(event) {
5163	return getEventRendering(event) === 'inverse-background';
5164}
5165
5166
5167function getEventRendering(event) {
5168	return firstDefined((event.source || {}).rendering, event.rendering);
5169}
5170
5171
5172function groupEventsById(events) {
5173	var eventsById = {};
5174	var i, event;
5175
5176	for (i = 0; i < events.length; i++) {
5177		event = events[i];
5178		(eventsById[event._id] || (eventsById[event._id] = [])).push(event);
5179	}
5180
5181	return eventsById;
5182}
5183
5184
5185// A cmp function for determining which non-inverted "ranges" (see above) happen earlier
5186function compareRanges(range1, range2) {
5187	return range1.start - range2.start; // earlier ranges go first
5188}
5189
5190
5191/* External-Dragging-Element Data
5192----------------------------------------------------------------------------------------------------------------------*/
5193
5194// Require all HTML5 data-* attributes used by FullCalendar to have this prefix.
5195// A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event.
5196FC.dataAttrPrefix = '';
5197
5198// Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure
5199// to be used for Event Object creation.
5200// A defined `.eventProps`, even when empty, indicates that an event should be created.
5201function getDraggedElMeta(el) {
5202	var prefix = FC.dataAttrPrefix;
5203	var eventProps; // properties for creating the event, not related to date/time
5204	var startTime; // a Duration
5205	var duration;
5206	var stick;
5207
5208	if (prefix) { prefix += '-'; }
5209	eventProps = el.data(prefix + 'event') || null;
5210
5211	if (eventProps) {
5212		if (typeof eventProps === 'object') {
5213			eventProps = $.extend({}, eventProps); // make a copy
5214		}
5215		else { // something like 1 or true. still signal event creation
5216			eventProps = {};
5217		}
5218
5219		// pluck special-cased date/time properties
5220		startTime = eventProps.start;
5221		if (startTime == null) { startTime = eventProps.time; } // accept 'time' as well
5222		duration = eventProps.duration;
5223		stick = eventProps.stick;
5224		delete eventProps.start;
5225		delete eventProps.time;
5226		delete eventProps.duration;
5227		delete eventProps.stick;
5228	}
5229
5230	// fallback to standalone attribute values for each of the date/time properties
5231	if (startTime == null) { startTime = el.data(prefix + 'start'); }
5232	if (startTime == null) { startTime = el.data(prefix + 'time'); } // accept 'time' as well
5233	if (duration == null) { duration = el.data(prefix + 'duration'); }
5234	if (stick == null) { stick = el.data(prefix + 'stick'); }
5235
5236	// massage into correct data types
5237	startTime = startTime != null ? moment.duration(startTime) : null;
5238	duration = duration != null ? moment.duration(duration) : null;
5239	stick = Boolean(stick);
5240
5241	return { eventProps: eventProps, startTime: startTime, duration: duration, stick: stick };
5242}
5243
5244
5245;;
5246
5247/*
5248A set of rendering and date-related methods for a visual component comprised of one or more rows of day columns.
5249Prerequisite: the object being mixed into needs to be a *Grid*
5250*/
5251var DayTableMixin = FC.DayTableMixin = {
5252
5253	breakOnWeeks: false, // should create a new row for each week?
5254	dayDates: null, // whole-day dates for each column. left to right
5255	dayIndices: null, // for each day from start, the offset
5256	daysPerRow: null,
5257	rowCnt: null,
5258	colCnt: null,
5259	colHeadFormat: null,
5260
5261
5262	// Populates internal variables used for date calculation and rendering
5263	updateDayTable: function() {
5264		var view = this.view;
5265		var date = this.start.clone();
5266		var dayIndex = -1;
5267		var dayIndices = [];
5268		var dayDates = [];
5269		var daysPerRow;
5270		var firstDay;
5271		var rowCnt;
5272
5273		while (date.isBefore(this.end)) { // loop each day from start to end
5274			if (view.isHiddenDay(date)) {
5275				dayIndices.push(dayIndex + 0.5); // mark that it's between indices
5276			}
5277			else {
5278				dayIndex++;
5279				dayIndices.push(dayIndex);
5280				dayDates.push(date.clone());
5281			}
5282			date.add(1, 'days');
5283		}
5284
5285		if (this.breakOnWeeks) {
5286			// count columns until the day-of-week repeats
5287			firstDay = dayDates[0].day();
5288			for (daysPerRow = 1; daysPerRow < dayDates.length; daysPerRow++) {
5289				if (dayDates[daysPerRow].day() == firstDay) {
5290					break;
5291				}
5292			}
5293			rowCnt = Math.ceil(dayDates.length / daysPerRow);
5294		}
5295		else {
5296			rowCnt = 1;
5297			daysPerRow = dayDates.length;
5298		}
5299
5300		this.dayDates = dayDates;
5301		this.dayIndices = dayIndices;
5302		this.daysPerRow = daysPerRow;
5303		this.rowCnt = rowCnt;
5304		
5305		this.updateDayTableCols();
5306	},
5307
5308
5309	// Computes and assigned the colCnt property and updates any options that may be computed from it
5310	updateDayTableCols: function() {
5311		this.colCnt = this.computeColCnt();
5312		this.colHeadFormat = this.view.opt('columnFormat') || this.computeColHeadFormat();
5313	},
5314
5315
5316	// Determines how many columns there should be in the table
5317	computeColCnt: function() {
5318		return this.daysPerRow;
5319	},
5320
5321
5322	// Computes the ambiguously-timed moment for the given cell
5323	getCellDate: function(row, col) {
5324		return this.dayDates[
5325				this.getCellDayIndex(row, col)
5326			].clone();
5327	},
5328
5329
5330	// Computes the ambiguously-timed date range for the given cell
5331	getCellRange: function(row, col) {
5332		var start = this.getCellDate(row, col);
5333		var end = start.clone().add(1, 'days');
5334
5335		return { start: start, end: end };
5336	},
5337
5338
5339	// Returns the number of day cells, chronologically, from the first of the grid (0-based)
5340	getCellDayIndex: function(row, col) {
5341		return row * this.daysPerRow + this.getColDayIndex(col);
5342	},
5343
5344
5345	// Returns the numner of day cells, chronologically, from the first cell in *any given row*
5346	getColDayIndex: function(col) {
5347		if (this.isRTL) {
5348			return this.colCnt - 1 - col;
5349		}
5350		else {
5351			return col;
5352		}
5353	},
5354
5355
5356	// Given a date, returns its chronolocial cell-index from the first cell of the grid.
5357	// If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets.
5358	// If before the first offset, returns a negative number.
5359	// If after the last offset, returns an offset past the last cell offset.
5360	// Only works for *start* dates of cells. Will not work for exclusive end dates for cells.
5361	getDateDayIndex: function(date) {
5362		var dayIndices = this.dayIndices;
5363		var dayOffset = date.diff(this.start, 'days');
5364
5365		if (dayOffset < 0) {
5366			return dayIndices[0] - 1;
5367		}
5368		else if (dayOffset >= dayIndices.length) {
5369			return dayIndices[dayIndices.length - 1] + 1;
5370		}
5371		else {
5372			return dayIndices[dayOffset];
5373		}
5374	},
5375
5376
5377	/* Options
5378	------------------------------------------------------------------------------------------------------------------*/
5379
5380
5381	// Computes a default column header formatting string if `colFormat` is not explicitly defined
5382	computeColHeadFormat: function() {
5383		// if more than one week row, or if there are a lot of columns with not much space,
5384		// put just the day numbers will be in each cell
5385		if (this.rowCnt > 1 || this.colCnt > 10) {
5386			return 'ddd'; // "Sat"
5387		}
5388		// multiple days, so full single date string WON'T be in title text
5389		else if (this.colCnt > 1) {
5390			return this.view.opt('dayOfMonthFormat'); // "Sat 12/10"
5391		}
5392		// single day, so full single date string will probably be in title text
5393		else {
5394			return 'dddd'; // "Saturday"
5395		}
5396	},
5397
5398
5399	/* Slicing
5400	------------------------------------------------------------------------------------------------------------------*/
5401
5402
5403	// Slices up a date range into a segment for every week-row it intersects with
5404	sliceRangeByRow: function(range) {
5405		var daysPerRow = this.daysPerRow;
5406		var normalRange = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold
5407		var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index
5408		var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index
5409		var segs = [];
5410		var row;
5411		var rowFirst, rowLast; // inclusive day-index range for current row
5412		var segFirst, segLast; // inclusive day-index range for segment
5413
5414		for (row = 0; row < this.rowCnt; row++) {
5415			rowFirst = row * daysPerRow;
5416			rowLast = rowFirst + daysPerRow - 1;
5417
5418			// intersect segment's offset range with the row's
5419			segFirst = Math.max(rangeFirst, rowFirst);
5420			segLast = Math.min(rangeLast, rowLast);
5421
5422			// deal with in-between indices
5423			segFirst = Math.ceil(segFirst); // in-between starts round to next cell
5424			segLast = Math.floor(segLast); // in-between ends round to prev cell
5425
5426			if (segFirst <= segLast) { // was there any intersection with the current row?
5427				segs.push({
5428					row: row,
5429
5430					// normalize to start of row
5431					firstRowDayIndex: segFirst - rowFirst,
5432					lastRowDayIndex: segLast - rowFirst,
5433
5434					// must be matching integers to be the segment's start/end
5435					isStart: segFirst === rangeFirst,
5436					isEnd: segLast === rangeLast
5437				});
5438			}
5439		}
5440
5441		return segs;
5442	},
5443
5444
5445	// Slices up a date range into a segment for every day-cell it intersects with.
5446	// TODO: make more DRY with sliceRangeByRow somehow.
5447	sliceRangeByDay: function(range) {
5448		var daysPerRow = this.daysPerRow;
5449		var normalRange = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold
5450		var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index
5451		var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index
5452		var segs = [];
5453		var row;
5454		var rowFirst, rowLast; // inclusive day-index range for current row
5455		var i;
5456		var segFirst, segLast; // inclusive day-index range for segment
5457
5458		for (row = 0; row < this.rowCnt; row++) {
5459			rowFirst = row * daysPerRow;
5460			rowLast = rowFirst + daysPerRow - 1;
5461
5462			for (i = rowFirst; i <= rowLast; i++) {
5463
5464				// intersect segment's offset range with the row's
5465				segFirst = Math.max(rangeFirst, i);
5466				segLast = Math.min(rangeLast, i);
5467
5468				// deal with in-between indices
5469				segFirst = Math.ceil(segFirst); // in-between starts round to next cell
5470				segLast = Math.floor(segLast); // in-between ends round to prev cell
5471
5472				if (segFirst <= segLast) { // was there any intersection with the current row?
5473					segs.push({
5474						row: row,
5475
5476						// normalize to start of row
5477						firstRowDayIndex: segFirst - rowFirst,
5478						lastRowDayIndex: segLast - rowFirst,
5479
5480						// must be matching integers to be the segment's start/end
5481						isStart: segFirst === rangeFirst,
5482						isEnd: segLast === rangeLast
5483					});
5484				}
5485			}
5486		}
5487
5488		return segs;
5489	},
5490
5491
5492	/* Header Rendering
5493	------------------------------------------------------------------------------------------------------------------*/
5494
5495
5496	renderHeadHtml: function() {
5497		var view = this.view;
5498
5499		return '' +
5500			'<div class="fc-row ' + view.widgetHeaderClass + '">' +
5501				'<table>' +
5502					'<thead>' +
5503						this.renderHeadTrHtml() +
5504					'</thead>' +
5505				'</table>' +
5506			'</div>';
5507	},
5508
5509
5510	renderHeadIntroHtml: function() {
5511		return this.renderIntroHtml(); // fall back to generic
5512	},
5513
5514
5515	renderHeadTrHtml: function() {
5516		return '' +
5517			'<tr>' +
5518				(this.isRTL ? '' : this.renderHeadIntroHtml()) +
5519				this.renderHeadDateCellsHtml() +
5520				(this.isRTL ? this.renderHeadIntroHtml() : '') +
5521			'</tr>';
5522	},
5523
5524
5525	renderHeadDateCellsHtml: function() {
5526		var htmls = [];
5527		var col, date;
5528
5529		for (col = 0; col < this.colCnt; col++) {
5530			date = this.getCellDate(0, col);
5531			htmls.push(this.renderHeadDateCellHtml(date));
5532		}
5533
5534		return htmls.join('');
5535	},
5536
5537
5538	// TODO: when internalApiVersion, accept an object for HTML attributes
5539	// (colspan should be no different)
5540	renderHeadDateCellHtml: function(date, colspan, otherAttrs) {
5541		var view = this.view;
5542
5543		return '' +
5544			'<th class="fc-day-header ' + view.widgetHeaderClass + ' fc-' + dayIDs[date.day()] + '"' +
5545				(this.rowCnt == 1 ?
5546					' data-date="' + date.format('YYYY-MM-DD') + '"' :
5547					'') +
5548				(colspan > 1 ?
5549					' colspan="' + colspan + '"' :
5550					'') +
5551				(otherAttrs ?
5552					' ' + otherAttrs :
5553					'') +
5554			'>' +
5555				htmlEscape(date.format(this.colHeadFormat)) +
5556			'</th>';
5557	},
5558
5559
5560	/* Background Rendering
5561	------------------------------------------------------------------------------------------------------------------*/
5562
5563
5564	renderBgTrHtml: function(row) {
5565		return '' +
5566			'<tr>' +
5567				(this.isRTL ? '' : this.renderBgIntroHtml(row)) +
5568				this.renderBgCellsHtml(row) +
5569				(this.isRTL ? this.renderBgIntroHtml(row) : '') +
5570			'</tr>';
5571	},
5572
5573
5574	renderBgIntroHtml: function(row) {
5575		return this.renderIntroHtml(); // fall back to generic
5576	},
5577
5578
5579	renderBgCellsHtml: function(row) {
5580		var htmls = [];
5581		var col, date;
5582
5583		for (col = 0; col < this.colCnt; col++) {
5584			date = this.getCellDate(row, col);
5585			htmls.push(this.renderBgCellHtml(date));
5586		}
5587
5588		return htmls.join('');
5589	},
5590
5591
5592	renderBgCellHtml: function(date, otherAttrs) {
5593		var view = this.view;
5594		var classes = this.getDayClasses(date);
5595
5596		classes.unshift('fc-day', view.widgetContentClass);
5597
5598		return '<td class="' + classes.join(' ') + '"' +
5599			' data-date="' + date.format('YYYY-MM-DD') + '"' + // if date has a time, won't format it
5600			(otherAttrs ?
5601				' ' + otherAttrs :
5602				'') +
5603			'></td>';
5604	},
5605
5606
5607	/* Generic
5608	------------------------------------------------------------------------------------------------------------------*/
5609
5610
5611	// Generates the default HTML intro for any row. User classes should override
5612	renderIntroHtml: function() {
5613	},
5614
5615
5616	// TODO: a generic method for dealing with <tr>, RTL, intro
5617	// when increment internalApiVersion
5618	// wrapTr (scheduler)
5619
5620
5621	/* Utils
5622	------------------------------------------------------------------------------------------------------------------*/
5623
5624
5625	// Applies the generic "intro" and "outro" HTML to the given cells.
5626	// Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro.
5627	bookendCells: function(trEl) {
5628		var introHtml = this.renderIntroHtml();
5629
5630		if (introHtml) {
5631			if (this.isRTL) {
5632				trEl.append(introHtml);
5633			}
5634			else {
5635				trEl.prepend(introHtml);
5636			}
5637		}
5638	}
5639
5640};
5641
5642;;
5643
5644/* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week.
5645----------------------------------------------------------------------------------------------------------------------*/
5646
5647var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
5648
5649	numbersVisible: false, // should render a row for day/week numbers? set by outside view. TODO: make internal
5650	bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid
5651
5652	rowEls: null, // set of fake row elements
5653	cellEls: null, // set of whole-day elements comprising the row's background
5654	helperEls: null, // set of cell skeleton elements for rendering the mock event "helper"
5655
5656	rowCoordCache: null,
5657	colCoordCache: null,
5658
5659
5660	// Renders the rows and columns into the component's `this.el`, which should already be assigned.
5661	// isRigid determins whether the individual rows should ignore the contents and be a constant height.
5662	// Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient.
5663	renderDates: function(isRigid) {
5664		var view = this.view;
5665		var rowCnt = this.rowCnt;
5666		var colCnt = this.colCnt;
5667		var html = '';
5668		var row;
5669		var col;
5670
5671		for (row = 0; row < rowCnt; row++) {
5672			html += this.renderDayRowHtml(row, isRigid);
5673		}
5674		this.el.html(html);
5675
5676		this.rowEls = this.el.find('.fc-row');
5677		this.cellEls = this.el.find('.fc-day');
5678
5679		this.rowCoordCache = new CoordCache({
5680			els: this.rowEls,
5681			isVertical: true
5682		});
5683		this.colCoordCache = new CoordCache({
5684			els: this.cellEls.slice(0, this.colCnt), // only the first row
5685			isHorizontal: true
5686		});
5687
5688		// trigger dayRender with each cell's element
5689		for (row = 0; row < rowCnt; row++) {
5690			for (col = 0; col < colCnt; col++) {
5691				view.trigger(
5692					'dayRender',
5693					null,
5694					this.getCellDate(row, col),
5695					this.getCellEl(row, col)
5696				);
5697			}
5698		}
5699	},
5700
5701
5702	unrenderDates: function() {
5703		this.removeSegPopover();
5704	},
5705
5706
5707	renderBusinessHours: function() {
5708		var segs = this.buildBusinessHourSegs(true); // wholeDay=true
5709		this.renderFill('businessHours', segs, 'bgevent');
5710	},
5711
5712
5713	unrenderBusinessHours: function() {
5714		this.unrenderFill('businessHours');
5715	},
5716
5717
5718	// Generates the HTML for a single row, which is a div that wraps a table.
5719	// `row` is the row number.
5720	renderDayRowHtml: function(row, isRigid) {
5721		var view = this.view;
5722		var classes = [ 'fc-row', 'fc-week', view.widgetContentClass ];
5723
5724		if (isRigid) {
5725			classes.push('fc-rigid');
5726		}
5727
5728		return '' +
5729			'<div class="' + classes.join(' ') + '">' +
5730				'<div class="fc-bg">' +
5731					'<table>' +
5732						this.renderBgTrHtml(row) +
5733					'</table>' +
5734				'</div>' +
5735				'<div class="fc-content-skeleton">' +
5736					'<table>' +
5737						(this.numbersVisible ?
5738							'<thead>' +
5739								this.renderNumberTrHtml(row) +
5740							'</thead>' :
5741							''
5742							) +
5743					'</table>' +
5744				'</div>' +
5745			'</div>';
5746	},
5747
5748
5749	/* Grid Number Rendering
5750	------------------------------------------------------------------------------------------------------------------*/
5751
5752
5753	renderNumberTrHtml: function(row) {
5754		return '' +
5755			'<tr>' +
5756				(this.isRTL ? '' : this.renderNumberIntroHtml(row)) +
5757				this.renderNumberCellsHtml(row) +
5758				(this.isRTL ? this.renderNumberIntroHtml(row) : '') +
5759			'</tr>';
5760	},
5761
5762
5763	renderNumberIntroHtml: function(row) {
5764		return this.renderIntroHtml();
5765	},
5766
5767
5768	renderNumberCellsHtml: function(row) {
5769		var htmls = [];
5770		var col, date;
5771
5772		for (col = 0; col < this.colCnt; col++) {
5773			date = this.getCellDate(row, col);
5774			htmls.push(this.renderNumberCellHtml(date));
5775		}
5776
5777		return htmls.join('');
5778	},
5779
5780
5781	// Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton.
5782	// The number row will only exist if either day numbers or week numbers are turned on.
5783	renderNumberCellHtml: function(date) {
5784		var classes;
5785
5786		if (!this.view.dayNumbersVisible) { // if there are week numbers but not day numbers
5787			return '<td/>'; //  will create an empty space above events :(
5788		}
5789
5790		classes = this.getDayClasses(date);
5791		classes.unshift('fc-day-number');
5792
5793		return '' +
5794			'<td class="' + classes.join(' ') + '" data-date="' + date.format() + '">' +
5795				date.date() +
5796			'</td>';
5797	},
5798
5799
5800	/* Options
5801	------------------------------------------------------------------------------------------------------------------*/
5802
5803
5804	// Computes a default event time formatting string if `timeFormat` is not explicitly defined
5805	computeEventTimeFormat: function() {
5806		return this.view.opt('extraSmallTimeFormat'); // like "6p" or "6:30p"
5807	},
5808
5809
5810	// Computes a default `displayEventEnd` value if one is not expliclty defined
5811	computeDisplayEventEnd: function() {
5812		return this.colCnt == 1; // we'll likely have space if there's only one day
5813	},
5814
5815
5816	/* Dates
5817	------------------------------------------------------------------------------------------------------------------*/
5818
5819
5820	rangeUpdated: function() {
5821		this.updateDayTable();
5822	},
5823
5824
5825	// Slices up the given span (unzoned start/end with other misc data) into an array of segments
5826	spanToSegs: function(span) {
5827		var segs = this.sliceRangeByRow(span);
5828		var i, seg;
5829
5830		for (i = 0; i < segs.length; i++) {
5831			seg = segs[i];
5832			if (this.isRTL) {
5833				seg.leftCol = this.daysPerRow - 1 - seg.lastRowDayIndex;
5834				seg.rightCol = this.daysPerRow - 1 - seg.firstRowDayIndex;
5835			}
5836			else {
5837				seg.leftCol = seg.firstRowDayIndex;
5838				seg.rightCol = seg.lastRowDayIndex;
5839			}
5840		}
5841
5842		return segs;
5843	},
5844
5845
5846	/* Hit System
5847	------------------------------------------------------------------------------------------------------------------*/
5848
5849
5850	prepareHits: function() {
5851		this.colCoordCache.build();
5852		this.rowCoordCache.build();
5853		this.rowCoordCache.bottoms[this.rowCnt - 1] += this.bottomCoordPadding; // hack
5854	},
5855
5856
5857	releaseHits: function() {
5858		this.colCoordCache.clear();
5859		this.rowCoordCache.clear();
5860	},
5861
5862
5863	queryHit: function(leftOffset, topOffset) {
5864		if (this.colCoordCache.isLeftInBounds(leftOffset) && this.rowCoordCache.isTopInBounds(topOffset)) {
5865			var col = this.colCoordCache.getHorizontalIndex(leftOffset);
5866			var row = this.rowCoordCache.getVerticalIndex(topOffset);
5867
5868			if (row != null && col != null) {
5869				return this.getCellHit(row, col);
5870			}
5871		}
5872	},
5873
5874
5875	getHitSpan: function(hit) {
5876		return this.getCellRange(hit.row, hit.col);
5877	},
5878
5879
5880	getHitEl: function(hit) {
5881		return this.getCellEl(hit.row, hit.col);
5882	},
5883
5884
5885	/* Cell System
5886	------------------------------------------------------------------------------------------------------------------*/
5887	// FYI: the first column is the leftmost column, regardless of date
5888
5889
5890	getCellHit: function(row, col) {
5891		return {
5892			row: row,
5893			col: col,
5894			component: this, // needed unfortunately :(
5895			left: this.colCoordCache.getLeftOffset(col),
5896			right: this.colCoordCache.getRightOffset(col),
5897			top: this.rowCoordCache.getTopOffset(row),
5898			bottom: this.rowCoordCache.getBottomOffset(row)
5899		};
5900	},
5901
5902
5903	getCellEl: function(row, col) {
5904		return this.cellEls.eq(row * this.colCnt + col);
5905	},
5906
5907
5908	/* Event Drag Visualization
5909	------------------------------------------------------------------------------------------------------------------*/
5910	// TODO: move to DayGrid.event, similar to what we did with Grid's drag methods
5911
5912
5913	// Renders a visual indication of an event or external element being dragged.
5914	// `eventLocation` has zoned start and end (optional)
5915	renderDrag: function(eventLocation, seg) {
5916
5917		// always render a highlight underneath
5918		this.renderHighlight(this.eventToSpan(eventLocation));
5919
5920		// if a segment from the same calendar but another component is being dragged, render a helper event
5921		if (seg && seg.component !== this) {
5922			return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements
5923		}
5924	},
5925
5926
5927	// Unrenders any visual indication of a hovering event
5928	unrenderDrag: function() {
5929		this.unrenderHighlight();
5930		this.unrenderHelper();
5931	},
5932
5933
5934	/* Event Resize Visualization
5935	------------------------------------------------------------------------------------------------------------------*/
5936
5937
5938	// Renders a visual indication of an event being resized
5939	renderEventResize: function(eventLocation, seg) {
5940		this.renderHighlight(this.eventToSpan(eventLocation));
5941		return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements
5942	},
5943
5944
5945	// Unrenders a visual indication of an event being resized
5946	unrenderEventResize: function() {
5947		this.unrenderHighlight();
5948		this.unrenderHelper();
5949	},
5950
5951
5952	/* Event Helper
5953	------------------------------------------------------------------------------------------------------------------*/
5954
5955
5956	// Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null.
5957	renderHelper: function(event, sourceSeg) {
5958		var helperNodes = [];
5959		var segs = this.eventToSegs(event);
5960		var rowStructs;
5961
5962		segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered
5963		rowStructs = this.renderSegRows(segs);
5964
5965		// inject each new event skeleton into each associated row
5966		this.rowEls.each(function(row, rowNode) {
5967			var rowEl = $(rowNode); // the .fc-row
5968			var skeletonEl = $('<div class="fc-helper-skeleton"><table/></div>'); // will be absolutely positioned
5969			var skeletonTop;
5970
5971			// If there is an original segment, match the top position. Otherwise, put it at the row's top level
5972			if (sourceSeg && sourceSeg.row === row) {
5973				skeletonTop = sourceSeg.el.position().top;
5974			}
5975			else {
5976				skeletonTop = rowEl.find('.fc-content-skeleton tbody').position().top;
5977			}
5978
5979			skeletonEl.css('top', skeletonTop)
5980				.find('table')
5981					.append(rowStructs[row].tbodyEl);
5982
5983			rowEl.append(skeletonEl);
5984			helperNodes.push(skeletonEl[0]);
5985		});
5986
5987		return ( // must return the elements rendered
5988			this.helperEls = $(helperNodes) // array -> jQuery set
5989		);
5990	},
5991
5992
5993	// Unrenders any visual indication of a mock helper event
5994	unrenderHelper: function() {
5995		if (this.helperEls) {
5996			this.helperEls.remove();
5997			this.helperEls = null;
5998		}
5999	},
6000
6001
6002	/* Fill System (highlight, background events, business hours)
6003	------------------------------------------------------------------------------------------------------------------*/
6004
6005
6006	fillSegTag: 'td', // override the default tag name
6007
6008
6009	// Renders a set of rectangles over the given segments of days.
6010	// Only returns segments that successfully rendered.
6011	renderFill: function(type, segs, className) {
6012		var nodes = [];
6013		var i, seg;
6014		var skeletonEl;
6015
6016		segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs
6017
6018		for (i = 0; i < segs.length; i++) {
6019			seg = segs[i];
6020			skeletonEl = this.renderFillRow(type, seg, className);
6021			this.rowEls.eq(seg.row).append(skeletonEl);
6022			nodes.push(skeletonEl[0]);
6023		}
6024
6025		this.elsByFill[type] = $(nodes);
6026
6027		return segs;
6028	},
6029
6030
6031	// Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered.
6032	renderFillRow: function(type, seg, className) {
6033		var colCnt = this.colCnt;
6034		var startCol = seg.leftCol;
6035		var endCol = seg.rightCol + 1;
6036		var skeletonEl;
6037		var trEl;
6038
6039		className = className || type.toLowerCase();
6040
6041		skeletonEl = $(
6042			'<div class="fc-' + className + '-skeleton">' +
6043				'<table><tr/></table>' +
6044			'</div>'
6045		);
6046		trEl = skeletonEl.find('tr');
6047
6048		if (startCol > 0) {
6049			trEl.append('<td colspan="' + startCol + '"/>');
6050		}
6051
6052		trEl.append(
6053			seg.el.attr('colspan', endCol - startCol)
6054		);
6055
6056		if (endCol < colCnt) {
6057			trEl.append('<td colspan="' + (colCnt - endCol) + '"/>');
6058		}
6059
6060		this.bookendCells(trEl);
6061
6062		return skeletonEl;
6063	}
6064
6065});
6066
6067;;
6068
6069/* Event-rendering methods for the DayGrid class
6070----------------------------------------------------------------------------------------------------------------------*/
6071
6072DayGrid.mixin({
6073
6074	rowStructs: null, // an array of objects, each holding information about a row's foreground event-rendering
6075
6076
6077	// Unrenders all events currently rendered on the grid
6078	unrenderEvents: function() {
6079		this.removeSegPopover(); // removes the "more.." events popover
6080		Grid.prototype.unrenderEvents.apply(this, arguments); // calls the super-method
6081	},
6082
6083
6084	// Retrieves all rendered segment objects currently rendered on the grid
6085	getEventSegs: function() {
6086		return Grid.prototype.getEventSegs.call(this) // get the segments from the super-method
6087			.concat(this.popoverSegs || []); // append the segments from the "more..." popover
6088	},
6089
6090
6091	// Renders the given background event segments onto the grid
6092	renderBgSegs: function(segs) {
6093
6094		// don't render timed background events
6095		var allDaySegs = $.grep(segs, function(seg) {
6096			return seg.event.allDay;
6097		});
6098
6099		return Grid.prototype.renderBgSegs.call(this, allDaySegs); // call the super-method
6100	},
6101
6102
6103	// Renders the given foreground event segments onto the grid
6104	renderFgSegs: function(segs) {
6105		var rowStructs;
6106
6107		// render an `.el` on each seg
6108		// returns a subset of the segs. segs that were actually rendered
6109		segs = this.renderFgSegEls(segs);
6110
6111		rowStructs = this.rowStructs = this.renderSegRows(segs);
6112
6113		// append to each row's content skeleton
6114		this.rowEls.each(function(i, rowNode) {
6115			$(rowNode).find('.fc-content-skeleton > table').append(
6116				rowStructs[i].tbodyEl
6117			);
6118		});
6119
6120		return segs; // return only the segs that were actually rendered
6121	},
6122
6123
6124	// Unrenders all currently rendered foreground event segments
6125	unrenderFgSegs: function() {
6126		var rowStructs = this.rowStructs || [];
6127		var rowStruct;
6128
6129		while ((rowStruct = rowStructs.pop())) {
6130			rowStruct.tbodyEl.remove();
6131		}
6132
6133		this.rowStructs = null;
6134	},
6135
6136
6137	// Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton.
6138	// Returns an array of rowStruct objects (see the bottom of `renderSegRow`).
6139	// PRECONDITION: each segment shoud already have a rendered and assigned `.el`
6140	renderSegRows: function(segs) {
6141		var rowStructs = [];
6142		var segRows;
6143		var row;
6144
6145		segRows = this.groupSegRows(segs); // group into nested arrays
6146
6147		// iterate each row of segment groupings
6148		for (row = 0; row < segRows.length; row++) {
6149			rowStructs.push(
6150				this.renderSegRow(row, segRows[row])
6151			);
6152		}
6153
6154		return rowStructs;
6155	},
6156
6157
6158	// Builds the HTML to be used for the default element for an individual segment
6159	fgSegHtml: function(seg, disableResizing) {
6160		var view = this.view;
6161		var event = seg.event;
6162		var isDraggable = view.isEventDraggable(event);
6163		var isResizableFromStart = !disableResizing && event.allDay &&
6164			seg.isStart && view.isEventResizableFromStart(event);
6165		var isResizableFromEnd = !disableResizing && event.allDay &&
6166			seg.isEnd && view.isEventResizableFromEnd(event);
6167		var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd);
6168		var skinCss = cssToStr(this.getSegSkinCss(seg));
6169		var timeHtml = '';
6170		var timeText;
6171		var titleHtml;
6172
6173		classes.unshift('fc-day-grid-event', 'fc-h-event');
6174
6175		// Only display a timed events time if it is the starting segment
6176		if (seg.isStart) {
6177			timeText = this.getEventTimeText(event);
6178			if (timeText) {
6179				timeHtml = '<span class="fc-time">' + htmlEscape(timeText) + '</span>';
6180			}
6181		}
6182
6183		titleHtml =
6184			'<span class="fc-title">' +
6185				(htmlEscape(event.title || '') || '&nbsp;') + // we always want one line of height
6186			'</span>';
6187		
6188		return '<a class="' + classes.join(' ') + '"' +
6189				(event.url ?
6190					' href="' + htmlEscape(event.url) + '"' :
6191					''
6192					) +
6193				(skinCss ?
6194					' style="' + skinCss + '"' :
6195					''
6196					) +
6197			'>' +
6198				'<div class="fc-content">' +
6199					(this.isRTL ?
6200						titleHtml + ' ' + timeHtml : // put a natural space in between
6201						timeHtml + ' ' + titleHtml   //
6202						) +
6203				'</div>' +
6204				(isResizableFromStart ?
6205					'<div class="fc-resizer fc-start-resizer" />' :
6206					''
6207					) +
6208				(isResizableFromEnd ?
6209					'<div class="fc-resizer fc-end-resizer" />' :
6210					''
6211					) +
6212			'</a>';
6213	},
6214
6215
6216	// Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains
6217	// the segments. Returns object with a bunch of internal data about how the render was calculated.
6218	// NOTE: modifies rowSegs
6219	renderSegRow: function(row, rowSegs) {
6220		var colCnt = this.colCnt;
6221		var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels
6222		var levelCnt = Math.max(1, segLevels.length); // ensure at least one level
6223		var tbody = $('<tbody/>');
6224		var segMatrix = []; // lookup for which segments are rendered into which level+col cells
6225		var cellMatrix = []; // lookup for all <td> elements of the level+col matrix
6226		var loneCellMatrix = []; // lookup for <td> elements that only take up a single column
6227		var i, levelSegs;
6228		var col;
6229		var tr;
6230		var j, seg;
6231		var td;
6232
6233		// populates empty cells from the current column (`col`) to `endCol`
6234		function emptyCellsUntil(endCol) {
6235			while (col < endCol) {
6236				// try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell
6237				td = (loneCellMatrix[i - 1] || [])[col];
6238				if (td) {
6239					td.attr(
6240						'rowspan',
6241						parseInt(td.attr('rowspan') || 1, 10) + 1
6242					);
6243				}
6244				else {
6245					td = $('<td/>');
6246					tr.append(td);
6247				}
6248				cellMatrix[i][col] = td;
6249				loneCellMatrix[i][col] = td;
6250				col++;
6251			}
6252		}
6253
6254		for (i = 0; i < levelCnt; i++) { // iterate through all levels
6255			levelSegs = segLevels[i];
6256			col = 0;
6257			tr = $('<tr/>');
6258
6259			segMatrix.push([]);
6260			cellMatrix.push([]);
6261			loneCellMatrix.push([]);
6262
6263			// levelCnt might be 1 even though there are no actual levels. protect against this.
6264			// this single empty row is useful for styling.
6265			if (levelSegs) {
6266				for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level
6267					seg = levelSegs[j];
6268
6269					emptyCellsUntil(seg.leftCol);
6270
6271					// create a container that occupies or more columns. append the event element.
6272					td = $('<td class="fc-event-container"/>').append(seg.el);
6273					if (seg.leftCol != seg.rightCol) {
6274						td.attr('colspan', seg.rightCol - seg.leftCol + 1);
6275					}
6276					else { // a single-column segment
6277						loneCellMatrix[i][col] = td;
6278					}
6279
6280					while (col <= seg.rightCol) {
6281						cellMatrix[i][col] = td;
6282						segMatrix[i][col] = seg;
6283						col++;
6284					}
6285
6286					tr.append(td);
6287				}
6288			}
6289
6290			emptyCellsUntil(colCnt); // finish off the row
6291			this.bookendCells(tr);
6292			tbody.append(tr);
6293		}
6294
6295		return { // a "rowStruct"
6296			row: row, // the row number
6297			tbodyEl: tbody,
6298			cellMatrix: cellMatrix,
6299			segMatrix: segMatrix,
6300			segLevels: segLevels,
6301			segs: rowSegs
6302		};
6303	},
6304
6305
6306	// Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels.
6307	// NOTE: modifies segs
6308	buildSegLevels: function(segs) {
6309		var levels = [];
6310		var i, seg;
6311		var j;
6312
6313		// Give preference to elements with certain criteria, so they have
6314		// a chance to be closer to the top.
6315		this.sortEventSegs(segs);
6316		
6317		for (i = 0; i < segs.length; i++) {
6318			seg = segs[i];
6319
6320			// loop through levels, starting with the topmost, until the segment doesn't collide with other segments
6321			for (j = 0; j < levels.length; j++) {
6322				if (!isDaySegCollision(seg, levels[j])) {
6323					break;
6324				}
6325			}
6326			// `j` now holds the desired subrow index
6327			seg.level = j;
6328
6329			// create new level array if needed and append segment
6330			(levels[j] || (levels[j] = [])).push(seg);
6331		}
6332
6333		// order segments left-to-right. very important if calendar is RTL
6334		for (j = 0; j < levels.length; j++) {
6335			levels[j].sort(compareDaySegCols);
6336		}
6337
6338		return levels;
6339	},
6340
6341
6342	// Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row
6343	groupSegRows: function(segs) {
6344		var segRows = [];
6345		var i;
6346
6347		for (i = 0; i < this.rowCnt; i++) {
6348			segRows.push([]);
6349		}
6350
6351		for (i = 0; i < segs.length; i++) {
6352			segRows[segs[i].row].push(segs[i]);
6353		}
6354
6355		return segRows;
6356	}
6357
6358});
6359
6360
6361// Computes whether two segments' columns collide. They are assumed to be in the same row.
6362function isDaySegCollision(seg, otherSegs) {
6363	var i, otherSeg;
6364
6365	for (i = 0; i < otherSegs.length; i++) {
6366		otherSeg = otherSegs[i];
6367
6368		if (
6369			otherSeg.leftCol <= seg.rightCol &&
6370			otherSeg.rightCol >= seg.leftCol
6371		) {
6372			return true;
6373		}
6374	}
6375
6376	return false;
6377}
6378
6379
6380// A cmp function for determining the leftmost event
6381function compareDaySegCols(a, b) {
6382	return a.leftCol - b.leftCol;
6383}
6384
6385;;
6386
6387/* Methods relate to limiting the number events for a given day on a DayGrid
6388----------------------------------------------------------------------------------------------------------------------*/
6389// NOTE: all the segs being passed around in here are foreground segs
6390
6391DayGrid.mixin({
6392
6393	segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible
6394	popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible
6395
6396
6397	removeSegPopover: function() {
6398		if (this.segPopover) {
6399			this.segPopover.hide(); // in handler, will call segPopover's removeElement
6400		}
6401	},
6402
6403
6404	// Limits the number of "levels" (vertically stacking layers of events) for each row of the grid.
6405	// `levelLimit` can be false (don't limit), a number, or true (should be computed).
6406	limitRows: function(levelLimit) {
6407		var rowStructs = this.rowStructs || [];
6408		var row; // row #
6409		var rowLevelLimit;
6410
6411		for (row = 0; row < rowStructs.length; row++) {
6412			this.unlimitRow(row);
6413
6414			if (!levelLimit) {
6415				rowLevelLimit = false;
6416			}
6417			else if (typeof levelLimit === 'number') {
6418				rowLevelLimit = levelLimit;
6419			}
6420			else {
6421				rowLevelLimit = this.computeRowLevelLimit(row);
6422			}
6423
6424			if (rowLevelLimit !== false) {
6425				this.limitRow(row, rowLevelLimit);
6426			}
6427		}
6428	},
6429
6430
6431	// Computes the number of levels a row will accomodate without going outside its bounds.
6432	// Assumes the row is "rigid" (maintains a constant height regardless of what is inside).
6433	// `row` is the row number.
6434	computeRowLevelLimit: function(row) {
6435		var rowEl = this.rowEls.eq(row); // the containing "fake" row div
6436		var rowHeight = rowEl.height(); // TODO: cache somehow?
6437		var trEls = this.rowStructs[row].tbodyEl.children();
6438		var i, trEl;
6439		var trHeight;
6440
6441		function iterInnerHeights(i, childNode) {
6442			trHeight = Math.max(trHeight, $(childNode).outerHeight());
6443		}
6444
6445		// Reveal one level <tr> at a time and stop when we find one out of bounds
6446		for (i = 0; i < trEls.length; i++) {
6447			trEl = trEls.eq(i).removeClass('fc-limited'); // reset to original state (reveal)
6448
6449			// with rowspans>1 and IE8, trEl.outerHeight() would return the height of the largest cell,
6450			// so instead, find the tallest inner content element.
6451			trHeight = 0;
6452			trEl.find('> td > :first-child').each(iterInnerHeights);
6453
6454			if (trEl.position().top + trHeight > rowHeight) {
6455				return i;
6456			}
6457		}
6458
6459		return false; // should not limit at all
6460	},
6461
6462
6463	// Limits the given grid row to the maximum number of levels and injects "more" links if necessary.
6464	// `row` is the row number.
6465	// `levelLimit` is a number for the maximum (inclusive) number of levels allowed.
6466	limitRow: function(row, levelLimit) {
6467		var _this = this;
6468		var rowStruct = this.rowStructs[row];
6469		var moreNodes = []; // array of "more" <a> links and <td> DOM nodes
6470		var col = 0; // col #, left-to-right (not chronologically)
6471		var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right
6472		var cellMatrix; // a matrix (by level, then column) of all <td> jQuery elements in the row
6473		var limitedNodes; // array of temporarily hidden level <tr> and segment <td> DOM nodes
6474		var i, seg;
6475		var segsBelow; // array of segment objects below `seg` in the current `col`
6476		var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies
6477		var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column)
6478		var td, rowspan;
6479		var segMoreNodes; // array of "more" <td> cells that will stand-in for the current seg's cell
6480		var j;
6481		var moreTd, moreWrap, moreLink;
6482
6483		// Iterates through empty level cells and places "more" links inside if need be
6484		function emptyCellsUntil(endCol) { // goes from current `col` to `endCol`
6485			while (col < endCol) {
6486				segsBelow = _this.getCellSegs(row, col, levelLimit);
6487				if (segsBelow.length) {
6488					td = cellMatrix[levelLimit - 1][col];
6489					moreLink = _this.renderMoreLink(row, col, segsBelow);
6490					moreWrap = $('<div/>').append(moreLink);
6491					td.append(moreWrap);
6492					moreNodes.push(moreWrap[0]);
6493				}
6494				col++;
6495			}
6496		}
6497
6498		if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit?
6499			levelSegs = rowStruct.segLevels[levelLimit - 1];
6500			cellMatrix = rowStruct.cellMatrix;
6501
6502			limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level <tr> elements past the limit
6503				.addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array
6504
6505			// iterate though segments in the last allowable level
6506			for (i = 0; i < levelSegs.length; i++) {
6507				seg = levelSegs[i];
6508				emptyCellsUntil(seg.leftCol); // process empty cells before the segment
6509
6510				// determine *all* segments below `seg` that occupy the same columns
6511				colSegsBelow = [];
6512				totalSegsBelow = 0;
6513				while (col <= seg.rightCol) {
6514					segsBelow = this.getCellSegs(row, col, levelLimit);
6515					colSegsBelow.push(segsBelow);
6516					totalSegsBelow += segsBelow.length;
6517					col++;
6518				}
6519
6520				if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links?
6521					td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell
6522					rowspan = td.attr('rowspan') || 1;
6523					segMoreNodes = [];
6524
6525					// make a replacement <td> for each column the segment occupies. will be one for each colspan
6526					for (j = 0; j < colSegsBelow.length; j++) {
6527						moreTd = $('<td class="fc-more-cell"/>').attr('rowspan', rowspan);
6528						segsBelow = colSegsBelow[j];
6529						moreLink = this.renderMoreLink(
6530							row,
6531							seg.leftCol + j,
6532							[ seg ].concat(segsBelow) // count seg as hidden too
6533						);
6534						moreWrap = $('<div/>').append(moreLink);
6535						moreTd.append(moreWrap);
6536						segMoreNodes.push(moreTd[0]);
6537						moreNodes.push(moreTd[0]);
6538					}
6539
6540					td.addClass('fc-limited').after($(segMoreNodes)); // hide original <td> and inject replacements
6541					limitedNodes.push(td[0]);
6542				}
6543			}
6544
6545			emptyCellsUntil(this.colCnt); // finish off the level
6546			rowStruct.moreEls = $(moreNodes); // for easy undoing later
6547			rowStruct.limitedEls = $(limitedNodes); // for easy undoing later
6548		}
6549	},
6550
6551
6552	// Reveals all levels and removes all "more"-related elements for a grid's row.
6553	// `row` is a row number.
6554	unlimitRow: function(row) {
6555		var rowStruct = this.rowStructs[row];
6556
6557		if (rowStruct.moreEls) {
6558			rowStruct.moreEls.remove();
6559			rowStruct.moreEls = null;
6560		}
6561
6562		if (rowStruct.limitedEls) {
6563			rowStruct.limitedEls.removeClass('fc-limited');
6564			rowStruct.limitedEls = null;
6565		}
6566	},
6567
6568
6569	// Renders an <a> element that represents hidden event element for a cell.
6570	// Responsible for attaching click handler as well.
6571	renderMoreLink: function(row, col, hiddenSegs) {
6572		var _this = this;
6573		var view = this.view;
6574
6575		return $('<a class="fc-more"/>')
6576			.text(
6577				this.getMoreLinkText(hiddenSegs.length)
6578			)
6579			.on('click', function(ev) {
6580				var clickOption = view.opt('eventLimitClick');
6581				var date = _this.getCellDate(row, col);
6582				var moreEl = $(this);
6583				var dayEl = _this.getCellEl(row, col);
6584				var allSegs = _this.getCellSegs(row, col);
6585
6586				// rescope the segments to be within the cell's date
6587				var reslicedAllSegs = _this.resliceDaySegs(allSegs, date);
6588				var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date);
6589
6590				if (typeof clickOption === 'function') {
6591					// the returned value can be an atomic option
6592					clickOption = view.trigger('eventLimitClick', null, {
6593						date: date,
6594						dayEl: dayEl,
6595						moreEl: moreEl,
6596						segs: reslicedAllSegs,
6597						hiddenSegs: reslicedHiddenSegs
6598					}, ev);
6599				}
6600
6601				if (clickOption === 'popover') {
6602					_this.showSegPopover(row, col, moreEl, reslicedAllSegs);
6603				}
6604				else if (typeof clickOption === 'string') { // a view name
6605					view.calendar.zoomTo(date, clickOption);
6606				}
6607			});
6608	},
6609
6610
6611	// Reveals the popover that displays all events within a cell
6612	showSegPopover: function(row, col, moreLink, segs) {
6613		var _this = this;
6614		var view = this.view;
6615		var moreWrap = moreLink.parent(); // the <div> wrapper around the <a>
6616		var topEl; // the element we want to match the top coordinate of
6617		var options;
6618
6619		if (this.rowCnt == 1) {
6620			topEl = view.el; // will cause the popover to cover any sort of header
6621		}
6622		else {
6623			topEl = this.rowEls.eq(row); // will align with top of row
6624		}
6625
6626		options = {
6627			className: 'fc-more-popover',
6628			content: this.renderSegPopoverContent(row, col, segs),
6629			parentEl: this.view.el, // attach to root of view. guarantees outside of scrollbars.
6630			top: topEl.offset().top,
6631			autoHide: true, // when the user clicks elsewhere, hide the popover
6632			viewportConstrain: view.opt('popoverViewportConstrain'),
6633			hide: function() {
6634				// kill everything when the popover is hidden
6635				_this.segPopover.removeElement();
6636				_this.segPopover = null;
6637				_this.popoverSegs = null;
6638			}
6639		};
6640
6641		// Determine horizontal coordinate.
6642		// We use the moreWrap instead of the <td> to avoid border confusion.
6643		if (this.isRTL) {
6644			options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border
6645		}
6646		else {
6647			options.left = moreWrap.offset().left - 1; // -1 to be over cell border
6648		}
6649
6650		this.segPopover = new Popover(options);
6651		this.segPopover.show();
6652
6653		// the popover doesn't live within the grid's container element, and thus won't get the event
6654		// delegated-handlers for free. attach event-related handlers to the popover.
6655		this.bindSegHandlersToEl(this.segPopover.el);
6656	},
6657
6658
6659	// Builds the inner DOM contents of the segment popover
6660	renderSegPopoverContent: function(row, col, segs) {
6661		var view = this.view;
6662		var isTheme = view.opt('theme');
6663		var title = this.getCellDate(row, col).format(view.opt('dayPopoverFormat'));
6664		var content = $(
6665			'<div class="fc-header ' + view.widgetHeaderClass + '">' +
6666				'<span class="fc-close ' +
6667					(isTheme ? 'ui-icon ui-icon-closethick' : 'fc-icon fc-icon-x') +
6668				'"></span>' +
6669				'<span class="fc-title">' +
6670					htmlEscape(title) +
6671				'</span>' +
6672				'<div class="fc-clear"/>' +
6673			'</div>' +
6674			'<div class="fc-body ' + view.widgetContentClass + '">' +
6675				'<div class="fc-event-container"></div>' +
6676			'</div>'
6677		);
6678		var segContainer = content.find('.fc-event-container');
6679		var i;
6680
6681		// render each seg's `el` and only return the visible segs
6682		segs = this.renderFgSegEls(segs, true); // disableResizing=true
6683		this.popoverSegs = segs;
6684
6685		for (i = 0; i < segs.length; i++) {
6686
6687			// because segments in the popover are not part of a grid coordinate system, provide a hint to any
6688			// grids that want to do drag-n-drop about which cell it came from
6689			this.prepareHits();
6690			segs[i].hit = this.getCellHit(row, col);
6691			this.releaseHits();
6692
6693			segContainer.append(segs[i].el);
6694		}
6695
6696		return content;
6697	},
6698
6699
6700	// Given the events within an array of segment objects, reslice them to be in a single day
6701	resliceDaySegs: function(segs, dayDate) {
6702
6703		// build an array of the original events
6704		var events = $.map(segs, function(seg) {
6705			return seg.event;
6706		});
6707
6708		var dayStart = dayDate.clone();
6709		var dayEnd = dayStart.clone().add(1, 'days');
6710		var dayRange = { start: dayStart, end: dayEnd };
6711
6712		// slice the events with a custom slicing function
6713		segs = this.eventsToSegs(
6714			events,
6715			function(range) {
6716				var seg = intersectRanges(range, dayRange); // undefind if no intersection
6717				return seg ? [ seg ] : []; // must return an array of segments
6718			}
6719		);
6720
6721		// force an order because eventsToSegs doesn't guarantee one
6722		this.sortEventSegs(segs);
6723
6724		return segs;
6725	},
6726
6727
6728	// Generates the text that should be inside a "more" link, given the number of events it represents
6729	getMoreLinkText: function(num) {
6730		var opt = this.view.opt('eventLimitText');
6731
6732		if (typeof opt === 'function') {
6733			return opt(num);
6734		}
6735		else {
6736			return '+' + num + ' ' + opt;
6737		}
6738	},
6739
6740
6741	// Returns segments within a given cell.
6742	// If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs.
6743	getCellSegs: function(row, col, startLevel) {
6744		var segMatrix = this.rowStructs[row].segMatrix;
6745		var level = startLevel || 0;
6746		var segs = [];
6747		var seg;
6748
6749		while (level < segMatrix.length) {
6750			seg = segMatrix[level][col];
6751			if (seg) {
6752				segs.push(seg);
6753			}
6754			level++;
6755		}
6756
6757		return segs;
6758	}
6759
6760});
6761
6762;;
6763
6764/* A component that renders one or more columns of vertical time slots
6765----------------------------------------------------------------------------------------------------------------------*/
6766// We mixin DayTable, even though there is only a single row of days
6767
6768var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
6769
6770	slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines
6771	snapDuration: null, // granularity of time for dragging and selecting
6772	snapsPerSlot: null,
6773	minTime: null, // Duration object that denotes the first visible time of any given day
6774	maxTime: null, // Duration object that denotes the exclusive visible end time of any given day
6775	labelFormat: null, // formatting string for times running along vertical axis
6776	labelInterval: null, // duration of how often a label should be displayed for a slot
6777
6778	colEls: null, // cells elements in the day-row background
6779	slatContainerEl: null, // div that wraps all the slat rows
6780	slatEls: null, // elements running horizontally across all columns
6781	nowIndicatorEls: null,
6782
6783	colCoordCache: null,
6784	slatCoordCache: null,
6785
6786
6787	constructor: function() {
6788		Grid.apply(this, arguments); // call the super-constructor
6789
6790		this.processOptions();
6791	},
6792
6793
6794	// Renders the time grid into `this.el`, which should already be assigned.
6795	// Relies on the view's colCnt. In the future, this component should probably be self-sufficient.
6796	renderDates: function() {
6797		this.el.html(this.renderHtml());
6798		this.colEls = this.el.find('.fc-day');
6799		this.slatContainerEl = this.el.find('.fc-slats');
6800		this.slatEls = this.slatContainerEl.find('tr');
6801
6802		this.colCoordCache = new CoordCache({
6803			els: this.colEls,
6804			isHorizontal: true
6805		});
6806		this.slatCoordCache = new CoordCache({
6807			els: this.slatEls,
6808			isVertical: true
6809		});
6810
6811		this.renderContentSkeleton();
6812	},
6813
6814
6815	// Renders the basic HTML skeleton for the grid
6816	renderHtml: function() {
6817		return '' +
6818			'<div class="fc-bg">' +
6819				'<table>' +
6820					this.renderBgTrHtml(0) + // row=0
6821				'</table>' +
6822			'</div>' +
6823			'<div class="fc-slats">' +
6824				'<table>' +
6825					this.renderSlatRowHtml() +
6826				'</table>' +
6827			'</div>';
6828	},
6829
6830
6831	// Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL.
6832	renderSlatRowHtml: function() {
6833		var view = this.view;
6834		var isRTL = this.isRTL;
6835		var html = '';
6836		var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations
6837		var slotDate; // will be on the view's first day, but we only care about its time
6838		var isLabeled;
6839		var axisHtml;
6840
6841		// Calculate the time for each slot
6842		while (slotTime < this.maxTime) {
6843			slotDate = this.start.clone().time(slotTime);
6844			isLabeled = isInt(divideDurationByDuration(slotTime, this.labelInterval));
6845
6846			axisHtml =
6847				'<td class="fc-axis fc-time ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' +
6848					(isLabeled ?
6849						'<span>' + // for matchCellWidths
6850							htmlEscape(slotDate.format(this.labelFormat)) +
6851						'</span>' :
6852						''
6853						) +
6854				'</td>';
6855
6856			html +=
6857				'<tr data-time="' + slotDate.format('HH:mm:ss') + '"' +
6858					(isLabeled ? '' : ' class="fc-minor"') +
6859					'>' +
6860					(!isRTL ? axisHtml : '') +
6861					'<td class="' + view.widgetContentClass + '"/>' +
6862					(isRTL ? axisHtml : '') +
6863				"</tr>";
6864
6865			slotTime.add(this.slotDuration);
6866		}
6867
6868		return html;
6869	},
6870
6871
6872	/* Options
6873	------------------------------------------------------------------------------------------------------------------*/
6874
6875
6876	// Parses various options into properties of this object
6877	processOptions: function() {
6878		var view = this.view;
6879		var slotDuration = view.opt('slotDuration');
6880		var snapDuration = view.opt('snapDuration');
6881		var input;
6882
6883		slotDuration = moment.duration(slotDuration);
6884		snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration;
6885
6886		this.slotDuration = slotDuration;
6887		this.snapDuration = snapDuration;
6888		this.snapsPerSlot = slotDuration / snapDuration; // TODO: ensure an integer multiple?
6889
6890		this.minResizeDuration = snapDuration; // hack
6891
6892		this.minTime = moment.duration(view.opt('minTime'));
6893		this.maxTime = moment.duration(view.opt('maxTime'));
6894
6895		// might be an array value (for TimelineView).
6896		// if so, getting the most granular entry (the last one probably).
6897		input = view.opt('slotLabelFormat');
6898		if ($.isArray(input)) {
6899			input = input[input.length - 1];
6900		}
6901
6902		this.labelFormat =
6903			input ||
6904			view.opt('axisFormat') || // deprecated
6905			view.opt('smallTimeFormat'); // the computed default
6906
6907		input = view.opt('slotLabelInterval');
6908		this.labelInterval = input ?
6909			moment.duration(input) :
6910			this.computeLabelInterval(slotDuration);
6911	},
6912
6913
6914	// Computes an automatic value for slotLabelInterval
6915	computeLabelInterval: function(slotDuration) {
6916		var i;
6917		var labelInterval;
6918		var slotsPerLabel;
6919
6920		// find the smallest stock label interval that results in more than one slots-per-label
6921		for (i = AGENDA_STOCK_SUB_DURATIONS.length - 1; i >= 0; i--) {
6922			labelInterval = moment.duration(AGENDA_STOCK_SUB_DURATIONS[i]);
6923			slotsPerLabel = divideDurationByDuration(labelInterval, slotDuration);
6924			if (isInt(slotsPerLabel) && slotsPerLabel > 1) {
6925				return labelInterval;
6926			}
6927		}
6928
6929		return moment.duration(slotDuration); // fall back. clone
6930	},
6931
6932
6933	// Computes a default event time formatting string if `timeFormat` is not explicitly defined
6934	computeEventTimeFormat: function() {
6935		return this.view.opt('noMeridiemTimeFormat'); // like "6:30" (no AM/PM)
6936	},
6937
6938
6939	// Computes a default `displayEventEnd` value if one is not expliclty defined
6940	computeDisplayEventEnd: function() {
6941		return true;
6942	},
6943
6944
6945	/* Hit System
6946	------------------------------------------------------------------------------------------------------------------*/
6947
6948
6949	prepareHits: function() {
6950		this.colCoordCache.build();
6951		this.slatCoordCache.build();
6952	},
6953
6954
6955	releaseHits: function() {
6956		this.colCoordCache.clear();
6957		// NOTE: don't clear slatCoordCache because we rely on it for computeTimeTop
6958	},
6959
6960
6961	queryHit: function(leftOffset, topOffset) {
6962		var snapsPerSlot = this.snapsPerSlot;
6963		var colCoordCache = this.colCoordCache;
6964		var slatCoordCache = this.slatCoordCache;
6965
6966		if (colCoordCache.isLeftInBounds(leftOffset) && slatCoordCache.isTopInBounds(topOffset)) {
6967			var colIndex = colCoordCache.getHorizontalIndex(leftOffset);
6968			var slatIndex = slatCoordCache.getVerticalIndex(topOffset);
6969
6970			if (colIndex != null && slatIndex != null) {
6971				var slatTop = slatCoordCache.getTopOffset(slatIndex);
6972				var slatHeight = slatCoordCache.getHeight(slatIndex);
6973				var partial = (topOffset - slatTop) / slatHeight; // floating point number between 0 and 1
6974				var localSnapIndex = Math.floor(partial * snapsPerSlot); // the snap # relative to start of slat
6975				var snapIndex = slatIndex * snapsPerSlot + localSnapIndex;
6976				var snapTop = slatTop + (localSnapIndex / snapsPerSlot) * slatHeight;
6977				var snapBottom = slatTop + ((localSnapIndex + 1) / snapsPerSlot) * slatHeight;
6978
6979				return {
6980					col: colIndex,
6981					snap: snapIndex,
6982					component: this, // needed unfortunately :(
6983					left: colCoordCache.getLeftOffset(colIndex),
6984					right: colCoordCache.getRightOffset(colIndex),
6985					top: snapTop,
6986					bottom: snapBottom
6987				};
6988			}
6989		}
6990	},
6991
6992
6993	getHitSpan: function(hit) {
6994		var start = this.getCellDate(0, hit.col); // row=0
6995		var time = this.computeSnapTime(hit.snap); // pass in the snap-index
6996		var end;
6997
6998		start.time(time);
6999		end = start.clone().add(this.snapDuration);
7000
7001		return { start: start, end: end };
7002	},
7003
7004
7005	getHitEl: function(hit) {
7006		return this.colEls.eq(hit.col);
7007	},
7008
7009
7010	/* Dates
7011	------------------------------------------------------------------------------------------------------------------*/
7012
7013
7014	rangeUpdated: function() {
7015		this.updateDayTable();
7016	},
7017
7018
7019	// Given a row number of the grid, representing a "snap", returns a time (Duration) from its start-of-day
7020	computeSnapTime: function(snapIndex) {
7021		return moment.duration(this.minTime + this.snapDuration * snapIndex);
7022	},
7023
7024
7025	// Slices up the given span (unzoned start/end with other misc data) into an array of segments
7026	spanToSegs: function(span) {
7027		var segs = this.sliceRangeByTimes(span);
7028		var i;
7029
7030		for (i = 0; i < segs.length; i++) {
7031			if (this.isRTL) {
7032				segs[i].col = this.daysPerRow - 1 - segs[i].dayIndex;
7033			}
7034			else {
7035				segs[i].col = segs[i].dayIndex;
7036			}
7037		}
7038
7039		return segs;
7040	},
7041
7042
7043	sliceRangeByTimes: function(range) {
7044		var segs = [];
7045		var seg;
7046		var dayIndex;
7047		var dayDate;
7048		var dayRange;
7049
7050		for (dayIndex = 0; dayIndex < this.daysPerRow; dayIndex++) {
7051			dayDate = this.dayDates[dayIndex].clone(); // TODO: better API for this?
7052			dayRange = {
7053				start: dayDate.clone().time(this.minTime),
7054				end: dayDate.clone().time(this.maxTime)
7055			};
7056			seg = intersectRanges(range, dayRange); // both will be ambig timezone
7057			if (seg) {
7058				seg.dayIndex = dayIndex;
7059				segs.push(seg);
7060			}
7061		}
7062
7063		return segs;
7064	},
7065
7066
7067	/* Coordinates
7068	------------------------------------------------------------------------------------------------------------------*/
7069
7070
7071	updateSize: function(isResize) { // NOT a standard Grid method
7072		this.slatCoordCache.build();
7073
7074		if (isResize) {
7075			this.updateSegVerticals(
7076				[].concat(this.fgSegs || [], this.bgSegs || [], this.businessSegs || [])
7077			);
7078		}
7079	},
7080
7081
7082	getTotalSlatHeight: function() {
7083		return this.slatContainerEl.outerHeight();
7084	},
7085
7086
7087	// Computes the top coordinate, relative to the bounds of the grid, of the given date.
7088	// A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
7089	computeDateTop: function(date, startOfDayDate) {
7090		return this.computeTimeTop(
7091			moment.duration(
7092				date - startOfDayDate.clone().stripTime()
7093			)
7094		);
7095	},
7096
7097
7098	// Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
7099	computeTimeTop: function(time) {
7100		var len = this.slatEls.length;
7101		var slatCoverage = (time - this.minTime) / this.slotDuration; // floating-point value of # of slots covered
7102		var slatIndex;
7103		var slatRemainder;
7104
7105		// compute a floating-point number for how many slats should be progressed through.
7106		// from 0 to number of slats (inclusive)
7107		// constrained because minTime/maxTime might be customized.
7108		slatCoverage = Math.max(0, slatCoverage);
7109		slatCoverage = Math.min(len, slatCoverage);
7110
7111		// an integer index of the furthest whole slat
7112		// from 0 to number slats (*exclusive*, so len-1)
7113		slatIndex = Math.floor(slatCoverage);
7114		slatIndex = Math.min(slatIndex, len - 1);
7115
7116		// how much further through the slatIndex slat (from 0.0-1.0) must be covered in addition.
7117		// could be 1.0 if slatCoverage is covering *all* the slots
7118		slatRemainder = slatCoverage - slatIndex;
7119
7120		return this.slatCoordCache.getTopPosition(slatIndex) +
7121			this.slatCoordCache.getHeight(slatIndex) * slatRemainder;
7122	},
7123
7124
7125
7126	/* Event Drag Visualization
7127	------------------------------------------------------------------------------------------------------------------*/
7128
7129
7130	// Renders a visual indication of an event being dragged over the specified date(s).
7131	// A returned value of `true` signals that a mock "helper" event has been rendered.
7132	renderDrag: function(eventLocation, seg) {
7133
7134		if (seg) { // if there is event information for this drag, render a helper event
7135
7136			// returns mock event elements
7137			// signal that a helper has been rendered
7138			return this.renderEventLocationHelper(eventLocation, seg);
7139		}
7140		else {
7141			// otherwise, just render a highlight
7142			this.renderHighlight(this.eventToSpan(eventLocation));
7143		}
7144	},
7145
7146
7147	// Unrenders any visual indication of an event being dragged
7148	unrenderDrag: function() {
7149		this.unrenderHelper();
7150		this.unrenderHighlight();
7151	},
7152
7153
7154	/* Event Resize Visualization
7155	------------------------------------------------------------------------------------------------------------------*/
7156
7157
7158	// Renders a visual indication of an event being resized
7159	renderEventResize: function(eventLocation, seg) {
7160		return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements
7161	},
7162
7163
7164	// Unrenders any visual indication of an event being resized
7165	unrenderEventResize: function() {
7166		this.unrenderHelper();
7167	},
7168
7169
7170	/* Event Helper
7171	------------------------------------------------------------------------------------------------------------------*/
7172
7173
7174	// Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag)
7175	renderHelper: function(event, sourceSeg) {
7176		return this.renderHelperSegs(this.eventToSegs(event), sourceSeg); // returns mock event elements
7177	},
7178
7179
7180	// Unrenders any mock helper event
7181	unrenderHelper: function() {
7182		this.unrenderHelperSegs();
7183	},
7184
7185
7186	/* Business Hours
7187	------------------------------------------------------------------------------------------------------------------*/
7188
7189
7190	renderBusinessHours: function() {
7191		this.renderBusinessSegs(
7192			this.buildBusinessHourSegs()
7193		);
7194	},
7195
7196
7197	unrenderBusinessHours: function() {
7198		this.unrenderBusinessSegs();
7199	},
7200
7201
7202	/* Now Indicator
7203	------------------------------------------------------------------------------------------------------------------*/
7204
7205
7206	getNowIndicatorUnit: function() {
7207		return 'minute'; // will refresh on the minute
7208	},
7209
7210
7211	renderNowIndicator: function(date) {
7212		// seg system might be overkill, but it handles scenario where line needs to be rendered
7213		//  more than once because of columns with the same date (resources columns for example)
7214		var segs = this.spanToSegs({ start: date, end: date });
7215		var top = this.computeDateTop(date, date);
7216		var nodes = [];
7217		var i;
7218
7219		// render lines within the columns
7220		for (i = 0; i < segs.length; i++) {
7221			nodes.push($('<div class="fc-now-indicator fc-now-indicator-line"></div>')
7222				.css('top', top)
7223				.appendTo(this.colContainerEls.eq(segs[i].col))[0]);
7224		}
7225
7226		// render an arrow over the axis
7227		if (segs.length > 0) { // is the current time in view?
7228			nodes.push($('<div class="fc-now-indicator fc-now-indicator-arrow"></div>')
7229				.css('top', top)
7230				.appendTo(this.el.find('.fc-content-skeleton'))[0]);
7231		}
7232
7233		this.nowIndicatorEls = $(nodes);
7234	},
7235
7236
7237	unrenderNowIndicator: function() {
7238		if (this.nowIndicatorEls) {
7239			this.nowIndicatorEls.remove();
7240			this.nowIndicatorEls = null;
7241		}
7242	},
7243
7244
7245	/* Selection
7246	------------------------------------------------------------------------------------------------------------------*/
7247
7248
7249	// Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight.
7250	renderSelection: function(span) {
7251		if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered
7252
7253			// normally acceps an eventLocation, span has a start/end, which is good enough
7254			this.renderEventLocationHelper(span);
7255		}
7256		else {
7257			this.renderHighlight(span);
7258		}
7259	},
7260
7261
7262	// Unrenders any visual indication of a selection
7263	unrenderSelection: function() {
7264		this.unrenderHelper();
7265		this.unrenderHighlight();
7266	},
7267
7268
7269	/* Highlight
7270	------------------------------------------------------------------------------------------------------------------*/
7271
7272
7273	renderHighlight: function(span) {
7274		this.renderHighlightSegs(this.spanToSegs(span));
7275	},
7276
7277
7278	unrenderHighlight: function() {
7279		this.unrenderHighlightSegs();
7280	}
7281
7282});
7283
7284;;
7285
7286/* Methods for rendering SEGMENTS, pieces of content that live on the view
7287 ( this file is no longer just for events )
7288----------------------------------------------------------------------------------------------------------------------*/
7289
7290TimeGrid.mixin({
7291
7292	colContainerEls: null, // containers for each column
7293
7294	// inner-containers for each column where different types of segs live
7295	fgContainerEls: null,
7296	bgContainerEls: null,
7297	helperContainerEls: null,
7298	highlightContainerEls: null,
7299	businessContainerEls: null,
7300
7301	// arrays of different types of displayed segments
7302	fgSegs: null,
7303	bgSegs: null,
7304	helperSegs: null,
7305	highlightSegs: null,
7306	businessSegs: null,
7307
7308
7309	// Renders the DOM that the view's content will live in
7310	renderContentSkeleton: function() {
7311		var cellHtml = '';
7312		var i;
7313		var skeletonEl;
7314
7315		for (i = 0; i < this.colCnt; i++) {
7316			cellHtml +=
7317				'<td>' +
7318					'<div class="fc-content-col">' +
7319						'<div class="fc-event-container fc-helper-container"></div>' +
7320						'<div class="fc-event-container"></div>' +
7321						'<div class="fc-highlight-container"></div>' +
7322						'<div class="fc-bgevent-container"></div>' +
7323						'<div class="fc-business-container"></div>' +
7324					'</div>' +
7325				'</td>';
7326		}
7327
7328		skeletonEl = $(
7329			'<div class="fc-content-skeleton">' +
7330				'<table>' +
7331					'<tr>' + cellHtml + '</tr>' +
7332				'</table>' +
7333			'</div>'
7334		);
7335
7336		this.colContainerEls = skeletonEl.find('.fc-content-col');
7337		this.helperContainerEls = skeletonEl.find('.fc-helper-container');
7338		this.fgContainerEls = skeletonEl.find('.fc-event-container:not(.fc-helper-container)');
7339		this.bgContainerEls = skeletonEl.find('.fc-bgevent-container');
7340		this.highlightContainerEls = skeletonEl.find('.fc-highlight-container');
7341		this.businessContainerEls = skeletonEl.find('.fc-business-container');
7342
7343		this.bookendCells(skeletonEl.find('tr')); // TODO: do this on string level
7344		this.el.append(skeletonEl);
7345	},
7346
7347
7348	/* Foreground Events
7349	------------------------------------------------------------------------------------------------------------------*/
7350
7351
7352	renderFgSegs: function(segs) {
7353		segs = this.renderFgSegsIntoContainers(segs, this.fgContainerEls);
7354		this.fgSegs = segs;
7355		return segs; // needed for Grid::renderEvents
7356	},
7357
7358
7359	unrenderFgSegs: function() {
7360		this.unrenderNamedSegs('fgSegs');
7361	},
7362
7363
7364	/* Foreground Helper Events
7365	------------------------------------------------------------------------------------------------------------------*/
7366
7367
7368	renderHelperSegs: function(segs, sourceSeg) {
7369		var helperEls = [];
7370		var i, seg;
7371		var sourceEl;
7372
7373		segs = this.renderFgSegsIntoContainers(segs, this.helperContainerEls);
7374
7375		// Try to make the segment that is in the same row as sourceSeg look the same
7376		for (i = 0; i < segs.length; i++) {
7377			seg = segs[i];
7378			if (sourceSeg && sourceSeg.col === seg.col) {
7379				sourceEl = sourceSeg.el;
7380				seg.el.css({
7381					left: sourceEl.css('left'),
7382					right: sourceEl.css('right'),
7383					'margin-left': sourceEl.css('margin-left'),
7384					'margin-right': sourceEl.css('margin-right')
7385				});
7386			}
7387			helperEls.push(seg.el[0]);
7388		}
7389
7390		this.helperSegs = segs;
7391
7392		return $(helperEls); // must return rendered helpers
7393	},
7394
7395
7396	unrenderHelperSegs: function() {
7397		this.unrenderNamedSegs('helperSegs');
7398	},
7399
7400
7401	/* Background Events
7402	------------------------------------------------------------------------------------------------------------------*/
7403
7404
7405	renderBgSegs: function(segs) {
7406		segs = this.renderFillSegEls('bgEvent', segs); // TODO: old fill system
7407		this.updateSegVerticals(segs);
7408		this.attachSegsByCol(this.groupSegsByCol(segs), this.bgContainerEls);
7409		this.bgSegs = segs;
7410		return segs; // needed for Grid::renderEvents
7411	},
7412
7413
7414	unrenderBgSegs: function() {
7415		this.unrenderNamedSegs('bgSegs');
7416	},
7417
7418
7419	/* Highlight
7420	------------------------------------------------------------------------------------------------------------------*/
7421
7422
7423	renderHighlightSegs: function(segs) {
7424		segs = this.renderFillSegEls('highlight', segs); // TODO: old fill system
7425		this.updateSegVerticals(segs);
7426		this.attachSegsByCol(this.groupSegsByCol(segs), this.highlightContainerEls);
7427		this.highlightSegs = segs;
7428	},
7429
7430
7431	unrenderHighlightSegs: function() {
7432		this.unrenderNamedSegs('highlightSegs');
7433	},
7434
7435
7436	/* Business Hours
7437	------------------------------------------------------------------------------------------------------------------*/
7438
7439
7440	renderBusinessSegs: function(segs) {
7441		segs = this.renderFillSegEls('businessHours', segs); // TODO: old fill system
7442		this.updateSegVerticals(segs);
7443		this.attachSegsByCol(this.groupSegsByCol(segs), this.businessContainerEls);
7444		this.businessSegs = segs;
7445	},
7446
7447
7448	unrenderBusinessSegs: function() {
7449		this.unrenderNamedSegs('businessSegs');
7450	},
7451
7452
7453	/* Seg Rendering Utils
7454	------------------------------------------------------------------------------------------------------------------*/
7455
7456
7457	// Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col
7458	groupSegsByCol: function(segs) {
7459		var segsByCol = [];
7460		var i;
7461
7462		for (i = 0; i < this.colCnt; i++) {
7463			segsByCol.push([]);
7464		}
7465
7466		for (i = 0; i < segs.length; i++) {
7467			segsByCol[segs[i].col].push(segs[i]);
7468		}
7469
7470		return segsByCol;
7471	},
7472
7473
7474	// Given segments grouped by column, insert the segments' elements into a parallel array of container
7475	// elements, each living within a column.
7476	attachSegsByCol: function(segsByCol, containerEls) {
7477		var col;
7478		var segs;
7479		var i;
7480
7481		for (col = 0; col < this.colCnt; col++) { // iterate each column grouping
7482			segs = segsByCol[col];
7483
7484			for (i = 0; i < segs.length; i++) {
7485				containerEls.eq(col).append(segs[i].el);
7486			}
7487		}
7488	},
7489
7490
7491	// Given the name of a property of `this` object, assumed to be an array of segments,
7492	// loops through each segment and removes from DOM. Will null-out the property afterwards.
7493	unrenderNamedSegs: function(propName) {
7494		var segs = this[propName];
7495		var i;
7496
7497		if (segs) {
7498			for (i = 0; i < segs.length; i++) {
7499				segs[i].el.remove();
7500			}
7501			this[propName] = null;
7502		}
7503	},
7504
7505
7506
7507	/* Foreground Event Rendering Utils
7508	------------------------------------------------------------------------------------------------------------------*/
7509
7510
7511	// Given an array of foreground segments, render a DOM element for each, computes position,
7512	// and attaches to the column inner-container elements.
7513	renderFgSegsIntoContainers: function(segs, containerEls) {
7514		var segsByCol;
7515		var col;
7516
7517		segs = this.renderFgSegEls(segs); // will call fgSegHtml
7518		segsByCol = this.groupSegsByCol(segs);
7519
7520		for (col = 0; col < this.colCnt; col++) {
7521			this.updateFgSegCoords(segsByCol[col]);
7522		}
7523
7524		this.attachSegsByCol(segsByCol, containerEls);
7525
7526		return segs;
7527	},
7528
7529
7530	// Renders the HTML for a single event segment's default rendering
7531	fgSegHtml: function(seg, disableResizing) {
7532		var view = this.view;
7533		var event = seg.event;
7534		var isDraggable = view.isEventDraggable(event);
7535		var isResizableFromStart = !disableResizing && seg.isStart && view.isEventResizableFromStart(event);
7536		var isResizableFromEnd = !disableResizing && seg.isEnd && view.isEventResizableFromEnd(event);
7537		var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd);
7538		var skinCss = cssToStr(this.getSegSkinCss(seg));
7539		var timeText;
7540		var fullTimeText; // more verbose time text. for the print stylesheet
7541		var startTimeText; // just the start time text
7542
7543		classes.unshift('fc-time-grid-event', 'fc-v-event');
7544
7545		if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day...
7546			// Don't display time text on segments that run entirely through a day.
7547			// That would appear as midnight-midnight and would look dumb.
7548			// Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am)
7549			if (seg.isStart || seg.isEnd) {
7550				timeText = this.getEventTimeText(seg);
7551				fullTimeText = this.getEventTimeText(seg, 'LT');
7552				startTimeText = this.getEventTimeText(seg, null, false); // displayEnd=false
7553			}
7554		} else {
7555			// Display the normal time text for the *event's* times
7556			timeText = this.getEventTimeText(event);
7557			fullTimeText = this.getEventTimeText(event, 'LT');
7558			startTimeText = this.getEventTimeText(event, null, false); // displayEnd=false
7559		}
7560
7561		return '<a class="' + classes.join(' ') + '"' +
7562			(event.url ?
7563				' href="' + htmlEscape(event.url) + '"' :
7564				''
7565				) +
7566			(skinCss ?
7567				' style="' + skinCss + '"' :
7568				''
7569				) +
7570			'>' +
7571				'<div class="fc-content">' +
7572					(timeText ?
7573						'<div class="fc-time"' +
7574						' data-start="' + htmlEscape(startTimeText) + '"' +
7575						' data-full="' + htmlEscape(fullTimeText) + '"' +
7576						'>' +
7577							'<span>' + htmlEscape(timeText) + '</span>' +
7578						'</div>' :
7579						''
7580						) +
7581					(event.title ?
7582						'<div class="fc-title">' +
7583							htmlEscape(event.title) +
7584						'</div>' :
7585						''
7586						) +
7587				'</div>' +
7588				'<div class="fc-bg"/>' +
7589				/* TODO: write CSS for this
7590				(isResizableFromStart ?
7591					'<div class="fc-resizer fc-start-resizer" />' :
7592					''
7593					) +
7594				*/
7595				(isResizableFromEnd ?
7596					'<div class="fc-resizer fc-end-resizer" />' :
7597					''
7598					) +
7599			'</a>';
7600	},
7601
7602
7603	/* Seg Position Utils
7604	------------------------------------------------------------------------------------------------------------------*/
7605
7606
7607	// Refreshes the CSS top/bottom coordinates for each segment element.
7608	// Works when called after initial render, after a window resize/zoom for example.
7609	updateSegVerticals: function(segs) {
7610		this.computeSegVerticals(segs);
7611		this.assignSegVerticals(segs);
7612	},
7613
7614
7615	// For each segment in an array, computes and assigns its top and bottom properties
7616	computeSegVerticals: function(segs) {
7617		var i, seg;
7618
7619		for (i = 0; i < segs.length; i++) {
7620			seg = segs[i];
7621			seg.top = this.computeDateTop(seg.start, seg.start);
7622			seg.bottom = this.computeDateTop(seg.end, seg.start);
7623		}
7624	},
7625
7626
7627	// Given segments that already have their top/bottom properties computed, applies those values to
7628	// the segments' elements.
7629	assignSegVerticals: function(segs) {
7630		var i, seg;
7631
7632		for (i = 0; i < segs.length; i++) {
7633			seg = segs[i];
7634			seg.el.css(this.generateSegVerticalCss(seg));
7635		}
7636	},
7637
7638
7639	// Generates an object with CSS properties for the top/bottom coordinates of a segment element
7640	generateSegVerticalCss: function(seg) {
7641		return {
7642			top: seg.top,
7643			bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container
7644		};
7645	},
7646
7647
7648	/* Foreground Event Positioning Utils
7649	------------------------------------------------------------------------------------------------------------------*/
7650
7651
7652	// Given segments that are assumed to all live in the *same column*,
7653	// compute their verical/horizontal coordinates and assign to their elements.
7654	updateFgSegCoords: function(segs) {
7655		this.computeSegVerticals(segs); // horizontals relies on this
7656		this.computeFgSegHorizontals(segs); // compute horizontal coordinates, z-index's, and reorder the array
7657		this.assignSegVerticals(segs);
7658		this.assignFgSegHorizontals(segs);
7659	},
7660
7661
7662	// Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each.
7663	// NOTE: Also reorders the given array by date!
7664	computeFgSegHorizontals: function(segs) {
7665		var levels;
7666		var level0;
7667		var i;
7668
7669		this.sortEventSegs(segs); // order by certain criteria
7670		levels = buildSlotSegLevels(segs);
7671		computeForwardSlotSegs(levels);
7672
7673		if ((level0 = levels[0])) {
7674
7675			for (i = 0; i < level0.length; i++) {
7676				computeSlotSegPressures(level0[i]);
7677			}
7678
7679			for (i = 0; i < level0.length; i++) {
7680				this.computeFgSegForwardBack(level0[i], 0, 0);
7681			}
7682		}
7683	},
7684
7685
7686	// Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
7687	// from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
7688	// seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
7689	//
7690	// The segment might be part of a "series", which means consecutive segments with the same pressure
7691	// who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
7692	// segments behind this one in the current series, and `seriesBackwardCoord` is the starting
7693	// coordinate of the first segment in the series.
7694	computeFgSegForwardBack: function(seg, seriesBackwardPressure, seriesBackwardCoord) {
7695		var forwardSegs = seg.forwardSegs;
7696		var i;
7697
7698		if (seg.forwardCoord === undefined) { // not already computed
7699
7700			if (!forwardSegs.length) {
7701
7702				// if there are no forward segments, this segment should butt up against the edge
7703				seg.forwardCoord = 1;
7704			}
7705			else {
7706
7707				// sort highest pressure first
7708				this.sortForwardSegs(forwardSegs);
7709
7710				// this segment's forwardCoord will be calculated from the backwardCoord of the
7711				// highest-pressure forward segment.
7712				this.computeFgSegForwardBack(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord);
7713				seg.forwardCoord = forwardSegs[0].backwardCoord;
7714			}
7715
7716			// calculate the backwardCoord from the forwardCoord. consider the series
7717			seg.backwardCoord = seg.forwardCoord -
7718				(seg.forwardCoord - seriesBackwardCoord) / // available width for series
7719				(seriesBackwardPressure + 1); // # of segments in the series
7720
7721			// use this segment's coordinates to computed the coordinates of the less-pressurized
7722			// forward segments
7723			for (i=0; i<forwardSegs.length; i++) {
7724				this.computeFgSegForwardBack(forwardSegs[i], 0, seg.forwardCoord);
7725			}
7726		}
7727	},
7728
7729
7730	sortForwardSegs: function(forwardSegs) {
7731		forwardSegs.sort(proxy(this, 'compareForwardSegs'));
7732	},
7733
7734
7735	// A cmp function for determining which forward segment to rely on more when computing coordinates.
7736	compareForwardSegs: function(seg1, seg2) {
7737		// put higher-pressure first
7738		return seg2.forwardPressure - seg1.forwardPressure ||
7739			// put segments that are closer to initial edge first (and favor ones with no coords yet)
7740			(seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) ||
7741			// do normal sorting...
7742			this.compareEventSegs(seg1, seg2);
7743	},
7744
7745
7746	// Given foreground event segments that have already had their position coordinates computed,
7747	// assigns position-related CSS values to their elements.
7748	assignFgSegHorizontals: function(segs) {
7749		var i, seg;
7750
7751		for (i = 0; i < segs.length; i++) {
7752			seg = segs[i];
7753			seg.el.css(this.generateFgSegHorizontalCss(seg));
7754
7755			// if the height is short, add a className for alternate styling
7756			if (seg.bottom - seg.top < 30) {
7757				seg.el.addClass('fc-short');
7758			}
7759		}
7760	},
7761
7762
7763	// Generates an object with CSS properties/values that should be applied to an event segment element.
7764	// Contains important positioning-related properties that should be applied to any event element, customized or not.
7765	generateFgSegHorizontalCss: function(seg) {
7766		var shouldOverlap = this.view.opt('slotEventOverlap');
7767		var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point
7768		var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point
7769		var props = this.generateSegVerticalCss(seg); // get top/bottom first
7770		var left; // amount of space from left edge, a fraction of the total width
7771		var right; // amount of space from right edge, a fraction of the total width
7772
7773		if (shouldOverlap) {
7774			// double the width, but don't go beyond the maximum forward coordinate (1.0)
7775			forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2);
7776		}
7777
7778		if (this.isRTL) {
7779			left = 1 - forwardCoord;
7780			right = backwardCoord;
7781		}
7782		else {
7783			left = backwardCoord;
7784			right = 1 - forwardCoord;
7785		}
7786
7787		props.zIndex = seg.level + 1; // convert from 0-base to 1-based
7788		props.left = left * 100 + '%';
7789		props.right = right * 100 + '%';
7790
7791		if (shouldOverlap && seg.forwardPressure) {
7792			// add padding to the edge so that forward stacked events don't cover the resizer's icon
7793			props[this.isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width
7794		}
7795
7796		return props;
7797	}
7798
7799});
7800
7801
7802// Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is
7803// left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date.
7804function buildSlotSegLevels(segs) {
7805	var levels = [];
7806	var i, seg;
7807	var j;
7808
7809	for (i=0; i<segs.length; i++) {
7810		seg = segs[i];
7811
7812		// go through all the levels and stop on the first level where there are no collisions
7813		for (j=0; j<levels.length; j++) {
7814			if (!computeSlotSegCollisions(seg, levels[j]).length) {
7815				break;
7816			}
7817		}
7818
7819		seg.level = j;
7820
7821		(levels[j] || (levels[j] = [])).push(seg);
7822	}
7823
7824	return levels;
7825}
7826
7827
7828// For every segment, figure out the other segments that are in subsequent
7829// levels that also occupy the same vertical space. Accumulate in seg.forwardSegs
7830function computeForwardSlotSegs(levels) {
7831	var i, level;
7832	var j, seg;
7833	var k;
7834
7835	for (i=0; i<levels.length; i++) {
7836		level = levels[i];
7837
7838		for (j=0; j<level.length; j++) {
7839			seg = level[j];
7840
7841			seg.forwardSegs = [];
7842			for (k=i+1; k<levels.length; k++) {
7843				computeSlotSegCollisions(seg, levels[k], seg.forwardSegs);
7844			}
7845		}
7846	}
7847}
7848
7849
7850// Figure out which path forward (via seg.forwardSegs) results in the longest path until
7851// the furthest edge is reached. The number of segments in this path will be seg.forwardPressure
7852function computeSlotSegPressures(seg) {
7853	var forwardSegs = seg.forwardSegs;
7854	var forwardPressure = 0;
7855	var i, forwardSeg;
7856
7857	if (seg.forwardPressure === undefined) { // not already computed
7858
7859		for (i=0; i<forwardSegs.length; i++) {
7860			forwardSeg = forwardSegs[i];
7861
7862			// figure out the child's maximum forward path
7863			computeSlotSegPressures(forwardSeg);
7864
7865			// either use the existing maximum, or use the child's forward pressure
7866			// plus one (for the forwardSeg itself)
7867			forwardPressure = Math.max(
7868				forwardPressure,
7869				1 + forwardSeg.forwardPressure
7870			);
7871		}
7872
7873		seg.forwardPressure = forwardPressure;
7874	}
7875}
7876
7877
7878// Find all the segments in `otherSegs` that vertically collide with `seg`.
7879// Append into an optionally-supplied `results` array and return.
7880function computeSlotSegCollisions(seg, otherSegs, results) {
7881	results = results || [];
7882
7883	for (var i=0; i<otherSegs.length; i++) {
7884		if (isSlotSegCollision(seg, otherSegs[i])) {
7885			results.push(otherSegs[i]);
7886		}
7887	}
7888
7889	return results;
7890}
7891
7892
7893// Do these segments occupy the same vertical space?
7894function isSlotSegCollision(seg1, seg2) {
7895	return seg1.bottom > seg2.top && seg1.top < seg2.bottom;
7896}
7897
7898;;
7899
7900/* An abstract class from which other views inherit from
7901----------------------------------------------------------------------------------------------------------------------*/
7902
7903var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
7904
7905	type: null, // subclass' view name (string)
7906	name: null, // deprecated. use `type` instead
7907	title: null, // the text that will be displayed in the header's title
7908
7909	calendar: null, // owner Calendar object
7910	options: null, // hash containing all options. already merged with view-specific-options
7911	el: null, // the view's containing element. set by Calendar
7912
7913	displaying: null, // a promise representing the state of rendering. null if no render requested
7914	isSkeletonRendered: false,
7915	isEventsRendered: false,
7916
7917	// range the view is actually displaying (moments)
7918	start: null,
7919	end: null, // exclusive
7920
7921	// range the view is formally responsible for (moments)
7922	// may be different from start/end. for example, a month view might have 1st-31st, excluding padded dates
7923	intervalStart: null,
7924	intervalEnd: null, // exclusive
7925	intervalDuration: null,
7926	intervalUnit: null, // name of largest unit being displayed, like "month" or "week"
7927
7928	isRTL: false,
7929	isSelected: false, // boolean whether a range of time is user-selected or not
7930	selectedEvent: null,
7931
7932	eventOrderSpecs: null, // criteria for ordering events when they have same date/time
7933
7934	// classNames styled by jqui themes
7935	widgetHeaderClass: null,
7936	widgetContentClass: null,
7937	highlightStateClass: null,
7938
7939	// for date utils, computed from options
7940	nextDayThreshold: null,
7941	isHiddenDayHash: null,
7942
7943	// now indicator
7944	isNowIndicatorRendered: null,
7945	initialNowDate: null, // result first getNow call
7946	initialNowQueriedMs: null, // ms time the getNow was called
7947	nowIndicatorTimeoutID: null, // for refresh timing of now indicator
7948	nowIndicatorIntervalID: null, // "
7949
7950
7951	constructor: function(calendar, type, options, intervalDuration) {
7952
7953		this.calendar = calendar;
7954		this.type = this.name = type; // .name is deprecated
7955		this.options = options;
7956		this.intervalDuration = intervalDuration || moment.duration(1, 'day');
7957
7958		this.nextDayThreshold = moment.duration(this.opt('nextDayThreshold'));
7959		this.initThemingProps();
7960		this.initHiddenDays();
7961		this.isRTL = this.opt('isRTL');
7962
7963		this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder'));
7964
7965		this.initialize();
7966	},
7967
7968
7969	// A good place for subclasses to initialize member variables
7970	initialize: function() {
7971		// subclasses can implement
7972	},
7973
7974
7975	// Retrieves an option with the given name
7976	opt: function(name) {
7977		return this.options[name];
7978	},
7979
7980
7981	// Triggers handlers that are view-related. Modifies args before passing to calendar.
7982	trigger: function(name, thisObj) { // arguments beyond thisObj are passed along
7983		var calendar = this.calendar;
7984
7985		return calendar.trigger.apply(
7986			calendar,
7987			[name, thisObj || this].concat(
7988				Array.prototype.slice.call(arguments, 2), // arguments beyond thisObj
7989				[ this ] // always make the last argument a reference to the view. TODO: deprecate
7990			)
7991		);
7992	},
7993
7994
7995	/* Dates
7996	------------------------------------------------------------------------------------------------------------------*/
7997
7998
7999	// Updates all internal dates to center around the given current unzoned date.
8000	setDate: function(date) {
8001		this.setRange(this.computeRange(date));
8002	},
8003
8004
8005	// Updates all internal dates for displaying the given unzoned range.
8006	setRange: function(range) {
8007		$.extend(this, range); // assigns every property to this object's member variables
8008		this.updateTitle();
8009	},
8010
8011
8012	// Given a single current unzoned date, produce information about what range to display.
8013	// Subclasses can override. Must return all properties.
8014	computeRange: function(date) {
8015		var intervalUnit = computeIntervalUnit(this.intervalDuration);
8016		var intervalStart = date.clone().startOf(intervalUnit);
8017		var intervalEnd = intervalStart.clone().add(this.intervalDuration);
8018		var start, end;
8019
8020		// normalize the range's time-ambiguity
8021		if (/year|month|week|day/.test(intervalUnit)) { // whole-days?
8022			intervalStart.stripTime();
8023			intervalEnd.stripTime();
8024		}
8025		else { // needs to have a time?
8026			if (!intervalStart.hasTime()) {
8027				intervalStart = this.calendar.time(0); // give 00:00 time
8028			}
8029			if (!intervalEnd.hasTime()) {
8030				intervalEnd = this.calendar.time(0); // give 00:00 time
8031			}
8032		}
8033
8034		start = intervalStart.clone();
8035		start = this.skipHiddenDays(start);
8036		end = intervalEnd.clone();
8037		end = this.skipHiddenDays(end, -1, true); // exclusively move backwards
8038
8039		return {
8040			intervalUnit: intervalUnit,
8041			intervalStart: intervalStart,
8042			intervalEnd: intervalEnd,
8043			start: start,
8044			end: end
8045		};
8046	},
8047
8048
8049	// Computes the new date when the user hits the prev button, given the current date
8050	computePrevDate: function(date) {
8051		return this.massageCurrentDate(
8052			date.clone().startOf(this.intervalUnit).subtract(this.intervalDuration), -1
8053		);
8054	},
8055
8056
8057	// Computes the new date when the user hits the next button, given the current date
8058	computeNextDate: function(date) {
8059		return this.massageCurrentDate(
8060			date.clone().startOf(this.intervalUnit).add(this.intervalDuration)
8061		);
8062	},
8063
8064
8065	// Given an arbitrarily calculated current date of the calendar, returns a date that is ensured to be completely
8066	// visible. `direction` is optional and indicates which direction the current date was being
8067	// incremented or decremented (1 or -1).
8068	massageCurrentDate: function(date, direction) {
8069		if (this.intervalDuration.as('days') <= 1) { // if the view displays a single day or smaller
8070			if (this.isHiddenDay(date)) {
8071				date = this.skipHiddenDays(date, direction);
8072				date.startOf('day');
8073			}
8074		}
8075
8076		return date;
8077	},
8078
8079
8080	/* Title and Date Formatting
8081	------------------------------------------------------------------------------------------------------------------*/
8082
8083
8084	// Sets the view's title property to the most updated computed value
8085	updateTitle: function() {
8086		this.title = this.computeTitle();
8087	},
8088
8089
8090	// Computes what the title at the top of the calendar should be for this view
8091	computeTitle: function() {
8092		return this.formatRange(
8093			{
8094				// in case intervalStart/End has a time, make sure timezone is correct
8095				start: this.calendar.applyTimezone(this.intervalStart),
8096				end: this.calendar.applyTimezone(this.intervalEnd)
8097			},
8098			this.opt('titleFormat') || this.computeTitleFormat(),
8099			this.opt('titleRangeSeparator')
8100		);
8101	},
8102
8103
8104	// Generates the format string that should be used to generate the title for the current date range.
8105	// Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
8106	computeTitleFormat: function() {
8107		if (this.intervalUnit == 'year') {
8108			return 'YYYY';
8109		}
8110		else if (this.intervalUnit == 'month') {
8111			return this.opt('monthYearFormat'); // like "September 2014"
8112		}
8113		else if (this.intervalDuration.as('days') > 1) {
8114			return 'll'; // multi-day range. shorter, like "Sep 9 - 10 2014"
8115		}
8116		else {
8117			return 'LL'; // one day. longer, like "September 9 2014"
8118		}
8119	},
8120
8121
8122	// Utility for formatting a range. Accepts a range object, formatting string, and optional separator.
8123	// Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account.
8124	// The timezones of the dates within `range` will be respected.
8125	formatRange: function(range, formatStr, separator) {
8126		var end = range.end;
8127
8128		if (!end.hasTime()) { // all-day?
8129			end = end.clone().subtract(1); // convert to inclusive. last ms of previous day
8130		}
8131
8132		return formatRange(range.start, end, formatStr, separator, this.opt('isRTL'));
8133	},
8134
8135
8136	/* Rendering
8137	------------------------------------------------------------------------------------------------------------------*/
8138
8139
8140	// Sets the container element that the view should render inside of.
8141	// Does other DOM-related initializations.
8142	setElement: function(el) {
8143		this.el = el;
8144		this.bindGlobalHandlers();
8145	},
8146
8147
8148	// Removes the view's container element from the DOM, clearing any content beforehand.
8149	// Undoes any other DOM-related attachments.
8150	removeElement: function() {
8151		this.clear(); // clears all content
8152
8153		// clean up the skeleton
8154		if (this.isSkeletonRendered) {
8155			this.unrenderSkeleton();
8156			this.isSkeletonRendered = false;
8157		}
8158
8159		this.unbindGlobalHandlers();
8160
8161		this.el.remove();
8162
8163		// NOTE: don't null-out this.el in case the View was destroyed within an API callback.
8164		// We don't null-out the View's other jQuery element references upon destroy,
8165		//  so we shouldn't kill this.el either.
8166	},
8167
8168
8169	// Does everything necessary to display the view centered around the given unzoned date.
8170	// Does every type of rendering EXCEPT rendering events.
8171	// Is asychronous and returns a promise.
8172	display: function(date, explicitScrollState) {
8173		var _this = this;
8174		var prevScrollState = null;
8175
8176		if (explicitScrollState != null && this.displaying) { // don't need prevScrollState if explicitScrollState
8177			prevScrollState = this.queryScroll();
8178		}
8179
8180		this.calendar.freezeContentHeight();
8181
8182		return syncThen(this.clear(), function() { // clear the content first
8183			return (
8184				_this.displaying =
8185					syncThen(_this.displayView(date), function() { // displayView might return a promise
8186
8187						// caller of display() wants a specific scroll state?
8188						if (explicitScrollState != null) {
8189							// we make an assumption that this is NOT the initial render,
8190							// and thus don't need forceScroll (is inconveniently asynchronous)
8191							_this.setScroll(explicitScrollState);
8192						}
8193						else {
8194							_this.forceScroll(_this.computeInitialScroll(prevScrollState));
8195						}
8196
8197						_this.calendar.unfreezeContentHeight();
8198						_this.triggerRender();
8199					})
8200			);
8201		});
8202	},
8203
8204
8205	// Does everything necessary to clear the content of the view.
8206	// Clears dates and events. Does not clear the skeleton.
8207	// Is asychronous and returns a promise.
8208	clear: function() {
8209		var _this = this;
8210		var displaying = this.displaying;
8211
8212		if (displaying) { // previously displayed, or in the process of being displayed?
8213			return syncThen(displaying, function() { // wait for the display to finish
8214				_this.displaying = null;
8215				_this.clearEvents();
8216				return _this.clearView(); // might return a promise. chain it
8217			});
8218		}
8219		else {
8220			return $.when(); // an immediately-resolved promise
8221		}
8222	},
8223
8224
8225	// Displays the view's non-event content, such as date-related content or anything required by events.
8226	// Renders the view's non-content skeleton if necessary.
8227	// Can be asynchronous and return a promise.
8228	displayView: function(date) {
8229		if (!this.isSkeletonRendered) {
8230			this.renderSkeleton();
8231			this.isSkeletonRendered = true;
8232		}
8233		if (date) {
8234			this.setDate(date);
8235		}
8236		if (this.render) {
8237			this.render(); // TODO: deprecate
8238		}
8239		this.renderDates();
8240		this.updateSize();
8241		this.renderBusinessHours(); // might need coordinates, so should go after updateSize()
8242		this.startNowIndicator();
8243	},
8244
8245
8246	// Unrenders the view content that was rendered in displayView.
8247	// Can be asynchronous and return a promise.
8248	clearView: function() {
8249		this.unselect();
8250		this.stopNowIndicator();
8251		this.triggerUnrender();
8252		this.unrenderBusinessHours();
8253		this.unrenderDates();
8254		if (this.destroy) {
8255			this.destroy(); // TODO: deprecate
8256		}
8257	},
8258
8259
8260	// Renders the basic structure of the view before any content is rendered
8261	renderSkeleton: function() {
8262		// subclasses should implement
8263	},
8264
8265
8266	// Unrenders the basic structure of the view
8267	unrenderSkeleton: function() {
8268		// subclasses should implement
8269	},
8270
8271
8272	// Renders the view's date-related content.
8273	// Assumes setRange has already been called and the skeleton has already been rendered.
8274	renderDates: function() {
8275		// subclasses should implement
8276	},
8277
8278
8279	// Unrenders the view's date-related content
8280	unrenderDates: function() {
8281		// subclasses should override
8282	},
8283
8284
8285	// Signals that the view's content has been rendered
8286	triggerRender: function() {
8287		this.trigger('viewRender', this, this, this.el);
8288	},
8289
8290
8291	// Signals that the view's content is about to be unrendered
8292	triggerUnrender: function() {
8293		this.trigger('viewDestroy', this, this, this.el);
8294	},
8295
8296
8297	// Binds DOM handlers to elements that reside outside the view container, such as the document
8298	bindGlobalHandlers: function() {
8299		this.listenTo($(document), 'mousedown', this.handleDocumentMousedown);
8300		this.listenTo($(document), 'touchstart', this.processUnselect);
8301	},
8302
8303
8304	// Unbinds DOM handlers from elements that reside outside the view container
8305	unbindGlobalHandlers: function() {
8306		this.stopListeningTo($(document));
8307	},
8308
8309
8310	// Initializes internal variables related to theming
8311	initThemingProps: function() {
8312		var tm = this.opt('theme') ? 'ui' : 'fc';
8313
8314		this.widgetHeaderClass = tm + '-widget-header';
8315		this.widgetContentClass = tm + '-widget-content';
8316		this.highlightStateClass = tm + '-state-highlight';
8317	},
8318
8319
8320	/* Business Hours
8321	------------------------------------------------------------------------------------------------------------------*/
8322
8323
8324	// Renders business-hours onto the view. Assumes updateSize has already been called.
8325	renderBusinessHours: function() {
8326		// subclasses should implement
8327	},
8328
8329
8330	// Unrenders previously-rendered business-hours
8331	unrenderBusinessHours: function() {
8332		// subclasses should implement
8333	},
8334
8335
8336	/* Now Indicator
8337	------------------------------------------------------------------------------------------------------------------*/
8338
8339
8340	// Immediately render the current time indicator and begins re-rendering it at an interval,
8341	// which is defined by this.getNowIndicatorUnit().
8342	// TODO: somehow do this for the current whole day's background too
8343	startNowIndicator: function() {
8344		var _this = this;
8345		var unit;
8346		var update;
8347		var delay; // ms wait value
8348
8349		if (this.opt('nowIndicator')) {
8350			unit = this.getNowIndicatorUnit();
8351			if (unit) {
8352				update = proxy(this, 'updateNowIndicator'); // bind to `this`
8353
8354				this.initialNowDate = this.calendar.getNow();
8355				this.initialNowQueriedMs = +new Date();
8356				this.renderNowIndicator(this.initialNowDate);
8357				this.isNowIndicatorRendered = true;
8358
8359				// wait until the beginning of the next interval
8360				delay = this.initialNowDate.clone().startOf(unit).add(1, unit) - this.initialNowDate;
8361				this.nowIndicatorTimeoutID = setTimeout(function() {
8362					_this.nowIndicatorTimeoutID = null;
8363					update();
8364					delay = +moment.duration(1, unit);
8365					delay = Math.max(100, delay); // prevent too frequent
8366					_this.nowIndicatorIntervalID = setInterval(update, delay); // update every interval
8367				}, delay);
8368			}
8369		}
8370	},
8371
8372
8373	// rerenders the now indicator, computing the new current time from the amount of time that has passed
8374	// since the initial getNow call.
8375	updateNowIndicator: function() {
8376		if (this.isNowIndicatorRendered) {
8377			this.unrenderNowIndicator();
8378			this.renderNowIndicator(
8379				this.initialNowDate.clone().add(new Date() - this.initialNowQueriedMs) // add ms
8380			);
8381		}
8382	},
8383
8384
8385	// Immediately unrenders the view's current time indicator and stops any re-rendering timers.
8386	// Won't cause side effects if indicator isn't rendered.
8387	stopNowIndicator: function() {
8388		if (this.isNowIndicatorRendered) {
8389
8390			if (this.nowIndicatorTimeoutID) {
8391				clearTimeout(this.nowIndicatorTimeoutID);
8392				this.nowIndicatorTimeoutID = null;
8393			}
8394			if (this.nowIndicatorIntervalID) {
8395				clearTimeout(this.nowIndicatorIntervalID);
8396				this.nowIndicatorIntervalID = null;
8397			}
8398
8399			this.unrenderNowIndicator();
8400			this.isNowIndicatorRendered = false;
8401		}
8402	},
8403
8404
8405	// Returns a string unit, like 'second' or 'minute' that defined how often the current time indicator
8406	// should be refreshed. If something falsy is returned, no time indicator is rendered at all.
8407	getNowIndicatorUnit: function() {
8408		// subclasses should implement
8409	},
8410
8411
8412	// Renders a current time indicator at the given datetime
8413	renderNowIndicator: function(date) {
8414		// subclasses should implement
8415	},
8416
8417
8418	// Undoes the rendering actions from renderNowIndicator
8419	unrenderNowIndicator: function() {
8420		// subclasses should implement
8421	},
8422
8423
8424	/* Dimensions
8425	------------------------------------------------------------------------------------------------------------------*/
8426
8427
8428	// Refreshes anything dependant upon sizing of the container element of the grid
8429	updateSize: function(isResize) {
8430		var scrollState;
8431
8432		if (isResize) {
8433			scrollState = this.queryScroll();
8434		}
8435
8436		this.updateHeight(isResize);
8437		this.updateWidth(isResize);
8438		this.updateNowIndicator();
8439
8440		if (isResize) {
8441			this.setScroll(scrollState);
8442		}
8443	},
8444
8445
8446	// Refreshes the horizontal dimensions of the calendar
8447	updateWidth: function(isResize) {
8448		// subclasses should implement
8449	},
8450
8451
8452	// Refreshes the vertical dimensions of the calendar
8453	updateHeight: function(isResize) {
8454		var calendar = this.calendar; // we poll the calendar for height information
8455
8456		this.setHeight(
8457			calendar.getSuggestedViewHeight(),
8458			calendar.isHeightAuto()
8459		);
8460	},
8461
8462
8463	// Updates the vertical dimensions of the calendar to the specified height.
8464	// if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height.
8465	setHeight: function(height, isAuto) {
8466		// subclasses should implement
8467	},
8468
8469
8470	/* Scroller
8471	------------------------------------------------------------------------------------------------------------------*/
8472
8473
8474	// Computes the initial pre-configured scroll state prior to allowing the user to change it.
8475	// Given the scroll state from the previous rendering. If first time rendering, given null.
8476	computeInitialScroll: function(previousScrollState) {
8477		return 0;
8478	},
8479
8480
8481	// Retrieves the view's current natural scroll state. Can return an arbitrary format.
8482	queryScroll: function() {
8483		// subclasses must implement
8484	},
8485
8486
8487	// Sets the view's scroll state. Will accept the same format computeInitialScroll and queryScroll produce.
8488	setScroll: function(scrollState) {
8489		// subclasses must implement
8490	},
8491
8492
8493	// Sets the scroll state, making sure to overcome any predefined scroll value the browser has in mind
8494	forceScroll: function(scrollState) {
8495		var _this = this;
8496
8497		this.setScroll(scrollState);
8498		setTimeout(function() {
8499			_this.setScroll(scrollState);
8500		}, 0);
8501	},
8502
8503
8504	/* Event Elements / Segments
8505	------------------------------------------------------------------------------------------------------------------*/
8506
8507
8508	// Does everything necessary to display the given events onto the current view
8509	displayEvents: function(events) {
8510		var scrollState = this.queryScroll();
8511
8512		this.clearEvents();
8513		this.renderEvents(events);
8514		this.isEventsRendered = true;
8515		this.setScroll(scrollState);
8516		this.triggerEventRender();
8517	},
8518
8519
8520	// Does everything necessary to clear the view's currently-rendered events
8521	clearEvents: function() {
8522		var scrollState;
8523
8524		if (this.isEventsRendered) {
8525
8526			// TODO: optimize: if we know this is part of a displayEvents call, don't queryScroll/setScroll
8527			scrollState = this.queryScroll();
8528
8529			this.triggerEventUnrender();
8530			if (this.destroyEvents) {
8531				this.destroyEvents(); // TODO: deprecate
8532			}
8533			this.unrenderEvents();
8534			this.setScroll(scrollState);
8535			this.isEventsRendered = false;
8536		}
8537	},
8538
8539
8540	// Renders the events onto the view.
8541	renderEvents: function(events) {
8542		// subclasses should implement
8543	},
8544
8545
8546	// Removes event elements from the view.
8547	unrenderEvents: function() {
8548		// subclasses should implement
8549	},
8550
8551
8552	// Signals that all events have been rendered
8553	triggerEventRender: function() {
8554		this.renderedEventSegEach(function(seg) {
8555			this.trigger('eventAfterRender', seg.event, seg.event, seg.el);
8556		});
8557		this.trigger('eventAfterAllRender');
8558	},
8559
8560
8561	// Signals that all event elements are about to be removed
8562	triggerEventUnrender: function() {
8563		this.renderedEventSegEach(function(seg) {
8564			this.trigger('eventDestroy', seg.event, seg.event, seg.el);
8565		});
8566	},
8567
8568
8569	// Given an event and the default element used for rendering, returns the element that should actually be used.
8570	// Basically runs events and elements through the eventRender hook.
8571	resolveEventEl: function(event, el) {
8572		var custom = this.trigger('eventRender', event, event, el);
8573
8574		if (custom === false) { // means don't render at all
8575			el = null;
8576		}
8577		else if (custom && custom !== true) {
8578			el = $(custom);
8579		}
8580
8581		return el;
8582	},
8583
8584
8585	// Hides all rendered event segments linked to the given event
8586	showEvent: function(event) {
8587		this.renderedEventSegEach(function(seg) {
8588			seg.el.css('visibility', '');
8589		}, event);
8590	},
8591
8592
8593	// Shows all rendered event segments linked to the given event
8594	hideEvent: function(event) {
8595		this.renderedEventSegEach(function(seg) {
8596			seg.el.css('visibility', 'hidden');
8597		}, event);
8598	},
8599
8600
8601	// Iterates through event segments that have been rendered (have an el). Goes through all by default.
8602	// If the optional `event` argument is specified, only iterates through segments linked to that event.
8603	// The `this` value of the callback function will be the view.
8604	renderedEventSegEach: function(func, event) {
8605		var segs = this.getEventSegs();
8606		var i;
8607
8608		for (i = 0; i < segs.length; i++) {
8609			if (!event || segs[i].event._id === event._id) {
8610				if (segs[i].el) {
8611					func.call(this, segs[i]);
8612				}
8613			}
8614		}
8615	},
8616
8617
8618	// Retrieves all the rendered segment objects for the view
8619	getEventSegs: function() {
8620		// subclasses must implement
8621		return [];
8622	},
8623
8624
8625	/* Event Drag-n-Drop
8626	------------------------------------------------------------------------------------------------------------------*/
8627
8628
8629	// Computes if the given event is allowed to be dragged by the user
8630	isEventDraggable: function(event) {
8631		var source = event.source || {};
8632
8633		return firstDefined(
8634			event.startEditable,
8635			source.startEditable,
8636			this.opt('eventStartEditable'),
8637			event.editable,
8638			source.editable,
8639			this.opt('editable')
8640		);
8641	},
8642
8643
8644	// Must be called when an event in the view is dropped onto new location.
8645	// `dropLocation` is an object that contains the new zoned start/end/allDay values for the event.
8646	reportEventDrop: function(event, dropLocation, largeUnit, el, ev) {
8647		var calendar = this.calendar;
8648		var mutateResult = calendar.mutateEvent(event, dropLocation, largeUnit);
8649		var undoFunc = function() {
8650			mutateResult.undo();
8651			calendar.reportEventChange();
8652		};
8653
8654		this.triggerEventDrop(event, mutateResult.dateDelta, undoFunc, el, ev);
8655		calendar.reportEventChange(); // will rerender events
8656	},
8657
8658
8659	// Triggers event-drop handlers that have subscribed via the API
8660	triggerEventDrop: function(event, dateDelta, undoFunc, el, ev) {
8661		this.trigger('eventDrop', el[0], event, dateDelta, undoFunc, ev, {}); // {} = jqui dummy
8662	},
8663
8664
8665	/* External Element Drag-n-Drop
8666	------------------------------------------------------------------------------------------------------------------*/
8667
8668
8669	// Must be called when an external element, via jQuery UI, has been dropped onto the calendar.
8670	// `meta` is the parsed data that has been embedded into the dragging event.
8671	// `dropLocation` is an object that contains the new zoned start/end/allDay values for the event.
8672	reportExternalDrop: function(meta, dropLocation, el, ev, ui) {
8673		var eventProps = meta.eventProps;
8674		var eventInput;
8675		var event;
8676
8677		// Try to build an event object and render it. TODO: decouple the two
8678		if (eventProps) {
8679			eventInput = $.extend({}, eventProps, dropLocation);
8680			event = this.calendar.renderEvent(eventInput, meta.stick)[0]; // renderEvent returns an array
8681		}
8682
8683		this.triggerExternalDrop(event, dropLocation, el, ev, ui);
8684	},
8685
8686
8687	// Triggers external-drop handlers that have subscribed via the API
8688	triggerExternalDrop: function(event, dropLocation, el, ev, ui) {
8689
8690		// trigger 'drop' regardless of whether element represents an event
8691		this.trigger('drop', el[0], dropLocation.start, ev, ui);
8692
8693		if (event) {
8694			this.trigger('eventReceive', null, event); // signal an external event landed
8695		}
8696	},
8697
8698
8699	/* Drag-n-Drop Rendering (for both events and external elements)
8700	------------------------------------------------------------------------------------------------------------------*/
8701
8702
8703	// Renders a visual indication of a event or external-element drag over the given drop zone.
8704	// If an external-element, seg will be `null`.
8705	// Must return elements used for any mock events.
8706	renderDrag: function(dropLocation, seg) {
8707		// subclasses must implement
8708	},
8709
8710
8711	// Unrenders a visual indication of an event or external-element being dragged.
8712	unrenderDrag: function() {
8713		// subclasses must implement
8714	},
8715
8716
8717	/* Event Resizing
8718	------------------------------------------------------------------------------------------------------------------*/
8719
8720
8721	// Computes if the given event is allowed to be resized from its starting edge
8722	isEventResizableFromStart: function(event) {
8723		return this.opt('eventResizableFromStart') && this.isEventResizable(event);
8724	},
8725
8726
8727	// Computes if the given event is allowed to be resized from its ending edge
8728	isEventResizableFromEnd: function(event) {
8729		return this.isEventResizable(event);
8730	},
8731
8732
8733	// Computes if the given event is allowed to be resized by the user at all
8734	isEventResizable: function(event) {
8735		var source = event.source || {};
8736
8737		return firstDefined(
8738			event.durationEditable,
8739			source.durationEditable,
8740			this.opt('eventDurationEditable'),
8741			event.editable,
8742			source.editable,
8743			this.opt('editable')
8744		);
8745	},
8746
8747
8748	// Must be called when an event in the view has been resized to a new length
8749	reportEventResize: function(event, resizeLocation, largeUnit, el, ev) {
8750		var calendar = this.calendar;
8751		var mutateResult = calendar.mutateEvent(event, resizeLocation, largeUnit);
8752		var undoFunc = function() {
8753			mutateResult.undo();
8754			calendar.reportEventChange();
8755		};
8756
8757		this.triggerEventResize(event, mutateResult.durationDelta, undoFunc, el, ev);
8758		calendar.reportEventChange(); // will rerender events
8759	},
8760
8761
8762	// Triggers event-resize handlers that have subscribed via the API
8763	triggerEventResize: function(event, durationDelta, undoFunc, el, ev) {
8764		this.trigger('eventResize', el[0], event, durationDelta, undoFunc, ev, {}); // {} = jqui dummy
8765	},
8766
8767
8768	/* Selection (time range)
8769	------------------------------------------------------------------------------------------------------------------*/
8770
8771
8772	// Selects a date span on the view. `start` and `end` are both Moments.
8773	// `ev` is the native mouse event that begin the interaction.
8774	select: function(span, ev) {
8775		this.unselect(ev);
8776		this.renderSelection(span);
8777		this.reportSelection(span, ev);
8778	},
8779
8780
8781	// Renders a visual indication of the selection
8782	renderSelection: function(span) {
8783		// subclasses should implement
8784	},
8785
8786
8787	// Called when a new selection is made. Updates internal state and triggers handlers.
8788	reportSelection: function(span, ev) {
8789		this.isSelected = true;
8790		this.triggerSelect(span, ev);
8791	},
8792
8793
8794	// Triggers handlers to 'select'
8795	triggerSelect: function(span, ev) {
8796		this.trigger(
8797			'select',
8798			null,
8799			this.calendar.applyTimezone(span.start), // convert to calendar's tz for external API
8800			this.calendar.applyTimezone(span.end), // "
8801			ev
8802		);
8803	},
8804
8805
8806	// Undoes a selection. updates in the internal state and triggers handlers.
8807	// `ev` is the native mouse event that began the interaction.
8808	unselect: function(ev) {
8809		if (this.isSelected) {
8810			this.isSelected = false;
8811			if (this.destroySelection) {
8812				this.destroySelection(); // TODO: deprecate
8813			}
8814			this.unrenderSelection();
8815			this.trigger('unselect', null, ev);
8816		}
8817	},
8818
8819
8820	// Unrenders a visual indication of selection
8821	unrenderSelection: function() {
8822		// subclasses should implement
8823	},
8824
8825
8826	/* Event Selection
8827	------------------------------------------------------------------------------------------------------------------*/
8828
8829
8830	selectEvent: function(event) {
8831		if (!this.selectedEvent || this.selectedEvent !== event) {
8832			this.unselectEvent();
8833			this.renderedEventSegEach(function(seg) {
8834				seg.el.addClass('fc-selected');
8835			}, event);
8836			this.selectedEvent = event;
8837		}
8838	},
8839
8840
8841	unselectEvent: function() {
8842		if (this.selectedEvent) {
8843			this.renderedEventSegEach(function(seg) {
8844				seg.el.removeClass('fc-selected');
8845			}, this.selectedEvent);
8846			this.selectedEvent = null;
8847		}
8848	},
8849
8850
8851	isEventSelected: function(event) {
8852		// event references might change on refetchEvents(), while selectedEvent doesn't,
8853		// so compare IDs
8854		return this.selectedEvent && this.selectedEvent._id === event._id;
8855	},
8856
8857
8858	/* Mouse / Touch Unselecting (time range & event unselection)
8859	------------------------------------------------------------------------------------------------------------------*/
8860	// TODO: move consistently to down/start or up/end?
8861	// TODO: don't kill previous selection if touch scrolling
8862
8863
8864	handleDocumentMousedown: function(ev) {
8865		if (isPrimaryMouseButton(ev)) {
8866			this.processUnselect(ev);
8867		}
8868	},
8869
8870
8871	processUnselect: function(ev) {
8872		this.processRangeUnselect(ev);
8873		this.processEventUnselect(ev);
8874	},
8875
8876
8877	processRangeUnselect: function(ev) {
8878		var ignore;
8879
8880		// is there a time-range selection?
8881		if (this.isSelected && this.opt('unselectAuto')) {
8882			// only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element
8883			ignore = this.opt('unselectCancel');
8884			if (!ignore || !$(ev.target).closest(ignore).length) {
8885				this.unselect(ev);
8886			}
8887		}
8888	},
8889
8890
8891	processEventUnselect: function(ev) {
8892		if (this.selectedEvent) {
8893			if (!$(ev.target).closest('.fc-selected').length) {
8894				this.unselectEvent();
8895			}
8896		}
8897	},
8898
8899
8900	/* Day Click
8901	------------------------------------------------------------------------------------------------------------------*/
8902
8903
8904	// Triggers handlers to 'dayClick'
8905	// Span has start/end of the clicked area. Only the start is useful.
8906	triggerDayClick: function(span, dayEl, ev) {
8907		this.trigger(
8908			'dayClick',
8909			dayEl,
8910			this.calendar.applyTimezone(span.start), // convert to calendar's timezone for external API
8911			ev
8912		);
8913	},
8914
8915
8916	/* Date Utils
8917	------------------------------------------------------------------------------------------------------------------*/
8918
8919
8920	// Initializes internal variables related to calculating hidden days-of-week
8921	initHiddenDays: function() {
8922		var hiddenDays = this.opt('hiddenDays') || []; // array of day-of-week indices that are hidden
8923		var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
8924		var dayCnt = 0;
8925		var i;
8926
8927		if (this.opt('weekends') === false) {
8928			hiddenDays.push(0, 6); // 0=sunday, 6=saturday
8929		}
8930
8931		for (i = 0; i < 7; i++) {
8932			if (
8933				!(isHiddenDayHash[i] = $.inArray(i, hiddenDays) !== -1)
8934			) {
8935				dayCnt++;
8936			}
8937		}
8938
8939		if (!dayCnt) {
8940			throw 'invalid hiddenDays'; // all days were hidden? bad.
8941		}
8942
8943		this.isHiddenDayHash = isHiddenDayHash;
8944	},
8945
8946
8947	// Is the current day hidden?
8948	// `day` is a day-of-week index (0-6), or a Moment
8949	isHiddenDay: function(day) {
8950		if (moment.isMoment(day)) {
8951			day = day.day();
8952		}
8953		return this.isHiddenDayHash[day];
8954	},
8955
8956
8957	// Incrementing the current day until it is no longer a hidden day, returning a copy.
8958	// If the initial value of `date` is not a hidden day, don't do anything.
8959	// Pass `isExclusive` as `true` if you are dealing with an end date.
8960	// `inc` defaults to `1` (increment one day forward each time)
8961	skipHiddenDays: function(date, inc, isExclusive) {
8962		var out = date.clone();
8963		inc = inc || 1;
8964		while (
8965			this.isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7]
8966		) {
8967			out.add(inc, 'days');
8968		}
8969		return out;
8970	},
8971
8972
8973	// Returns the date range of the full days the given range visually appears to occupy.
8974	// Returns a new range object.
8975	computeDayRange: function(range) {
8976		var startDay = range.start.clone().stripTime(); // the beginning of the day the range starts
8977		var end = range.end;
8978		var endDay = null;
8979		var endTimeMS;
8980
8981		if (end) {
8982			endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends
8983			endTimeMS = +end.time(); // # of milliseconds into `endDay`
8984
8985			// If the end time is actually inclusively part of the next day and is equal to or
8986			// beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
8987			// Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
8988			if (endTimeMS && endTimeMS >= this.nextDayThreshold) {
8989				endDay.add(1, 'days');
8990			}
8991		}
8992
8993		// If no end was specified, or if it is within `startDay` but not past nextDayThreshold,
8994		// assign the default duration of one day.
8995		if (!end || endDay <= startDay) {
8996			endDay = startDay.clone().add(1, 'days');
8997		}
8998
8999		return { start: startDay, end: endDay };
9000	},
9001
9002
9003	// Does the given event visually appear to occupy more than one day?
9004	isMultiDayEvent: function(event) {
9005		var range = this.computeDayRange(event); // event is range-ish
9006
9007		return range.end.diff(range.start, 'days') > 1;
9008	}
9009
9010});
9011
9012;;
9013
9014/*
9015Embodies a div that has potential scrollbars
9016*/
9017var Scroller = FC.Scroller = Class.extend({
9018
9019	el: null, // the guaranteed outer element
9020	scrollEl: null, // the element with the scrollbars
9021	overflowX: null,
9022	overflowY: null,
9023
9024
9025	constructor: function(options) {
9026		options = options || {};
9027		this.overflowX = options.overflowX || options.overflow || 'auto';
9028		this.overflowY = options.overflowY || options.overflow || 'auto';
9029	},
9030
9031
9032	render: function() {
9033		this.el = this.renderEl();
9034		this.applyOverflow();
9035	},
9036
9037
9038	renderEl: function() {
9039		return (this.scrollEl = $('<div class="fc-scroller"></div>'));
9040	},
9041
9042
9043	// sets to natural height, unlocks overflow
9044	clear: function() {
9045		this.setHeight('auto');
9046		this.applyOverflow();
9047	},
9048
9049
9050	destroy: function() {
9051		this.el.remove();
9052	},
9053
9054
9055	// Overflow
9056	// -----------------------------------------------------------------------------------------------------------------
9057
9058
9059	applyOverflow: function() {
9060		this.scrollEl.css({
9061			'overflow-x': this.overflowX,
9062			'overflow-y': this.overflowY
9063		});
9064	},
9065
9066
9067	// Causes any 'auto' overflow values to resolves to 'scroll' or 'hidden'.
9068	// Useful for preserving scrollbar widths regardless of future resizes.
9069	// Can pass in scrollbarWidths for optimization.
9070	lockOverflow: function(scrollbarWidths) {
9071		var overflowX = this.overflowX;
9072		var overflowY = this.overflowY;
9073
9074		scrollbarWidths = scrollbarWidths || this.getScrollbarWidths();
9075
9076		if (overflowX === 'auto') {
9077			overflowX = (
9078					scrollbarWidths.top || scrollbarWidths.bottom || // horizontal scrollbars?
9079					// OR scrolling pane with massless scrollbars?
9080					this.scrollEl[0].scrollWidth - 1 > this.scrollEl[0].clientWidth
9081						// subtract 1 because of IE off-by-one issue
9082				) ? 'scroll' : 'hidden';
9083		}
9084
9085		if (overflowY === 'auto') {
9086			overflowY = (
9087					scrollbarWidths.left || scrollbarWidths.right || // vertical scrollbars?
9088					// OR scrolling pane with massless scrollbars?
9089					this.scrollEl[0].scrollHeight - 1 > this.scrollEl[0].clientHeight
9090						// subtract 1 because of IE off-by-one issue
9091				) ? 'scroll' : 'hidden';
9092		}
9093
9094		this.scrollEl.css({ 'overflow-x': overflowX, 'overflow-y': overflowY });
9095	},
9096
9097
9098	// Getters / Setters
9099	// -----------------------------------------------------------------------------------------------------------------
9100
9101
9102	setHeight: function(height) {
9103		this.scrollEl.height(height);
9104	},
9105
9106
9107	getScrollTop: function() {
9108		return this.scrollEl.scrollTop();
9109	},
9110
9111
9112	setScrollTop: function(top) {
9113		this.scrollEl.scrollTop(top);
9114	},
9115
9116
9117	getClientWidth: function() {
9118		return this.scrollEl[0].clientWidth;
9119	},
9120
9121
9122	getClientHeight: function() {
9123		return this.scrollEl[0].clientHeight;
9124	},
9125
9126
9127	getScrollbarWidths: function() {
9128		return getScrollbarWidths(this.scrollEl);
9129	}
9130
9131});
9132
9133;;
9134
9135var Calendar = FC.Calendar = Class.extend({
9136
9137	dirDefaults: null, // option defaults related to LTR or RTL
9138	langDefaults: null, // option defaults related to current locale
9139	overrides: null, // option overrides given to the fullCalendar constructor
9140	dynamicOverrides: null, // options set with dynamic setter method. higher precedence than view overrides.
9141	options: null, // all defaults combined with overrides
9142	viewSpecCache: null, // cache of view definitions
9143	view: null, // current View object
9144	header: null,
9145	loadingLevel: 0, // number of simultaneous loading tasks
9146
9147
9148	// a lot of this class' OOP logic is scoped within this constructor function,
9149	// but in the future, write individual methods on the prototype.
9150	constructor: Calendar_constructor,
9151
9152
9153	// Subclasses can override this for initialization logic after the constructor has been called
9154	initialize: function() {
9155	},
9156
9157
9158	// Computes the flattened options hash for the calendar and assigns to `this.options`.
9159	// Assumes this.overrides and this.dynamicOverrides have already been initialized.
9160	populateOptionsHash: function() {
9161		var lang, langDefaults;
9162		var isRTL, dirDefaults;
9163
9164		lang = firstDefined( // explicit lang option given?
9165			this.dynamicOverrides.lang,
9166			this.overrides.lang
9167		);
9168		langDefaults = langOptionHash[lang];
9169		if (!langDefaults) { // explicit lang option not given or invalid?
9170			lang = Calendar.defaults.lang;
9171			langDefaults = langOptionHash[lang] || {};
9172		}
9173
9174		isRTL = firstDefined( // based on options computed so far, is direction RTL?
9175			this.dynamicOverrides.isRTL,
9176			this.overrides.isRTL,
9177			langDefaults.isRTL,
9178			Calendar.defaults.isRTL
9179		);
9180		dirDefaults = isRTL ? Calendar.rtlDefaults : {};
9181
9182		this.dirDefaults = dirDefaults;
9183		this.langDefaults = langDefaults;
9184		this.options = mergeOptions([ // merge defaults and overrides. lowest to highest precedence
9185			Calendar.defaults, // global defaults
9186			dirDefaults,
9187			langDefaults,
9188			this.overrides,
9189			this.dynamicOverrides
9190		]);
9191		populateInstanceComputableOptions(this.options); // fill in gaps with computed options
9192	},
9193
9194
9195	// Gets information about how to create a view. Will use a cache.
9196	getViewSpec: function(viewType) {
9197		var cache = this.viewSpecCache;
9198
9199		return cache[viewType] || (cache[viewType] = this.buildViewSpec(viewType));
9200	},
9201
9202
9203	// Given a duration singular unit, like "week" or "day", finds a matching view spec.
9204	// Preference is given to views that have corresponding buttons.
9205	getUnitViewSpec: function(unit) {
9206		var viewTypes;
9207		var i;
9208		var spec;
9209
9210		if ($.inArray(unit, intervalUnits) != -1) {
9211
9212			// put views that have buttons first. there will be duplicates, but oh well
9213			viewTypes = this.header.getViewsWithButtons();
9214			$.each(FC.views, function(viewType) { // all views
9215				viewTypes.push(viewType);
9216			});
9217
9218			for (i = 0; i < viewTypes.length; i++) {
9219				spec = this.getViewSpec(viewTypes[i]);
9220				if (spec) {
9221					if (spec.singleUnit == unit) {
9222						return spec;
9223					}
9224				}
9225			}
9226		}
9227	},
9228
9229
9230	// Builds an object with information on how to create a given view
9231	buildViewSpec: function(requestedViewType) {
9232		var viewOverrides = this.overrides.views || {};
9233		var specChain = []; // for the view. lowest to highest priority
9234		var defaultsChain = []; // for the view. lowest to highest priority
9235		var overridesChain = []; // for the view. lowest to highest priority
9236		var viewType = requestedViewType;
9237		var spec; // for the view
9238		var overrides; // for the view
9239		var duration;
9240		var unit;
9241
9242		// iterate from the specific view definition to a more general one until we hit an actual View class
9243		while (viewType) {
9244			spec = fcViews[viewType];
9245			overrides = viewOverrides[viewType];
9246			viewType = null; // clear. might repopulate for another iteration
9247
9248			if (typeof spec === 'function') { // TODO: deprecate
9249				spec = { 'class': spec };
9250			}
9251
9252			if (spec) {
9253				specChain.unshift(spec);
9254				defaultsChain.unshift(spec.defaults || {});
9255				duration = duration || spec.duration;
9256				viewType = viewType || spec.type;
9257			}
9258
9259			if (overrides) {
9260				overridesChain.unshift(overrides); // view-specific option hashes have options at zero-level
9261				duration = duration || overrides.duration;
9262				viewType = viewType || overrides.type;
9263			}
9264		}
9265
9266		spec = mergeProps(specChain);
9267		spec.type = requestedViewType;
9268		if (!spec['class']) {
9269			return false;
9270		}
9271
9272		if (duration) {
9273			duration = moment.duration(duration);
9274			if (duration.valueOf()) { // valid?
9275				spec.duration = duration;
9276				unit = computeIntervalUnit(duration);
9277
9278				// view is a single-unit duration, like "week" or "day"
9279				// incorporate options for this. lowest priority
9280				if (duration.as(unit) === 1) {
9281					spec.singleUnit = unit;
9282					overridesChain.unshift(viewOverrides[unit] || {});
9283				}
9284			}
9285		}
9286
9287		spec.defaults = mergeOptions(defaultsChain);
9288		spec.overrides = mergeOptions(overridesChain);
9289
9290		this.buildViewSpecOptions(spec);
9291		this.buildViewSpecButtonText(spec, requestedViewType);
9292
9293		return spec;
9294	},
9295
9296
9297	// Builds and assigns a view spec's options object from its already-assigned defaults and overrides
9298	buildViewSpecOptions: function(spec) {
9299		spec.options = mergeOptions([ // lowest to highest priority
9300			Calendar.defaults, // global defaults
9301			spec.defaults, // view's defaults (from ViewSubclass.defaults)
9302			this.dirDefaults,
9303			this.langDefaults, // locale and dir take precedence over view's defaults!
9304			this.overrides, // calendar's overrides (options given to constructor)
9305			spec.overrides, // view's overrides (view-specific options)
9306			this.dynamicOverrides // dynamically set via setter. highest precedence
9307		]);
9308		populateInstanceComputableOptions(spec.options);
9309	},
9310
9311
9312	// Computes and assigns a view spec's buttonText-related options
9313	buildViewSpecButtonText: function(spec, requestedViewType) {
9314
9315		// given an options object with a possible `buttonText` hash, lookup the buttonText for the
9316		// requested view, falling back to a generic unit entry like "week" or "day"
9317		function queryButtonText(options) {
9318			var buttonText = options.buttonText || {};
9319			return buttonText[requestedViewType] ||
9320				(spec.singleUnit ? buttonText[spec.singleUnit] : null);
9321		}
9322
9323		// highest to lowest priority
9324		spec.buttonTextOverride =
9325			queryButtonText(this.dynamicOverrides) ||
9326			queryButtonText(this.overrides) || // constructor-specified buttonText lookup hash takes precedence
9327			spec.overrides.buttonText; // `buttonText` for view-specific options is a string
9328
9329		// highest to lowest priority. mirrors buildViewSpecOptions
9330		spec.buttonTextDefault =
9331			queryButtonText(this.langDefaults) ||
9332			queryButtonText(this.dirDefaults) ||
9333			spec.defaults.buttonText || // a single string. from ViewSubclass.defaults
9334			queryButtonText(Calendar.defaults) ||
9335			(spec.duration ? this.humanizeDuration(spec.duration) : null) || // like "3 days"
9336			requestedViewType; // fall back to given view name
9337	},
9338
9339
9340	// Given a view name for a custom view or a standard view, creates a ready-to-go View object
9341	instantiateView: function(viewType) {
9342		var spec = this.getViewSpec(viewType);
9343
9344		return new spec['class'](this, viewType, spec.options, spec.duration);
9345	},
9346
9347
9348	// Returns a boolean about whether the view is okay to instantiate at some point
9349	isValidViewType: function(viewType) {
9350		return Boolean(this.getViewSpec(viewType));
9351	},
9352
9353
9354	// Should be called when any type of async data fetching begins
9355	pushLoading: function() {
9356		if (!(this.loadingLevel++)) {
9357			this.trigger('loading', null, true, this.view);
9358		}
9359	},
9360
9361
9362	// Should be called when any type of async data fetching completes
9363	popLoading: function() {
9364		if (!(--this.loadingLevel)) {
9365			this.trigger('loading', null, false, this.view);
9366		}
9367	},
9368
9369
9370	// Given arguments to the select method in the API, returns a span (unzoned start/end and other info)
9371	buildSelectSpan: function(zonedStartInput, zonedEndInput) {
9372		var start = this.moment(zonedStartInput).stripZone();
9373		var end;
9374
9375		if (zonedEndInput) {
9376			end = this.moment(zonedEndInput).stripZone();
9377		}
9378		else if (start.hasTime()) {
9379			end = start.clone().add(this.defaultTimedEventDuration);
9380		}
9381		else {
9382			end = start.clone().add(this.defaultAllDayEventDuration);
9383		}
9384
9385		return { start: start, end: end };
9386	}
9387
9388});
9389
9390
9391Calendar.mixin(EmitterMixin);
9392
9393
9394function Calendar_constructor(element, overrides) {
9395	var t = this;
9396
9397
9398	// Exports
9399	// -----------------------------------------------------------------------------------
9400
9401	t.render = render;
9402	t.destroy = destroy;
9403	t.refetchEvents = refetchEvents;
9404	t.refetchEventSources = refetchEventSources;
9405	t.reportEvents = reportEvents;
9406	t.reportEventChange = reportEventChange;
9407	t.rerenderEvents = renderEvents; // `renderEvents` serves as a rerender. an API method
9408	t.changeView = renderView; // `renderView` will switch to another view
9409	t.select = select;
9410	t.unselect = unselect;
9411	t.prev = prev;
9412	t.next = next;
9413	t.prevYear = prevYear;
9414	t.nextYear = nextYear;
9415	t.today = today;
9416	t.gotoDate = gotoDate;
9417	t.incrementDate = incrementDate;
9418	t.zoomTo = zoomTo;
9419	t.getDate = getDate;
9420	t.getCalendar = getCalendar;
9421	t.getView = getView;
9422	t.option = option; // getter/setter method
9423	t.trigger = trigger;
9424
9425
9426	// Options
9427	// -----------------------------------------------------------------------------------
9428
9429	t.dynamicOverrides = {};
9430	t.viewSpecCache = {};
9431	t.optionHandlers = {}; // for Calendar.options.js
9432
9433	// convert legacy options into non-legacy ones.
9434	// in the future, when this is removed, don't use `overrides` reference. make a copy.
9435	t.overrides = massageOverrides(overrides || {});
9436
9437	t.populateOptionsHash(); // sets this.options
9438
9439
9440
9441	// Language-data Internals
9442	// -----------------------------------------------------------------------------------
9443	// Apply overrides to the current language's data
9444
9445	var localeData;
9446
9447	// Called immediately, and when any of the options change.
9448	// Happens before any internal objects rebuild or rerender, because this is very core.
9449	t.bindOptions([
9450		'lang', 'monthNames', 'monthNamesShort', 'dayNames', 'dayNamesShort', 'firstDay', 'weekNumberCalculation'
9451	], function(lang, monthNames, monthNamesShort, dayNames, dayNamesShort, firstDay, weekNumberCalculation) {
9452
9453		localeData = createObject( // make a cheap copy
9454			getMomentLocaleData(lang) // will fall back to en
9455		);
9456
9457		if (monthNames) {
9458			localeData._months = monthNames;
9459		}
9460		if (monthNamesShort) {
9461			localeData._monthsShort = monthNamesShort;
9462		}
9463		if (dayNames) {
9464			localeData._weekdays = dayNames;
9465		}
9466		if (dayNamesShort) {
9467			localeData._weekdaysShort = dayNamesShort;
9468		}
9469		if (firstDay != null) {
9470			var _week = createObject(localeData._week); // _week: { dow: # }
9471			_week.dow = firstDay;
9472			localeData._week = _week;
9473		}
9474
9475		if (weekNumberCalculation === 'iso') {
9476			weekNumberCalculation = 'ISO'; // normalize
9477		}
9478		if ( // whitelist certain kinds of input
9479			weekNumberCalculation === 'ISO' ||
9480			weekNumberCalculation === 'local' ||
9481			typeof weekNumberCalculation === 'function'
9482		) {
9483			localeData._fullCalendar_weekCalc = weekNumberCalculation; // moment-ext will know what to do with it
9484		}
9485
9486		// If the internal current date object already exists, move to new locale.
9487		// We do NOT need to do this technique for event dates, because this happens when converting to "segments".
9488		if (date) {
9489			localizeMoment(date); // sets to localeData
9490		}
9491	});
9492
9493
9494
9495	// Calendar-specific Date Utilities
9496	// -----------------------------------------------------------------------------------
9497
9498
9499	t.defaultAllDayEventDuration = moment.duration(t.options.defaultAllDayEventDuration);
9500	t.defaultTimedEventDuration = moment.duration(t.options.defaultTimedEventDuration);
9501
9502
9503	// Builds a moment using the settings of the current calendar: timezone and language.
9504	// Accepts anything the vanilla moment() constructor accepts.
9505	t.moment = function() {
9506		var mom;
9507
9508		if (t.options.timezone === 'local') {
9509			mom = FC.moment.apply(null, arguments);
9510
9511			// Force the moment to be local, because FC.moment doesn't guarantee it.
9512			if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone
9513				mom.local();
9514			}
9515		}
9516		else if (t.options.timezone === 'UTC') {
9517			mom = FC.moment.utc.apply(null, arguments); // process as UTC
9518		}
9519		else {
9520			mom = FC.moment.parseZone.apply(null, arguments); // let the input decide the zone
9521		}
9522
9523		localizeMoment(mom);
9524
9525		return mom;
9526	};
9527
9528
9529	// Updates the given moment's locale settings to the current calendar locale settings.
9530	function localizeMoment(mom) {
9531		if ('_locale' in mom) { // moment 2.8 and above
9532			mom._locale = localeData;
9533		}
9534		else { // pre-moment-2.8
9535			mom._lang = localeData;
9536		}
9537	}
9538
9539
9540	// Returns a boolean about whether or not the calendar knows how to calculate
9541	// the timezone offset of arbitrary dates in the current timezone.
9542	t.getIsAmbigTimezone = function() {
9543		return t.options.timezone !== 'local' && t.options.timezone !== 'UTC';
9544	};
9545
9546
9547	// Returns a copy of the given date in the current timezone. Has no effect on dates without times.
9548	t.applyTimezone = function(date) {
9549		if (!date.hasTime()) {
9550			return date.clone();
9551		}
9552
9553		var zonedDate = t.moment(date.toArray());
9554		var timeAdjust = date.time() - zonedDate.time();
9555		var adjustedZonedDate;
9556
9557		// Safari sometimes has problems with this coersion when near DST. Adjust if necessary. (bug #2396)
9558		if (timeAdjust) { // is the time result different than expected?
9559			adjustedZonedDate = zonedDate.clone().add(timeAdjust); // add milliseconds
9560			if (date.time() - adjustedZonedDate.time() === 0) { // does it match perfectly now?
9561				zonedDate = adjustedZonedDate;
9562			}
9563		}
9564
9565		return zonedDate;
9566	};
9567
9568
9569	// Returns a moment for the current date, as defined by the client's computer or from the `now` option.
9570	// Will return an moment with an ambiguous timezone.
9571	t.getNow = function() {
9572		var now = t.options.now;
9573		if (typeof now === 'function') {
9574			now = now();
9575		}
9576		return t.moment(now).stripZone();
9577	};
9578
9579
9580	// Get an event's normalized end date. If not present, calculate it from the defaults.
9581	t.getEventEnd = function(event) {
9582		if (event.end) {
9583			return event.end.clone();
9584		}
9585		else {
9586			return t.getDefaultEventEnd(event.allDay, event.start);
9587		}
9588	};
9589
9590
9591	// Given an event's allDay status and start date, return what its fallback end date should be.
9592	// TODO: rename to computeDefaultEventEnd
9593	t.getDefaultEventEnd = function(allDay, zonedStart) {
9594		var end = zonedStart.clone();
9595
9596		if (allDay) {
9597			end.stripTime().add(t.defaultAllDayEventDuration);
9598		}
9599		else {
9600			end.add(t.defaultTimedEventDuration);
9601		}
9602
9603		if (t.getIsAmbigTimezone()) {
9604			end.stripZone(); // we don't know what the tzo should be
9605		}
9606
9607		return end;
9608	};
9609
9610
9611	// Produces a human-readable string for the given duration.
9612	// Side-effect: changes the locale of the given duration.
9613	t.humanizeDuration = function(duration) {
9614		return (duration.locale || duration.lang).call(duration, t.options.lang) // works moment-pre-2.8
9615			.humanize();
9616	};
9617
9618
9619	
9620	// Imports
9621	// -----------------------------------------------------------------------------------
9622
9623
9624	EventManager.call(t);
9625	var isFetchNeeded = t.isFetchNeeded;
9626	var fetchEvents = t.fetchEvents;
9627	var fetchEventSources = t.fetchEventSources;
9628
9629
9630
9631	// Locals
9632	// -----------------------------------------------------------------------------------
9633
9634
9635	var _element = element[0];
9636	var header;
9637	var content;
9638	var tm; // for making theme classes
9639	var currentView; // NOTE: keep this in sync with this.view
9640	var viewsByType = {}; // holds all instantiated view instances, current or not
9641	var suggestedViewHeight;
9642	var windowResizeProxy; // wraps the windowResize function
9643	var ignoreWindowResize = 0;
9644	var events = [];
9645	var date; // unzoned
9646	
9647	
9648	
9649	// Main Rendering
9650	// -----------------------------------------------------------------------------------
9651
9652
9653	// compute the initial ambig-timezone date
9654	if (t.options.defaultDate != null) {
9655		date = t.moment(t.options.defaultDate).stripZone();
9656	}
9657	else {
9658		date = t.getNow(); // getNow already returns unzoned
9659	}
9660	
9661	
9662	function render() {
9663		if (!content) {
9664			initialRender();
9665		}
9666		else if (elementVisible()) {
9667			// mainly for the public API
9668			calcSize();
9669			renderView();
9670		}
9671	}
9672	
9673	
9674	function initialRender() {
9675		element.addClass('fc');
9676
9677		// called immediately, and upon option change
9678		t.bindOption('theme', function(theme) {
9679			tm = theme ? 'ui' : 'fc'; // affects a larger scope
9680			element.toggleClass('ui-widget', theme);
9681			element.toggleClass('fc-unthemed', !theme);
9682		});
9683
9684		// called immediately, and upon option change.
9685		// HACK: lang often affects isRTL, so we explicitly listen to that too.
9686		t.bindOptions([ 'isRTL', 'lang' ], function(isRTL) {
9687			element.toggleClass('fc-ltr', !isRTL);
9688			element.toggleClass('fc-rtl', isRTL);
9689		});
9690
9691		content = $("<div class='fc-view-container'/>").prependTo(element);
9692
9693		header = t.header = new Header(t);
9694		renderHeader();
9695
9696		renderView(t.options.defaultView);
9697
9698		if (t.options.handleWindowResize) {
9699			windowResizeProxy = debounce(windowResize, t.options.windowResizeDelay); // prevents rapid calls
9700			$(window).resize(windowResizeProxy);
9701		}
9702	}
9703
9704
9705	// can be called repeatedly and Header will rerender
9706	function renderHeader() {
9707		header.render();
9708		if (header.el) {
9709			element.prepend(header.el);
9710		}
9711	}
9712	
9713	
9714	function destroy() {
9715
9716		if (currentView) {
9717			currentView.removeElement();
9718
9719			// NOTE: don't null-out currentView/t.view in case API methods are called after destroy.
9720			// It is still the "current" view, just not rendered.
9721		}
9722
9723		header.removeElement();
9724		content.remove();
9725		element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');
9726
9727		if (windowResizeProxy) {
9728			$(window).unbind('resize', windowResizeProxy);
9729		}
9730	}
9731	
9732	
9733	function elementVisible() {
9734		return element.is(':visible');
9735	}
9736	
9737	
9738
9739	// View Rendering
9740	// -----------------------------------------------------------------------------------
9741
9742
9743	// Renders a view because of a date change, view-type change, or for the first time.
9744	// If not given a viewType, keep the current view but render different dates.
9745	// Accepts an optional scroll state to restore to.
9746	function renderView(viewType, explicitScrollState) {
9747		ignoreWindowResize++;
9748
9749		// if viewType is changing, remove the old view's rendering
9750		if (currentView && viewType && currentView.type !== viewType) {
9751			freezeContentHeight(); // prevent a scroll jump when view element is removed
9752			clearView();
9753		}
9754
9755		// if viewType changed, or the view was never created, create a fresh view
9756		if (!currentView && viewType) {
9757			currentView = t.view =
9758				viewsByType[viewType] ||
9759				(viewsByType[viewType] = t.instantiateView(viewType));
9760
9761			currentView.setElement(
9762				$("<div class='fc-view fc-" + viewType + "-view' />").appendTo(content)
9763			);
9764			header.activateButton(viewType);
9765		}
9766
9767		if (currentView) {
9768
9769			// in case the view should render a period of time that is completely hidden
9770			date = currentView.massageCurrentDate(date);
9771
9772			// render or rerender the view
9773			if (
9774				!currentView.displaying ||
9775				!date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change
9776			) {
9777				if (elementVisible()) {
9778
9779					currentView.display(date, explicitScrollState); // will call freezeContentHeight
9780					unfreezeContentHeight(); // immediately unfreeze regardless of whether display is async
9781
9782					// need to do this after View::render, so dates are calculated
9783					updateHeaderTitle();
9784					updateTodayButton();
9785
9786					getAndRenderEvents();
9787				}
9788			}
9789		}
9790
9791		unfreezeContentHeight(); // undo any lone freezeContentHeight calls
9792		ignoreWindowResize--;
9793	}
9794
9795
9796	// Unrenders the current view and reflects this change in the Header.
9797	// Unregsiters the `currentView`, but does not remove from viewByType hash.
9798	function clearView() {
9799		header.deactivateButton(currentView.type);
9800		currentView.removeElement();
9801		currentView = t.view = null;
9802	}
9803
9804
9805	// Destroys the view, including the view object. Then, re-instantiates it and renders it.
9806	// Maintains the same scroll state.
9807	// TODO: maintain any other user-manipulated state.
9808	function reinitView() {
9809		ignoreWindowResize++;
9810		freezeContentHeight();
9811
9812		var viewType = currentView.type;
9813		var scrollState = currentView.queryScroll();
9814		clearView();
9815		renderView(viewType, scrollState);
9816
9817		unfreezeContentHeight();
9818		ignoreWindowResize--;
9819	}
9820
9821	
9822
9823	// Resizing
9824	// -----------------------------------------------------------------------------------
9825
9826
9827	t.getSuggestedViewHeight = function() {
9828		if (suggestedViewHeight === undefined) {
9829			calcSize();
9830		}
9831		return suggestedViewHeight;
9832	};
9833
9834
9835	t.isHeightAuto = function() {
9836		return t.options.contentHeight === 'auto' || t.options.height === 'auto';
9837	};
9838	
9839	
9840	function updateSize(shouldRecalc) {
9841		if (elementVisible()) {
9842
9843			if (shouldRecalc) {
9844				_calcSize();
9845			}
9846
9847			ignoreWindowResize++;
9848			currentView.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto()
9849			ignoreWindowResize--;
9850
9851			return true; // signal success
9852		}
9853	}
9854
9855
9856	function calcSize() {
9857		if (elementVisible()) {
9858			_calcSize();
9859		}
9860	}
9861	
9862	
9863	function _calcSize() { // assumes elementVisible
9864		var contentHeightInput = t.options.contentHeight;
9865		var heightInput = t.options.height;
9866
9867		if (typeof contentHeightInput === 'number') { // exists and not 'auto'
9868			suggestedViewHeight = contentHeightInput;
9869		}
9870		else if (typeof contentHeightInput === 'function') { // exists and is a function
9871			suggestedViewHeight = contentHeightInput();
9872		}
9873		else if (typeof heightInput === 'number') { // exists and not 'auto'
9874			suggestedViewHeight = heightInput - queryHeaderHeight();
9875		}
9876		else if (typeof heightInput === 'function') { // exists and is a function
9877			suggestedViewHeight = heightInput() - queryHeaderHeight();
9878		}
9879		else if (heightInput === 'parent') { // set to height of parent element
9880			suggestedViewHeight = element.parent().height() - queryHeaderHeight();
9881		}
9882		else {
9883			suggestedViewHeight = Math.round(content.width() / Math.max(t.options.aspectRatio, .5));
9884		}
9885	}
9886
9887
9888	function queryHeaderHeight() {
9889		return header.el ? header.el.outerHeight(true) : 0; // includes margin
9890	}
9891	
9892	
9893	function windowResize(ev) {
9894		if (
9895			!ignoreWindowResize &&
9896			ev.target === window && // so we don't process jqui "resize" events that have bubbled up
9897			currentView.start // view has already been rendered
9898		) {
9899			if (updateSize(true)) {
9900				currentView.trigger('windowResize', _element);
9901			}
9902		}
9903	}
9904	
9905	
9906	
9907	/* Event Fetching/Rendering
9908	-----------------------------------------------------------------------------*/
9909	// TODO: going forward, most of this stuff should be directly handled by the view
9910
9911
9912	function refetchEvents() { // can be called as an API method
9913		fetchAndRenderEvents();
9914	}
9915
9916
9917	// TODO: move this into EventManager?
9918	function refetchEventSources(matchInputs) {
9919		fetchEventSources(t.getEventSourcesByMatchArray(matchInputs));
9920	}
9921
9922
9923	function renderEvents() { // destroys old events if previously rendered
9924		if (elementVisible()) {
9925			freezeContentHeight();
9926			currentView.displayEvents(events);
9927			unfreezeContentHeight();
9928		}
9929	}
9930	
9931
9932	function getAndRenderEvents() {
9933		if (!t.options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) {
9934			fetchAndRenderEvents();
9935		}
9936		else {
9937			renderEvents();
9938		}
9939	}
9940
9941
9942	function fetchAndRenderEvents() {
9943		fetchEvents(currentView.start, currentView.end);
9944			// ... will call reportEvents
9945			// ... which will call renderEvents
9946	}
9947
9948	
9949	// called when event data arrives
9950	function reportEvents(_events) {
9951		events = _events;
9952		renderEvents();
9953	}
9954
9955
9956	// called when a single event's data has been changed
9957	function reportEventChange() {
9958		renderEvents();
9959	}
9960
9961
9962
9963	/* Header Updating
9964	-----------------------------------------------------------------------------*/
9965
9966
9967	function updateHeaderTitle() {
9968		header.updateTitle(currentView.title);
9969	}
9970
9971
9972	function updateTodayButton() {
9973		var now = t.getNow();
9974		if (now.isWithin(currentView.intervalStart, currentView.intervalEnd)) {
9975			header.disableButton('today');
9976		}
9977		else {
9978			header.enableButton('today');
9979		}
9980	}
9981	
9982
9983
9984	/* Selection
9985	-----------------------------------------------------------------------------*/
9986	
9987
9988	// this public method receives start/end dates in any format, with any timezone
9989	function select(zonedStartInput, zonedEndInput) {
9990		currentView.select(
9991			t.buildSelectSpan.apply(t, arguments)
9992		);
9993	}
9994	
9995
9996	function unselect() { // safe to be called before renderView
9997		if (currentView) {
9998			currentView.unselect();
9999		}
10000	}
10001	
10002	
10003	
10004	/* Date
10005	-----------------------------------------------------------------------------*/
10006	
10007	
10008	function prev() {
10009		date = currentView.computePrevDate(date);
10010		renderView();
10011	}
10012	
10013	
10014	function next() {
10015		date = currentView.computeNextDate(date);
10016		renderView();
10017	}
10018	
10019	
10020	function prevYear() {
10021		date.add(-1, 'years');
10022		renderView();
10023	}
10024	
10025	
10026	function nextYear() {
10027		date.add(1, 'years');
10028		renderView();
10029	}
10030	
10031	
10032	function today() {
10033		date = t.getNow();
10034		renderView();
10035	}
10036	
10037	
10038	function gotoDate(zonedDateInput) {
10039		date = t.moment(zonedDateInput).stripZone();
10040		renderView();
10041	}
10042	
10043	
10044	function incrementDate(delta) {
10045		date.add(moment.duration(delta));
10046		renderView();
10047	}
10048
10049
10050	// Forces navigation to a view for the given date.
10051	// `viewType` can be a specific view name or a generic one like "week" or "day".
10052	function zoomTo(newDate, viewType) {
10053		var spec;
10054
10055		viewType = viewType || 'day'; // day is default zoom
10056		spec = t.getViewSpec(viewType) || t.getUnitViewSpec(viewType);
10057
10058		date = newDate.clone();
10059		renderView(spec ? spec.type : null);
10060	}
10061	
10062	
10063	// for external API
10064	function getDate() {
10065		return t.applyTimezone(date); // infuse the calendar's timezone
10066	}
10067
10068
10069
10070	/* Height "Freezing"
10071	-----------------------------------------------------------------------------*/
10072	// TODO: move this into the view
10073
10074	t.freezeContentHeight = freezeContentHeight;
10075	t.unfreezeContentHeight = unfreezeContentHeight;
10076
10077
10078	function freezeContentHeight() {
10079		content.css({
10080			width: '100%',
10081			height: content.height(),
10082			overflow: 'hidden'
10083		});
10084	}
10085
10086
10087	function unfreezeContentHeight() {
10088		content.css({
10089			width: '',
10090			height: '',
10091			overflow: ''
10092		});
10093	}
10094	
10095	
10096	
10097	/* Misc
10098	-----------------------------------------------------------------------------*/
10099	
10100
10101	function getCalendar() {
10102		return t;
10103	}
10104
10105	
10106	function getView() {
10107		return currentView;
10108	}
10109	
10110	
10111	function option(name, value) {
10112		var newOptionHash;
10113
10114		if (typeof name === 'string') {
10115			if (value === undefined) { // getter
10116				return t.options[name];
10117			}
10118			else { // setter for individual option
10119				newOptionHash = {};
10120				newOptionHash[name] = value;
10121				setOptions(newOptionHash);
10122			}
10123		}
10124		else if (typeof name === 'object') { // compound setter with object input
10125			setOptions(name);
10126		}
10127	}
10128
10129
10130	function setOptions(newOptionHash) {
10131		var optionCnt = 0;
10132		var optionName;
10133
10134		for (optionName in newOptionHash) {
10135			t.dynamicOverrides[optionName] = newOptionHash[optionName];
10136		}
10137
10138		t.viewSpecCache = {}; // the dynamic override invalidates the options in this cache, so just clear it
10139		t.populateOptionsHash(); // this.options needs to be recomputed after the dynamic override
10140
10141		// trigger handlers after this.options has been updated
10142		for (optionName in newOptionHash) {
10143			t.triggerOptionHandlers(optionName); // recall bindOption/bindOptions
10144			optionCnt++;
10145		}
10146
10147		// special-case handling of single option change.
10148		// if only one option change, `optionName` will be its name.
10149		if (optionCnt === 1) {
10150			if (optionName === 'height' || optionName === 'contentHeight' || optionName === 'aspectRatio') {
10151				updateSize(true); // true = allow recalculation of height
10152				return;
10153			}
10154			else if (optionName === 'defaultDate') {
10155				return; // can't change date this way. use gotoDate instead
10156			}
10157			else if (optionName === 'businessHours') {
10158				if (currentView) {
10159					currentView.unrenderBusinessHours();
10160					currentView.renderBusinessHours();
10161				}
10162				return;
10163			}
10164			else if (optionName === 'timezone') {
10165				t.rezoneArrayEventSources();
10166				refetchEvents();
10167				return;
10168			}
10169		}
10170
10171		// catch-all. rerender the header and rebuild/rerender the current view
10172		renderHeader();
10173		viewsByType = {}; // even non-current views will be affected by this option change. do before rerender
10174		reinitView();
10175	}
10176	
10177	
10178	function trigger(name, thisObj) { // overrides the Emitter's trigger method :(
10179		var args = Array.prototype.slice.call(arguments, 2);
10180
10181		thisObj = thisObj || _element;
10182		this.triggerWith(name, thisObj, args); // Emitter's method
10183
10184		if (t.options[name]) {
10185			return t.options[name].apply(thisObj, args);
10186		}
10187	}
10188
10189	t.initialize();
10190}
10191
10192;;
10193/*
10194Options binding/triggering system.
10195*/
10196Calendar.mixin({
10197
10198	// A map of option names to arrays of handler objects. Initialized to {} in Calendar.
10199	// Format for a handler object:
10200	// {
10201	//   func // callback function to be called upon change
10202	//   names // option names whose values should be given to func
10203	// }
10204	optionHandlers: null, 
10205
10206	// Calls handlerFunc immediately, and when the given option has changed.
10207	// handlerFunc will be given the option value.
10208	bindOption: function(optionName, handlerFunc) {
10209		this.bindOptions([ optionName ], handlerFunc);
10210	},
10211
10212	// Calls handlerFunc immediately, and when any of the given options change.
10213	// handlerFunc will be given each option value as ordered function arguments.
10214	bindOptions: function(optionNames, handlerFunc) {
10215		var handlerObj = { func: handlerFunc, names: optionNames };
10216		var i;
10217
10218		for (i = 0; i < optionNames.length; i++) {
10219			this.registerOptionHandlerObj(optionNames[i], handlerObj);
10220		}
10221
10222		this.triggerOptionHandlerObj(handlerObj);
10223	},
10224
10225	// Puts the given handler object into the internal hash
10226	registerOptionHandlerObj: function(optionName, handlerObj) {
10227		(this.optionHandlers[optionName] || (this.optionHandlers[optionName] = []))
10228			.push(handlerObj);
10229	},
10230
10231	// Reports that the given option has changed, and calls all appropriate handlers.
10232	triggerOptionHandlers: function(optionName) {
10233		var handlerObjs = this.optionHandlers[optionName] || [];
10234		var i;
10235
10236		for (i = 0; i < handlerObjs.length; i++) {
10237			this.triggerOptionHandlerObj(handlerObjs[i]);
10238		}
10239	},
10240
10241	// Calls the callback for a specific handler object, passing in the appropriate arguments.
10242	triggerOptionHandlerObj: function(handlerObj) {
10243		var optionNames = handlerObj.names;
10244		var optionValues = [];
10245		var i;
10246
10247		for (i = 0; i < optionNames.length; i++) {
10248			optionValues.push(this.options[optionNames[i]]);
10249		}
10250
10251		handlerObj.func.apply(this, optionValues); // maintain the Calendar's `this` context
10252	}
10253
10254});
10255
10256;;
10257
10258Calendar.defaults = {
10259
10260	titleRangeSeparator: ' \u2013 ', // en dash
10261	monthYearFormat: 'MMMM YYYY', // required for en. other languages rely on datepicker computable option
10262
10263	defaultTimedEventDuration: '02:00:00',
10264	defaultAllDayEventDuration: { days: 1 },
10265	forceEventDuration: false,
10266	nextDayThreshold: '09:00:00', // 9am
10267
10268	// display
10269	defaultView: 'month',
10270	aspectRatio: 1.35,
10271	header: {
10272		left: 'title',
10273		center: '',
10274		right: 'today prev,next'
10275	},
10276	weekends: true,
10277	weekNumbers: false,
10278
10279	weekNumberTitle: 'W',
10280	weekNumberCalculation: 'local',
10281	
10282	//editable: false,
10283
10284	//nowIndicator: false,
10285
10286	scrollTime: '06:00:00',
10287	
10288	// event ajax
10289	lazyFetching: true,
10290	startParam: 'start',
10291	endParam: 'end',
10292	timezoneParam: 'timezone',
10293
10294	timezone: false,
10295
10296	//allDayDefault: undefined,
10297
10298	// locale
10299	isRTL: false,
10300	buttonText: {
10301		prev: "prev",
10302		next: "next",
10303		prevYear: "prev year",
10304		nextYear: "next year",
10305		year: 'year', // TODO: locale files need to specify this
10306		today: 'today',
10307		month: 'month',
10308		week: 'week',
10309		day: 'day'
10310	},
10311
10312	buttonIcons: {
10313		prev: 'left-single-arrow',
10314		next: 'right-single-arrow',
10315		prevYear: 'left-double-arrow',
10316		nextYear: 'right-double-arrow'
10317	},
10318	
10319	// jquery-ui theming
10320	theme: false,
10321	themeButtonIcons: {
10322		prev: 'circle-triangle-w',
10323		next: 'circle-triangle-e',
10324		prevYear: 'seek-prev',
10325		nextYear: 'seek-next'
10326	},
10327
10328	//eventResizableFromStart: false,
10329	dragOpacity: .75,
10330	dragRevertDuration: 500,
10331	dragScroll: true,
10332	
10333	//selectable: false,
10334	unselectAuto: true,
10335	
10336	dropAccept: '*',
10337
10338	eventOrder: 'title',
10339
10340	eventLimit: false,
10341	eventLimitText: 'more',
10342	eventLimitClick: 'popover',
10343	dayPopoverFormat: 'LL',
10344	
10345	handleWindowResize: true,
10346	windowResizeDelay: 100, // milliseconds before an updateSize happens
10347
10348	longPressDelay: 1000
10349	
10350};
10351
10352
10353Calendar.englishDefaults = { // used by lang.js
10354	dayPopoverFormat: 'dddd, MMMM D'
10355};
10356
10357
10358Calendar.rtlDefaults = { // right-to-left defaults
10359	header: { // TODO: smarter solution (first/center/last ?)
10360		left: 'next,prev today',
10361		center: '',
10362		right: 'title'
10363	},
10364	buttonIcons: {
10365		prev: 'right-single-arrow',
10366		next: 'left-single-arrow',
10367		prevYear: 'right-double-arrow',
10368		nextYear: 'left-double-arrow'
10369	},
10370	themeButtonIcons: {
10371		prev: 'circle-triangle-e',
10372		next: 'circle-triangle-w',
10373		nextYear: 'seek-prev',
10374		prevYear: 'seek-next'
10375	}
10376};
10377
10378;;
10379
10380var langOptionHash = FC.langs = {}; // initialize and expose
10381
10382
10383// TODO: document the structure and ordering of a FullCalendar lang file
10384// TODO: rename everything "lang" to "locale", like what the moment project did
10385
10386
10387// Initialize jQuery UI datepicker translations while using some of the translations
10388// Will set this as the default language for datepicker.
10389FC.datepickerLang = function(langCode, dpLangCode, dpOptions) {
10390
10391	// get the FullCalendar internal option hash for this language. create if necessary
10392	var fcOptions = langOptionHash[langCode] || (langOptionHash[langCode] = {});
10393
10394	// transfer some simple options from datepicker to fc
10395	fcOptions.isRTL = dpOptions.isRTL;
10396	fcOptions.weekNumberTitle = dpOptions.weekHeader;
10397
10398	// compute some more complex options from datepicker
10399	$.each(dpComputableOptions, function(name, func) {
10400		fcOptions[name] = func(dpOptions);
10401	});
10402
10403	// is jQuery UI Datepicker is on the page?
10404	if ($.datepicker) {
10405
10406		// Register the language data.
10407		// FullCalendar and MomentJS use language codes like "pt-br" but Datepicker
10408		// does it like "pt-BR" or if it doesn't have the language, maybe just "pt".
10409		// Make an alias so the language can be referenced either way.
10410		$.datepicker.regional[dpLangCode] =
10411			$.datepicker.regional[langCode] = // alias
10412				dpOptions;
10413
10414		// Alias 'en' to the default language data. Do this every time.
10415		$.datepicker.regional.en = $.datepicker.regional[''];
10416
10417		// Set as Datepicker's global defaults.
10418		$.datepicker.setDefaults(dpOptions);
10419	}
10420};
10421
10422
10423// Sets FullCalendar-specific translations. Will set the language as the global default.
10424FC.lang = function(langCode, newFcOptions) {
10425	var fcOptions;
10426	var momOptions;
10427
10428	// get the FullCalendar internal option hash for this language. create if necessary
10429	fcOptions = langOptionHash[langCode] || (langOptionHash[langCode] = {});
10430
10431	// provided new options for this language? merge them in
10432	if (newFcOptions) {
10433		fcOptions = langOptionHash[langCode] = mergeOptions([ fcOptions, newFcOptions ]);
10434	}
10435
10436	// compute language options that weren't defined.
10437	// always do this. newFcOptions can be undefined when initializing from i18n file,
10438	// so no way to tell if this is an initialization or a default-setting.
10439	momOptions = getMomentLocaleData(langCode); // will fall back to en
10440	$.each(momComputableOptions, function(name, func) {
10441		if (fcOptions[name] == null) {
10442			fcOptions[name] = func(momOptions, fcOptions);
10443		}
10444	});
10445
10446	// set it as the default language for FullCalendar
10447	Calendar.defaults.lang = langCode;
10448};
10449
10450
10451// NOTE: can't guarantee any of these computations will run because not every language has datepicker
10452// configs, so make sure there are English fallbacks for these in the defaults file.
10453var dpComputableOptions = {
10454
10455	buttonText: function(dpOptions) {
10456		return {
10457			// the translations sometimes wrongly contain HTML entities
10458			prev: stripHtmlEntities(dpOptions.prevText),
10459			next: stripHtmlEntities(dpOptions.nextText),
10460			today: stripHtmlEntities(dpOptions.currentText)
10461		};
10462	},
10463
10464	// Produces format strings like "MMMM YYYY" -> "September 2014"
10465	monthYearFormat: function(dpOptions) {
10466		return dpOptions.showMonthAfterYear ?
10467			'YYYY[' + dpOptions.yearSuffix + '] MMMM' :
10468			'MMMM YYYY[' + dpOptions.yearSuffix + ']';
10469	}
10470
10471};
10472
10473var momComputableOptions = {
10474
10475	// Produces format strings like "ddd M/D" -> "Fri 9/15"
10476	dayOfMonthFormat: function(momOptions, fcOptions) {
10477		var format = momOptions.longDateFormat('l'); // for the format like "M/D/YYYY"
10478
10479		// strip the year off the edge, as well as other misc non-whitespace chars
10480		format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, '');
10481
10482		if (fcOptions.isRTL) {
10483			format += ' ddd'; // for RTL, add day-of-week to end
10484		}
10485		else {
10486			format = 'ddd ' + format; // for LTR, add day-of-week to beginning
10487		}
10488		return format;
10489	},
10490
10491	// Produces format strings like "h:mma" -> "6:00pm"
10492	mediumTimeFormat: function(momOptions) { // can't be called `timeFormat` because collides with option
10493		return momOptions.longDateFormat('LT')
10494			.replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
10495	},
10496
10497	// Produces format strings like "h(:mm)a" -> "6pm" / "6:30pm"
10498	smallTimeFormat: function(momOptions) {
10499		return momOptions.longDateFormat('LT')
10500			.replace(':mm', '(:mm)')
10501			.replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs
10502			.replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
10503	},
10504
10505	// Produces format strings like "h(:mm)t" -> "6p" / "6:30p"
10506	extraSmallTimeFormat: function(momOptions) {
10507		return momOptions.longDateFormat('LT')
10508			.replace(':mm', '(:mm)')
10509			.replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs
10510			.replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand
10511	},
10512
10513	// Produces format strings like "ha" / "H" -> "6pm" / "18"
10514	hourFormat: function(momOptions) {
10515		return momOptions.longDateFormat('LT')
10516			.replace(':mm', '')
10517			.replace(/(\Wmm)$/, '') // like above, but for foreign langs
10518			.replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
10519	},
10520
10521	// Produces format strings like "h:mm" -> "6:30" (with no AM/PM)
10522	noMeridiemTimeFormat: function(momOptions) {
10523		return momOptions.longDateFormat('LT')
10524			.replace(/\s*a$/i, ''); // remove trailing AM/PM
10525	}
10526
10527};
10528
10529
10530// options that should be computed off live calendar options (considers override options)
10531// TODO: best place for this? related to lang?
10532// TODO: flipping text based on isRTL is a bad idea because the CSS `direction` might want to handle it
10533var instanceComputableOptions = {
10534
10535	// Produces format strings for results like "Mo 16"
10536	smallDayDateFormat: function(options) {
10537		return options.isRTL ?
10538			'D dd' :
10539			'dd D';
10540	},
10541
10542	// Produces format strings for results like "Wk 5"
10543	weekFormat: function(options) {
10544		return options.isRTL ?
10545			'w[ ' + options.weekNumberTitle + ']' :
10546			'[' + options.weekNumberTitle + ' ]w';
10547	},
10548
10549	// Produces format strings for results like "Wk5"
10550	smallWeekFormat: function(options) {
10551		return options.isRTL ?
10552			'w[' + options.weekNumberTitle + ']' :
10553			'[' + options.weekNumberTitle + ']w';
10554	}
10555
10556};
10557
10558function populateInstanceComputableOptions(options) {
10559	$.each(instanceComputableOptions, function(name, func) {
10560		if (options[name] == null) {
10561			options[name] = func(options);
10562		}
10563	});
10564}
10565
10566
10567// Returns moment's internal locale data. If doesn't exist, returns English.
10568// Works with moment-pre-2.8
10569function getMomentLocaleData(langCode) {
10570	var func = moment.localeData || moment.langData;
10571	return func.call(moment, langCode) ||
10572		func.call(moment, 'en'); // the newer localData could return null, so fall back to en
10573}
10574
10575
10576// Initialize English by forcing computation of moment-derived options.
10577// Also, sets it as the default.
10578FC.lang('en', Calendar.englishDefaults);
10579
10580;;
10581
10582/* Top toolbar area with buttons and title
10583----------------------------------------------------------------------------------------------------------------------*/
10584// TODO: rename all header-related things to "toolbar"
10585
10586function Header(calendar) {
10587	var t = this;
10588	
10589	// exports
10590	t.render = render;
10591	t.removeElement = removeElement;
10592	t.updateTitle = updateTitle;
10593	t.activateButton = activateButton;
10594	t.deactivateButton = deactivateButton;
10595	t.disableButton = disableButton;
10596	t.enableButton = enableButton;
10597	t.getViewsWithButtons = getViewsWithButtons;
10598	t.el = null; // mirrors local `el`
10599	
10600	// locals
10601	var el;
10602	var viewsWithButtons = [];
10603	var tm;
10604
10605
10606	// can be called repeatedly and will rerender
10607	function render() {
10608		var options = calendar.options;
10609		var sections = options.header;
10610
10611		tm = options.theme ? 'ui' : 'fc';
10612
10613		if (sections) {
10614			if (!el) {
10615				el = this.el = $("<div class='fc-toolbar'/>");
10616			}
10617			else {
10618				el.empty();
10619			}
10620			el.append(renderSection('left'))
10621				.append(renderSection('right'))
10622				.append(renderSection('center'))
10623				.append('<div class="fc-clear"/>');
10624		}
10625		else {
10626			removeElement();
10627		}
10628	}
10629	
10630	
10631	function removeElement() {
10632		if (el) {
10633			el.remove();
10634			el = t.el = null;
10635		}
10636	}
10637	
10638	
10639	function renderSection(position) {
10640		var sectionEl = $('<div class="fc-' + position + '"/>');
10641		var options = calendar.options;
10642		var buttonStr = options.header[position];
10643
10644		if (buttonStr) {
10645			$.each(buttonStr.split(' '), function(i) {
10646				var groupChildren = $();
10647				var isOnlyButtons = true;
10648				var groupEl;
10649
10650				$.each(this.split(','), function(j, buttonName) {
10651					var customButtonProps;
10652					var viewSpec;
10653					var buttonClick;
10654					var overrideText; // text explicitly set by calendar's constructor options. overcomes icons
10655					var defaultText;
10656					var themeIcon;
10657					var normalIcon;
10658					var innerHtml;
10659					var classes;
10660					var button; // the element
10661
10662					if (buttonName == 'title') {
10663						groupChildren = groupChildren.add($('<h2>&nbsp;</h2>')); // we always want it to take up height
10664						isOnlyButtons = false;
10665					}
10666					else {
10667						if ((customButtonProps = (options.customButtons || {})[buttonName])) {
10668							buttonClick = function(ev) {
10669								if (customButtonProps.click) {
10670									customButtonProps.click.call(button[0], ev);
10671								}
10672							};
10673							overrideText = ''; // icons will override text
10674							defaultText = customButtonProps.text;
10675						}
10676						else if ((viewSpec = calendar.getViewSpec(buttonName))) {
10677							buttonClick = function() {
10678								calendar.changeView(buttonName);
10679							};
10680							viewsWithButtons.push(buttonName);
10681							overrideText = viewSpec.buttonTextOverride;
10682							defaultText = viewSpec.buttonTextDefault;
10683						}
10684						else if (calendar[buttonName]) { // a calendar method
10685							buttonClick = function() {
10686								calendar[buttonName]();
10687							};
10688							overrideText = (calendar.overrides.buttonText || {})[buttonName];
10689							defaultText = options.buttonText[buttonName]; // everything else is considered default
10690						}
10691
10692						if (buttonClick) {
10693
10694							themeIcon =
10695								customButtonProps ?
10696									customButtonProps.themeIcon :
10697									options.themeButtonIcons[buttonName];
10698
10699							normalIcon =
10700								customButtonProps ?
10701									customButtonProps.icon :
10702									options.buttonIcons[buttonName];
10703
10704							if (overrideText) {
10705								innerHtml = htmlEscape(overrideText);
10706							}
10707							else if (themeIcon && options.theme) {
10708								innerHtml = "<span class='ui-icon ui-icon-" + themeIcon + "'></span>";
10709							}
10710							else if (normalIcon && !options.theme) {
10711								innerHtml = "<span class='fc-icon fc-icon-" + normalIcon + "'></span>";
10712							}
10713							else {
10714								innerHtml = htmlEscape(defaultText);
10715							}
10716
10717							classes = [
10718								'fc-' + buttonName + '-button',
10719								tm + '-button',
10720								tm + '-state-default'
10721							];
10722
10723							button = $( // type="button" so that it doesn't submit a form
10724								'<button type="button" class="' + classes.join(' ') + '">' +
10725									innerHtml +
10726								'</button>'
10727								)
10728								.click(function(ev) {
10729									// don't process clicks for disabled buttons
10730									if (!button.hasClass(tm + '-state-disabled')) {
10731
10732										buttonClick(ev);
10733
10734										// after the click action, if the button becomes the "active" tab, or disabled,
10735										// it should never have a hover class, so remove it now.
10736										if (
10737											button.hasClass(tm + '-state-active') ||
10738											button.hasClass(tm + '-state-disabled')
10739										) {
10740											button.removeClass(tm + '-state-hover');
10741										}
10742									}
10743								})
10744								.mousedown(function() {
10745									// the *down* effect (mouse pressed in).
10746									// only on buttons that are not the "active" tab, or disabled
10747									button
10748										.not('.' + tm + '-state-active')
10749										.not('.' + tm + '-state-disabled')
10750										.addClass(tm + '-state-down');
10751								})
10752								.mouseup(function() {
10753									// undo the *down* effect
10754									button.removeClass(tm + '-state-down');
10755								})
10756								.hover(
10757									function() {
10758										// the *hover* effect.
10759										// only on buttons that are not the "active" tab, or disabled
10760										button
10761											.not('.' + tm + '-state-active')
10762											.not('.' + tm + '-state-disabled')
10763											.addClass(tm + '-state-hover');
10764									},
10765									function() {
10766										// undo the *hover* effect
10767										button
10768											.removeClass(tm + '-state-hover')
10769											.removeClass(tm + '-state-down'); // if mouseleave happens before mouseup
10770									}
10771								);
10772
10773							groupChildren = groupChildren.add(button);
10774						}
10775					}
10776				});
10777
10778				if (isOnlyButtons) {
10779					groupChildren
10780						.first().addClass(tm + '-corner-left').end()
10781						.last().addClass(tm + '-corner-right').end();
10782				}
10783
10784				if (groupChildren.length > 1) {
10785					groupEl = $('<div/>');
10786					if (isOnlyButtons) {
10787						groupEl.addClass('fc-button-group');
10788					}
10789					groupEl.append(groupChildren);
10790					sectionEl.append(groupEl);
10791				}
10792				else {
10793					sectionEl.append(groupChildren); // 1 or 0 children
10794				}
10795			});
10796		}
10797
10798		return sectionEl;
10799	}
10800	
10801	
10802	function updateTitle(text) {
10803		if (el) {
10804			el.find('h2').text(text);
10805		}
10806	}
10807	
10808	
10809	function activateButton(buttonName) {
10810		if (el) {
10811			el.find('.fc-' + buttonName + '-button')
10812				.addClass(tm + '-state-active');
10813		}
10814	}
10815	
10816	
10817	function deactivateButton(buttonName) {
10818		if (el) {
10819			el.find('.fc-' + buttonName + '-button')
10820				.removeClass(tm + '-state-active');
10821		}
10822	}
10823	
10824	
10825	function disableButton(buttonName) {
10826		if (el) {
10827			el.find('.fc-' + buttonName + '-button')
10828				.prop('disabled', true)
10829				.addClass(tm + '-state-disabled');
10830		}
10831	}
10832	
10833	
10834	function enableButton(buttonName) {
10835		if (el) {
10836			el.find('.fc-' + buttonName + '-button')
10837				.prop('disabled', false)
10838				.removeClass(tm + '-state-disabled');
10839		}
10840	}
10841
10842
10843	function getViewsWithButtons() {
10844		return viewsWithButtons;
10845	}
10846
10847}
10848
10849;;
10850
10851FC.sourceNormalizers = [];
10852FC.sourceFetchers = [];
10853
10854var ajaxDefaults = {
10855	dataType: 'json',
10856	cache: false
10857};
10858
10859var eventGUID = 1;
10860
10861
10862function EventManager() { // assumed to be a calendar
10863	var t = this;
10864	
10865	
10866	// exports
10867	t.isFetchNeeded = isFetchNeeded;
10868	t.fetchEvents = fetchEvents;
10869	t.fetchEventSources = fetchEventSources;
10870	t.getEventSources = getEventSources;
10871	t.getEventSourceById = getEventSourceById;
10872	t.getEventSourcesByMatchArray = getEventSourcesByMatchArray;
10873	t.getEventSourcesByMatch = getEventSourcesByMatch;
10874	t.addEventSource = addEventSource;
10875	t.removeEventSource = removeEventSource;
10876	t.removeEventSources = removeEventSources;
10877	t.updateEvent = updateEvent;
10878	t.renderEvent = renderEvent;
10879	t.removeEvents = removeEvents;
10880	t.clientEvents = clientEvents;
10881	t.mutateEvent = mutateEvent;
10882	t.normalizeEventDates = normalizeEventDates;
10883	t.normalizeEventTimes = normalizeEventTimes;
10884	
10885	
10886	// imports
10887	var reportEvents = t.reportEvents;
10888	
10889	
10890	// locals
10891	var stickySource = { events: [] };
10892	var sources = [ stickySource ];
10893	var rangeStart, rangeEnd;
10894	var pendingSourceCnt = 0; // outstanding fetch requests, max one per source
10895	var cache = []; // holds events that have already been expanded
10896
10897
10898	$.each(
10899		(t.options.events ? [ t.options.events ] : []).concat(t.options.eventSources || []),
10900		function(i, sourceInput) {
10901			var source = buildEventSource(sourceInput);
10902			if (source) {
10903				sources.push(source);
10904			}
10905		}
10906	);
10907	
10908	
10909	
10910	/* Fetching
10911	-----------------------------------------------------------------------------*/
10912
10913
10914	// start and end are assumed to be unzoned
10915	function isFetchNeeded(start, end) {
10916		return !rangeStart || // nothing has been fetched yet?
10917			start < rangeStart || end > rangeEnd; // is part of the new range outside of the old range?
10918	}
10919	
10920	
10921	function fetchEvents(start, end) {
10922		rangeStart = start;
10923		rangeEnd = end;
10924		fetchEventSources(sources, 'reset');
10925	}
10926
10927
10928	// expects an array of event source objects (the originals, not copies)
10929	// `specialFetchType` is an optimization parameter that affects purging of the event cache.
10930	function fetchEventSources(specificSources, specialFetchType) {
10931		var i, source;
10932
10933		if (specialFetchType === 'reset') {
10934			cache = [];
10935		}
10936		else if (specialFetchType !== 'add') {
10937			cache = excludeEventsBySources(cache, specificSources);
10938		}
10939
10940		for (i = 0; i < specificSources.length; i++) {
10941			source = specificSources[i];
10942
10943			// already-pending sources have already been accounted for in pendingSourceCnt
10944			if (source._status !== 'pending') {
10945				pendingSourceCnt++;
10946			}
10947
10948			source._fetchId = (source._fetchId || 0) + 1;
10949			source._status = 'pending';
10950		}
10951
10952		for (i = 0; i < specificSources.length; i++) {
10953			source = specificSources[i];
10954
10955			tryFetchEventSource(source, source._fetchId);
10956		}
10957	}
10958
10959
10960	// fetches an event source and processes its result ONLY if it is still the current fetch.
10961	// caller is responsible for incrementing pendingSourceCnt first.
10962	function tryFetchEventSource(source, fetchId) {
10963		_fetchEventSource(source, function(eventInputs) {
10964			var isArraySource = $.isArray(source.events);
10965			var i, eventInput;
10966			var abstractEvent;
10967
10968			if (
10969				// is this the source's most recent fetch?
10970				// if not, rely on an upcoming fetch of this source to decrement pendingSourceCnt
10971				fetchId === source._fetchId &&
10972				// event source no longer valid?
10973				source._status !== 'rejected'
10974			) {
10975				source._status = 'resolved';
10976
10977				if (eventInputs) {
10978					for (i = 0; i < eventInputs.length; i++) {
10979						eventInput = eventInputs[i];
10980
10981						if (isArraySource) { // array sources have already been convert to Event Objects
10982							abstractEvent = eventInput;
10983						}
10984						else {
10985							abstractEvent = buildEventFromInput(eventInput, source);
10986						}
10987
10988						if (abstractEvent) { // not false (an invalid event)
10989							cache.push.apply(
10990								cache,
10991								expandEvent(abstractEvent) // add individual expanded events to the cache
10992							);
10993						}
10994					}
10995				}
10996
10997				decrementPendingSourceCnt();
10998			}
10999		});
11000	}
11001
11002
11003	function rejectEventSource(source) {
11004		var wasPending = source._status === 'pending';
11005
11006		source._status = 'rejected';
11007
11008		if (wasPending) {
11009			decrementPendingSourceCnt();
11010		}
11011	}
11012
11013
11014	function decrementPendingSourceCnt() {
11015		pendingSourceCnt--;
11016		if (!pendingSourceCnt) {
11017			reportEvents(cache);
11018		}
11019	}
11020	
11021	
11022	function _fetchEventSource(source, callback) {
11023		var i;
11024		var fetchers = FC.sourceFetchers;
11025		var res;
11026
11027		for (i=0; i<fetchers.length; i++) {
11028			res = fetchers[i].call(
11029				t, // this, the Calendar object
11030				source,
11031				rangeStart.clone(),
11032				rangeEnd.clone(),
11033				t.options.timezone,
11034				callback
11035			);
11036
11037			if (res === true) {
11038				// the fetcher is in charge. made its own async request
11039				return;
11040			}
11041			else if (typeof res == 'object') {
11042				// the fetcher returned a new source. process it
11043				_fetchEventSource(res, callback);
11044				return;
11045			}
11046		}
11047
11048		var events = source.events;
11049		if (events) {
11050			if ($.isFunction(events)) {
11051				t.pushLoading();
11052				events.call(
11053					t, // this, the Calendar object
11054					rangeStart.clone(),
11055					rangeEnd.clone(),
11056					t.options.timezone,
11057					function(events) {
11058						callback(events);
11059						t.popLoading();
11060					}
11061				);
11062			}
11063			else if ($.isArray(events)) {
11064				callback(events);
11065			}
11066			else {
11067				callback();
11068			}
11069		}else{
11070			var url = source.url;
11071			if (url) {
11072				var success = source.success;
11073				var error = source.error;
11074				var complete = source.complete;
11075
11076				// retrieve any outbound GET/POST $.ajax data from the options
11077				var customData;
11078				if ($.isFunction(source.data)) {
11079					// supplied as a function that returns a key/value object
11080					customData = source.data();
11081				}
11082				else {
11083					// supplied as a straight key/value object
11084					customData = source.data;
11085				}
11086
11087				// use a copy of the custom data so we can modify the parameters
11088				// and not affect the passed-in object.
11089				var data = $.extend({}, customData || {});
11090
11091				var startParam = firstDefined(source.startParam, t.options.startParam);
11092				var endParam = firstDefined(source.endParam, t.options.endParam);
11093				var timezoneParam = firstDefined(source.timezoneParam, t.options.timezoneParam);
11094
11095				if (startParam) {
11096					data[startParam] = rangeStart.format();
11097				}
11098				if (endParam) {
11099					data[endParam] = rangeEnd.format();
11100				}
11101				if (t.options.timezone && t.options.timezone != 'local') {
11102					data[timezoneParam] = t.options.timezone;
11103				}
11104
11105				t.pushLoading();
11106				$.ajax($.extend({}, ajaxDefaults, source, {
11107					data: data,
11108					success: function(events) {
11109						events = events || [];
11110						var res = applyAll(success, this, arguments);
11111						if ($.isArray(res)) {
11112							events = res;
11113						}
11114						callback(events);
11115					},
11116					error: function() {
11117						applyAll(error, this, arguments);
11118						callback();
11119					},
11120					complete: function() {
11121						applyAll(complete, this, arguments);
11122						t.popLoading();
11123					}
11124				}));
11125			}else{
11126				callback();
11127			}
11128		}
11129	}
11130	
11131	
11132	
11133	/* Sources
11134	-----------------------------------------------------------------------------*/
11135
11136
11137	function addEventSource(sourceInput) {
11138		var source = buildEventSource(sourceInput);
11139		if (source) {
11140			sources.push(source);
11141			fetchEventSources([ source ], 'add'); // will eventually call reportEvents
11142		}
11143	}
11144
11145
11146	function buildEventSource(sourceInput) { // will return undefined if invalid source
11147		var normalizers = FC.sourceNormalizers;
11148		var source;
11149		var i;
11150
11151		if ($.isFunction(sourceInput) || $.isArray(sourceInput)) {
11152			source = { events: sourceInput };
11153		}
11154		else if (typeof sourceInput === 'string') {
11155			source = { url: sourceInput };
11156		}
11157		else if (typeof sourceInput === 'object') {
11158			source = $.extend({}, sourceInput); // shallow copy
11159		}
11160
11161		if (source) {
11162
11163			// TODO: repeat code, same code for event classNames
11164			if (source.className) {
11165				if (typeof source.className === 'string') {
11166					source.className = source.className.split(/\s+/);
11167				}
11168				// otherwise, assumed to be an array
11169			}
11170			else {
11171				source.className = [];
11172			}
11173
11174			// for array sources, we convert to standard Event Objects up front
11175			if ($.isArray(source.events)) {
11176				source.origArray = source.events; // for removeEventSource
11177				source.events = $.map(source.events, function(eventInput) {
11178					return buildEventFromInput(eventInput, source);
11179				});
11180			}
11181
11182			for (i=0; i<normalizers.length; i++) {
11183				normalizers[i].call(t, source);
11184			}
11185
11186			return source;
11187		}
11188	}
11189
11190
11191	function removeEventSource(matchInput) {
11192		removeSpecificEventSources(
11193			getEventSourcesByMatch(matchInput)
11194		);
11195	}
11196
11197
11198	// if called with no arguments, removes all.
11199	function removeEventSources(matchInputs) {
11200		if (matchInputs == null) {
11201			removeSpecificEventSources(sources, true); // isAll=true
11202		}
11203		else {
11204			removeSpecificEventSources(
11205				getEventSourcesByMatchArray(matchInputs)
11206			);
11207		}
11208	}
11209
11210
11211	function removeSpecificEventSources(targetSources, isAll) {
11212		var i;
11213
11214		// cancel pending requests
11215		for (i = 0; i < targetSources.length; i++) {
11216			rejectEventSource(targetSources[i]);
11217		}
11218
11219		if (isAll) { // an optimization
11220			sources = [];
11221			cache = [];
11222		}
11223		else {
11224			// remove from persisted source list
11225			sources = $.grep(sources, function(source) {
11226				for (i = 0; i < targetSources.length; i++) {
11227					if (source === targetSources[i]) {
11228						return false; // exclude
11229					}
11230				}
11231				return true; // include
11232			});
11233
11234			cache = excludeEventsBySources(cache, targetSources);
11235		}
11236
11237		reportEvents(cache);
11238	}
11239
11240
11241	function getEventSources() {
11242		return sources.slice(1); // returns a shallow copy of sources with stickySource removed
11243	}
11244
11245
11246	function getEventSourceById(id) {
11247		return $.grep(sources, function(source) {
11248			return source.id && source.id === id;
11249		})[0];
11250	}
11251
11252
11253	// like getEventSourcesByMatch, but accepts multple match criteria (like multiple IDs)
11254	function getEventSourcesByMatchArray(matchInputs) {
11255
11256		// coerce into an array
11257		if (!matchInputs) {
11258			matchInputs = [];
11259		}
11260		else if (!$.isArray(matchInputs)) {
11261			matchInputs = [ matchInputs ];
11262		}
11263
11264		var matchingSources = [];
11265		var i;
11266
11267		// resolve raw inputs to real event source objects
11268		for (i = 0; i < matchInputs.length; i++) {
11269			matchingSources.push.apply( // append
11270				matchingSources,
11271				getEventSourcesByMatch(matchInputs[i])
11272			);
11273		}
11274
11275		return matchingSources;
11276	}
11277
11278
11279	// matchInput can either by a real event source object, an ID, or the function/URL for the source.
11280	// returns an array of matching source objects.
11281	function getEventSourcesByMatch(matchInput) {
11282		var i, source;
11283
11284		// given an proper event source object
11285		for (i = 0; i < sources.length; i++) {
11286			source = sources[i];
11287			if (source === matchInput) {
11288				return [ source ];
11289			}
11290		}
11291
11292		// an ID match
11293		source = getEventSourceById(matchInput);
11294		if (source) {
11295			return [ source ];
11296		}
11297
11298		return $.grep(sources, function(source) {
11299			return isSourcesEquivalent(matchInput, source);
11300		});
11301	}
11302
11303
11304	function isSourcesEquivalent(source1, source2) {
11305		return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2);
11306	}
11307
11308
11309	function getSourcePrimitive(source) {
11310		return (
11311			(typeof source === 'object') ? // a normalized event source?
11312				(source.origArray || source.googleCalendarId || source.url || source.events) : // get the primitive
11313				null
11314		) ||
11315		source; // the given argument *is* the primitive
11316	}
11317
11318
11319	// util
11320	// returns a filtered array without events that are part of any of the given sources
11321	function excludeEventsBySources(specificEvents, specificSources) {
11322		return $.grep(specificEvents, function(event) {
11323			for (var i = 0; i < specificSources.length; i++) {
11324				if (event.source === specificSources[i]) {
11325					return false; // exclude
11326				}
11327			}
11328			return true; // keep
11329		});
11330	}
11331	
11332	
11333	
11334	/* Manipulation
11335	-----------------------------------------------------------------------------*/
11336
11337
11338	// Only ever called from the externally-facing API
11339	function updateEvent(event) {
11340
11341		// massage start/end values, even if date string values
11342		event.start = t.moment(event.start);
11343		if (event.end) {
11344			event.end = t.moment(event.end);
11345		}
11346		else {
11347			event.end = null;
11348		}
11349
11350		mutateEvent(event, getMiscEventProps(event)); // will handle start/end/allDay normalization
11351		reportEvents(cache); // reports event modifications (so we can redraw)
11352	}
11353
11354
11355	// Returns a hash of misc event properties that should be copied over to related events.
11356	function getMiscEventProps(event) {
11357		var props = {};
11358
11359		$.each(event, function(name, val) {
11360			if (isMiscEventPropName(name)) {
11361				if (val !== undefined && isAtomic(val)) { // a defined non-object
11362					props[name] = val;
11363				}
11364			}
11365		});
11366
11367		return props;
11368	}
11369
11370	// non-date-related, non-id-related, non-secret
11371	function isMiscEventPropName(name) {
11372		return !/^_|^(id|allDay|start|end)$/.test(name);
11373	}
11374
11375	
11376	// returns the expanded events that were created
11377	function renderEvent(eventInput, stick) {
11378		var abstractEvent = buildEventFromInput(eventInput);
11379		var events;
11380		var i, event;
11381
11382		if (abstractEvent) { // not false (a valid input)
11383			events = expandEvent(abstractEvent);
11384
11385			for (i = 0; i < events.length; i++) {
11386				event = events[i];
11387
11388				if (!event.source) {
11389					if (stick) {
11390						stickySource.events.push(event);
11391						event.source = stickySource;
11392					}
11393					cache.push(event);
11394				}
11395			}
11396
11397			reportEvents(cache);
11398
11399			return events;
11400		}
11401
11402		return [];
11403	}
11404	
11405	
11406	function removeEvents(filter) {
11407		var eventID;
11408		var i;
11409
11410		if (filter == null) { // null or undefined. remove all events
11411			filter = function() { return true; }; // will always match
11412		}
11413		else if (!$.isFunction(filter)) { // an event ID
11414			eventID = filter + '';
11415			filter = function(event) {
11416				return event._id == eventID;
11417			};
11418		}
11419
11420		// Purge event(s) from our local cache
11421		cache = $.grep(cache, filter, true); // inverse=true
11422
11423		// Remove events from array sources.
11424		// This works because they have been converted to official Event Objects up front.
11425		// (and as a result, event._id has been calculated).
11426		for (i=0; i<sources.length; i++) {
11427			if ($.isArray(sources[i].events)) {
11428				sources[i].events = $.grep(sources[i].events, filter, true);
11429			}
11430		}
11431
11432		reportEvents(cache);
11433	}
11434
11435	
11436	function clientEvents(filter) {
11437		if ($.isFunction(filter)) {
11438			return $.grep(cache, filter);
11439		}
11440		else if (filter != null) { // not null, not undefined. an event ID
11441			filter += '';
11442			return $.grep(cache, function(e) {
11443				return e._id == filter;
11444			});
11445		}
11446		return cache; // else, return all
11447	}
11448
11449
11450	// Makes sure all array event sources have their internal event objects
11451	// converted over to the Calendar's current timezone.
11452	t.rezoneArrayEventSources = function() {
11453		var i;
11454		var events;
11455		var j;
11456
11457		for (i = 0; i < sources.length; i++) {
11458			events = sources[i].events;
11459			if ($.isArray(events)) {
11460
11461				for (j = 0; j < events.length; j++) {
11462					rezoneEventDates(events[j]);
11463				}
11464			}
11465		}
11466	};
11467
11468	function rezoneEventDates(event) {
11469		event.start = t.moment(event.start);
11470		if (event.end) {
11471			event.end = t.moment(event.end);
11472		}
11473		backupEventDates(event);
11474	}
11475	
11476	
11477	/* Event Normalization
11478	-----------------------------------------------------------------------------*/
11479
11480
11481	// Given a raw object with key/value properties, returns an "abstract" Event object.
11482	// An "abstract" event is an event that, if recurring, will not have been expanded yet.
11483	// Will return `false` when input is invalid.
11484	// `source` is optional
11485	function buildEventFromInput(input, source) {
11486		var out = {};
11487		var start, end;
11488		var allDay;
11489
11490		if (t.options.eventDataTransform) {
11491			input = t.options.eventDataTransform(input);
11492		}
11493		if (source && source.eventDataTransform) {
11494			input = source.eventDataTransform(input);
11495		}
11496
11497		// Copy all properties over to the resulting object.
11498		// The special-case properties will be copied over afterwards.
11499		$.extend(out, input);
11500
11501		if (source) {
11502			out.source = source;
11503		}
11504
11505		out._id = input._id || (input.id === undefined ? '_fc' + eventGUID++ : input.id + '');
11506
11507		if (input.className) {
11508			if (typeof input.className == 'string') {
11509				out.className = input.className.split(/\s+/);
11510			}
11511			else { // assumed to be an array
11512				out.className = input.className;
11513			}
11514		}
11515		else {
11516			out.className = [];
11517		}
11518
11519		start = input.start || input.date; // "date" is an alias for "start"
11520		end = input.end;
11521
11522		// parse as a time (Duration) if applicable
11523		if (isTimeString(start)) {
11524			start = moment.duration(start);
11525		}
11526		if (isTimeString(end)) {
11527			end = moment.duration(end);
11528		}
11529
11530		if (input.dow || moment.isDuration(start) || moment.isDuration(end)) {
11531
11532			// the event is "abstract" (recurring) so don't calculate exact start/end dates just yet
11533			out.start = start ? moment.duration(start) : null; // will be a Duration or null
11534			out.end = end ? moment.duration(end) : null; // will be a Duration or null
11535			out._recurring = true; // our internal marker
11536		}
11537		else {
11538
11539			if (start) {
11540				start = t.moment(start);
11541				if (!start.isValid()) {
11542					return false;
11543				}
11544			}
11545
11546			if (end) {
11547				end = t.moment(end);
11548				if (!end.isValid()) {
11549					end = null; // let defaults take over
11550				}
11551			}
11552
11553			allDay = input.allDay;
11554			if (allDay === undefined) { // still undefined? fallback to default
11555				allDay = firstDefined(
11556					source ? source.allDayDefault : undefined,
11557					t.options.allDayDefault
11558				);
11559				// still undefined? normalizeEventDates will calculate it
11560			}
11561
11562			assignDatesToEvent(start, end, allDay, out);
11563		}
11564
11565		t.normalizeEvent(out); // hook for external use. a prototype method
11566
11567		return out;
11568	}
11569	t.buildEventFromInput = buildEventFromInput;
11570
11571
11572	// Normalizes and assigns the given dates to the given partially-formed event object.
11573	// NOTE: mutates the given start/end moments. does not make a copy.
11574	function assignDatesToEvent(start, end, allDay, event) {
11575		event.start = start;
11576		event.end = end;
11577		event.allDay = allDay;
11578		normalizeEventDates(event);
11579		backupEventDates(event);
11580	}
11581
11582
11583	// Ensures proper values for allDay/start/end. Accepts an Event object, or a plain object with event-ish properties.
11584	// NOTE: Will modify the given object.
11585	function normalizeEventDates(eventProps) {
11586
11587		normalizeEventTimes(eventProps);
11588
11589		if (eventProps.end && !eventProps.end.isAfter(eventProps.start)) {
11590			eventProps.end = null;
11591		}
11592
11593		if (!eventProps.end) {
11594			if (t.options.forceEventDuration) {
11595				eventProps.end = t.getDefaultEventEnd(eventProps.allDay, eventProps.start);
11596			}
11597			else {
11598				eventProps.end = null;
11599			}
11600		}
11601	}
11602
11603
11604	// Ensures the allDay property exists and the timeliness of the start/end dates are consistent
11605	function normalizeEventTimes(eventProps) {
11606		if (eventProps.allDay == null) {
11607			eventProps.allDay = !(eventProps.start.hasTime() || (eventProps.end && eventProps.end.hasTime()));
11608		}
11609
11610		if (eventProps.allDay) {
11611			eventProps.start.stripTime();
11612			if (eventProps.end) {
11613				// TODO: consider nextDayThreshold here? If so, will require a lot of testing and adjustment
11614				eventProps.end.stripTime();
11615			}
11616		}
11617		else {
11618			if (!eventProps.start.hasTime()) {
11619				eventProps.start = t.applyTimezone(eventProps.start.time(0)); // will assign a 00:00 time
11620			}
11621			if (eventProps.end && !eventProps.end.hasTime()) {
11622				eventProps.end = t.applyTimezone(eventProps.end.time(0)); // will assign a 00:00 time
11623			}
11624		}
11625	}
11626
11627
11628	// If the given event is a recurring event, break it down into an array of individual instances.
11629	// If not a recurring event, return an array with the single original event.
11630	// If given a falsy input (probably because of a failed buildEventFromInput call), returns an empty array.
11631	// HACK: can override the recurring window by providing custom rangeStart/rangeEnd (for businessHours).
11632	function expandEvent(abstractEvent, _rangeStart, _rangeEnd) {
11633		var events = [];
11634		var dowHash;
11635		var dow;
11636		var i;
11637		var date;
11638		var startTime, endTime;
11639		var start, end;
11640		var event;
11641
11642		_rangeStart = _rangeStart || rangeStart;
11643		_rangeEnd = _rangeEnd || rangeEnd;
11644
11645		if (abstractEvent) {
11646			if (abstractEvent._recurring) {
11647
11648				// make a boolean hash as to whether the event occurs on each day-of-week
11649				if ((dow = abstractEvent.dow)) {
11650					dowHash = {};
11651					for (i = 0; i < dow.length; i++) {
11652						dowHash[dow[i]] = true;
11653					}
11654				}
11655
11656				// iterate through every day in the current range
11657				date = _rangeStart.clone().stripTime(); // holds the date of the current day
11658				while (date.isBefore(_rangeEnd)) {
11659
11660					if (!dowHash || dowHash[date.day()]) { // if everyday, or this particular day-of-week
11661
11662						startTime = abstractEvent.start; // the stored start and end properties are times (Durations)
11663						endTime = abstractEvent.end; // "
11664						start = date.clone();
11665						end = null;
11666
11667						if (startTime) {
11668							start = start.time(startTime);
11669						}
11670						if (endTime) {
11671							end = date.clone().time(endTime);
11672						}
11673
11674						event = $.extend({}, abstractEvent); // make a copy of the original
11675						assignDatesToEvent(
11676							start, end,
11677							!startTime && !endTime, // allDay?
11678							event
11679						);
11680						events.push(event);
11681					}
11682
11683					date.add(1, 'days');
11684				}
11685			}
11686			else {
11687				events.push(abstractEvent); // return the original event. will be a one-item array
11688			}
11689		}
11690
11691		return events;
11692	}
11693	t.expandEvent = expandEvent;
11694
11695
11696
11697	/* Event Modification Math
11698	-----------------------------------------------------------------------------------------*/
11699
11700
11701	// Modifies an event and all related events by applying the given properties.
11702	// Special date-diffing logic is used for manipulation of dates.
11703	// If `props` does not contain start/end dates, the updated values are assumed to be the event's current start/end.
11704	// All date comparisons are done against the event's pristine _start and _end dates.
11705	// Returns an object with delta information and a function to undo all operations.
11706	// For making computations in a granularity greater than day/time, specify largeUnit.
11707	// NOTE: The given `newProps` might be mutated for normalization purposes.
11708	function mutateEvent(event, newProps, largeUnit) {
11709		var miscProps = {};
11710		var oldProps;
11711		var clearEnd;
11712		var startDelta;
11713		var endDelta;
11714		var durationDelta;
11715		var undoFunc;
11716
11717		// diffs the dates in the appropriate way, returning a duration
11718		function diffDates(date1, date0) { // date1 - date0
11719			if (largeUnit) {
11720				return diffByUnit(date1, date0, largeUnit);
11721			}
11722			else if (newProps.allDay) {
11723				return diffDay(date1, date0);
11724			}
11725			else {
11726				return diffDayTime(date1, date0);
11727			}
11728		}
11729
11730		newProps = newProps || {};
11731
11732		// normalize new date-related properties
11733		if (!newProps.start) {
11734			newProps.start = event.start.clone();
11735		}
11736		if (newProps.end === undefined) {
11737			newProps.end = event.end ? event.end.clone() : null;
11738		}
11739		if (newProps.allDay == null) { // is null or undefined?
11740			newProps.allDay = event.allDay;
11741		}
11742		normalizeEventDates(newProps);
11743
11744		// create normalized versions of the original props to compare against
11745		// need a real end value, for diffing
11746		oldProps = {
11747			start: event._start.clone(),
11748			end: event._end ? event._end.clone() : t.getDefaultEventEnd(event._allDay, event._start),
11749			allDay: newProps.allDay // normalize the dates in the same regard as the new properties
11750		};
11751		normalizeEventDates(oldProps);
11752
11753		// need to clear the end date if explicitly changed to null
11754		clearEnd = event._end !== null && newProps.end === null;
11755
11756		// compute the delta for moving the start date
11757		startDelta = diffDates(newProps.start, oldProps.start);
11758
11759		// compute the delta for moving the end date
11760		if (newProps.end) {
11761			endDelta = diffDates(newProps.end, oldProps.end);
11762			durationDelta = endDelta.subtract(startDelta);
11763		}
11764		else {
11765			durationDelta = null;
11766		}
11767
11768		// gather all non-date-related properties
11769		$.each(newProps, function(name, val) {
11770			if (isMiscEventPropName(name)) {
11771				if (val !== undefined) {
11772					miscProps[name] = val;
11773				}
11774			}
11775		});
11776
11777		// apply the operations to the event and all related events
11778		undoFunc = mutateEvents(
11779			clientEvents(event._id), // get events with this ID
11780			clearEnd,
11781			newProps.allDay,
11782			startDelta,
11783			durationDelta,
11784			miscProps
11785		);
11786
11787		return {
11788			dateDelta: startDelta,
11789			durationDelta: durationDelta,
11790			undo: undoFunc
11791		};
11792	}
11793
11794
11795	// Modifies an array of events in the following ways (operations are in order):
11796	// - clear the event's `end`
11797	// - convert the event to allDay
11798	// - add `dateDelta` to the start and end
11799	// - add `durationDelta` to the event's duration
11800	// - assign `miscProps` to the event
11801	//
11802	// Returns a function that can be called to undo all the operations.
11803	//
11804	// TODO: don't use so many closures. possible memory issues when lots of events with same ID.
11805	//
11806	function mutateEvents(events, clearEnd, allDay, dateDelta, durationDelta, miscProps) {
11807		var isAmbigTimezone = t.getIsAmbigTimezone();
11808		var undoFunctions = [];
11809
11810		// normalize zero-length deltas to be null
11811		if (dateDelta && !dateDelta.valueOf()) { dateDelta = null; }
11812		if (durationDelta && !durationDelta.valueOf()) { durationDelta = null; }
11813
11814		$.each(events, function(i, event) {
11815			var oldProps;
11816			var newProps;
11817
11818			// build an object holding all the old values, both date-related and misc.
11819			// for the undo function.
11820			oldProps = {
11821				start: event.start.clone(),
11822				end: event.end ? event.end.clone() : null,
11823				allDay: event.allDay
11824			};
11825			$.each(miscProps, function(name) {
11826				oldProps[name] = event[name];
11827			});
11828
11829			// new date-related properties. work off the original date snapshot.
11830			// ok to use references because they will be thrown away when backupEventDates is called.
11831			newProps = {
11832				start: event._start,
11833				end: event._end,
11834				allDay: allDay // normalize the dates in the same regard as the new properties
11835			};
11836			normalizeEventDates(newProps); // massages start/end/allDay
11837
11838			// strip or ensure the end date
11839			if (clearEnd) {
11840				newProps.end = null;
11841			}
11842			else if (durationDelta && !newProps.end) { // the duration translation requires an end date
11843				newProps.end = t.getDefaultEventEnd(newProps.allDay, newProps.start);
11844			}
11845
11846			if (dateDelta) {
11847				newProps.start.add(dateDelta);
11848				if (newProps.end) {
11849					newProps.end.add(dateDelta);
11850				}
11851			}
11852
11853			if (durationDelta) {
11854				newProps.end.add(durationDelta); // end already ensured above
11855			}
11856
11857			// if the dates have changed, and we know it is impossible to re
11857compute the
11858			// timezone offsets, strip the zone.
11859			if (
11860				isAmbigTimezone &&
11861				!newProps.allDay &&
11862				(dateDelta || durationDelta)
11863			) {
11864				newProps.start.stripZone();
11865				if (newProps.end) {
11866					newProps.end.stripZone();
11867				}
11868			}
11869
11870			$.extend(event, miscProps, newProps); // copy over misc props, then date-related props
11871			backupEventDates(event); // regenerate internal _start/_end/_allDay
11872
11873			undoFunctions.push(function() {
11874				$.extend(event, oldProps);
11875				backupEventDates(event); // regenerate internal _start/_end/_allDay
11876			});
11877		});
11878
11879		return function() {
11880			for (var i = 0; i < undoFunctions.length; i++) {
11881				undoFunctions[i]();
11882			}
11883		};
11884	}
11885
11886
11887	/* Overlapping / Constraining
11888	-----------------------------------------------------------------------------------------*/
11889
11890	t.isEventSpanAllowed = isEventSpanAllowed;
11891	t.isExternalSpanAllowed = isExternalSpanAllowed;
11892	t.isSelectionSpanAllowed = isSelectionSpanAllowed;
11893
11894
11895	// Determines if the given event can be relocated to the given span (unzoned start/end with other misc data)
11896	function isEventSpanAllowed(span, event) {
11897		var source = event.source || {};
11898		var constraint = firstDefined(
11899			event.constraint,
11900			source.constraint,
11901			t.options.eventConstraint
11902		);
11903		var overlap = firstDefined(
11904			event.overlap,
11905			source.overlap,
11906			t.options.eventOverlap
11907		);
11908		return isSpanAllowed(span, constraint, overlap, event);
11909	}
11910
11911
11912	// Determines if an external event can be relocated to the given span (unzoned start/end with other misc data)
11913	function isExternalSpanAllowed(eventSpan, eventLocation, eventProps) {
11914		var eventInput;
11915		var event;
11916
11917		// note: very similar logic is in View's reportExternalDrop
11918		if (eventProps) {
11919			eventInput = $.extend({}, eventProps, eventLocation);
11920			event = expandEvent(buildEventFromInput(eventInput))[0];
11921		}
11922
11923		if (event) {
11924			return isEventSpanAllowed(eventSpan, event);
11925		}
11926		else { // treat it as a selection
11927
11928			return isSelectionSpanAllowed(eventSpan);
11929		}
11930	}
11931
11932
11933	// Determines the given span (unzoned start/end with other misc data) can be selected.
11934	function isSelectionSpanAllowed(span) {
11935		return isSpanAllowed(span, t.options.selectConstraint, t.options.selectOverlap);
11936	}
11937
11938
11939	// Returns true if the given span (caused by an event drop/resize or a selection) is allowed to exist
11940	// according to the constraint/overlap settings.
11941	// `event` is not required if checking a selection.
11942	function isSpanAllowed(span, constraint, overlap, event) {
11943		var constraintEvents;
11944		var anyContainment;
11945		var peerEvents;
11946		var i, peerEvent;
11947		var peerOverlap;
11948
11949		// the range must be fully contained by at least one of produced constraint events
11950		if (constraint != null) {
11951
11952			// not treated as an event! intermediate data structure
11953			// TODO: use ranges in the future
11954			constraintEvents = constraintToEvents(constraint);
11955
11956			anyContainment = false;
11957			for (i = 0; i < constraintEvents.length; i++) {
11958				if (t.spanContainsSpan(constraintEvents[i], span)) {
11959					anyContainment = true;
11960					break;
11961				}
11962			}
11963
11964			if (!anyContainment) {
11965				return false;
11966			}
11967		}
11968
11969		peerEvents = t.getPeerEvents(span, event);
11970
11971		for (i = 0; i < peerEvents.length; i++)  {
11972			peerEvent = peerEvents[i];
11973
11974			// there needs to be an actual intersection before disallowing anything
11975			if (eventIntersectsRange(peerEvent, span)) {
11976
11977				// evaluate overlap for the given range and short-circuit if necessary
11978				if (overlap === false) {
11979					return false;
11980				}
11981				// if the event's overlap is a test function, pass the peer event in question as the first param
11982				else if (typeof overlap === 'function' && !overlap(peerEvent, event)) {
11983					return false;
11984				}
11985
11986				// if we are computing if the given range is allowable for an event, consider the other event's
11987				// EventObject-specific or Source-specific `overlap` property
11988				if (event) {
11989					peerOverlap = firstDefined(
11990						peerEvent.overlap,
11991						(peerEvent.source || {}).overlap
11992						// we already considered the global `eventOverlap`
11993					);
11994					if (peerOverlap === false) {
11995						return false;
11996					}
11997					// if the peer event's overlap is a test function, pass the subject event as the first param
11998					if (typeof peerOverlap === 'function' && !peerOverlap(event, peerEvent)) {
11999						return false;
12000					}
12001				}
12002			}
12003		}
12004
12005		return true;
12006	}
12007
12008
12009	// Given an event input from the API, produces an array of event objects. Possible event inputs:
12010	// 'businessHours'
12011	// An event ID (number or string)
12012	// An object with specific start/end dates or a recurring event (like what businessHours accepts)
12013	function constraintToEvents(constraintInput) {
12014
12015		if (constraintInput === 'businessHours') {
12016			return t.getCurrentBusinessHourEvents();
12017		}
12018
12019		if (typeof constraintInput === 'object') {
12020			return expandEvent(buildEventFromInput(constraintInput));
12021		}
12022
12023		return clientEvents(constraintInput); // probably an ID
12024	}
12025
12026
12027	// Does the event's date range intersect with the given range?
12028	// start/end already assumed to have stripped zones :(
12029	function eventIntersectsRange(event, range) {
12030		var eventStart = event.start.clone().stripZone();
12031		var eventEnd = t.getEventEnd(event).stripZone();
12032
12033		return range.start < eventEnd && range.end > eventStart;
12034	}
12035
12036
12037	t.getEventCache = function() {
12038		return cache;
12039	};
12040
12041}
12042
12043
12044// hook for external libs to manipulate event properties upon creation.
12045// should manipulate the event in-place.
12046Calendar.prototype.normalizeEvent = function(event) {
12047};
12048
12049
12050// Does the given span (start, end, and other location information)
12051// fully contain the other?
12052Calendar.prototype.spanContainsSpan = function(outerSpan, innerSpan) {
12053	var eventStart = outerSpan.start.clone().stripZone();
12054	var eventEnd = this.getEventEnd(outerSpan).stripZone();
12055
12056	return innerSpan.start >= eventStart && innerSpan.end <= eventEnd;
12057};
12058
12059
12060// Returns a list of events that the given event should be compared against when being considered for a move to
12061// the specified span. Attached to the Calendar's prototype because EventManager is a mixin for a Calendar.
12062Calendar.prototype.getPeerEvents = function(span, event) {
12063	var cache = this.getEventCache();
12064	var peerEvents = [];
12065	var i, otherEvent;
12066
12067	for (i = 0; i < cache.length; i++) {
12068		otherEvent = cache[i];
12069		if (
12070			!event ||
12071			event._id !== otherEvent._id // don't compare the event to itself or other related [repeating] events
12072		) {
12073			peerEvents.push(otherEvent);
12074		}
12075	}
12076
12077	return peerEvents;
12078};
12079
12080
12081// updates the "backup" properties, which are preserved in order to compute diffs later on.
12082function backupEventDates(event) {
12083	event._allDay = event.allDay;
12084	event._start = event.start.clone();
12085	event._end = event.end ? event.end.clone() : null;
12086}
12087
12088
12089/* Business Hours
12090-----------------------------------------------------------------------------------------*/
12091
12092var BUSINESS_HOUR_EVENT_DEFAULTS = {
12093	id: '_fcBusinessHours', // will relate events from different calls to expandEvent
12094	start: '09:00',
12095	end: '17:00',
12096	dow: [ 1, 2, 3, 4, 5 ], // monday - friday
12097	rendering: 'inverse-background'
12098	// classNames are defined in businessHoursSegClasses
12099};
12100
12101// Return events objects for business hours within the current view.
12102// Abuse of our event system :(
12103Calendar.prototype.getCurrentBusinessHourEvents = function(wholeDay) {
12104	return this.computeBusinessHourEvents(wholeDay, this.options.businessHours);
12105};
12106
12107// Given a raw input value from options, return events objects for business hours within the current view.
12108Calendar.prototype.computeBusinessHourEvents = function(wholeDay, input) {
12109	if (input === true) {
12110		return this.expandBusinessHourEvents(wholeDay, [ {} ]);
12111	}
12112	else if ($.isPlainObject(input)) {
12113		return this.expandBusinessHourEvents(wholeDay, [ input ]);
12114	}
12115	else if ($.isArray(input)) {
12116		return this.expandBusinessHourEvents(wholeDay, input, true);
12117	}
12118	else {
12119		return [];
12120	}
12121};
12122
12123// inputs expected to be an array of objects.
12124// if ignoreNoDow is true, will ignore entries that don't specify a day-of-week (dow) key.
12125Calendar.prototype.expandBusinessHourEvents = function(wholeDay, inputs, ignoreNoDow) {
12126	var view = this.getView();
12127	var events = [];
12128	var i, input;
12129
12130	for (i = 0; i < inputs.length; i++) {
12131		input = inputs[i];
12132
12133		if (ignoreNoDow && !input.dow) {
12134			continue;
12135		}
12136
12137		// give defaults. will make a copy
12138		input = $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, input);
12139
12140		// if a whole-day series is requested, clear the start/end times
12141		if (wholeDay) {
12142			input.start = null;
12143			input.end = null;
12144		}
12145
12146		events.push.apply(events, // append
12147			this.expandEvent(
12148				this.buildEventFromInput(input),
12149				view.start,
12150				view.end
12151			)
12152		);
12153	}
12154
12155	return events;
12156};
12157
12158;;
12159
12160/* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells.
12161----------------------------------------------------------------------------------------------------------------------*/
12162// It is a manager for a DayGrid subcomponent, which does most of the heavy lifting.
12163// It is responsible for managing width/height.
12164
12165var BasicView = FC.BasicView = View.extend({
12166
12167	scroller: null,
12168
12169	dayGridClass: DayGrid, // class the dayGrid will be instantiated from (overridable by subclasses)
12170	dayGrid: null, // the main subcomponent that does most of the heavy lifting
12171
12172	dayNumbersVisible: false, // display day numbers on each day cell?
12173	weekNumbersVisible: false, // display week numbers along the side?
12174
12175	weekNumberWidth: null, // width of all the week-number cells running down the side
12176
12177	headContainerEl: null, // div that hold's the dayGrid's rendered date header
12178	headRowEl: null, // the fake row element of the day-of-week header
12179
12180
12181	initialize: function() {
12182		this.dayGrid = this.instantiateDayGrid();
12183
12184		this.scroller = new Scroller({
12185			overflowX: 'hidden',
12186			overflowY: 'auto'
12187		});
12188	},
12189
12190
12191	// Generates the DayGrid object this view needs. Draws from this.dayGridClass
12192	instantiateDayGrid: function() {
12193		// generate a subclass on the fly with BasicView-specific behavior
12194		// TODO: cache this subclass
12195		var subclass = this.dayGridClass.extend(basicDayGridMethods);
12196
12197		return new subclass(this);
12198	},
12199
12200
12201	// Sets the display range and computes all necessary dates
12202	setRange: function(range) {
12203		View.prototype.setRange.call(this, range); // call the super-method
12204
12205		this.dayGrid.breakOnWeeks = /year|month|week/.test(this.intervalUnit); // do before setRange
12206		this.dayGrid.setRange(range);
12207	},
12208
12209
12210	// Compute the value to feed into setRange. Overrides superclass.
12211	computeRange: function(date) {
12212		var range = View.prototype.computeRange.call(this, date); // get value from the super-method
12213
12214		// year and month views should be aligned with weeks. this is already done for week
12215		if (/year|month/.test(range.intervalUnit)) {
12216			range.start.startOf('week');
12217			range.start = this.skipHiddenDays(range.start);
12218
12219			// make end-of-week if not already
12220			if (range.end.weekday()) {
12221				range.end.add(1, 'week').startOf('week');
12222				range.end = this.skipHiddenDays(range.end, -1, true); // exclusively move backwards
12223			}
12224		}
12225
12226		return range;
12227	},
12228
12229
12230	// Renders the view into `this.el`, which should already be assigned
12231	renderDates: function() {
12232
12233		this.dayNumbersVisible = this.dayGrid.rowCnt > 1; // TODO: make grid responsible
12234		this.weekNumbersVisible = this.opt('weekNumbers');
12235		this.dayGrid.numbersVisible = this.dayNumbersVisible || this.weekNumbersVisible;
12236
12237		this.el.addClass('fc-basic-view').html(this.renderSkeletonHtml());
12238		this.renderHead();
12239
12240		this.scroller.render();
12241		var dayGridContainerEl = this.scroller.el.addClass('fc-day-grid-container');
12242		var dayGridEl = $('<div class="fc-day-grid" />').appendTo(dayGridContainerEl);
12243		this.el.find('.fc-body > tr > td').append(dayGridContainerEl);
12244
12245		this.dayGrid.setElement(dayGridEl);
12246		this.dayGrid.renderDates(this.hasRigidRows());
12247	},
12248
12249
12250	// render the day-of-week headers
12251	renderHead: function() {
12252		this.headContainerEl =
12253			this.el.find('.fc-head-container')
12254				.html(this.dayGrid.renderHeadHtml());
12255		this.headRowEl = this.headContainerEl.find('.fc-row');
12256	},
12257
12258
12259	// Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering,
12260	// always completely kill the dayGrid's rendering.
12261	unrenderDates: function() {
12262		this.dayGrid.unrenderDates();
12263		this.dayGrid.removeElement();
12264		this.scroller.destroy();
12265	},
12266
12267
12268	renderBusinessHours: function() {
12269		this.dayGrid.renderBusinessHours();
12270	},
12271
12272
12273	unrenderBusinessHours: function() {
12274		this.dayGrid.unrenderBusinessHours();
12275	},
12276
12277
12278	// Builds the HTML skeleton for the view.
12279	// The day-grid component will render inside of a container defined by this HTML.
12280	renderSkeletonHtml: function() {
12281		return '' +
12282			'<table>' +
12283				'<thead class="fc-head">' +
12284					'<tr>' +
12285						'<td class="fc-head-container ' + this.widgetHeaderClass + '"></td>' +
12286					'</tr>' +
12287				'</thead>' +
12288				'<tbody class="fc-body">' +
12289					'<tr>' +
12290						'<td class="' + this.widgetContentClass + '"></td>' +
12291					'</tr>' +
12292				'</tbody>' +
12293			'</table>';
12294	},
12295
12296
12297	// Generates an HTML attribute string for setting the width of the week number column, if it is known
12298	weekNumberStyleAttr: function() {
12299		if (this.weekNumberWidth !== null) {
12300			return 'style="width:' + this.weekNumberWidth + 'px"';
12301		}
12302		return '';
12303	},
12304
12305
12306	// Determines whether each row should have a constant height
12307	hasRigidRows: function() {
12308		var eventLimit = this.opt('eventLimit');
12309		return eventLimit && typeof eventLimit !== 'number';
12310	},
12311
12312
12313	/* Dimensions
12314	------------------------------------------------------------------------------------------------------------------*/
12315
12316
12317	// Refreshes the horizontal dimensions of the view
12318	updateWidth: function() {
12319		if (this.weekNumbersVisible) {
12320			// Make sure all week number cells running down the side have the same width.
12321			// Record the width for cells created later.
12322			this.weekNumberWidth = matchCellWidths(
12323				this.el.find('.fc-week-number')
12324			);
12325		}
12326	},
12327
12328
12329	// Adjusts the vertical dimensions of the view to the specified values
12330	setHeight: function(totalHeight, isAuto) {
12331		var eventLimit = this.opt('eventLimit');
12332		var scrollerHeight;
12333		var scrollbarWidths;
12334
12335		// reset all heights to be natural
12336		this.scroller.clear();
12337		uncompensateScroll(this.headRowEl);
12338
12339		this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed
12340
12341		// is the event limit a constant level number?
12342		if (eventLimit && typeof eventLimit === 'number') {
12343			this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after
12344		}
12345
12346		// distribute the height to the rows
12347		// (totalHeight is a "recommended" value if isAuto)
12348		scrollerHeight = this.computeScrollerHeight(totalHeight);
12349		this.setGridHeight(scrollerHeight, isAuto);
12350
12351		// is the event limit dynamically calculated?
12352		if (eventLimit && typeof eventLimit !== 'number') {
12353			this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set
12354		}
12355
12356		if (!isAuto) { // should we force dimensions of the scroll container?
12357
12358			this.scroller.setHeight(scrollerHeight);
12359			scrollbarWidths = this.scroller.getScrollbarWidths();
12360
12361			if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars?
12362
12363				compensateScroll(this.headRowEl, scrollbarWidths);
12364
12365				// doing the scrollbar compensation might have created text overflow which created more height. redo
12366				scrollerHeight = this.computeScrollerHeight(totalHeight);
12367				this.scroller.setHeight(scrollerHeight);
12368			}
12369
12370			// guarantees the same scrollbar widths
12371			this.scroller.lockOverflow(scrollbarWidths);
12372		}
12373	},
12374
12375
12376	// given a desired total height of the view, returns what the height of the scroller should be
12377	computeScrollerHeight: function(totalHeight) {
12378		return totalHeight -
12379			subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
12380	},
12381
12382
12383	// Sets the height of just the DayGrid component in this view
12384	setGridHeight: function(height, isAuto) {
12385		if (isAuto) {
12386			undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding
12387		}
12388		else {
12389			distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows
12390		}
12391	},
12392
12393
12394	/* Scroll
12395	------------------------------------------------------------------------------------------------------------------*/
12396
12397
12398	queryScroll: function() {
12399		return this.scroller.getScrollTop();
12400	},
12401
12402
12403	setScroll: function(top) {
12404		this.scroller.setScrollTop(top);
12405	},
12406
12407
12408	/* Hit Areas
12409	------------------------------------------------------------------------------------------------------------------*/
12410	// forward all hit-related method calls to dayGrid
12411
12412
12413	prepareHits: function() {
12414		this.dayGrid.prepareHits();
12415	},
12416
12417
12418	releaseHits: function() {
12419		this.dayGrid.releaseHits();
12420	},
12421
12422
12423	queryHit: function(left, top) {
12424		return this.dayGrid.queryHit(left, top);
12425	},
12426
12427
12428	getHitSpan: function(hit) {
12429		return this.dayGrid.getHitSpan(hit);
12430	},
12431
12432
12433	getHitEl: function(hit) {
12434		return this.dayGrid.getHitEl(hit);
12435	},
12436
12437
12438	/* Events
12439	------------------------------------------------------------------------------------------------------------------*/
12440
12441
12442	// Renders the given events onto the view and populates the segments array
12443	renderEvents: function(events) {
12444		this.dayGrid.renderEvents(events);
12445
12446		this.updateHeight(); // must compensate for events that overflow the row
12447	},
12448
12449
12450	// Retrieves all segment objects that are rendered in the view
12451	getEventSegs: function() {
12452		return this.dayGrid.getEventSegs();
12453	},
12454
12455
12456	// Unrenders all event elements and clears internal segment data
12457	unrenderEvents: function() {
12458		this.dayGrid.unrenderEvents();
12459
12460		// we DON'T need to call updateHeight() because:
12461		// A) a renderEvents() call always happens after this, which will eventually call updateHeight()
12462		// B) in IE8, this causes a flash whenever events are rerendered
12463	},
12464
12465
12466	/* Dragging (for both events and external elements)
12467	------------------------------------------------------------------------------------------------------------------*/
12468
12469
12470	// A returned value of `true` signals that a mock "helper" event has been rendered.
12471	renderDrag: function(dropLocation, seg) {
12472		return this.dayGrid.renderDrag(dropLocation, seg);
12473	},
12474
12475
12476	unrenderDrag: function() {
12477		this.dayGrid.unrenderDrag();
12478	},
12479
12480
12481	/* Selection
12482	------------------------------------------------------------------------------------------------------------------*/
12483
12484
12485	// Renders a visual indication of a selection
12486	renderSelection: function(span) {
12487		this.dayGrid.renderSelection(span);
12488	},
12489
12490
12491	// Unrenders a visual indications of a selection
12492	unrenderSelection: function() {
12493		this.dayGrid.unrenderSelection();
12494	}
12495
12496});
12497
12498
12499// Methods that will customize the rendering behavior of the BasicView's dayGrid
12500var basicDayGridMethods = {
12501
12502
12503	// Generates the HTML that will go before the day-of week header cells
12504	renderHeadIntroHtml: function() {
12505		var view = this.view;
12506
12507		if (view.weekNumbersVisible) {
12508			return '' +
12509				'<th class="fc-week-number ' + view.widgetHeaderClass + '" ' + view.weekNumberStyleAttr() + '>' +
12510					'<span>' + // needed for matchCellWidths
12511						htmlEscape(view.opt('weekNumberTitle')) +
12512					'</span>' +
12513				'</th>';
12514		}
12515
12516		return '';
12517	},
12518
12519
12520	// Generates the HTML that will go before content-skeleton cells that display the day/week numbers
12521	renderNumberIntroHtml: function(row) {
12522		var view = this.view;
12523
12524		if (view.weekNumbersVisible) {
12525			return '' +
12526				'<td class="fc-week-number" ' + view.weekNumberStyleAttr() + '>' +
12527					'<span>' + // needed for matchCellWidths
12528						this.getCellDate(row, 0).format('w') +
12529					'</span>' +
12530				'</td>';
12531		}
12532
12533		return '';
12534	},
12535
12536
12537	// Generates the HTML that goes before the day bg cells for each day-row
12538	renderBgIntroHtml: function() {
12539		var view = this.view;
12540
12541		if (view.weekNumbersVisible) {
12542			return '<td class="fc-week-number ' + view.widgetContentClass + '" ' +
12543				view.weekNumberStyleAttr() + '></td>';
12544		}
12545
12546		return '';
12547	},
12548
12549
12550	// Generates the HTML that goes before every other type of row generated by DayGrid.
12551	// Affects helper-skeleton and highlight-skeleton rows.
12552	renderIntroHtml: function() {
12553		var view = this.view;
12554
12555		if (view.weekNumbersVisible) {
12556			return '<td class="fc-week-number" ' + view.weekNumberStyleAttr() + '></td>';
12557		}
12558
12559		return '';
12560	}
12561
12562};
12563
12564;;
12565
12566/* A month view with day cells running in rows (one-per-week) and columns
12567----------------------------------------------------------------------------------------------------------------------*/
12568
12569var MonthView = FC.MonthView = BasicView.extend({
12570
12571	// Produces information about what range to display
12572	computeRange: function(date) {
12573		var range = BasicView.prototype.computeRange.call(this, date); // get value from super-method
12574		var rowCnt;
12575
12576		// ensure 6 weeks
12577		if (this.isFixedWeeks()) {
12578			rowCnt = Math.ceil(range.end.diff(range.start, 'weeks', true)); // could be partial weeks due to hiddenDays
12579			range.end.add(6 - rowCnt, 'weeks');
12580		}
12581
12582		return range;
12583	},
12584
12585
12586	// Overrides the default BasicView behavior to have special multi-week auto-height logic
12587	setGridHeight: function(height, isAuto) {
12588
12589		isAuto = isAuto || this.opt('weekMode') === 'variable'; // LEGACY: weekMode is deprecated
12590
12591		// if auto, make the height of each row the height that it would be if there were 6 weeks
12592		if (isAuto) {
12593			height *= this.rowCnt / 6;
12594		}
12595
12596		distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows
12597	},
12598
12599
12600	isFixedWeeks: function() {
12601		var weekMode = this.opt('weekMode'); // LEGACY: weekMode is deprecated
12602		if (weekMode) {
12603			return weekMode === 'fixed'; // if any other type of weekMode, assume NOT fixed
12604		}
12605
12606		return this.opt('fixedWeekCount');
12607	}
12608
12609});
12610
12611;;
12612
12613fcViews.basic = {
12614	'class': BasicView
12615};
12616
12617fcViews.basicDay = {
12618	type: 'basic',
12619	duration: { days: 1 }
12620};
12621
12622fcViews.basicWeek = {
12623	type: 'basic',
12624	duration: { weeks: 1 }
12625};
12626
12627fcViews.month = {
12628	'class': MonthView,
12629	duration: { months: 1 }, // important for prev/next
12630	defaults: {
12631		fixedWeekCount: true
12632	}
12633};
12634;;
12635
12636/* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically.
12637----------------------------------------------------------------------------------------------------------------------*/
12638// Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on).
12639// Responsible for managing width/height.
12640
12641var AgendaView = FC.AgendaView = View.extend({
12642
12643	scroller: null,
12644
12645	timeGridClass: TimeGrid, // class used to instantiate the timeGrid. subclasses can override
12646	timeGrid: null, // the main time-grid subcomponent of this view
12647
12648	dayGridClass: DayGrid, // class used to instantiate the dayGrid. subclasses can override
12649	dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null
12650
12651	axisWidth: null, // the width of the time axis running down the side
12652
12653	headContainerEl: null, // div that hold's the timeGrid's rendered date header
12654	noScrollRowEls: null, // set of fake row elements that must compensate when scroller has scrollbars
12655
12656	// when the time-grid isn't tall enough to occupy the given height, we render an <hr> underneath
12657	bottomRuleEl: null,
12658
12659
12660	initialize: function() {
12661		this.timeGrid = this.instantiateTimeGrid();
12662
12663		if (this.opt('allDaySlot')) { // should we display the "all-day" area?
12664			this.dayGrid = this.instantiateDayGrid(); // the all-day subcomponent of this view
12665		}
12666
12667		this.scroller = new Scroller({
12668			overflowX: 'hidden',
12669			overflowY: 'auto'
12670		});
12671	},
12672
12673
12674	// Instantiates the TimeGrid object this view needs. Draws from this.timeGridClass
12675	instantiateTimeGrid: function() {
12676		var subclass = this.timeGridClass.extend(agendaTimeGridMethods);
12677
12678		return new subclass(this);
12679	},
12680
12681
12682	// Instantiates the DayGrid object this view might need. Draws from this.dayGridClass
12683	instantiateDayGrid: function() {
12684		var subclass = this.dayGridClass.extend(agendaDayGridMethods);
12685
12686		return new subclass(this);
12687	},
12688
12689
12690	/* Rendering
12691	------------------------------------------------------------------------------------------------------------------*/
12692
12693
12694	// Sets the display range and computes all necessary dates
12695	setRange: function(range) {
12696		View.prototype.setRange.call(this, range); // call the super-method
12697
12698		this.timeGrid.setRange(range);
12699		if (this.dayGrid) {
12700			this.dayGrid.setRange(range);
12701		}
12702	},
12703
12704
12705	// Renders the view into `this.el`, which has already been assigned
12706	renderDates: function() {
12707
12708		this.el.addClass('fc-agenda-view').html(this.renderSkeletonHtml());
12709		this.renderHead();
12710
12711		this.scroller.render();
12712		var timeGridWrapEl = this.scroller.el.addClass('fc-time-grid-container');
12713		var timeGridEl = $('<div class="fc-time-grid" />').appendTo(timeGridWrapEl);
12714		this.el.find('.fc-body > tr > td').append(timeGridWrapEl);
12715
12716		this.timeGrid.setElement(timeGridEl);
12717		this.timeGrid.renderDates();
12718
12719		// the <hr> that sometimes displays under the time-grid
12720		this.bottomRuleEl = $('<hr class="fc-divider ' + this.widgetHeaderClass + '"/>')
12721			.appendTo(this.timeGrid.el); // inject it into the time-grid
12722
12723		if (this.dayGrid) {
12724			this.dayGrid.setElement(this.el.find('.fc-day-grid'));
12725			this.dayGrid.renderDates();
12726
12727			// have the day-grid extend it's coordinate area over the <hr> dividing the two grids
12728			this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight();
12729		}
12730
12731		this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller
12732	},
12733
12734
12735	// render the day-of-week headers
12736	renderHead: function() {
12737		this.headContainerEl =
12738			this.el.find('.fc-head-container')
12739				.html(this.timeGrid.renderHeadHtml());
12740	},
12741
12742
12743	// Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering,
12744	// always completely kill each grid's rendering.
12745	unrenderDates: function() {
12746		this.timeGrid.unrenderDates();
12747		this.timeGrid.removeElement();
12748
12749		if (this.dayGrid) {
12750			this.dayGrid.unrenderDates();
12751			this.dayGrid.removeElement();
12752		}
12753
12754		this.scroller.destroy();
12755	},
12756
12757
12758	// Builds the HTML skeleton for the view.
12759	// The day-grid and time-grid components will render inside containers defined by this HTML.
12760	renderSkeletonHtml: function() {
12761		return '' +
12762			'<table>' +
12763				'<thead class="fc-head">' +
12764					'<tr>' +
12765						'<td class="fc-head-container ' + this.widgetHeaderClass + '"></td>' +
12766					'</tr>' +
12767				'</thead>' +
12768				'<tbody class="fc-body">' +
12769					'<tr>' +
12770						'<td class="' + this.widgetContentClass + '">' +
12771							(this.dayGrid ?
12772								'<div class="fc-day-grid"/>' +
12773								'<hr class="fc-divider ' + this.widgetHeaderClass + '"/>' :
12774								''
12775								) +
12776						'</td>' +
12777					'</tr>' +
12778				'</tbody>' +
12779			'</table>';
12780	},
12781
12782
12783	// Generates an HTML attribute string for setting the width of the axis, if it is known
12784	axisStyleAttr: function() {
12785		if (this.axisWidth !== null) {
12786			 return 'style="width:' + this.axisWidth + 'px"';
12787		}
12788		return '';
12789	},
12790
12791
12792	/* Business Hours
12793	------------------------------------------------------------------------------------------------------------------*/
12794
12795
12796	renderBusinessHours: function() {
12797		this.timeGrid.renderBusinessHours();
12798
12799		if (this.dayGrid) {
12800			this.dayGrid.renderBusinessHours();
12801		}
12802	},
12803
12804
12805	unrenderBusinessHours: function() {
12806		this.timeGrid.unrenderBusinessHours();
12807
12808		if (this.dayGrid) {
12809			this.dayGrid.unrenderBusinessHours();
12810		}
12811	},
12812
12813
12814	/* Now Indicator
12815	------------------------------------------------------------------------------------------------------------------*/
12816
12817
12818	getNowIndicatorUnit: function() {
12819		return this.timeGrid.getNowIndicatorUnit();
12820	},
12821
12822
12823	renderNowIndicator: function(date) {
12824		this.timeGrid.renderNowIndicator(date);
12825	},
12826
12827
12828	unrenderNowIndicator: function() {
12829		this.timeGrid.unrenderNowIndicator();
12830	},
12831
12832
12833	/* Dimensions
12834	------------------------------------------------------------------------------------------------------------------*/
12835
12836
12837	updateSize: function(isResize) {
12838		this.timeGrid.updateSize(isResize);
12839
12840		View.prototype.updateSize.call(this, isResize); // call the super-method
12841	},
12842
12843
12844	// Refreshes the horizontal dimensions of the view
12845	updateWidth: function() {
12846		// make all axis cells line up, and record the width so newly created axis cells will have it
12847		this.axisWidth = matchCellWidths(this.el.find('.fc-axis'));
12848	},
12849
12850
12851	// Adjusts the vertical dimensions of the view to the specified values
12852	setHeight: function(totalHeight, isAuto) {
12853		var eventLimit;
12854		var scrollerHeight;
12855		var scrollbarWidths;
12856
12857		// reset all dimensions back to the original state
12858		this.bottomRuleEl.hide(); // .show() will be called later if this <hr> is necessary
12859		this.scroller.clear(); // sets height to 'auto' and clears overflow
12860		uncompensateScroll(this.noScrollRowEls);
12861
12862		// limit number of events in the all-day area
12863		if (this.dayGrid) {
12864			this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed
12865
12866			eventLimit = this.opt('eventLimit');
12867			if (eventLimit && typeof eventLimit !== 'number') {
12868				eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number
12869			}
12870			if (eventLimit) {
12871				this.dayGrid.limitRows(eventLimit);
12872			}
12873		}
12874
12875		if (!isAuto) { // should we force dimensions of the scroll container?
12876
12877			scrollerHeight = this.computeScrollerHeight(totalHeight);
12878			this.scroller.setHeight(scrollerHeight);
12879			scrollbarWidths = this.scroller.getScrollbarWidths();
12880
12881			if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars?
12882
12883				// make the all-day and header rows lines up
12884				compensateScroll(this.noScrollRowEls, scrollbarWidths);
12885
12886				// the scrollbar compensation might have changed text flow, which might affect height, so recalculate
12887				// and reapply the desired height to the scroller.
12888				scrollerHeight = this.computeScrollerHeight(totalHeight);
12889				this.scroller.setHeight(scrollerHeight);
12890			}
12891
12892			// guarantees the same scrollbar widths
12893			this.scroller.lockOverflow(scrollbarWidths);
12894
12895			// if there's any space below the slats, show the horizontal rule.
12896			// this won't cause any new overflow, because lockOverflow already called.
12897			if (this.timeGrid.getTotalSlatHeight() < scrollerHeight) {
12898				this.bottomRuleEl.show();
12899			}
12900		}
12901	},
12902
12903
12904	// given a desired total height of the view, returns what the height of the scroller should be
12905	computeScrollerHeight: function(totalHeight) {
12906		return totalHeight -
12907			subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
12908	},
12909
12910
12911	/* Scroll
12912	------------------------------------------------------------------------------------------------------------------*/
12913
12914
12915	// Computes the initial pre-configured scroll state prior to allowing the user to change it
12916	computeInitialScroll: function() {
12917		var scrollTime = moment.duration(this.opt('scrollTime'));
12918		var top = this.timeGrid.computeTimeTop(scrollTime);
12919
12920		// zoom can give weird floating-point values. rather scroll a little bit further
12921		top = Math.ceil(top);
12922
12923		if (top) {
12924			top++; // to overcome top border that slots beyond the first have. looks better
12925		}
12926
12927		return top;
12928	},
12929
12930
12931	queryScroll: function() {
12932		return this.scroller.getScrollTop();
12933	},
12934
12935
12936	setScroll: function(top) {
12937		this.scroller.setScrollTop(top);
12938	},
12939
12940
12941	/* Hit Areas
12942	------------------------------------------------------------------------------------------------------------------*/
12943	// forward all hit-related method calls to the grids (dayGrid might not be defined)
12944
12945
12946	prepareHits: function() {
12947		this.timeGrid.prepareHits();
12948		if (this.dayGrid) {
12949			this.dayGrid.prepareHits();
12950		}
12951	},
12952
12953
12954	releaseHits: function() {
12955		this.timeGrid.releaseHits();
12956		if (this.dayGrid) {
12957			this.dayGrid.releaseHits();
12958		}
12959	},
12960
12961
12962	queryHit: function(left, top) {
12963		var hit = this.timeGrid.queryHit(left, top);
12964
12965		if (!hit && this.dayGrid) {
12966			hit = this.dayGrid.queryHit(left, top);
12967		}
12968
12969		return hit;
12970	},
12971
12972
12973	getHitSpan: function(hit) {
12974		// TODO: hit.component is set as a hack to identify where the hit came from
12975		return hit.component.getHitSpan(hit);
12976	},
12977
12978
12979	getHitEl: function(hit) {
12980		// TODO: hit.component is set as a hack to identify where the hit came from
12981		return hit.component.getHitEl(hit);
12982	},
12983
12984
12985	/* Events
12986	------------------------------------------------------------------------------------------------------------------*/
12987
12988
12989	// Renders events onto the view and populates the View's segment array
12990	renderEvents: function(events) {
12991		var dayEvents = [];
12992		var timedEvents = [];
12993		var daySegs = [];
12994		var timedSegs;
12995		var i;
12996
12997		// separate the events into all-day and timed
12998		for (i = 0; i < events.length; i++) {
12999			if (events[i].allDay) {
13000				dayEvents.push(events[i]);
13001			}
13002			else {
13003				timedEvents.push(events[i]);
13004			}
13005		}
13006
13007		// render the events in the subcomponents
13008		timedSegs = this.timeGrid.renderEvents(timedEvents);
13009		if (this.dayGrid) {
13010			daySegs = this.dayGrid.renderEvents(dayEvents);
13011		}
13012
13013		// the all-day area is flexible and might have a lot of events, so shift the height
13014		this.updateHeight();
13015	},
13016
13017
13018	// Retrieves all segment objects that are rendered in the view
13019	getEventSegs: function() {
13020		return this.timeGrid.getEventSegs().concat(
13021			this.dayGrid ? this.dayGrid.getEventSegs() : []
13022		);
13023	},
13024
13025
13026	// Unrenders all event elements and clears internal segment data
13027	unrenderEvents: function() {
13028
13029		// unrender the events in the subcomponents
13030		this.timeGrid.unrenderEvents();
13031		if (this.dayGrid) {
13032			this.dayGrid.unrenderEvents();
13033		}
13034
13035		// we DON'T need to call updateHeight() because:
13036		// A) a renderEvents() call always happens after this, which will eventually call updateHeight()
13037		// B) in IE8, this causes a flash whenever events are rerendered
13038	},
13039
13040
13041	/* Dragging (for events and external elements)
13042	------------------------------------------------------------------------------------------------------------------*/
13043
13044
13045	// A returned value of `true` signals that a mock "helper" event has been rendered.
13046	renderDrag: function(dropLocation, seg) {
13047		if (dropLocation.start.hasTime()) {
13048			return this.timeGrid.renderDrag(dropLocation, seg);
13049		}
13050		else if (this.dayGrid) {
13051			return this.dayGrid.renderDrag(dropLocation, seg);
13052		}
13053	},
13054
13055
13056	unrenderDrag: function() {
13057		this.timeGrid.unrenderDrag();
13058		if (this.dayGrid) {
13059			this.dayGrid.unrenderDrag();
13060		}
13061	},
13062
13063
13064	/* Selection
13065	------------------------------------------------------------------------------------------------------------------*/
13066
13067
13068	// Renders a visual indication of a selection
13069	renderSelection: function(span) {
13070		if (span.start.hasTime() || span.end.hasTime()) {
13071			this.timeGrid.renderSelection(span);
13072		}
13073		else if (this.dayGrid) {
13074			this.dayGrid.renderSelection(span);
13075		}
13076	},
13077
13078
13079	// Unrenders a visual indications of a selection
13080	unrenderSelection: function() {
13081		this.timeGrid.unrenderSelection();
13082		if (this.dayGrid) {
13083			this.dayGrid.unrenderSelection();
13084		}
13085	}
13086
13087});
13088
13089
13090// Methods that will customize the rendering behavior of the AgendaView's timeGrid
13091// TODO: move into TimeGrid
13092var agendaTimeGridMethods = {
13093
13094
13095	// Generates the HTML that will go before the day-of week header cells
13096	renderHeadIntroHtml: function() {
13097		var view = this.view;
13098		var weekText;
13099
13100		if (view.opt('weekNumbers')) {
13101			weekText = this.start.format(view.opt('smallWeekFormat'));
13102
13103			return '' +
13104				'<th class="fc-axis fc-week-number ' + view.widgetHeaderClass + '" ' + view.axisStyleAttr() + '>' +
13105					'<span>' + // needed for matchCellWidths
13106						htmlEscape(weekText) +
13107					'</span>' +
13108				'</th>';
13109		}
13110		else {
13111			return '<th class="fc-axis ' + view.widgetHeaderClass + '" ' + view.axisStyleAttr() + '></th>';
13112		}
13113	},
13114
13115
13116	// Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column.
13117	renderBgIntroHtml: function() {
13118		var view = this.view;
13119
13120		return '<td class="fc-axis ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '></td>';
13121	},
13122
13123
13124	// Generates the HTML that goes before all other types of cells.
13125	// Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
13126	renderIntroHtml: function() {
13127		var view = this.view;
13128
13129		return '<td class="fc-axis" ' + view.axisStyleAttr() + '></td>';
13130	}
13131
13132};
13133
13134
13135// Methods that will customize the rendering behavior of the AgendaView's dayGrid
13136var agendaDayGridMethods = {
13137
13138
13139	// Generates the HTML that goes before the all-day cells
13140	renderBgIntroHtml: function() {
13141		var view = this.view;
13142
13143		return '' +
13144			'<td class="fc-axis ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' +
13145				'<span>' + // needed for matchCellWidths
13146					(view.opt('allDayHtml') || htmlEscape(view.opt('allDayText'))) +
13147				'</span>' +
13148			'</td>';
13149	},
13150
13151
13152	// Generates the HTML that goes before all other types of cells.
13153	// Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
13154	renderIntroHtml: function() {
13155		var view = this.view;
13156
13157		return '<td class="fc-axis" ' + view.axisStyleAttr() + '></td>';
13158	}
13159
13160};
13161
13162;;
13163
13164var AGENDA_ALL_DAY_EVENT_LIMIT = 5;
13165
13166// potential nice values for the slot-duration and interval-duration
13167// from largest to smallest
13168var AGENDA_STOCK_SUB_DURATIONS = [
13169	{ hours: 1 },
13170	{ minutes: 30 },
13171	{ minutes: 15 },
13172	{ seconds: 30 },
13173	{ seconds: 15 }
13174];
13175
13176fcViews.agenda = {
13177	'class': AgendaView,
13178	defaults: {
13179		allDaySlot: true,
13180		allDayText: 'all-day',
13181		slotDuration: '00:30:00',
13182		minTime: '00:00:00',
13183		maxTime: '24:00:00',
13184		slotEventOverlap: true // a bad name. confused with overlap/constraint system
13185	}
13186};
13187
13188fcViews.agendaDay = {
13189	type: 'agenda',
13190	duration: { days: 1 }
13191};
13192
13193fcViews.agendaWeek = {
13194	type: 'agenda',
13195	duration: { weeks: 1 }
13196};
13197;;
13198
13199return FC; // export for Node/CommonJS
13200});

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.