PageSourceSearch

https://www.reginfo.gov/public/Javascript/fullCalendar/fullcalenda…=51CFBAF58C4EB99867C732C8481712C0

js reginfo.gov collected 2026-09-24 15:20:33 UTC 151,963 bytes, 6,118 lines download raw bytes

1/*!
2 * FullCalendar v1.6.4
3 * Docs & License: http://arshaw.com/fullcalendar/
4 * (c) 2013 Adam Shaw
5 */
6
7/*
8 * Use fullcalendar.css for basic styling.
9 * For event drag & drop, requires jQuery UI draggable.
10 * For event resizing, requires jQuery UI resizable.
11 */
12 
13(function($, undefined) {
14
15
16;;
17
18var defaults = {
19
20	// display
21	defaultView: 'month',
22	aspectRatio: 1.35,
23	header: {
24		left: 'title',
25		center: '',
26		right: 'today prev,next'
27	},
28	weekends: true,
29	weekNumbers: false,
30	weekNumberCalculation: 'iso',
31	weekNumberTitle: 'W',
32	
33	// editing
34	//editable: false,
35	//disableDragging: false,
36	//disableResizing: false,
37	
38	allDayDefault: true,
39	ignoreTimezone: true,
40	
41	// event ajax
42	lazyFetching: true,
43	startParam: 'start',
44	endParam: 'end',
45	
46	// time formats
47	titleFormat: {
48		month: 'MMMM yyyy',
49		week: "MMM d[ yyyy]{ '—'[ MMM] d yyyy}",
50		day: 'dddd, MMM d, yyyy'
51	},
52	columnFormat: {
53		month: 'ddd',
54		week: 'ddd M/d',
55		day: 'dddd M/d'
56	},
57	timeFormat: { // for event elements
58		'': 'h(:mm)t' // default
59	},
60	
61	// locale
62	isRTL: false,
63	firstDay: 0,
64	monthNames: ['January','February','March','April','May','June','July','August','September','October','November','December'],
65	monthNamesShort: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
66	dayNames: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
67	dayNamesShort: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'],
68	buttonText: {
69		prev: "<span class='fc-text-arrow'>&lsaquo;</span>",
70		next: "<span class='fc-text-arrow'>&rsaquo;</span>",
71		prevYear: "<span class='fc-text-arrow'>&laquo;</span>",
72		nextYear: "<span class='fc-text-arrow'>&raquo;</span>",
73		today: 'today',
74		month: 'month',
75		week: 'week',
76		day: 'day'
77	},
78	
79	// jquery-ui theming
80	theme: false,
81	buttonIcons: {
82		prev: 'circle-triangle-w',
83		next: 'circle-triangle-e'
84	},
85	
86	//selectable: false,
87	unselectAuto: true,
88	
89	dropAccept: '*',
90	
91	handleWindowResize: true
92	
93};
94
95// right-to-left defaults
96var rtlDefaults = {
97	header: {
98		left: 'next,prev today',
99		center: '',
100		right: 'title'
101	},
102	buttonText: {
103		prev: "<span class='fc-text-arrow'>&rsaquo;</span>",
104		next: "<span class='fc-text-arrow'>&lsaquo;</span>",
105		prevYear: "<span class='fc-text-arrow'>&raquo;</span>",
106		nextYear: "<span class='fc-text-arrow'>&laquo;</span>"
107	},
108	buttonIcons: {
109		prev: 'circle-triangle-e',
110		next: 'circle-triangle-w'
111	}
112};
113
114
115
116;;
117
118var fc = $.fullCalendar = { version: "1.6.4" };
119var fcViews = fc.views = {};
120
121
122$.fn.fullCalendar = function(options) {
123
124
125	// method calling
126	if (typeof options == 'string') {
127		var args = Array.prototype.slice.call(arguments, 1);
128		var res;
129		this.each(function() {
130			var calendar = $.data(this, 'fullCalendar');
131			if (calendar && $.isFunction(calendar[options])) {
132				var r = calendar[options].apply(calendar, args);
133				if (res === undefined) {
134					res = r;
135				}
136				if (options == 'destroy') {
137					$.removeData(this, 'fullCalendar');
138				}
139			}
140		});
141		if (res !== undefined) {
142			return res;
143		}
144		return this;
145	}
146
147	options = options || {};
148	
149	// would like to have this logic in EventManager, but needs to happen before options are recursively extended
150	var eventSources = options.eventSources || [];
151	delete options.eventSources;
152	if (options.events) {
153		eventSources.push(options.events);
154		delete options.events;
155	}
156	
157
158	options = $.extend(true, {},
159		defaults,
160		(options.isRTL || options.isRTL===undefined && defaults.isRTL) ? rtlDefaults : {},
161		options
162	);
163	
164	
165	this.each(function(i, _element) {
166		var element = $(_element);
167		var calendar = new Calendar(element, options, eventSources);
168		element.data('fullCalendar', calendar); // TODO: look into memory leak implications
169		calendar.render();
170	});
171	
172	
173	return this;
174	
175};
176
177
178// function for adding/overriding defaults
179function setDefaults(d) {
180	$.extend(true, defaults, d);
181}
182
183
184
185;;
186
187 
188function Calendar(element, options, eventSources) {
189	var t = this;
190	
191	
192	// exports
193	t.options = options;
194	t.render = render;
195	t.destroy = destroy;
196	t.refetchEvents = refetchEvents;
197	t.reportEvents = reportEvents;
198	t.reportEventChange = reportEventChange;
199	t.rerenderEvents = rerenderEvents;
200	t.changeView = changeView;
201	t.select = select;
202	t.unselect = unselect;
203	t.prev = prev;
204	t.next = next;
205	t.prevYear = prevYear;
206	t.nextYear = nextYear;
207	t.today = today;
208	t.gotoDate = gotoDate;
209	t.incrementDate = incrementDate;
210	t.formatDate = function(format, date) { return formatDate(format, date, options) };
211	t.formatDates = function(format, date1, date2) { return formatDates(format, date1, date2, options) };
212	t.getDate = getDate;
213	t.getView = getView;
214	t.option = option;
215	t.trigger = trigger;
216	
217	
218	// imports
219	EventManager.call(t, options, eventSources);
220	var isFetchNeeded = t.isFetchNeeded;
221	var fetchEvents = t.fetchEvents;
222	
223	
224	// locals
225	var _element = element[0];
226	var header;
227	var headerElement;
228	var content;
229	var tm; // for making theme classes
230	var currentView;
231	var elementOuterWidth;
232	var suggestedViewHeight;
233	var resizeUID = 0;
234	var ignoreWindowResize = 0;
235	var date = new Date();
236	var events = [];
237	var _dragElement;
238	
239	
240	
241	/* Main Rendering
242	-----------------------------------------------------------------------------*/
243	
244	
245	setYMD(date, options.year, options.month, options.date);
246	
247	
248	function render(inc) {
249		if (!content) {
250			initialRender();
251		}
252		else if (elementVisible()) {
253			// mainly for the public API
254			calcSize();
255			_renderView(inc);
256		}
257	}
258	
259	
260	function initialRender() {
261		tm = options.theme ? 'ui' : 'fc';
262		element.addClass('fc');
263		if (options.isRTL) {
264			element.addClass('fc-rtl');
265		}
266		else {
267			element.addClass('fc-ltr');
268		}
269		if (options.theme) {
270			element.addClass('ui-widget');
271		}
272
273		content = $("<div class='fc-content' style='position:relative'/>")
274			.prependTo(element);
275
276		header = new Header(t, options);
277		headerElement = header.render();
278		if (headerElement) {
279			element.prepend(headerElement);
280		}
281
282		changeView(options.defaultView);
283
284		if (options.handleWindowResize) {
285			$(window).resize(windowResize);
286		}
287
288		// needed for IE in a 0x0 iframe, b/c when it is resized, never triggers a windowResize
289		if (!bodyVisible()) {
290			lateRender();
291		}
292	}
293	
294	
295	// called when we know the calendar couldn't be rendered when it was initialized,
296	// but we think it's ready now
297	function lateRender() {
298		setTimeout(function() { // IE7 needs this so dimensions are calculated correctly
299			if (!currentView.start && bodyVisible()) { // !currentView.start makes sure this never happens more than once
300				renderView();
301			}
302		},0);
303	}
304	
305	
306	function destroy() {
307
308		if (currentView) {
309			trigger('viewDestroy', currentView, currentView, currentView.element);
310			currentView.triggerEventDestroy();
311		}
312
313		$(window).unbind('resize', windowResize);
314
315		header.destroy();
316		content.remove();
317		element.removeClass('fc fc-rtl ui-widget');
318	}
319	
320	
321	function elementVisible() {
322		return element.is(':visible');
323	}
324	
325	
326	function bodyVisible() {
327		return $('body').is(':visible');
328	}
329	
330	
331	
332	/* View Rendering
333	-----------------------------------------------------------------------------*/
334	
335
336	function changeView(newViewName) {
337		if (!currentView || newViewName != currentView.name) {
338			_changeView(newViewName);
339		}
340	}
341
342
343	function _changeView(newViewName) {
344		ignoreWindowResize++;
345
346		if (currentView) {
347			trigger('viewDestroy', currentView, currentView, currentView.element);
348			unselect();
349			currentView.triggerEventDestroy(); // trigger 'eventDestroy' for each event
350			freezeContentHeight();
351			currentView.element.remove();
352			header.deactivateButton(currentView.name);
353		}
354
355		header.activateButton(newViewName);
356
357		currentView = new fcViews[newViewName](
358			$("<div class='fc-view fc-view-" + newViewName + "' style='position:relative'/>")
359				.appendTo(content),
360			t // the calendar object
361		);
362
363		renderView();
364		unfreezeContentHeight();
365
366		ignoreWindowResize--;
367	}
368
369
370	function renderView(inc) {
371		if (
372			!currentView.start || // never rendered before
373			inc || date < currentView.start || date >= currentView.end // or new date range
374		) {
375			if (elementVisible()) {
376				_renderView(inc);
377			}
378		}
379	}
380
381
382	function _renderView(inc) { // assumes elementVisible
383		ignoreWindowResize++;
384
385		if (currentView.start) { // already been rendered?
386			trigger('viewDestroy', currentView, currentView, currentView.element);
387			unselect();
388			clearEvents();
389		}
390
391		freezeContentHeight();
392		currentView.render(date, inc || 0); // the view's render method ONLY renders the skeleton, nothing else
393		setSize();
394		unfreezeContentHeight();
395		(currentView.afterRender || noop)();
396
397		updateTitle();
398		updateTodayButton();
399
400		trigger('viewRender', currentView, currentView, currentView.element);
401		currentView.trigger('viewDisplay', _element); // deprecated
402
403		ignoreWindowResize--;
404
405		getAndRenderEvents();
406	}
407	
408	
409
410	/* Resizing
411	-----------------------------------------------------------------------------*/
412	
413	
414	function updateSize() {
415		if (elementVisible()) {
416			unselect();
417			clearEvents();
418			calcSize();
419			setSize();
420			renderEvents();
421		}
422	}
423	
424	
425	function calcSize() { // assumes elementVisible
426		if (options.contentHeight) {
427			suggestedViewHeight = options.contentHeight;
428		}
429		else if (options.height) {
430			suggestedViewHeight = options.height - (headerElement ? headerElement.height() : 0) - vsides(content);
431		}
432		else {
433			suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5));
434		}
435	}
436	
437	
438	function setSize() { // assumes elementVisible
439
440		if (suggestedViewHeight === undefined) {
441			calcSize(); // for first time
442				// NOTE: we don't want to recalculate on every renderView because
443				// it could result in oscillating heights due to scrollbars.
444		}
445
446		ignoreWindowResize++;
447		currentView.setHeight(suggestedViewHeight);
448		currentView.setWidth(content.width());
449		ignoreWindowResize--;
450
451		elementOuterWidth = element.outerWidth();
452	}
453	
454	
455	function windowResize() {
456		if (!ignoreWindowResize) {
457			if (currentView.start) { // view has already been rendered
458				var uid = ++resizeUID;
459				setTimeout(function() { // add a delay
460					if (uid == resizeUID && !ignoreWindowResize && elementVisible()) {
461						if (elementOuterWidth != (elementOuterWidth = element.outerWidth())) {
462							ignoreWindowResize++; // in case the windowResize callback changes the height
463							updateSize();
464							currentView.trigger('windowResize', _element);
465							ignoreWindowResize--;
466						}
467					}
468				}, 200);
469			}else{
470				// calendar must have been initialized in a 0x0 iframe that has just been resized
471				lateRender();
472			}
473		}
474	}
475	
476	
477	
478	/* Event Fetching/Rendering
479	-----------------------------------------------------------------------------*/
480	// TODO: going forward, most of this stuff should be directly handled by the view
481
482
483	function refetchEvents() { // can be called as an API method
484		clearEvents();
485		fetchAndRenderEvents();
486	}
487
488
489	function rerenderEvents(modifiedEventID) { // can be called as an API method
490		clearEvents();
491		renderEvents(modifiedEventID);
492	}
493
494
495	function renderEvents(modifiedEventID) { // TODO: remove modifiedEventID hack
496		if (elementVisible()) {
497			currentView.setEventData(events); // for View.js, TODO: unify with renderEvents
498			currentView.renderEvents(events, modifiedEventID); // actually render the DOM elements
499			currentView.trigger('eventAfterAllRender');
500		}
501	}
502
503
504	function clearEvents() {
505		currentView.triggerEventDestroy(); // trigger 'eventDestroy' for each event
506		currentView.clearEvents(); // actually remove the DOM elements
507		currentView.clearEventData(); // for View.js, TODO: unify with clearEvents
508	}
509	
510
511	function getAndRenderEvents() {
512		if (!options.lazyFetching || isFetchNeeded(currentView.visStart, currentView.visEnd)) {
513			fetchAndRenderEvents();
514		}
515		else {
516			renderEvents();
517		}
518	}
519
520
521	function fetchAndRenderEvents() {
522		fetchEvents(currentView.visStart, currentView.visEnd);
523			// ... will call reportEvents
524			// ... which will call renderEvents
525	}
526
527	
528	// called when event data arrives
529	function reportEvents(_events) {
530		events = _events;
531		renderEvents();
532	}
533
534
535	// called when a single event's data has been changed
536	function reportEventChange(eventID) {
537		rerenderEvents(eventID);
538	}
539
540
541
542	/* Header Updating
543	-----------------------------------------------------------------------------*/
544
545
546	function updateTitle() {
547		header.updateTitle(currentView.title);
548	}
549
550
551	function updateTodayButton() {
552		var today = new Date();
553		if (today >= currentView.start && today < currentView.end) {
554			header.activateButton('today');
555		}
556		else {
557			header.deactivateButton('today');
558		}
559	}
560	
561
562
563	/* Selection
564	-----------------------------------------------------------------------------*/
565	
566
567	function select(start, end, allDay) {
568		currentView.select(start, end, allDay===undefined ? true : allDay);
569	}
570	
571
572	function unselect() { // safe to be called before renderView
573		if (currentView) {
574			currentView.unselect();
575		}
576	}
577	
578	
579	
580	/* Date
581	-----------------------------------------------------------------------------*/
582	
583	
584	function prev() {
585		renderView(-1);
586	}
587	
588	
589	function next() {
590		renderView(1);
591	}
592	
593	
594	function prevYear() {
595		addYears(date, -1);
596		renderView();
597	}
598	
599	
600	function nextYear() {
601		addYears(date, 1);
602		renderView();
603	}
604	
605	
606	function today() {
607		date = new Date();
608		renderView();
609	}
610	
611	
612	function gotoDate(year, month, dateOfMonth) {
613		if (year instanceof Date) {
614			date = cloneDate(year); // provided 1 argument, a Date
615		}else{
616			setYMD(date, year, month, dateOfMonth);
617		}
618		renderView();
619	}
620	
621	
622	function incrementDate(years, months, days) {
623		if (years !== undefined) {
624			addYears(date, years);
625		}
626		if (months !== undefined) {
627			addMonths(date, months);
628		}
629		if (days !== undefined) {
630			addDays(date, days);
631		}
632		renderView();
633	}
634	
635	
636	function getDate() {
637		return cloneDate(date);
638	}
639
640
641
642	/* Height "Freezing"
643	-----------------------------------------------------------------------------*/
644
645
646	function freezeContentHeight() {
647		content.css({
648			width: '100%',
649			height: content.height(),
650			overflow: 'hidden'
651		});
652	}
653
654
655	function unfreezeContentHeight() {
656		content.css({
657			width: '',
658			height: '',
659			overflow: ''
660		});
661	}
662	
663	
664	
665	/* Misc
666	-----------------------------------------------------------------------------*/
667	
668	
669	function getView() {
670		return currentView;
671	}
672	
673	
674	function option(name, value) {
675		if (value === undefined) {
676			return options[name];
677		}
678		if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') {
679			options[name] = value;
680			updateSize();
681		}
682	}
683	
684	
685	function trigger(name, thisObj) {
686		if (options[name]) {
687			return options[name].apply(
688				thisObj || _element,
689				Array.prototype.slice.call(arguments, 2)
690			);
691		}
692	}
693	
694	
695	
696	/* External Dragging
697	------------------------------------------------------------------------*/
698	
699	if (options.droppable) {
700		$(document)
701			.bind('dragstart', function(ev, ui) {
702				var _e = ev.target;
703				var e = $(_e);
704				if (!e.parents('.fc').length) { // not already inside a calendar
705					var accept = options.dropAccept;
706					if ($.isFunction(accept) ? accept.call(_e, e) : e.is(accept)) {
707						_dragElement = _e;
708						currentView.dragStart(_dragElement, ev, ui);
709					}
710				}
711			})
712			.bind('dragstop', function(ev, ui) {
713				if (_dragElement) {
714					currentView.dragStop(_dragElement, ev, ui);
715					_dragElement = null;
716				}
717			});
718	}
719	
720
721}
722
723;;
724
725function Header(calendar, options) {
726	var t = this;
727	
728	
729	// exports
730	t.render = render;
731	t.destroy = destroy;
732	t.updateTitle = updateTitle;
733	t.activateButton = activateButton;
734	t.deactivateButton = deactivateButton;
735	t.disableButton = disableButton;
736	t.enableButton = enableButton;
737	
738	
739	// locals
740	var element = $([]);
741	var tm;
742	
743
744
745	function render() {
746		tm = options.theme ? 'ui' : 'fc';
747		var sections = options.header;
748		if (sections) {
749			element = $("<table class='fc-header' style='width:100%'/>")
750				.append(
751					$("<tr/>")
752						.append(renderSection('left'))
753						.append(renderSection('center'))
754						.append(renderSection('right'))
755				);
756			return element;
757		}
758	}
759	
760	
761	function destroy() {
762		element.remove();
763	}
764	
765	
766	function renderSection(position) {
767		var e = $("<td class='fc-header-" + position + "'/>");
768		var buttonStr = options.header[position];
769		if (buttonStr) {
770			$.each(buttonStr.split(' '), function(i) {
771				if (i > 0) {
772					e.append("<span class='fc-header-space'/>");
773				}
774				var prevButton;
775				$.each(this.split(','), function(j, buttonName) {
776					if (buttonName == 'title') {
777						e.append("<span class='fc-header-title'><h2>&nbsp;</h2></span>");
778						if (prevButton) {
779							prevButton.addClass(tm + '-corner-right');
780						}
781						prevButton = null;
782					}else{
783						var buttonClick;
784						if (calendar[buttonName]) {
785							buttonClick = calendar[buttonName]; // calendar method
786						}
787						else if (fcViews[buttonName]) {
788							buttonClick = function() {
789								button.removeClass(tm + '-state-hover'); // forget why
790								calendar.changeView(buttonName);
791							};
792						}
793						if (buttonClick) {
794							var icon = options.theme ? smartProperty(options.buttonIcons, buttonName) : null; // why are we using smartProperty here?
795							var text = smartProperty(options.buttonText, buttonName); // why are we using smartProperty here?
796							var button = $(
797								"<span class='fc-button fc-button-" + buttonName + " " + tm + "-state-default'>" +
798									(icon ?
799										"<span class='fc-icon-wrap'>" +
800											"<span class='ui-icon ui-icon-" + icon + "'/>" +
801										"</span>" :
802										text
803										) +
804								"</span>"
805								)
806								.click(function() {
807									if (!button.hasClass(tm + '-state-disabled')) {
808										buttonClick();
809									}
810								})
811								.mousedown(function() {
812									button
813										.not('.' + tm + '-state-active')
814										.not('.' + tm + '-state-disabled')
815										.addClass(tm + '-state-down');
816								})
817								.mouseup(function() {
818									button.removeClass(tm + '-state-down');
819								})
820								.hover(
821									function() {
822										button
823											.not('.' + tm + '-state-active')
824											.not('.' + tm + '-state-disabled')
825											.addClass(tm + '-state-hover');
826									},
827									function() {
828										button
829											.removeClass(tm + '-state-hover')
830											.removeClass(tm + '-state-down');
831									}
832								)
833								.appendTo(e);
834							disableTextSelection(button);
835							if (!prevButton) {
836								button.addClass(tm + '-corner-left');
837							}
838							prevButton = button;
839						}
840					}
841				});
842				if (prevButton) {
843					prevButton.addClass(tm + '-corner-right');
844				}
845			});
846		}
847		return e;
848	}
849	
850	
851	function updateTitle(html) {
852		element.find('h2')
853			.html(html);
854	}
855	
856	
857	function activateButton(buttonName) {
858		element.find('span.fc-button-' + buttonName)
859			.addClass(tm + '-state-active');
860	}
861	
862	
863	function deactivateButton(buttonName) {
864		element.find('span.fc-button-' + buttonName)
865			.removeClass(tm + '-state-active');
866	}
867	
868	
869	function disableButton(buttonName) {
870		element.find('span.fc-button-' + buttonName)
871			.addClass(tm + '-state-disabled');
872	}
873	
874	
875	function enableButton(buttonName) {
876		element.find('span.fc-button-' + buttonName)
877			.removeClass(tm + '-state-disabled');
878	}
879
880
881}
882
883;;
884
885fc.sourceNormalizers = [];
886fc.sourceFetchers = [];
887
888var ajaxDefaults = {
889	dataType: 'json',
890	cache: false
891};
892
893var eventGUID = 1;
894
895
896function EventManager(options, _sources) {
897	var t = this;
898	
899	
900	// exports
901	t.isFetchNeeded = isFetchNeeded;
902	t.fetchEvents = fetchEvents;
903	t.addEventSource = addEventSource;
904	t.removeEventSource = removeEventSource;
905	t.updateEvent = updateEvent;
906	t.renderEvent = renderEvent;
907	t.removeEvents = removeEvents;
908	t.clientEvents = clientEvents;
909	t.normalizeEvent = normalizeEvent;
910	
911	
912	// imports
913	var trigger = t.trigger;
914	var getView = t.getView;
915	var reportEvents = t.reportEvents;
916	
917	
918	// locals
919	var stickySource = { events: [] };
920	var sources = [ stickySource ];
921	var rangeStart, rangeEnd;
922	var currentFetchID = 0;
923	var pendingSourceCnt = 0;
924	var loadingLevel = 0;
925	var cache = [];
926	
927	
928	for (var i=0; i<_sources.length; i++) {
929		_addEventSource(_sources[i]);
930	}
931	
932	
933	
934	/* Fetching
935	-----------------------------------------------------------------------------*/
936	
937	
938	function isFetchNeeded(start, end) {
939		return !rangeStart || start < rangeStart || end > rangeEnd;
940	}
941	
942	
943	function fetchEvents(start, end) {
944		rangeStart = start;
945		rangeEnd = end;
946		cache = [];
947		var fetchID = ++currentFetchID;
948		var len = sources.length;
949		pendingSourceCnt = len;
950		for (var i=0; i<len; i++) {
951			fetchEventSource(sources[i], fetchID);
952		}
953	}
954	
955	
956	function fetchEventSource(source, fetchID) {
957		_fetchEventSource(source, function(events) {
958			if (fetchID == currentFetchID) {
959				if (events) {
960
961					if (options.eventDataTransform) {
962						events = $.map(events, options.eventDataTransform);
963					}
964					if (source.eventDataTransform) {
965						events = $.map(events, source.eventDataTransform);
966					}
967					// TODO: this technique is not ideal for static array event sources.
968					//  For arrays, we'll want to process all events right in the beginning, then never again.
969				
970					for (var i=0; i<events.length; i++) {
971						events[i].source = source;
972						normalizeEvent(events[i]);
973					}
974					cache = cache.concat(events);
975				}
976				pendingSourceCnt--;
977				if (!pendingSourceCnt) {
978					reportEvents(cache);
979				}
980			}
981		});
982	}
983	
984	
985	function _fetchEventSource(source, callback) {
986		var i;
987		var fetchers = fc.sourceFetchers;
988		var res;
989		for (i=0; i<fetchers.length; i++) {
990			res = fetchers[i](source, rangeStart, rangeEnd, callback);
991			if (res === true) {
992				// the fetcher is in charge. made its own async request
993				return;
994			}
995			else if (typeof res == 'object') {
996				// the fetcher returned a new source. process it
997				_fetchEventSource(res, callback);
998				return;
999			}
1000		}
1001		var events = source.events;
1002		if (events) {
1003			if ($.isFunction(events)) {
1004				pushLoading();
1005				events(cloneDate(rangeStart), cloneDate(rangeEnd), function(events) {
1006					callback(events);
1007					popLoading();
1008				});
1009			}
1010			else if ($.isArray(events)) {
1011				callback(events);
1012			}
1013			else {
1014				callback();
1015			}
1016		}else{
1017			var url = source.url;
1018			if (url) {
1019				var success = source.success;
1020				var error = source.error;
1021				var complete = source.complete;
1022
1023				// retrieve any outbound GET/POST $.ajax data from the options
1024				var customData;
1025				if ($.isFunction(source.data)) {
1026					// supplied as a function that returns a key/value object
1027					customData = source.data();
1028				}
1029				else {
1030					// supplied as a straight key/value object
1031					customData = source.data;
1032				}
1033
1034				// use a copy of the custom data so we can modify the parameters
1035				// and not affect the passed-in object.
1036				var data = $.extend({}, customData || {});
1037
1038				var startParam = firstDefined(source.startParam, options.startParam);
1039				var endParam = firstDefined(source.endParam, options.endParam);
1040				if (startParam) {
1041					data[startParam] = Math.round(+rangeStart / 1000);
1042				}
1043				if (endParam) {
1044					data[endParam] = Math.round(+rangeEnd / 1000);
1045				}
1046
1047				pushLoading();
1048				$.ajax($.extend({}, ajaxDefaults, source, {
1049					data: data,
1050					success: function(events) {
1051						events = events || [];
1052						var res = applyAll(success, this, arguments);
1053						if ($.isArray(res)) {
1054							events = res;
1055						}
1056						callback(events);
1057					},
1058					error: function() {
1059						applyAll(error, this, arguments);
1060						callback();
1061					},
1062					complete: function() {
1063						applyAll(complete, this, arguments);
1064						popLoading();
1065					}
1066				}));
1067			}else{
1068				callback();
1069			}
1070		}
1071	}
1072	
1073	
1074	
1075	/* Sources
1076	-----------------------------------------------------------------------------*/
1077	
1078
1079	function addEventSource(source) {
1080		source = _addEventSource(source);
1081		if (source) {
1082			pendingSourceCnt++;
1083			fetchEventSource(source, currentFetchID); // will eventually call reportEvents
1084		}
1085	}
1086	
1087	
1088	function _addEventSource(source) {
1089		if ($.isFunction(source) || $.isArray(source)) {
1090			source = { events: source };
1091		}
1092		else if (typeof source == 'string') {
1093			source = { url: source };
1094		}
1095		if (typeof source == 'object') {
1096			normalizeSource(source);
1097			sources.push(source);
1098			return source;
1099		}
1100	}
1101	
1102
1103	function removeEventSource(source) {
1104		sources = $.grep(sources, function(src) {
1105			return !isSourcesEqual(src, source);
1106		});
1107		// remove all client events from that source
1108		cache = $.grep(cache, function(e) {
1109			return !isSourcesEqual(e.source, source);
1110		});
1111		reportEvents(cache);
1112	}
1113	
1114	
1115	
1116	/* Manipulation
1117	-----------------------------------------------------------------------------*/
1118	
1119	
1120	function updateEvent(event) { // update an existing event
1121		var i, len = cache.length, e,
1122			defaultEventEnd = getView().defaultEventEnd, // getView???
1123			startDelta = event.start - event._start,
1124			endDelta = event.end ?
1125				(event.end - (event._end || defaultEventEnd(event))) // event._end would be null if event.end
1126				: 0;                                                      // was null and event was just resized
1127		for (i=0; i<len; i++) {
1128			e = cache[i];
1129			if (e._id == event._id && e != event) {
1130				e.start = new Date(+e.start + startDelta);
1131				if (event.end) {
1132					if (e.end) {
1133						e.end = new Date(+e.end + endDelta);
1134					}else{
1135						e.end = new Date(+defaultEventEnd(e) + endDelta);
1136					}
1137				}else{
1138					e.end = null;
1139				}
1140				e.title = event.title;
1141				e.url = event.url;
1142				e.allDay = event.allDay;
1143				e.className = event.className;
1144				e.editable = event.editable;
1145				e.color = event.color;
1146				e.backgroundColor = event.backgroundColor;
1147				e.borderColor = event.borderColor;
1148				e.textColor = event.textColor;
1149				normalizeEvent(e);
1150			}
1151		}
1152		normalizeEvent(event);
1153		reportEvents(cache);
1154	}
1155	
1156	
1157	function renderEvent(event, stick) {
1158		normalizeEvent(event);
1159		if (!event.source) {
1160			if (stick) {
1161				stickySource.events.push(event);
1162				event.source = stickySource;
1163			}
1164			cache.push(event);
1165		}
1166		reportEvents(cache);
1167	}
1168	
1169	
1170	function removeEvents(filter) {
1171		if (!filter) { // remove all
1172			cache = [];
1173			// clear all array sources
1174			for (var i=0; i<sources.length; i++) {
1175				if ($.isArray(sources[i].events)) {
1176					sources[i].events = [];
1177				}
1178			}
1179		}else{
1180			if (!$.isFunction(filter)) { // an event ID
1181				var id = filter + '';
1182				filter = function(e) {
1183					return e._id == id;
1184				};
1185			}
1186			cache = $.grep(cache, filter, true);
1187			// remove events from array sources
1188			for (var i=0; i<sources.length; i++) {
1189				if ($.isArray(sources[i].events)) {
1190					sources[i].events = $.grep(sources[i].events, filter, true);
1191				}
1192			}
1193		}
1194		reportEvents(cache);
1195	}
1196	
1197	
1198	function clientEvents(filter) {
1199		if ($.isFunction(filter)) {
1200			return $.grep(cache, filter);
1201		}
1202		else if (filter) { // an event ID
1203			filter += '';
1204			return $.grep(cache, function(e) {
1205				return e._id == filter;
1206			});
1207		}
1208		return cache; // else, return all
1209	}
1210	
1211	
1212	
1213	/* Loading State
1214	-----------------------------------------------------------------------------*/
1215	
1216	
1217	function pushLoading() {
1218		if (!loadingLevel++) {
1219			trigger('loading', null, true, getView());
1220		}
1221	}
1222	
1223	
1224	function popLoading() {
1225		if (!--loadingLevel) {
1226			trigger('loading', null, false, getView());
1227		}
1228	}
1229	
1230	
1231	
1232	/* Event Normalization
1233	-----------------------------------------------------------------------------*/
1234	
1235	
1236	function normalizeEvent(event) {
1237		var source = event.source || {};
1238		var ignoreTimezone = firstDefined(source.ignoreTimezone, options.ignoreTimezone);
1239		event._id = event._id || (event.id === undefined ? '_fc' + eventGUID++ : event.id + '');
1240		if (event.date) {
1241			if (!event.start) {
1242				event.start = event.date;
1243			}
1244			delete event.date;
1245		}
1246		event._start = cloneDate(event.start = parseDate(event.start, ignoreTimezone));
1247		event.end = parseDate(event.end, ignoreTimezone);
1248		if (event.end && event.end <= event.start) {
1249			event.end = null;
1250		}
1251		event._end = event.end ? cloneDate(event.end) : null;
1252		if (event.allDay === undefined) {
1253			event.allDay = firstDefined(source.allDayDefault, options.allDayDefault);
1254		}
1255		if (event.className) {
1256			if (typeof event.className == 'string') {
1257				event.className = event.className.split(/\s+/);
1258			}
1259		}else{
1260			event.className = [];
1261		}
1262		// TODO: if there is no start date, return false to indicate an invalid event
1263	}
1264	
1265	
1266	
1267	/* Utils
1268	------------------------------------------------------------------------------*/
1269	
1270	
1271	function normalizeSource(source) {
1272		if (source.className) {
1273			// TODO: repeat code, same code for event classNames
1274			if (typeof source.className == 'string') {
1275				source.className = source.className.split(/\s+/);
1276			}
1277		}else{
1278			source.className = [];
1279		}
1280		var normalizers = fc.sourceNormalizers;
1281		for (var i=0; i<normalizers.length; i++) {
1282			normalizers[i](source);
1283		}
1284	}
1285	
1286	
1287	function isSourcesEqual(source1, source2) {
1288		return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2);
1289	}
1290	
1291	
1292	function getSourcePrimitive(source) {
1293		return ((typeof source == 'object') ? (source.events || source.url) : '') || source;
1294	}
1295
1296
1297}
1298
1299;;
1300
1301
1302fc.addDays = addDays;
1303fc.cloneDate = cloneDate;
1304fc.parseDate = parseDate;
1305fc.parseISO8601 = parseISO8601;
1306fc.parseTime = parseTime;
1307fc.formatDate = formatDate;
1308fc.formatDates = formatDates;
1309
1310
1311
1312/* Date Math
1313-----------------------------------------------------------------------------*/
1314
1315var dayIDs = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'],
1316	DAY_MS = 86400000,
1317	HOUR_MS = 3600000,
1318	MINUTE_MS = 60000;
1319	
1320
1321function addYears(d, n, keepTime) {
1322	d.setFullYear(d.getFullYear() + n);
1323	if (!keepTime) {
1324		clearTime(d);
1325	}
1326	return d;
1327}
1328
1329
1330function addMonths(d, n, keepTime) { // prevents day overflow/underflow
1331	if (+d) { // prevent infinite looping on invalid dates
1332		var m = d.getMonth() + n,
1333			check = cloneDate(d);
1334		check.setDate(1);
1335		check.setMonth(m);
1336		d.setMonth(m);
1337		if (!keepTime) {
1338			clearTime(d);
1339		}
1340		while (d.getMonth() != check.getMonth()) {
1341			d.setDate(d.getDate() + (d < check ? 1 : -1));
1342		}
1343	}
1344	return d;
1345}
1346
1347
1348function addDays(d, n, keepTime) { // deals with daylight savings
1349	if (+d) {
1350		var dd = d.getDate() + n,
1351			check = cloneDate(d);
1352		check.setHours(9); // set to middle of day
1353		check.setDate(dd);
1354		d.setDate(dd);
1355		if (!keepTime) {
1356			clearTime(d);
1357		}
1358		fixDate(d, check);
1359	}
1360	return d;
1361}
1362
1363
1364function fixDate(d, check) { // force d to be on check's YMD, for daylight savings purposes
1365	if (+d) { // prevent infinite looping on invalid dates
1366		while (d.getDate() != check.getDate()) {
1367			d.setTime(+d + (d < check ? 1 : -1) * HOUR_MS);
1368		}
1369	}
1370}
1371
1372
1373function addMinutes(d, n) {
1374	d.setMinutes(d.getMinutes() + n);
1375	return d;
1376}
1377
1378
1379function clearTime(d) {
1380	d.setHours(0);
1381	d.setMinutes(0);
1382	d.setSeconds(0); 
1383	d.setMilliseconds(0);
1384	return d;
1385}
1386
1387
1388function cloneDate(d, dontKeepTime) {
1389	if (dontKeepTime) {
1390		return clearTime(new Date(+d));
1391	}
1392	return new Date(+d);
1393}
1394
1395
1396function zeroDate() { // returns a Date with time 00:00:00 and dateOfMonth=1
1397	var i=0, d;
1398	do {
1399		d = new Date(1970, i++, 1);
1400	} while (d.getHours()); // != 0
1401	return d;
1402}
1403
1404
1405function dayDiff(d1, d2) { // d1 - d2
1406	return Math.round((cloneDate(d1, true) - cloneDate(d2, true)) / DAY_MS);
1407}
1408
1409
1410function setYMD(date, y, m, d) {
1411	if (y !== undefined && y != date.getFullYear()) {
1412		date.setDate(1);
1413		date.setMonth(0);
1414		date.setFullYear(y);
1415	}
1416	if (m !== undefined && m != date.getMonth()) {
1417		date.setDate(1);
1418		date.setMonth(m);
1419	}
1420	if (d !== undefined) {
1421		date.setDate(d);
1422	}
1423}
1424
1425
1426
1427/* Date Parsing
1428-----------------------------------------------------------------------------*/
1429
1430
1431function parseDate(s, ignoreTimezone) { // ignoreTimezone defaults to true
1432	if (typeof s == 'object') { // already a Date object
1433		return s;
1434	}
1435	if (typeof s == 'number') { // a UNIX timestamp
1436		return new Date(s * 1000);
1437	}
1438	if (typeof s == 'string') {
1439		if (s.match(/^\d+(\.\d+)?$/)) { // a UNIX timestamp
1440			return new Date(parseFloat(s) * 1000);
1441		}
1442		if (ignoreTimezone === undefined) {
1443			ignoreTimezone = true;
1444		}
1445		return parseISO8601(s, ignoreTimezone) || (s ? new Date(s) : null);
1446	}
1447	// TODO: never return invalid dates (like from new Date(<string>)), return null instead
1448	return null;
1449}
1450
1451
1452function parseISO8601(s, ignoreTimezone) { // ignoreTimezone defaults to false
1453	// derived from http://delete.me.uk/2005/03/iso8601.html
1454	// TODO: for a know glitch/feature, read tests/issue_206_parseDate_dst.html
1455	var m = s.match(/^([0-9]{4})(-([0-9]{2})(-([0-9]{2})([T ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2})(:?([0-9]{2}))?))?)?)?)?$/);
1456	if (!m) {
1457		return null;
1458	}
1459	var date = new Date(m[1], 0, 1);
1460	if (ignoreTimezone || !m[13]) {
1461		var check = new Date(m[1], 0, 1, 9, 0);
1462		if (m[3]) {
1463			date.setMonth(m[3] - 1);
1464			check.setMonth(m[3] - 1);
1465		}
1466		if (m[5]) {
1467			date.setDate(m[5]);
1468			check.setDate(m[5]);
1469		}
1470		fixDate(date, check);
1471		if (m[7]) {
1472			date.setHours(m[7]);
1473		}
1474		if (m[8]) {
1475			date.setMinutes(m[8]);
1476		}
1477		if (m[10]) {
1478			date.setSeconds(m[10]);
1479		}
1480		if (m[12]) {
1481			date.setMilliseconds(Number("0." + m[12]) * 1000);
1482		}
1483		fixDate(date, check);
1484	}else{
1485		date.setUTCFullYear(
1486			m[1],
1487			m[3] ? m[3] - 1 : 0,
1488			m[5] || 1
1489		);
1490		date.setUTCHours(
1491			m[7] || 0,
1492			m[8] || 0,
1493			m[10] || 0,
1494			m[12] ? Number("0." + m[12]) * 1000 : 0
1495		);
1496		if (m[14]) {
1497			var offset = Number(m[16]) * 60 + (m[18] ? Number(m[18]) : 0);
1498			offset *= m[15] == '-' ? 1 : -1;
1499			date = new Date(+date + (offset * 60 * 1000));
1500		}
1501	}
1502	return date;
1503}
1504
1505
1506function parseTime(s) { // returns minutes since start of day
1507	if (typeof s == 'number') { // an hour
1508		return s * 60;
1509	}
1510	if (typeof s == 'object') { // a Date object
1511		return s.getHours() * 60 + s.getMinutes();
1512	}
1513	var m = s.match(/(\d+)(?::(\d+))?\s*(\w+)?/);
1514	if (m) {
1515		var h = parseInt(m[1], 10);
1516		if (m[3]) {
1517			h %= 12;
1518			if (m[3].toLowerCase().charAt(0) == 'p') {
1519				h += 12;
1520			}
1521		}
1522		return h * 60 + (m[2] ? parseInt(m[2], 10) : 0);
1523	}
1524}
1525
1526
1527
1528/* Date Formatting
1529-----------------------------------------------------------------------------*/
1530// TODO: use same function formatDate(date, [date2], format, [options])
1531
1532
1533function formatDate(date, format, options) {
1534	return formatDates(date, null, format, options);
1535}
1536
1537
1538function formatDates(date1, date2, format, options) {
1539	options = options || defaults;
1540	var date = date1,
1541		otherDate = date2,
1542		i, len = format.length, c,
1543		i2, formatter,
1544		res = '';
1545	for (i=0; i<len; i++) {
1546		c = format.charAt(i);
1547		if (c == "'") {
1548			for (i2=i+1; i2<len; i2++) {
1549				if (format.charAt(i2) == "'") {
1550					if (date) {
1551						if (i2 == i+1) {
1552							res += "'";
1553						}else{
1554							res += format.substring(i+1, i2);
1555						}
1556						i = i2;
1557					}
1558					break;
1559				}
1560			}
1561		}
1562		else if (c == '(') {
1563			for (i2=i+1; i2<len; i2++) {
1564				if (format.charAt(i2) == ')') {
1565					var subres = formatDate(date, format.substring(i+1, i2), options);
1566					if (parseInt(subres.replace(/\D/, ''), 10)) {
1567						res += subres;
1568					}
1569					i = i2;
1570					break;
1571				}
1572			}
1573		}
1574		else if (c == '[') {
1575			for (i2=i+1; i2<len; i2++) {
1576				if (format.charAt(i2) == ']') {
1577					var subformat = format.substring(i+1, i2);
1578					var subres = formatDate(date, subformat, options);
1579					if (subres != formatDate(otherDate, subformat, options)) {
1580						res += subres;
1581					}
1582					i = i2;
1583					break;
1584				}
1585			}
1586		}
1587		else if (c == '{') {
1588			date = date2;
1589			otherDate = date1;
1590		}
1591		else if (c == '}') {
1592			date = date1;
1593			otherDate = date2;
1594		}
1595		else {
1596			for (i2=len; i2>i; i2--) {
1597				if (formatter = dateFormatters[format.substring(i, i2)]) {
1598					if (date) {
1599						res += formatter(date, options);
1600					}
1601					i = i2 - 1;
1602					break;
1603				}
1604			}
1605			if (i2 == i) {
1606				if (date) {
1607					res += c;
1608				}
1609			}
1610		}
1611	}
1612	return res;
1613};
1614
1615
1616var dateFormatters = {
1617	s	: function(d)	{ return d.getSeconds() },
1618	ss	: function(d)	{ return zeroPad(d.getSeconds()) },
1619	m	: function(d)	{ return d.getMinutes() },
1620	mm	: function(d)	{ return zeroPad(d.getMinutes()) },
1621	h	: function(d)	{ return d.getHours() % 12 || 12 },
1622	hh	: function(d)	{ return zeroPad(d.getHours() % 12 || 12) },
1623	H	: function(d)	{ return d.getHours() },
1624	HH	: function(d)	{ return zeroPad(d.getHours()) },
1625	d	: function(d)	{ return d.getDate() },
1626	dd	: function(d)	{ return zeroPad(d.getDate()) },
1627	ddd	: function(d,o)	{ return o.dayNamesShort[d.getDay()] },
1628	dddd: function(d,o)	{ return o.dayNames[d.getDay()] },
1629	M	: function(d)	{ return d.getMonth() + 1 },
1630	MM	: function(d)	{ return zeroPad(d.getMonth() + 1) },
1631	MMM	: function(d,o)	{ return o.monthNamesShort[d.getMonth()] },
1632	MMMM: function(d,o)	{ return o.monthNames[d.getMonth()] },
1633	yy	: function(d)	{ return (d.getFullYear()+'').substring(2) },
1634	yyyy: function(d)	{ return d.getFullYear() },
1635	t	: function(d)	{ return d.getHours() < 12 ? 'a' : 'p' },
1636	tt	: function(d)	{ return d.getHours() < 12 ? 'am' : 'pm' },
1637	T	: function(d)	{ return d.getHours() < 12 ? 'A' : 'P' },
1638	TT	: function(d)	{ return d.getHours() < 12 ? 'AM' : 'PM' },
1639	u	: function(d)	{ return formatDate(d, "yyyy-MM-dd'T'HH:mm:ss'Z'") },
1640	S	: function(d)	{
1641		var date = d.getDate();
1642		if (date > 10 && date < 20) {
1643			return 'th';
1644		}
1645		return ['st', 'nd', 'rd'][date%10-1] || 'th';
1646	},
1647	w   : function(d, o) { // local
1648		return o.weekNumberCalculation(d);
1649	},
1650	W   : function(d) { // ISO
1651		return iso8601Week(d);
1652	}
1653};
1654fc.dateFormatters = dateFormatters;
1655
1656
1657/* thanks jQuery UI (https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.datepicker.js)
1658 * 
1659 * Set as calculateWeek to determine the week of the year based on the ISO 8601 definition.
1660 * `date` - the date to get the week for
1661 * `number` - the number of the week within the year that contains this date
1662 */
1663function iso8601Week(date) {
1664	var time;
1665	var checkDate = new Date(date.getTime());
1666
1667	// Find Thursday of this week starting on Monday
1668	checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7));
1669
1670	time = checkDate.getTime();
1671	checkDate.setMonth(0); // Compare with Jan 1
1672	checkDate.setDate(1);
1673	return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
1674}
1675
1676
1677;;
1678
1679fc.applyAll = applyAll;
1680
1681
1682/* Event Date Math
1683-----------------------------------------------------------------------------*/
1684
1685
1686function exclEndDay(event) {
1687	if (event.end) {
1688		return _exclEndDay(event.end, event.allDay);
1689	}else{
1690		return addDays(cloneDate(event.start), 1);
1691	}
1692}
1693
1694
1695function _exclEndDay(end, allDay) {
1696	end = cloneDate(end);
1697	return allDay || end.getHours() || end.getMinutes() ? addDays(end, 1) : clearTime(end);
1698	// why don't we check for seconds/ms too?
1699}
1700
1701
1702
1703/* Event Element Binding
1704-----------------------------------------------------------------------------*/
1705
1706
1707function lazySegBind(container, segs, bindHandlers) {
1708	container.unbind('mouseover').mouseover(function(ev) {
1709		var parent=ev.target, e,
1710			i, seg;
1711		while (parent != this) {
1712			e = parent;
1713			parent = parent.parentNode;
1714		}
1715		if ((i = e._fci) !== undefined) {
1716			e._fci = undefined;
1717			seg = segs[i];
1718			bindHandlers(seg.event, seg.element, seg);
1719			$(ev.target).trigger(ev);
1720		}
1721		ev.stopPropagation();
1722	});
1723}
1724
1725
1726
1727/* Element Dimensions
1728-----------------------------------------------------------------------------*/
1729
1730
1731function setOuterWidth(element, width, includeMargins) {
1732	for (var i=0, e; i<element.length; i++) {
1733		e = $(element[i]);
1734		e.width(Math.max(0, width - hsides(e, includeMargins)));
1735	}
1736}
1737
1738
1739function setOuterHeight(element, height, includeMargins) {
1740	for (var i=0, e; i<element.length; i++) {
1741		e = $(element[i]);
1742		e.height(Math.max(0, height - vsides(e, includeMargins)));
1743	}
1744}
1745
1746
1747function hsides(element, includeMargins) {
1748	return hpadding(element) + hborders(element) + (includeMargins ? hmargins(element) : 0);
1749}
1750
1751
1752function hpadding(element) {
1753	return (parseFloat($.css(element[0], 'paddingLeft', true)) || 0) +
1754	       (parseFloat($.css(element[0], 'paddingRight', true)) || 0);
1755}
1756
1757
1758function hmargins(element) {
1759	return (parseFloat($.css(element[0], 'marginLeft', true)) || 0) +
1760	       (parseFloat($.css(element[0], 'marginRight', true)) || 0);
1761}
1762
1763
1764function hborders(element) {
1765	return (parseFloat($.css(element[0], 'borderLeftWidth', true)) || 0) +
1766	       (parseFloat($.css(element[0], 'borderRightWidth', true)) || 0);
1767}
1768
1769
1770function vsides(element, includeMargins) {
1771	return vpadding(element) +  vborders(element) + (includeMargins ? vmargins(element) : 0);
1772}
1773
1774
1775function vpadding(element) {
1776	return (parseFloat($.css(element[0], 'paddingTop', true)) || 0) +
1777	       (parseFloat($.css(element[0], 'paddingBottom', true)) || 0);
1778}
1779
1780
1781function vmargins(element) {
1782	return (parseFloat($.css(element[0], 'marginTop', true)) || 0) +
1783	       (parseFloat($.css(element[0], 'marginBottom', true)) || 0);
1784}
1785
1786
1787function vborders(element) {
1788	return (parseFloat($.css(element[0], 'borderTopWidth', true)) || 0) +
1789	       (parseFloat($.css(element[0], 'borderBottomWidth', true)) || 0);
1790}
1791
1792
1793
1794/* Misc Utils
1795-----------------------------------------------------------------------------*/
1796
1797
1798//TODO: arraySlice
1799//TODO: isFunction, grep ?
1800
1801
1802function noop() { }
1803
1804
1805function dateCompare(a, b) {
1806	return a - b;
1807}
1808
1809
1810function arrayMax(a) {
1811	return Math.max.apply(Math, a);
1812}
1813
1814
1815function zeroPad(n) {
1816	return (n < 10 ? '0' : '') + n;
1817}
1818
1819
1820function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object
1821	if (obj[name] !== undefined) {
1822		return obj[name];
1823	}
1824	var parts = name.split(/(?=[A-Z])/),
1825		i=parts.length-1, res;
1826	for (; i>=0; i--) {
1827		res = obj[parts[i].toLowerCase()];
1828		if (res !== undefined) {
1829			return res;
1830		}
1831	}
1832	return obj[''];
1833}
1834
1835
1836function htmlEscape(s) {
1837	return s.replace(/&/g, '&amp;')
1838		.replace(/</g, '&lt;')
1839		.replace(/>/g, '&gt;')
1840		.replace(/'/g, '&#039;')
1841		.replace(/"/g, '&quot;')
1842		.replace(/\n/g, '<br />');
1843}
1844
1845
1846function disableTextSelection(element) {
1847	element
1848		.attr('unselectable', 'on')
1849		.css('MozUserSelect', 'none')
1850		.bind('selectstart.ui', function() { return false; });
1851}
1852
1853
1854/*
1855function enableTextSelection(element) {
1856	element
1857		.attr('unselectable', 'off')
1858		.css('MozUserSelect', '')
1859		.unbind('selectstart.ui');
1860}
1861*/
1862
1863
1864function markFirstLast(e) {
1865	e.children()
1866		.removeClass('fc-first fc-last')
1867		.filter(':first-child')
1868			.addClass('fc-first')
1869		.end()
1870		.filter(':last-child')
1871			.addClass('fc-last');
1872}
1873
1874
1875function setDayID(cell, date) {
1876	cell.each(function(i, _cell) {
1877		_cell.className = _cell.className.replace(/^fc-\w*/, 'fc-' + dayIDs[date.getDay()]);
1878		// TODO: make a way that doesn't rely on order of classes
1879	});
1880}
1881
1882
1883function getSkinCss(event, opt) {
1884	var source = event.source || {};
1885	var eventColor = event.color;
1886	var sourceColor = source.color;
1887	var optionColor = opt('eventColor');
1888	var backgroundColor =
1889		event.backgroundColor ||
1890		eventColor ||
1891		source.backgroundColor ||
1892		sourceColor ||
1893		opt('eventBackgroundColor') ||
1894		optionColor;
1895	var borderColor =
1896		event.borderColor ||
1897		eventColor ||
1898		source.borderColor ||
1899		sourceColor ||
1900		opt('eventBorderColor') ||
1901		optionColor;
1902	var textColor =
1903		event.textColor ||
1904		source.textColor ||
1905		opt('eventTextColor');
1906	var statements = [];
1907	if (backgroundColor) {
1908		statements.push('background-color:' + backgroundColor);
1909	}
1910	if (borderColor) {
1911		statements.push('border-color:' + borderColor);
1912	}
1913	if (textColor) {
1914		statements.push('color:' + textColor);
1915	}
1916	return statements.join(';');
1917}
1918
1919
1920function applyAll(functions, thisObj, args) {
1921	if ($.isFunction(functions)) {
1922		functions = [ functions ];
1923	}
1924	if (functions) {
1925		var i;
1926		var ret;
1927		for (i=0; i<functions.length; i++) {
1928			ret = functions[i].apply(thisObj, args) || ret;
1929		}
1930		return ret;
1931	}
1932}
1933
1934
1935function firstDefined() {
1936	for (var i=0; i<arguments.length; i++) {
1937		if (arguments[i] !== undefined) {
1938			return arguments[i];
1939		}
1940	}
1941}
1942
1943
1944;;
1945
1946fcViews.month = MonthView;
1947
1948function MonthView(element, calendar) {
1949	var t = this;
1950	
1951	
1952	// exports
1953	t.render = render;
1954	
1955	
1956	// imports
1957	BasicView.call(t, element, calendar, 'month');
1958	var opt = t.opt;
1959	var renderBasic = t.renderBasic;
1960	var skipHiddenDays = t.skipHiddenDays;
1961	var getCellsPerWeek = t.getCellsPerWeek;
1962	var formatDate = calendar.formatDate;
1963	
1964	
1965	function render(date, delta) {
1966
1967		if (delta) {
1968			addMonths(date, delta);
1969			date.setDate(1);
1970		}
1971
1972		var firstDay = opt('firstDay');
1973
1974		var start = cloneDate(date, true);
1975		start.setDate(1);
1976
1977		var end = addMonths(cloneDate(start), 1);
1978
1979		var visStart = cloneDate(start);
1980		addDays(visStart, -((visStart.getDay() - firstDay + 7) % 7));
1981		skipHiddenDays(visStart);
1982
1983		var visEnd = cloneDate(end);
1984		addDays(visEnd, (7 - visEnd.getDay() + firstDay) % 7);
1985		skipHiddenDays(visEnd, -1, true);
1986
1987		var colCnt = getCellsPerWeek();
1988		var rowCnt = Math.round(dayDiff(visEnd, visStart) / 7); // should be no need for Math.round
1989
1990		if (opt('weekMode') == 'fixed') {
1991			addDays(visEnd, (6 - rowCnt) * 7); // add weeks to make up for it
1992			rowCnt = 6;
1993		}
1994
1995		t.title = formatDate(start, opt('titleFormat'));
1996
1997		t.start = start;
1998		t.end = end;
1999		t.visStart = visStart;
2000		t.visEnd = visEnd;
2001
2002		renderBasic(rowCnt, colCnt, true);
2003	}
2004	
2005	
2006}
2007
2008;;
2009
2010fcViews.basicWeek = BasicWeekView;
2011
2012function BasicWeekView(element, calendar) {
2013	var t = this;
2014	
2015	
2016	// exports
2017	t.render = render;
2018	
2019	
2020	// imports
2021	BasicView.call(t, element, calendar, 'basicWeek');
2022	var opt = t.opt;
2023	var renderBasic = t.renderBasic;
2024	var skipHiddenDays = t.skipHiddenDays;
2025	var getCellsPerWeek = t.getCellsPerWeek;
2026	var formatDates = calendar.formatDates;
2027	
2028	
2029	function render(date, delta) {
2030
2031		if (delta) {
2032			addDays(date, delta * 7);
2033		}
2034
2035		var start = addDays(cloneDate(date), -((date.getDay() - opt('firstDay') + 7) % 7));
2036		var end = addDays(cloneDate(start), 7);
2037
2038		var visStart = cloneDate(start);
2039		skipHiddenDays(visStart);
2040
2041		var visEnd = cloneDate(end);
2042		skipHiddenDays(visEnd, -1, true);
2043
2044		var colCnt = getCellsPerWeek();
2045
2046		t.start = start;
2047		t.end = end;
2048		t.visStart = visStart;
2049		t.visEnd = visEnd;
2050
2051		t.title = formatDates(
2052			visStart,
2053			addDays(cloneDate(visEnd), -1),
2054			opt('titleFormat')
2055		);
2056
2057		renderBasic(1, colCnt, false);
2058	}
2059	
2060	
2061}
2062
2063;;
2064
2065fcViews.basicDay = BasicDayView;
2066
2067
2068function BasicDayView(element, calendar) {
2069	var t = this;
2070	
2071	
2072	// exports
2073	t.render = render;
2074	
2075	
2076	// imports
2077	BasicView.call(t, element, calendar, 'basicDay');
2078	var opt = t.opt;
2079	var renderBasic = t.renderBasic;
2080	var skipHiddenDays = t.skipHiddenDays;
2081	var formatDate = calendar.formatDate;
2082	
2083	
2084	function render(date, delta) {
2085
2086		if (delta) {
2087			addDays(date, delta);
2088		}
2089		skipHiddenDays(date, delta < 0 ? -1 : 1);
2090
2091		var start = cloneDate(date, true);
2092		var end = addDays(cloneDate(start), 1);
2093
2094		t.title = formatDate(date, opt('titleFormat'));
2095
2096		t.start = t.visStart = start;
2097		t.end = t.visEnd = end;
2098
2099		renderBasic(1, 1, false);
2100	}
2101	
2102	
2103}
2104
2105;;
2106
2107setDefaults({
2108	weekMode: 'fixed'
2109});
2110
2111
2112function BasicView(element, calendar, viewName) {
2113	var t = this;
2114	
2115	
2116	// exports
2117	t.renderBasic = renderBasic;
2118	t.setHeight = setHeight;
2119	t.setWidth = setWidth;
2120	t.renderDayOverlay = renderDayOverlay;
2121	t.defaultSelectionEnd = defaultSelectionEnd;
2122	t.renderSelection = renderSelection;
2123	t.clearSelection = clearSelection;
2124	t.reportDayClick = reportDayClick; // for selection (kinda hacky)
2125	t.dragStart = dragStart;
2126	t.dragStop = dragStop;
2127	t.defaultEventEnd = defaultEventEnd;
2128	t.getHoverListener = function() { return hoverListener };
2129	t.colLeft = colLeft;
2130	t.colRight = colRight;
2131	t.colContentLeft = colContentLeft;
2132	t.colContentRight = colContentRight;
2133	t.getIsCellAllDay = function() { return true };
2134	t.allDayRow = allDayRow;
2135	t.getRowCnt = function() { return rowCnt };
2136	t.getColCnt = function() { return colCnt };
2137	t.getColWidth = function() { return colWidth };
2138	t.getDaySegmentContainer = function() { return daySegmentContainer };
2139	
2140	
2141	// imports
2142	View.call(t, element, calendar, viewName);
2143	OverlayManager.call(t);
2144	SelectionManager.call(t);
2145	BasicEventRenderer.call(t);
2146	var opt = t.opt;
2147	var trigger = t.trigger;
2148	var renderOverlay = t.renderOverlay;
2149	var clearOverlays = t.clearOverlays;
2150	var daySelectionMousedown = t.daySelectionMousedown;
2151	var cellToDate = t.cellToDate;
2152	var dateToCell = t.dateToCell;
2153	var rangeToSegments = t.rangeToSegments;
2154	var formatDate = calendar.formatDate;
2155	
2156	
2157	// locals
2158	
2159	var table;
2160	var head;
2161	var headCells;
2162	var body;
2163	var bodyRows;
2164	var bodyCells;
2165	var bodyFirstCells;
2166	var firstRowCellInners;
2167	var firstRowCellContentInners;
2168	var daySegmentContainer;
2169	
2170	var viewWidth;
2171	var viewHeight;
2172	var colWidth;
2173	var weekNumberWidth;
2174	
2175	var rowCnt, colCnt;
2176	var showNumbers;
2177	var coordinateGrid;
2178	var hoverListener;
2179	var colPositions;
2180	var colContentPositions;
2181	
2182	var tm;
2183	var colFormat;
2184	var showWeekNumbers;
2185	var weekNumberTitle;
2186	var weekNumberFormat;
2187	
2188	
2189	
2190	/* Rendering
2191	------------------------------------------------------------*/
2192	
2193	
2194	disableTextSelection(element.addClass('fc-grid'));
2195	
2196	
2197	function renderBasic(_rowCnt, _colCnt, _showNumbers) {
2198		rowCnt = _rowCnt;
2199		colCnt = _colCnt;
2200		showNumbers = _showNumbers;
2201		updateOptions();
2202
2203		if (!body) {
2204			buildEventContainer();
2205		}
2206
2207		buildTable();
2208	}
2209	
2210	
2211	function updateOptions() {
2212		tm = opt('theme') ? 'ui' : 'fc';
2213		colFormat = opt('columnFormat');
2214
2215		// week # options. (TODO: bad, logic also in other views)
2216		showWeekNumbers = opt('weekNumbers');
2217		weekNumberTitle = opt('weekNumberTitle');
2218		if (opt('weekNumberCalculation') != 'iso') {
2219			weekNumberFormat = "w";
2220		}
2221		else {
2222			weekNumberFormat = "W";
2223		}
2224	}
2225	
2226	
2227	function buildEventContainer() {
2228		daySegmentContainer =
2229			$("<div class='fc-event-container' style='position:absolute;z-index:8;top:0;left:0'/>")
2230				.appendTo(element);
2231	}
2232	
2233	
2234	function buildTable() {
2235		var html = buildTableHTML();
2236
2237		if (table) {
2238			table.remove();
2239		}
2240		table = $(html).appendTo(element);
2241
2242		head = table.find('thead');
2243		headCells = head.find('.fc-day-header');
2244		body = table.find('tbody');
2245		bodyRows = body.find('tr');
2246		bodyCells = body.find('.fc-day');
2247		bodyFirstCells = bodyRows.find('td:first-child');
2248
2249		firstRowCellInners = bodyRows.eq(0).find('.fc-day > div');
2250		firstRowCellContentInners = bodyRows.eq(0).find('.fc-day-content > div');
2251		
2252		markFirstLast(head.add(head.find('tr'))); // marks first+last tr/th's
2253		markFirstLast(bodyRows); // marks first+last td's
2254		bodyRows.eq(0).addClass('fc-first');
2255		bodyRows.filter(':last').addClass('fc-last');
2256
2257		bodyCells.each(function(i, _cell) {
2258			var date = cellToDate(
2259				Math.floor(i / colCnt),
2260				i % colCnt
2261			);
2262			trigger('dayRender', t, date, $(_cell));
2263		});
2264
2265		dayBind(bodyCells);
2266	}
2267
2268
2269
2270	/* HTML Building
2271	-----------------------------------------------------------*/
2272
2273
2274	function buildTableHTML() {
2275		var html =
2276			"<table class='fc-border-separate' style='width:100%' cellspacing='0'>" +
2277			buildHeadHTML() +
2278			buildBodyHTML() +
2279			"</table>";
2280
2281		return html;
2282	}
2283
2284
2285	function buildHeadHTML() {
2286		var headerClass = tm + "-widget-header";
2287		var html = '';
2288		var col;
2289		var date;
2290
2291		html += "<thead><tr>";
2292
2293		if (showWeekNumbers) {
2294			html +=
2295				"<th class='fc-week-number " + headerClass + "'>" +
2296				htmlEscape(weekNumberTitle) +
2297				"</th>";
2298		}
2299
2300		for (col=0; col<colCnt; col++) {
2301			date = cellToDate(0, col);
2302			html +=
2303				"<th class='fc-day-header fc-" + dayIDs[date.getDay()] + " " + headerClass + "'>" +
2304				htmlEscape(formatDate(date, colFormat)) +
2305				"</th>";
2306		}
2307
2308		html += "</tr></thead>";
2309
2310		return html;
2311	}
2312
2313
2314	function buildBodyHTML() {
2315		var contentClass = tm + "-widget-content";
2316		var html = '';
2317		var row;
2318		var col;
2319		var date;
2320
2321		html += "<tbody>";
2322
2323		for (row=0; row<rowCnt; row++) {
2324
2325			html += "<tr class='fc-week'>";
2326
2327			if (showWeekNumbers) {
2328				date = cellToDate(row, 0);
2329				html +=
2330					"<td class='fc-week-number " + contentClass + "'>" +
2331					"<div>" +
2332					htmlEscape(formatDate(date, weekNumberFormat)) +
2333					"</div>" +
2334					"</td>";
2335			}
2336
2337			for (col=0; col<colCnt; col++) {
2338				date = cellToDate(row, col);
2339				html += buildCellHTML(date);
2340			}
2341
2342			html += "</tr>";
2343		}
2344
2345		html += "</tbody>";
2346
2347		return html;
2348	}
2349
2350
2351	function buildCellHTML(date) {
2352		var contentClass = tm + "-widget-content";
2353		var month = t.start.getMonth();
2354		var today = clearTime(new Date());
2355		var html = '';
2356		var classNames = [
2357			'fc-day',
2358			'fc-' + dayIDs[date.getDay()],
2359			contentClass
2360		];
2361
2362		if (date.getMonth() != month) {
2363			classNames.push('fc-other-month');
2364		}
2365		if (+date == +today) {
2366			classNames.push(
2367				'fc-today',
2368				tm + '-state-highlight'
2369			);
2370		}
2371		else if (date < today) {
2372			classNames.push('fc-past');
2373		}
2374		else {
2375			classNames.push('fc-future');
2376		}
2377
2378		html +=
2379			"<td" +
2380			" class='" + classNames.join(' ') + "'" +
2381			" data-date='" + formatDate(date, 'yyyy-MM-dd') + "'" +
2382			">" +
2383			"<div>";
2384
2385		if (showNumbers) {
2386			html += "<div class='fc-day-number'>" + date.getDate() + "</div>";
2387		}
2388
2389		html +=
2390			"<div class='fc-day-content'>" +
2391			"<div style='position:relative'>&nbsp;</div>" +
2392			"</div>" +
2393			"</div>" +
2394			"</td>";
2395
2396		return html;
2397	}
2398
2399
2400
2401	/* Dimensions
2402	-----------------------------------------------------------*/
2403	
2404	
2405	function setHeight(height) {
2406		viewHeight = height;
2407		
2408		var bodyHeight = viewHeight - head.height();
2409		var rowHeight;
2410		var rowHeightLast;
2411		var cell;
2412			
2413		if (opt('weekMode') == 'variable') {
2414			rowHeight = rowHeightLast = Math.floor(bodyHeight / (rowCnt==1 ? 2 : 6));
2415		}else{
2416			rowHeight = Math.floor(bodyHeight / rowCnt);
2417			rowHeightLast = bodyHeight - rowHeight * (rowCnt-1);
2418		}
2419		
2420		bodyFirstCells.each(function(i, _cell) {
2421			if (i < rowCnt) {
2422				cell = $(_cell);
2423				cell.find('> div').css(
2424					'min-height',
2425					(i==rowCnt-1 ? rowHeightLast : rowHeight) - vsides(cell)
2426				);
2427			}
2428		});
2429		
2430	}
2431	
2432	
2433	function setWidth(width) {
2434		viewWidth = width;
2435		colPositions.clear();
2436		colContentPositions.clear();
2437
2438		weekNumberWidth = 0;
2439		if (showWeekNumbers) {
2440			weekNumberWidth = head.find('th.fc-week-number').outerWidth();
2441		}
2442
2443		colWidth = Math.floor((viewWidth - weekNumberWidth) / colCnt);
2444		setOuterWidth(headCells.slice(0, -1), colWidth);
2445	}
2446	
2447	
2448	
2449	/* Day clicking and binding
2450	-----------------------------------------------------------*/
2451	
2452	
2453	function dayBind(days) {
2454		days.click(dayClick)
2455			.mousedown(daySelectionMousedown);
2456	}
2457	
2458	
2459	function dayClick(ev) {
2460		if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick
2461			var date = parseISO8601($(this).data('date'));
2462			trigger('dayClick', this, date, true, ev);
2463		}
2464	}
2465	
2466	
2467	
2468	/* Semi-transparent Overlay Helpers
2469	------------------------------------------------------*/
2470	// TODO: should be consolidated with AgendaView's methods
2471
2472
2473	function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive
2474
2475		if (refreshCoordinateGrid) {
2476			coordinateGrid.build();
2477		}
2478
2479		var segments = rangeToSegments(overlayStart, overlayEnd);
2480
2481		for (var i=0; i<segments.length; i++) {
2482			var segment = segments[i];
2483			dayBind(
2484				renderCellOverlay(
2485					segment.row,
2486					segment.leftCol,
2487					segment.row,
2488					segment.rightCol
2489				)
2490			);
2491		}
2492	}
2493
2494	
2495	function renderCellOverlay(row0, col0, row1, col1) { // row1,col1 is inclusive
2496		var rect = coordinateGrid.rect(row0, col0, row1, col1, element);
2497		return renderOverlay(rect, element);
2498	}
2499	
2500	
2501	
2502	/* Selection
2503	-----------------------------------------------------------------------*/
2504	
2505	
2506	function defaultSelectionEnd(startDate, allDay) {
2507		return cloneDate(startDate);
2508	}
2509	
2510	
2511	function renderSelection(startDate, endDate, allDay) {
2512		renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true); // rebuild every time???
2513	}
2514	
2515	
2516	function clearSelection() {
2517		clearOverlays();
2518	}
2519	
2520	
2521	function reportDayClick(date, allDay, ev) {
2522		var cell = dateToCell(date);
2523		var _element = bodyCells[cell.row*colCnt + cell.col];
2524		trigger('dayClick', _element, date, allDay, ev);
2525	}
2526	
2527	
2528	
2529	/* External Dragging
2530	-----------------------------------------------------------------------*/
2531	
2532	
2533	function dragStart(_dragElement, ev, ui) {
2534		hoverListener.start(function(cell) {
2535			clearOverlays();
2536			if (cell) {
2537				renderCellOverlay(cell.row, cell.col, cell.row, cell.col);
2538			}
2539		}, ev);
2540	}
2541	
2542	
2543	function dragStop(_dragElement, ev, ui) {
2544		var cell = hoverListener.stop();
2545		clearOverlays();
2546		if (cell) {
2547			var d = cellToDate(cell);
2548			trigger('drop', _dragElement, d, true, ev, ui);
2549		}
2550	}
2551	
2552	
2553	
2554	/* Utilities
2555	--------------------------------------------------------*/
2556	
2557	
2558	function defaultEventEnd(event) {
2559		return cloneDate(event.start);
2560	}
2561	
2562	
2563	coordinateGrid = new CoordinateGrid(function(rows, cols) {
2564		var e, n, p;
2565		headCells.each(function(i, _e) {
2566			e = $(_e);
2567			n = e.offset().left;
2568			if (i) {
2569				p[1] = n;
2570			}
2571			p = [n];
2572			cols[i] = p;
2573		});
2574		p[1] = n + e.outerWidth();
2575		bodyRows.each(function(i, _e) {
2576			if (i < rowCnt) {
2577				e = $(_e);
2578				n = e.offset().top;
2579				if (i) {
2580					p[1] = n;
2581				}
2582				p = [n];
2583				rows[i] = p;
2584			}
2585		});
2586		p[1] = n + e.outerHeight();
2587	});
2588	
2589	
2590	hoverListener = new HoverListener(coordinateGrid);
2591	
2592	colPositions = new HorizontalPositionCache(function(col) {
2593		return firstRowCellInners.eq(col);
2594	});
2595
2596	colContentPositions = new HorizontalPositionCache(function(col) {
2597		return firstRowCellContentInners.eq(col);
2598	});
2599
2600
2601	function colLeft(col) {
2602		return colPositions.left(col);
2603	}
2604
2605
2606	function colRight(col) {
2607		return colPositions.right(col);
2608	}
2609	
2610	
2611	function colContentLeft(col) {
2612		return colContentPositions.left(col);
2613	}
2614	
2615	
2616	function colContentRight(col) {
2617		return colContentPositions.right(col);
2618	}
2619	
2620	
2621	function allDayRow(i) {
2622		return bodyRows.eq(i);
2623	}
2624	
2625}
2626
2627;;
2628
2629function BasicEventRenderer() {
2630	var t = this;
2631	
2632	
2633	// exports
2634	t.renderEvents = renderEvents;
2635	t.clearEvents = clearEvents;
2636	
2637
2638	// imports
2639	DayEventRenderer.call(t);
2640
2641	
2642	function renderEvents(events, modifiedEventId) {
2643		t.renderDayEvents(events, modifiedEventId);
2644	}
2645	
2646	
2647	function clearEvents() {
2648		t.getDaySegmentContainer().empty();
2649	}
2650
2651
2652	// TODO: have this class (and AgendaEventRenderer) be responsible for creating the event container div
2653
2654}
2655
2656;;
2657
2658fcViews.agendaWeek = AgendaWeekView;
2659
2660function AgendaWeekView(element, calendar) {
2661	var t = this;
2662	
2663	
2664	// exports
2665	t.render = render;
2666	
2667	
2668	// imports
2669	AgendaView.call(t, element, calendar, 'agendaWeek');
2670	var opt = t.opt;
2671	var renderAgenda = t.renderAgenda;
2672	var skipHiddenDays = t.skipHiddenDays;
2673	var getCellsPerWeek = t.getCellsPerWeek;
2674	var formatDates = calendar.formatDates;
2675
2676	
2677	function render(date, delta) {
2678
2679		if (delta) {
2680			addDays(date, delta * 7);
2681		}
2682
2683		var start = addDays(cloneDate(date), -((date.getDay() - opt('firstDay') + 7) % 7));
2684		var end = addDays(cloneDate(start), 7);
2685
2686		var visStart = cloneDate(start);
2687		skipHiddenDays(visStart);
2688
2689		var visEnd = cloneDate(end);
2690		skipHiddenDays(visEnd, -1, true);
2691
2692		var colCnt = getCellsPerWeek();
2693
2694		t.title = formatDates(
2695			visStart,
2696			addDays(cloneDate(visEnd), -1),
2697			opt('titleFormat')
2698		);
2699
2700		t.start = start;
2701		t.end = end;
2702		t.visStart = visStart;
2703		t.visEnd = visEnd;
2704
2705		renderAgenda(colCnt);
2706	}
2707
2708}
2709
2710;;
2711
2712fcViews.agendaDay = AgendaDayView;
2713
2714
2715function AgendaDayView(element, calendar) {
2716	var t = this;
2717	
2718	
2719	// exports
2720	t.render = render;
2721	
2722	
2723	// imports
2724	AgendaView.call(t, element, calendar, 'agendaDay');
2725	var opt = t.opt;
2726	var renderAgenda = t.renderAgenda;
2727	var skipHiddenDays = t.skipHiddenDays;
2728	var formatDate = calendar.formatDate;
2729	
2730	
2731	function render(date, delta) {
2732
2733		if (delta) {
2734			addDays(date, delta);
2735		}
2736		skipHiddenDays(date, delta < 0 ? -1 : 1);
2737
2738		var start = cloneDate(date, true);
2739		var end = addDays(cloneDate(start), 1);
2740
2741		t.title = formatDate(date, opt('titleFormat'));
2742
2743		t.start = t.visStart = start;
2744		t.end = t.visEnd = end;
2745
2746		renderAgenda(1);
2747	}
2748	
2749
2750}
2751
2752;;
2753
2754setDefaults({
2755	allDaySlot: true,
2756	allDayText: 'all-day',
2757	firstHour: 6,
2758	slotMinutes: 30,
2759	defaultEventMinutes: 120,
2760	axisFormat: 'h(:mm)tt',
2761	timeFormat: {
2762		agenda: 'h:mm{ - h:mm}'
2763	},
2764	dragOpacity: {
2765		agenda: .5
2766	},
2767	minTime: 0,
2768	maxTime: 24,
2769	slotEventOverlap: true
2770});
2771
2772
2773// TODO: make it work in quirks mode (event corners, all-day height)
2774// TODO: test liquid width, especially in IE6
2775
2776
2777function AgendaView(element, calendar, viewName) {
2778	var t = this;
2779	
2780	
2781	// exports
2782	t.renderAgenda = renderAgenda;
2783	t.setWidth = setWidth;
2784	t.setHeight = setHeight;
2785	t.afterRender = afterRender;
2786	t.defaultEventEnd = defaultEventEnd;
2787	t.timePosition = timePosition;
2788	t.getIsCellAllDay = getIsCellAllDay;
2789	t.allDayRow = getAllDayRow;
2790	t.getCoordinateGrid = function() { return coordinateGrid }; // specifically for AgendaEventRenderer
2791	t.getHoverListener = function() { return hoverListener };
2792	t.colLeft = colLeft;
2793	t.colRight = colRight;
2794	t.colContentLeft = colContentLeft;
2795	t.colContentRight = colContentRight;
2796	t.getDaySegmentContainer = function() { return daySegmentContainer };
2797	t.getSlotSegmentContainer = function() { return slotSegmentContainer };
2798	t.getMinMinute = function() { return minMinute };
2799	t.getMaxMinute = function() { return maxMinute };
2800	t.getSlotContainer = function() { return slotContainer };
2801	t.getRowCnt = function() { return 1 };
2802	t.getColCnt = function() { return colCnt };
2803	t.getColWidth = function() { return colWidth };
2804	t.getSnapHeight = function() { return snapHeight };
2805	t.getSnapMinutes = function() { return snapMinutes };
2806	t.defaultSelectionEnd = defaultSelectionEnd;
2807	t.renderDayOverlay = renderDayOverlay;
2808	t.renderSelection = renderSelection;
2809	t.clearSelection = clearSelection;
2810	t.reportDayClick = reportDayClick; // selection mousedown hack
2811	t.dragStart = dragStart;
2812	t.dragStop = dragStop;
2813	
2814	
2815	// imports
2816	View.call(t, element, calendar, viewName);
2817	OverlayManager.call(t);
2818	SelectionManager.call(t);
2819	AgendaEventRenderer.call(t);
2820	var opt = t.opt;
2821	var trigger = t.trigger;
2822	var renderOverlay = t.renderOverlay;
2823	var clearOverlays = t.clearOverlays;
2824	var reportSelection = t.reportSelection;
2825	var unselect = t.unselect;
2826	var daySelectionMousedown = t.daySelectionMousedown;
2827	var slotSegHtml = t.slotSegHtml;
2828	var cellToDate = t.cellToDate;
2829	var dateToCell = t.dateToCell;
2830	var rangeToSegments = t.rangeToSegments;
2831	var formatDate = calendar.formatDate;
2832	
2833	
2834	// locals
2835	
2836	var dayTable;
2837	var dayHead;
2838	var dayHeadCells;
2839	var dayBody;
2840	var dayBodyCells;
2841	var dayBodyCellInners;
2842	var dayBodyCellContentInners;
2843	var dayBodyFirstCell;
2844	var dayBodyFirstCellStretcher;
2845	var slotLayer;
2846	var daySegmentContainer;
2847	var allDayTable;
2848	var allDayRow;
2849	var slotScroller;
2850	var slotContainer;
2851	var slotSegmentContainer;
2852	var slotTable;
2853	var selectionHelper;
2854	
2855	var viewWidth;
2856	var viewHeight;
2857	var axisWidth;
2858	var colWidth;
2859	var gutterWidth;
2860	var slotHeight; // TODO: what if slotHeight changes? (see issue 650)
2861
2862	var snapMinutes;
2863	var snapRatio; // ratio of number of "selection" slots to normal slots. (ex: 1, 2, 4)
2864	var snapHeight; // holds the pixel hight of a "selection" slot
2865	
2866	var colCnt;
2867	var slotCnt;
2868	var coordinateGrid;
2869	var hoverListener;
2870	var colPositions;
2871	var colContentPositions;
2872	var slotTopCache = {};
2873	
2874	var tm;
2875	var rtl;
2876	var minMinute, maxMinute;
2877	var colFormat;
2878	var showWeekNumbers;
2879	var weekNumberTitle;
2880	var weekNumberFormat;
2881	
2882
2883	
2884	/* Rendering
2885	-----------------------------------------------------------------------------*/
2886	
2887	
2888	disableTextSelection(element.addClass('fc-agenda'));
2889	
2890	
2891	function renderAgenda(c) {
2892		colCnt = c;
2893		updateOptions();
2894
2895		if (!dayTable) { // first time rendering?
2896			buildSkeleton(); // builds day table, slot area, events containers
2897		}
2898		else {
2899			buildDayTable(); // rebuilds day table
2900		}
2901	}
2902	
2903	
2904	function updateOptions() {
2905
2906		tm = opt('theme') ? 'ui' : 'fc';
2907		rtl = opt('isRTL')
2908		minMinute = parseTime(opt('minTime'));
2909		maxMinute = parseTime(opt('maxTime'));
2910		colFormat = opt('columnFormat');
2911
2912		// week # options. (TODO: bad, logic also in other views)
2913		showWeekNumbers = opt('weekNumbers');
2914		weekNumberTitle = opt('weekNumberTitle');
2915		if (opt('weekNumberCalculation') != 'iso') {
2916			weekNumberFormat = "w";
2917		}
2918		else {
2919			weekNumberFormat = "W";
2920		}
2921
2922		snapMinutes = opt('snapMinutes') || opt('slotMinutes');
2923	}
2924
2925
2926
2927	/* Build DOM
2928	-----------------------------------------------------------------------*/
2929
2930
2931	function buildSkeleton() {
2932		var headerClass = tm + "-widget-header";
2933		var contentClass = tm + "-widget-content";
2934		var s;
2935		var d;
2936		var i;
2937		var maxd;
2938		var minutes;
2939		var slotNormal = opt('slotMinutes') % 15 == 0;
2940		
2941		buildDayTable();
2942		
2943		slotLayer =
2944			$("<div style='position:absolute;z-index:2;left:0;width:100%'/>")
2945				.appendTo(element);
2946				
2947		if (opt('allDaySlot')) {
2948		
2949			daySegmentContainer =
2950				$("<div class='fc-event-container' style='position:absolute;z-index:8;top:0;left:0'/>")
2951					.appendTo(slotLayer);
2952		
2953			s =
2954				"<table style='width:100%' class='fc-agenda-allday' cellspacing='0'>" +
2955				"<tr>" +
2956				"<th class='" + headerClass + " fc-agenda-axis'>" + opt('allDayText') + "</th>" +
2957				"<td>" +
2958				"<div class='fc-day-content'><div style='position:relative'/></div>" +
2959				"</td>" +
2960				"<th class='" + headerClass + " fc-agenda-gutter'>&nbsp;</th>" +
2961				"</tr>" +
2962				"</table>";
2963			allDayTable = $(s).appendTo(slotLayer);
2964			allDayRow = allDayTable.find('tr');
2965			
2966			dayBind(allDayRow.find('td'));
2967			
2968			slotLayer.append(
2969				"<div class='fc-agenda-divider " + headerClass + "'>" +
2970				"<div class='fc-agenda-divider-inner'/>" +
2971				"</div>"
2972			);
2973			
2974		}else{
2975		
2976			daySegmentContainer = $([]); // in jQuery 1.4, we can just do $()
2977		
2978		}
2979		
2980		slotScroller =
2981			$("<div style='position:absolute;width:100%;overflow-x:hidden;overflow-y:auto'/>")
2982				.appendTo(slotLayer);
2983				
2984		slotContainer =
2985			$("<div style='position:relative;width:100%;overflow:hidden'/>")
2986				.appendTo(slotScroller);
2987				
2988		slotSegmentContainer =
2989			$("<div class='fc-event-container' style='position:absolute;z-index:8;top:0;left:0'/>")
2990				.appendTo(slotContainer);
2991		
2992		s =
2993			"<table class='fc-agenda-slots' style='width:100%' cellspacing='0'>" +
2994			"<tbody>";
2995		d = zeroDate();
2996		maxd = addMinutes(cloneDate(d), maxMinute);
2997		addMinutes(d, minMinute);
2998		slotCnt = 0;
2999		for (i=0; d < maxd; i++) {
3000			minutes = d.getMinutes();
3001			s +=
3002				"<tr class='fc-slot" + i + ' ' + (!minutes ? '' : 'fc-minor') + "'>" +
3003				"<th class='fc-agenda-axis " + headerClass + "'>" +
3004				((!slotNormal || !minutes) ? formatDate(d, opt('axisFormat')) : '&nbsp;') +
3005				"</th>" +
3006				"<td class='" + contentClass + "'>" +
3007				"<div style='position:relative'>
3007&nbsp;</div>" +
3008				"</td>" +
3009				"</tr>";
3010			addMinutes(d, opt('slotMinutes'));
3011			slotCnt++;
3012		}
3013		s +=
3014			"</tbody>" +
3015			"</table>";
3016		slotTable = $(s).appendTo(slotContainer);
3017		
3018		slotBind(slotTable.find('td'));
3019	}
3020
3021
3022
3023	/* Build Day Table
3024	-----------------------------------------------------------------------*/
3025
3026
3027	function buildDayTable() {
3028		var html = buildDayTableHTML();
3029
3030		if (dayTable) {
3031			dayTable.remove();
3032		}
3033		dayTable = $(html).appendTo(element);
3034
3035		dayHead = dayTable.find('thead');
3036		dayHeadCells = dayHead.find('th').slice(1, -1); // exclude gutter
3037		dayBody = dayTable.find('tbody');
3038		dayBodyCells = dayBody.find('td').slice(0, -1); // exclude gutter
3039		dayBodyCellInners = dayBodyCells.find('> div');
3040		dayBodyCellContentInners = dayBodyCells.find('.fc-day-content > div');
3041
3042		dayBodyFirstCell = dayBodyCells.eq(0);
3043		dayBodyFirstCellStretcher = dayBodyCellInners.eq(0);
3044		
3045		markFirstLast(dayHead.add(dayHead.find('tr')));
3046		markFirstLast(dayBody.add(dayBody.find('tr')));
3047
3048		// TODO: now that we rebuild the cells every time, we should call dayRender
3049	}
3050
3051
3052	function buildDayTableHTML() {
3053		var html =
3054			"<table style='width:100%' class='fc-agenda-days fc-border-separate' cellspacing='0'>" +
3055			buildDayTableHeadHTML() +
3056			buildDayTableBodyHTML() +
3057			"</table>";
3058
3059		return html;
3060	}
3061
3062
3063	function buildDayTableHeadHTML() {
3064		var headerClass = tm + "-widget-header";
3065		var date;
3066		var html = '';
3067		var weekText;
3068		var col;
3069
3070		html +=
3071			"<thead>" +
3072			"<tr>";
3073
3074		if (showWeekNumbers) {
3075			date = cellToDate(0, 0);
3076			weekText = formatDate(date, weekNumberFormat);
3077			if (rtl) {
3078				weekText += weekNumberTitle;
3079			}
3080			else {
3081				weekText = weekNumberTitle + weekText;
3082			}
3083			html +=
3084				"<th class='fc-agenda-axis fc-week-number " + headerClass + "'>" +
3085				htmlEscape(weekText) +
3086				"</th>";
3087		}
3088		else {
3089			html += "<th class='fc-agenda-axis " + headerClass + "'>&nbsp;</th>";
3090		}
3091
3092		for (col=0; col<colCnt; col++) {
3093			date = cellToDate(0, col);
3094			html +=
3095				"<th class='fc-" + dayIDs[date.getDay()] + " fc-col" + col + ' ' + headerClass + "'>" +
3096				htmlEscape(formatDate(date, colFormat)) +
3097				"</th>";
3098		}
3099
3100		html +=
3101			"<th class='fc-agenda-gutter " + headerClass + "'>&nbsp;</th>" +
3102			"</tr>" +
3103			"</thead>";
3104
3105		return html;
3106	}
3107
3108
3109	function buildDayTableBodyHTML() {
3110		var headerClass = tm + "-widget-header"; // TODO: make these when updateOptions() called
3111		var contentClass = tm + "-widget-content";
3112		var date;
3113		var today = clearTime(new Date());
3114		var col;
3115		var cellsHTML;
3116		var cellHTML;
3117		var classNames;
3118		var html = '';
3119
3120		html +=
3121			"<tbody>" +
3122			"<tr>" +
3123			"<th class='fc-agenda-axis " + headerClass + "'>&nbsp;</th>";
3124
3125		cellsHTML = '';
3126
3127		for (col=0; col<colCnt; col++) {
3128
3129			date = cellToDate(0, col);
3130
3131			classNames = [
3132				'fc-col' + col,
3133				'fc-' + dayIDs[date.getDay()],
3134				contentClass
3135			];
3136			if (+date == +today) {
3137				classNames.push(
3138					tm + '-state-highlight',
3139					'fc-today'
3140				);
3141			}
3142			else if (date < today) {
3143				classNames.push('fc-past');
3144			}
3145			else {
3146				classNames.push('fc-future');
3147			}
3148
3149			cellHTML =
3150				"<td class='" + classNames.join(' ') + "' data-date='" + formatDate(date, 'yyyy-MM-dd') + "'>" +
3151				"<div>" +
3152				"<div class='fc-day-content'>" +
3153				"<div style='position:relative'>&nbsp;</div>" +
3154				"</div>" +
3155				"</div>" +
3156				"</td>";
3157
3158			cellsHTML += cellHTML;
3159		}
3160
3161		html += cellsHTML;
3162		html +=
3163			"<td class='fc-agenda-gutter " + contentClass + "'>&nbsp;</td>" +
3164			"</tr>" +
3165			"</tbody>";
3166
3167		return html;
3168	}
3169
3170
3171	// TODO: data-date on the cells
3172
3173	
3174	
3175	/* Dimensions
3176	-----------------------------------------------------------------------*/
3177
3178	
3179	function setHeight(height) {
3180		if (height === undefined) {
3181			height = viewHeight;
3182		}
3183		viewHeight = height;
3184		slotTopCache = {};
3185	
3186		var headHeight = dayBody.position().top;
3187		var allDayHeight = slotScroller.position().top; // including divider
3188		var bodyHeight = Math.min( // total body height, including borders
3189			height - headHeight,   // when scrollbars
3190			slotTable.height() + allDayHeight + 1 // when no scrollbars. +1 for bottom border
3191		);
3192
3193		dayBodyFirstCellStretcher
3194			.height(bodyHeight - vsides(dayBodyFirstCell));
3195		
3196		slotLayer.css('top', headHeight);
3197		
3198		slotScroller.height(bodyHeight - allDayHeight - 1);
3199		
3200		// the stylesheet guarantees that the first row has no border.
3201		// this allows .height() to work well cross-browser.
3202		slotHeight = slotTable.find('tr:first').height() + 1; // +1 for bottom border
3203
3204		snapRatio = opt('slotMinutes') / snapMinutes;
3205		snapHeight = slotHeight / snapRatio;
3206	}
3207	
3208	
3209	function setWidth(width) {
3210		viewWidth = width;
3211		colPositions.clear();
3212		colContentPositions.clear();
3213
3214		var axisFirstCells = dayHead.find('th:first');
3215		if (allDayTable) {
3216			axisFirstCells = axisFirstCells.add(allDayTable.find('th:first'));
3217		}
3218		axisFirstCells = axisFirstCells.add(slotTable.find('th:first'));
3219		
3220		axisWidth = 0;
3221		setOuterWidth(
3222			axisFirstCells
3223				.width('')
3224				.each(function(i, _cell) {
3225					axisWidth = Math.max(axisWidth, $(_cell).outerWidth());
3226				}),
3227			axisWidth
3228		);
3229		
3230		var gutterCells = dayTable.find('.fc-agenda-gutter');
3231		if (allDayTable) {
3232			gutterCells = gutterCells.add(allDayTable.find('th.fc-agenda-gutter'));
3233		}
3234
3235		var slotTableWidth = slotScroller[0].clientWidth; // needs to be done after axisWidth (for IE7)
3236		
3237		gutterWidth = slotScroller.width() - slotTableWidth;
3238		if (gutterWidth) {
3239			setOuterWidth(gutterCells, gutterWidth);
3240			gutterCells
3241				.show()
3242				.prev()
3243				.removeClass('fc-last');
3244		}else{
3245			gutterCells
3246				.hide()
3247				.prev()
3248				.addClass('fc-last');
3249		}
3250		
3251		colWidth = Math.floor((slotTableWidth - axisWidth) / colCnt);
3252		setOuterWidth(dayHeadCells.slice(0, -1), colWidth);
3253	}
3254	
3255
3256
3257	/* Scrolling
3258	-----------------------------------------------------------------------*/
3259
3260
3261	function resetScroll() {
3262		var d0 = zeroDate();
3263		var scrollDate = cloneDate(d0);
3264		scrollDate.setHours(opt('firstHour'));
3265		var top = timePosition(d0, scrollDate) + 1; // +1 for the border
3266		function scroll() {
3267			slotScroller.scrollTop(top);
3268		}
3269		scroll();
3270		setTimeout(scroll, 0); // overrides any previous scroll state made by the browser
3271	}
3272
3273
3274	function afterRender() { // after the view has been freshly rendered and sized
3275		resetScroll();
3276	}
3277	
3278	
3279	
3280	/* Slot/Day clicking and binding
3281	-----------------------------------------------------------------------*/
3282	
3283
3284	function dayBind(cells) {
3285		cells.click(slotClick)
3286			.mousedown(daySelectionMousedown);
3287	}
3288
3289
3290	function slotBind(cells) {
3291		cells.click(slotClick)
3292			.mousedown(slotSelectionMousedown);
3293	}
3294	
3295	
3296	function slotClick(ev) {
3297		if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick
3298			var col = Math.min(colCnt-1, Math.floor((ev.pageX - dayTable.offset().left - axisWidth) / colWidth));
3299			var date = cellToDate(0, col);
3300			var rowMatch = this.parentNode.className.match(/fc-slot(\d+)/); // TODO: maybe use data
3301			if (rowMatch) {
3302				var mins = parseInt(rowMatch[1]) * opt('slotMinutes');
3303				var hours = Math.floor(mins/60);
3304				date.setHours(hours);
3305				date.setMinutes(mins%60 + minMinute);
3306				trigger('dayClick', dayBodyCells[col], date, false, ev);
3307			}else{
3308				trigger('dayClick', dayBodyCells[col], date, true, ev);
3309			}
3310		}
3311	}
3312	
3313	
3314	
3315	/* Semi-transparent Overlay Helpers
3316	-----------------------------------------------------*/
3317	// TODO: should be consolidated with BasicView's methods
3318
3319
3320	function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive
3321
3322		if (refreshCoordinateGrid) {
3323			coordinateGrid.build();
3324		}
3325
3326		var segments = rangeToSegments(overlayStart, overlayEnd);
3327
3328		for (var i=0; i<segments.length; i++) {
3329			var segment = segments[i];
3330			dayBind(
3331				renderCellOverlay(
3332					segment.row,
3333					segment.leftCol,
3334					segment.row,
3335					segment.rightCol
3336				)
3337			);
3338		}
3339	}
3340	
3341	
3342	function renderCellOverlay(row0, col0, row1, col1) { // only for all-day?
3343		var rect = coordinateGrid.rect(row0, col0, row1, col1, slotLayer);
3344		return renderOverlay(rect, slotLayer);
3345	}
3346	
3347
3348	function renderSlotOverlay(overlayStart, overlayEnd) {
3349		for (var i=0; i<colCnt; i++) {
3350			var dayStart = cellToDate(0, i);
3351			var dayEnd = addDays(cloneDate(dayStart), 1);
3352			var stretchStart = new Date(Math.max(dayStart, overlayStart));
3353			var stretchEnd = new Date(Math.min(dayEnd, overlayEnd));
3354			if (stretchStart < stretchEnd) {
3355				var rect = coordinateGrid.rect(0, i, 0, i, slotContainer); // only use it for horizontal coords
3356				var top = timePosition(dayStart, stretchStart);
3357				var bottom = timePosition(dayStart, stretchEnd);
3358				rect.top = top;
3359				rect.height = bottom - top;
3360				slotBind(
3361					renderOverlay(rect, slotContainer)
3362				);
3363			}
3364		}
3365	}
3366	
3367	
3368	
3369	/* Coordinate Utilities
3370	-----------------------------------------------------------------------------*/
3371	
3372	
3373	coordinateGrid = new CoordinateGrid(function(rows, cols) {
3374		var e, n, p;
3375		dayHeadCells.each(function(i, _e) {
3376			e = $(_e);
3377			n = e.offset().left;
3378			if (i) {
3379				p[1] = n;
3380			}
3381			p = [n];
3382			cols[i] = p;
3383		});
3384		p[1] = n + e.outerWidth();
3385		if (opt('allDaySlot')) {
3386			e = allDayRow;
3387			n = e.offset().top;
3388			rows[0] = [n, n+e.outerHeight()];
3389		}
3390		var slotTableTop = slotContainer.offset().top;
3391		var slotScrollerTop = slotScroller.offset().top;
3392		var slotScrollerBottom = slotScrollerTop + slotScroller.outerHeight();
3393		function constrain(n) {
3394			return Math.max(slotScrollerTop, Math.min(slotScrollerBottom, n));
3395		}
3396		for (var i=0; i<slotCnt*snapRatio; i++) { // adapt slot count to increased/decreased selection slot count
3397			rows.push([
3398				constrain(slotTableTop + snapHeight*i),
3399				constrain(slotTableTop + snapHeight*(i+1))
3400			]);
3401		}
3402	});
3403	
3404	
3405	hoverListener = new HoverListener(coordinateGrid);
3406	
3407	colPositions = new HorizontalPositionCache(function(col) {
3408		return dayBodyCellInners.eq(col);
3409	});
3410	
3411	colContentPositions = new HorizontalPositionCache(function(col) {
3412		return dayBodyCellContentInners.eq(col);
3413	});
3414	
3415	
3416	function colLeft(col) {
3417		return colPositions.left(col);
3418	}
3419
3420
3421	function colContentLeft(col) {
3422		return colContentPositions.left(col);
3423	}
3424
3425
3426	function colRight(col) {
3427		return colPositions.right(col);
3428	}
3429	
3430	
3431	function colContentRight(col) {
3432		return colContentPositions.right(col);
3433	}
3434
3435
3436	function getIsCellAllDay(cell) {
3437		return opt('allDaySlot') && !cell.row;
3438	}
3439
3440
3441	function realCellToDate(cell) { // ugh "real" ... but blame it on our abuse of the "cell" system
3442		var d = cellToDate(0, cell.col);
3443		var slotIndex = cell.row;
3444		if (opt('allDaySlot')) {
3445			slotIndex--;
3446		}
3447		if (slotIndex >= 0) {
3448			addMinutes(d, minMinute + slotIndex * snapMinutes);
3449		}
3450		return d;
3451	}
3452	
3453	
3454	// get the Y coordinate of the given time on the given day (both Date objects)
3455	function timePosition(day, time) { // both date objects. day holds 00:00 of current day
3456		day = cloneDate(day, true);
3457		if (time < addMinutes(cloneDate(day), minMinute)) {
3458			return 0;
3459		}
3460		if (time >= addMinutes(cloneDate(day), maxMinute)) {
3461			return slotTable.height();
3462		}
3463		var slotMinutes = opt('slotMinutes'),
3464			minutes = time.getHours()*60 + time.getMinutes() - minMinute,
3465			slotI = Math.floor(minutes / slotMinutes),
3466			slotTop = slotTopCache[slotI];
3467		if (slotTop === undefined) {
3468			slotTop = slotTopCache[slotI] =
3469				slotTable.find('tr').eq(slotI).find('td div')[0].offsetTop;
3470				// .eq() is faster than ":eq()" selector
3471				// [0].offsetTop is faster than .position().top (do we really need this optimization?)
3472				// a better optimization would be to cache all these divs
3473		}
3474		return Math.max(0, Math.round(
3475			slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes)
3476		));
3477	}
3478	
3479	
3480	function getAllDayRow(index) {
3481		return allDayRow;
3482	}
3483	
3484	
3485	function defaultEventEnd(event) {
3486		var start = cloneDate(event.start);
3487		if (event.allDay) {
3488			return start;
3489		}
3490		return addMinutes(start, opt('defaultEventMinutes'));
3491	}
3492	
3493	
3494	
3495	/* Selection
3496	---------------------------------------------------------------------------------*/
3497	
3498	
3499	function defaultSelectionEnd(startDate, allDay) {
3500		if (allDay) {
3501			return cloneDate(startDate);
3502		}
3503		return addMinutes(cloneDate(startDate), opt('slotMinutes'));
3504	}
3505	
3506	
3507	function renderSelection(startDate, endDate, allDay) { // only for all-day
3508		if (allDay) {
3509			if (opt('allDaySlot')) {
3510				renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true);
3511			}
3512		}else{
3513			renderSlotSelection(startDate, endDate);
3514		}
3515	}
3516	
3517	
3518	function renderSlotSelection(startDate, endDate) {
3519		var helperOption = opt('selectHelper');
3520		coordinateGrid.build();
3521		if (helperOption) {
3522			var col = dateToCell(startDate).col;
3523			if (col >= 0 && col < colCnt) { // only works when times are on same day
3524				var rect = coordinateGrid.rect(0, col, 0, col, slotContainer); // only for horizontal coords
3525				var top = timePosition(startDate, startDate);
3526				var bottom = timePosition(startDate, endDate);
3527				if (bottom > top) { // protect against selections that are entirely before or after visible range
3528					rect.top = top;
3529					rect.height = bottom - top;
3530					rect.left += 2;
3531					rect.width -= 5;
3532					if ($.isFunction(helperOption)) {
3533						var helperRes = helperOption(startDate, endDate);
3534						if (helperRes) {
3535							rect.position = 'absolute';
3536							selectionHelper = $(helperRes)
3537								.css(rect)
3538								.appendTo(slotContainer);
3539						}
3540					}else{
3541						rect.isStart = true; // conside rect a "seg" now
3542						rect.isEnd = true;   //
3543						selectionHelper = $(slotSegHtml(
3544							{
3545								title: '',
3546								start: startDate,
3547								end: endDate,
3548								className: ['fc-select-helper'],
3549								editable: false
3550							},
3551							rect
3552						));
3553						selectionHelper.css('opacity', opt('dragOpacity'));
3554					}
3555					if (selectionHelper) {
3556						slotBind(selectionHelper);
3557						slotContainer.append(selectionHelper);
3558						setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended
3559						setOuterHeight(selectionHelper, rect.height, true);
3560					}
3561				}
3562			}
3563		}else{
3564			renderSlotOverlay(startDate, endDate);
3565		}
3566	}
3567	
3568	
3569	function clearSelection() {
3570		clearOverlays();
3571		if (selectionHelper) {
3572			selectionHelper.remove();
3573			selectionHelper = null;
3574		}
3575	}
3576	
3577	
3578	function slotSelectionMousedown(ev) {
3579		if (ev.which == 1 && opt('selectable')) { // ev.which==1 means left mouse button
3580			unselect(ev);
3581			var dates;
3582			hoverListener.start(function(cell, origCell) {
3583				clearSelection();
3584				if (cell && cell.col == origCell.col && !getIsCellAllDay(cell)) {
3585					var d1 = realCellToDate(origCell);
3586					var d2 = realCellToDate(cell);
3587					dates = [
3588						d1,
3589						addMinutes(cloneDate(d1), snapMinutes), // calculate minutes depending on selection slot minutes 
3590						d2,
3591						addMinutes(cloneDate(d2), snapMinutes)
3592					].sort(dateCompare);
3593					renderSlotSelection(dates[0], dates[3]);
3594				}else{
3595					dates = null;
3596				}
3597			}, ev);
3598			$(document).one('mouseup', function(ev) {
3599				hoverListener.stop();
3600				if (dates) {
3601					if (+dates[0] == +dates[1]) {
3602						reportDayClick(dates[0], false, ev);
3603					}
3604					reportSelection(dates[0], dates[3], false, ev);
3605				}
3606			});
3607		}
3608	}
3609
3610
3611	function reportDayClick(date, allDay, ev) {
3612		trigger('dayClick', dayBodyCells[dateToCell(date).col], date, allDay, ev);
3613	}
3614	
3615	
3616	
3617	/* External Dragging
3618	--------------------------------------------------------------------------------*/
3619	
3620	
3621	function dragStart(_dragElement, ev, ui) {
3622		hoverListener.start(function(cell) {
3623			clearOverlays();
3624			if (cell) {
3625				if (getIsCellAllDay(cell)) {
3626					renderCellOverlay(cell.row, cell.col, cell.row, cell.col);
3627				}else{
3628					var d1 = realCellToDate(cell);
3629					var d2 = addMinutes(cloneDate(d1), opt('defaultEventMinutes'));
3630					renderSlotOverlay(d1, d2);
3631				}
3632			}
3633		}, ev);
3634	}
3635	
3636	
3637	function dragStop(_dragElement, ev, ui) {
3638		var cell = hoverListener.stop();
3639		clearOverlays();
3640		if (cell) {
3641			trigger('drop', _dragElement, realCellToDate(cell), getIsCellAllDay(cell), ev, ui);
3642		}
3643	}
3644	
3645
3646}
3647
3648;;
3649
3650function AgendaEventRenderer() {
3651	var t = this;
3652	
3653	
3654	// exports
3655	t.renderEvents = renderEvents;
3656	t.clearEvents = clearEvents;
3657	t.slotSegHtml = slotSegHtml;
3658	
3659	
3660	// imports
3661	DayEventRenderer.call(t);
3662	var opt = t.opt;
3663	var trigger = t.trigger;
3664	var isEventDraggable = t.isEventDraggable;
3665	var isEventResizable = t.isEventResizable;
3666	var eventEnd = t.eventEnd;
3667	var eventElementHandlers = t.eventElementHandlers;
3668	var setHeight = t.setHeight;
3669	var getDaySegmentContainer = t.getDaySegmentContainer;
3670	var getSlotSegmentContainer = t.getSlotSegmentContainer;
3671	var getHoverListener = t.getHoverListener;
3672	var getMaxMinute = t.getMaxMinute;
3673	var getMinMinute = t.getMinMinute;
3674	var timePosition = t.timePosition;
3675	var getIsCellAllDay = t.getIsCellAllDay;
3676	var colContentLeft = t.colContentLeft;
3677	var colContentRight = t.colContentRight;
3678	var cellToDate = t.cellToDate;
3679	var getColCnt = t.getColCnt;
3680	var getColWidth = t.getColWidth;
3681	var getSnapHeight = t.getSnapHeight;
3682	var getSnapMinutes = t.getSnapMinutes;
3683	var getSlotContainer = t.getSlotContainer;
3684	var reportEventElement = t.reportEventElement;
3685	var showEvents = t.showEvents;
3686	var hideEvents = t.hideEvents;
3687	var eventDrop = t.eventDrop;
3688	var eventResize = t.eventResize;
3689	var renderDayOverlay = t.renderDayOverlay;
3690	var clearOverlays = t.clearOverlays;
3691	var renderDayEvents = t.renderDayEvents;
3692	var calendar = t.calendar;
3693	var formatDate = calendar.formatDate;
3694	var formatDates = calendar.formatDates;
3695
3696
3697	// overrides
3698	t.draggableDayEvent = draggableDayEvent;
3699
3700	
3701	
3702	/* Rendering
3703	----------------------------------------------------------------------------*/
3704	
3705
3706	function renderEvents(events, modifiedEventId) {
3707		var i, len=events.length,
3708			dayEvents=[],
3709			slotEvents=[];
3710		for (i=0; i<len; i++) {
3711			if (events[i].allDay) {
3712				dayEvents.push(events[i]);
3713			}else{
3714				slotEvents.push(events[i]);
3715			}
3716		}
3717
3718		if (opt('allDaySlot')) {
3719			renderDayEvents(dayEvents, modifiedEventId);
3720			setHeight(); // no params means set to viewHeight
3721		}
3722
3723		renderSlotSegs(compileSlotSegs(slotEvents), modifiedEventId);
3724	}
3725	
3726	
3727	function clearEvents() {
3728		getDaySegmentContainer().empty();
3729		getSlotSegmentContainer().empty();
3730	}
3731
3732	
3733	function compileSlotSegs(events) {
3734		var colCnt = getColCnt(),
3735			minMinute = getMinMinute(),
3736			maxMinute = getMaxMinute(),
3737			d,
3738			visEventEnds = $.map(events, slotEventEnd),
3739			i,
3740			j, seg,
3741			colSegs,
3742			segs = [];
3743
3744		for (i=0; i<colCnt; i++) {
3745
3746			d = cellToDate(0, i);
3747			addMinutes(d, minMinute);
3748
3749			colSegs = sliceSegs(
3750				events,
3751				visEventEnds,
3752				d,
3753				addMinutes(cloneDate(d), maxMinute-minMinute)
3754			);
3755
3756			colSegs = placeSlotSegs(colSegs); // returns a new order
3757
3758			for (j=0; j<colSegs.length; j++) {
3759				seg = colSegs[j];
3760				seg.col = i;
3761				segs.push(seg);
3762			}
3763		}
3764
3765		return segs;
3766	}
3767
3768
3769	function sliceSegs(events, visEventEnds, start, end) {
3770		var segs = [],
3771			i, len=events.length, event,
3772			eventStart, eventEnd,
3773			segStart, segEnd,
3774			isStart, isEnd;
3775		for (i=0; i<len; i++) {
3776			event = events[i];
3777			eventStart = event.start;
3778			eventEnd = visEventEnds[i];
3779			if (eventEnd > start && eventStart < end) {
3780				if (eventStart < start) {
3781					segStart = cloneDate(start);
3782					isStart = false;
3783				}else{
3784					segStart = eventStart;
3785					isStart = true;
3786				}
3787				if (eventEnd > end) {
3788					segEnd = cloneDate(end);
3789					isEnd = false;
3790				}else{
3791					segEnd = eventEnd;
3792					isEnd = true;
3793				}
3794				segs.push({
3795					event: event,
3796					start: segStart,
3797					end: segEnd,
3798					isStart: isStart,
3799					isEnd: isEnd
3800				});
3801			}
3802		}
3803		return segs.sort(compareSlotSegs);
3804	}
3805
3806
3807	function slotEventEnd(event) {
3808		if (event.end) {
3809			return cloneDate(event.end);
3810		}else{
3811			return addMinutes(cloneDate(event.start), opt('defaultEventMinutes'));
3812		}
3813	}
3814	
3815	
3816	// renders events in the 'time slots' at the bottom
3817	// TODO: when we refactor this, when user returns `false` eventRender, don't have empty space
3818	// TODO: refactor will include using pixels to detect collisions instead of dates (handy for seg cmp)
3819	
3820	function renderSlotSegs(segs, modifiedEventId) {
3821	
3822		var i, segCnt=segs.length, seg,
3823			event,
3824			top,
3825			bottom,
3826			columnLeft,
3827			columnRight,
3828			columnWidth,
3829			width,
3830			left,
3831			right,
3832			html = '',
3833			eventElements,
3834			eventElement,
3835			triggerRes,
3836			titleElement,
3837			height,
3838			slotSegmentContainer = getSlotSegmentContainer(),
3839			isRTL = opt('isRTL');
3840			
3841		// calculate position/dimensions, create html
3842		for (i=0; i<segCnt; i++) {
3843			seg = segs[i];
3844			event = seg.event;
3845			top = timePosition(seg.start, seg.start);
3846			bottom = timePosition(seg.start, seg.end);
3847			columnLeft = colContentLeft(seg.col);
3848			columnRight = colContentRight(seg.col);
3849			columnWidth = columnRight - columnLeft;
3850
3851			// shave off space on right near scrollbars (2.5%)
3852			// TODO: move this to CSS somehow
3853			columnRight -= columnWidth * .025;
3854			columnWidth = columnRight - columnLeft;
3855
3856			width = columnWidth * (seg.forwardCoord - seg.backwardCoord);
3857
3858			if (opt('slotEventOverlap')) {
3859				// double the width while making sure resize handle is visible
3860				// (assumed to be 20px wide)
3861				width = Math.max(
3862					(width - (20/2)) * 2,
3863					width // narrow columns will want to make the segment smaller than
3864						// the natural width. don't allow it
3865				);
3866			}
3867
3868			if (isRTL) {
3869				right = columnRight - seg.backwardCoord * columnWidth;
3870				left = right - width;
3871			}
3872			else {
3873				left = columnLeft + seg.backwardCoord * columnWidth;
3874				right = left + width;
3875			}
3876
3877			// make sure horizontal coordinates are in bounds
3878			left = Math.max(left, columnLeft);
3879			right = Math.min(right, columnRight);
3880			width = right - left;
3881
3882			seg.top = top;
3883			seg.left = left;
3884			seg.outerWidth = width;
3885			seg.outerHeight = bottom - top;
3886			html += slotSegHtml(event, seg);
3887		}
3888
3889		slotSegmentContainer[0].innerHTML = html; // faster than html()
3890		eventElements = slotSegmentContainer.children();
3891		
3892		// retrieve elements, run through eventRender callback, bind event handlers
3893		for (i=0; i<segCnt; i++) {
3894			seg = segs[i];
3895			event = seg.event;
3896			eventElement = $(eventElements[i]); // faster than eq()
3897			triggerRes = trigger('eventRender', event, event, eventElement);
3898			if (triggerRes === false) {
3899				eventElement.remove();
3900			}else{
3901				if (triggerRes && triggerRes !== true) {
3902					eventElement.remove();
3903					eventElement = $(triggerRes)
3904						.css({
3905							position: 'absolute',
3906							top: seg.top,
3907							left: seg.left
3908						})
3909						.appendTo(slotSegmentContainer);
3910				}
3911				seg.element = eventElement;
3912				if (event._id === modifiedEventId) {
3913					bindSlotSeg(event, eventElement, seg);
3914				}else{
3915					eventElement[0]._fci = i; // for lazySegBind
3916				}
3917				reportEventElement(event, eventElement);
3918			}
3919		}
3920		
3921		lazySegBind(slotSegmentContainer, segs, bindSlotSeg);
3922		
3923		// record event sides and title positions
3924		for (i=0; i<segCnt; i++) {
3925			seg = segs[i];
3926			if (eventElement = seg.element) {
3927				seg.vsides = vsides(eventElement, true);
3928				seg.hsides = hsides(eventElement, true);
3929				titleElement = eventElement.find('.fc-event-title');
3930				if (titleElement.length) {
3931					seg.contentTop = titleElement[0].offsetTop;
3932				}
3933			}
3934		}
3935		
3936		// set all positions/dimensions at once
3937		for (i=0; i<segCnt; i++) {
3938			seg = segs[i];
3939			if (eventElement = seg.element) {
3940				eventElement[0].style.width = Math.max(0, seg.outerWidth - seg.hsides) + 'px';
3941				height = Math.max(0, seg.outerHeight - seg.vsides);
3942				eventElement[0].style.height = height + 'px';
3943				event = seg.event;
3944				if (seg.contentTop !== undefined && height - seg.contentTop < 10) {
3945					// not enough room for title, put it in the time (TODO: maybe make both display:inline instead)
3946					eventElement.find('div.fc-event-time')
3947						.text(formatDate(event.start, opt('timeFormat')) + ' - ' + event.title);
3948					eventElement.find('div.fc-event-title')
3949						.remove();
3950				}
3951				trigger('eventAfterRender', event, event, eventElement);
3952			}
3953		}
3954					
3955	}
3956	
3957	
3958	function slotSegHtml(event, seg) {
3959		var html = "<";
3960		var url = event.url;
3961		var ariaLabel = event.ariaLabel;
3962		var skinCss = getSkinCss(event, opt);
3963		var classes = ['fc-event', 'fc-event-vert'];
3964		if (isEventDraggable(event)) {
3965			classes.push('fc-event-draggable');
3966		}
3967		if (seg.isStart) {
3968			classes.push('fc-event-start');
3969		}
3970		if (seg.isEnd) {
3971			classes.push('fc-event-end');
3972		}
3973		classes = classes.concat(event.className);
3974		if (event.source) {
3975			classes = classes.concat(event.source.className || []);
3976		}
3977		if (url) {
3978			html += "a href='" + htmlEscape(event.url) + "'";
3979			    if (ariaLabel){
3980                    html += " aria-label='" + htmlEscape(ariaLabel) + "'";
3981			 }
3982		}else{
3983			html += "div";
3984		}
3985		html +=
3986			" class='" + classes.join(' ') + "'" +
3987			" style=" +
3988				"'" +
3989				"position:absolute;" +
3990				"top:" + seg.top + "px;" +
3991				"left:" + seg.left + "px;" +
3992				skinCss +
3993				"'" +
3994			">" +
3995			"<div class='fc-event-inner'>" +
3996			"<div class='fc-event-time'>" +
3997			htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) +
3998			"</div>" +
3999			"<div class='fc-event-title'>" +
4000			htmlEscape(event.title || '') +
4001			"</div>" +
4002			"</div>" +
4003			"<div class='fc-event-bg'></div>";
4004		if (seg.isEnd && isEventResizable(event)) {
4005			html +=
4006				"<div class='ui-resizable-handle ui-resizable-s'>=</div>";
4007		}
4008		html +=
4009			"</" + (url ? "a" : "div") + ">";
4010		return html;
4011	}
4012	
4013	
4014	function bindSlotSeg(event, eventElement, seg) {
4015		var timeElement = eventElement.find('div.fc-event-time');
4016		if (isEventDraggable(event)) {
4017			draggableSlotEvent(event, eventElement, timeElement);
4018		}
4019		if (seg.isEnd && isEventResizable(event)) {
4020			resizableSlotEvent(event, eventElement, timeElement);
4021		}
4022		eventElementHandlers(event, eventElement);
4023	}
4024	
4025	
4026	
4027	/* Dragging
4028	-----------------------------------------------------------------------------------*/
4029	
4030	
4031	// when event starts out FULL-DAY
4032	// overrides DayEventRenderer's version because it needs to account for dragging elements
4033	// to and from the slot area.
4034	
4035	function draggableDayEvent(event, eventElement, seg) {
4036		var isStart = seg.isStart;
4037		var origWidth;
4038		var revert;
4039		var allDay = true;
4040		var dayDelta;
4041		var hoverListener = getHoverListener();
4042		var colWidth = getColWidth();
4043		var snapHeight = getSnapHeight();
4044		var snapMinutes = getSnapMinutes();
4045		var minMinute = getMinMinute();
4046		eventElement.draggable({
4047			opacity: opt('dragOpacity', 'month'), // use whatever the month view was using
4048			revertDuration: opt('dragRevertDuration'),
4049			start: function(ev, ui) {
4050				trigger('eventDragStart', eventElement, event, ev, ui);
4051				hideEvents(event, eventElement);
4052				origWidth = eventElement.width();
4053				hoverListener.start(function(cell, origCell) {
4054					clearOverlays();
4055					if (cell) {
4056						revert = false;
4057						var origDate = cellToDate(0, origCell.col);
4058						var date = cellToDate(0, cell.col);
4059						dayDelta = dayDiff(date, origDate);
4060						if (!cell.row) {
4061							// on full-days
4062							renderDayOverlay(
4063								addDays(cloneDate(event.start), dayDelta),
4064								addDays(exclEndDay(event), dayDelta)
4065							);
4066							resetElement();
4067						}else{
4068							// mouse is over bottom slots
4069							if (isStart) {
4070								if (allDay) {
4071									// convert event to temporary slot-event
4072									eventElement.width(colWidth - 10); // don't use entire width
4073									setOuterHeight(
4074										eventElement,
4075										snapHeight * Math.round(
4076											(event.end ? ((event.end - event.start) / MINUTE_MS) : opt('defaultEventMinutes')) /
4077												snapMinutes
4078										)
4079									);
4080									eventElement.draggable('option', 'grid', [colWidth, 1]);
4081									allDay = false;
4082								}
4083							}else{
4084								revert = true;
4085							}
4086						}
4087						revert = revert || (allDay && !dayDelta);
4088					}else{
4089						resetElement();
4090						revert = true;
4091					}
4092					eventElement.draggable('option', 'revert', revert);
4093				}, ev, 'drag');
4094			},
4095			stop: function(ev, ui) {
4096				hoverListener.stop();
4097				clearOverlays();
4098				trigger('eventDragStop', eventElement, event, ev, ui);
4099				if (revert) {
4100					// hasn't moved or is out of bounds (draggable has already reverted)
4101					resetElement();
4102					eventElement.css('filter', ''); // clear IE opacity side-effects
4103					showEvents(event, eventElement);
4104				}else{
4105					// changed!
4106					var minuteDelta = 0;
4107					if (!allDay) {
4108						minuteDelta = Math.round((eventElement.offset().top - getSlotContainer().offset().top) / snapHeight)
4109							* snapMinutes
4110							+ minMinute
4111							- (event.start.getHours() * 60 + event.start.getMinutes());
4112					}
4113					eventDrop(this, event, dayDelta, minuteDelta, allDay, ev, ui);
4114				}
4115			}
4116		});
4117		function resetElement() {
4118			if (!allDay) {
4119				eventElement
4120					.width(origWidth)
4121					.height('')
4122					.draggable('option', 'grid', null);
4123				allDay = true;
4124			}
4125		}
4126	}
4127	
4128	
4129	// when event starts out IN TIMESLOTS
4130	
4131	function draggableSlotEvent(event, eventElement, timeElement) {
4132		var coordinateGrid = t.getCoordinateGrid();
4133		var colCnt = getColCnt();
4134		var colWidth = getColWidth();
4135		var snapHeight = getSnapHeight();
4136		var snapMinutes = getSnapMinutes();
4137
4138		// states
4139		var origPosition; // original position of the element, not the mouse
4140		var origCell;
4141		var isInBounds, prevIsInBounds;
4142		var isAllDay, prevIsAllDay;
4143		var colDelta, prevColDelta;
4144		var dayDelta; // derived from colDelta
4145		var minuteDelta, prevMinuteDelta;
4146
4147		eventElement.draggable({
4148			scroll: false,
4149			grid: [ colWidth, snapHeight ],
4150			axis: colCnt==1 ? 'y' : false,
4151			opacity: opt('dragOpacity'),
4152			revertDuration: opt('dragRevertDuration'),
4153			start: function(ev, ui) {
4154
4155				trigger('eventDragStart', eventElement, event, ev, ui);
4156				hideEvents(event, eventElement);
4157
4158				coordinateGrid.build();
4159
4160				// initialize states
4161				origPosition = eventElement.position();
4162				origCell = coordinateGrid.cell(ev.pageX, ev.pageY);
4163				isInBounds = prevIsInBounds = true;
4164				isAllDay = prevIsAllDay = getIsCellAllDay(origCell);
4165				colDelta = prevColDelta = 0;
4166				dayDelta = 0;
4167				minuteDelta = prevMinuteDelta = 0;
4168
4169			},
4170			drag: function(ev, ui) {
4171
4172				// NOTE: this `cell` value is only useful for determining in-bounds and all-day.
4173				// Bad for anything else due to the discrepancy between the mouse position and the
4174				// element position while snapping. (problem revealed in PR #55)
4175				//
4176				// PS- the problem exists for draggableDayEvent() when dragging an all-day event to a slot event.
4177				// We should overhaul the dragging system and stop relying on jQuery UI.
4178				var cell = coordinateGrid.cell(ev.pageX, ev.pageY);
4179
4180				// update states
4181				isInBounds = !!cell;
4182				if (isInBounds) {
4183					isAllDay = getIsCellAllDay(cell);
4184
4185					// calculate column delta
4186					colDelta = Math.round((ui.position.left - origPosition.left) / colWidth);
4187					if (colDelta != prevColDelta) {
4188						// calculate the day delta based off of the original clicked column and the column delta
4189						var origDate = cellToDate(0, origCell.col);
4190						var col = origCell.col + colDelta;
4191						col = Math.max(0, col);
4192						col = Math.min(colCnt-1, col);
4193						var date = cellToDate(0, col);
4194						dayDelta = dayDiff(date, origDate);
4195					}
4196
4197					// calculate minute delta (only if over slots)
4198					if (!isAllDay) {
4199						minuteDelta = Math.round((ui.position.top - origPosition.top) / snapHeight) * snapMinutes;
4200					}
4201				}
4202
4203				// any state changes?
4204				if (
4205					isInBounds != prevIsInBounds ||
4206					isAllDay != prevIsAllDay ||
4207					colDelta != prevColDelta ||
4208					minuteDelta != prevMinuteDelta
4209				) {
4210
4211					updateUI();
4212
4213					// update previous states for next time
4214					prevIsInBounds = isInBounds;
4215					prevIsAllDay = isAllDay;
4216					prevColDelta = colDelta;
4217					prevMinuteDelta = minuteDelta;
4218				}
4219
4220				// if out-of-bounds, revert when done, and vice versa.
4221				eventElement.draggable('option', 'revert', !isInBounds);
4222
4223			},
4224			stop: function(ev, ui) {
4225
4226				clearOverlays();
4227				trigger('eventDragStop', eventElement, event, ev, ui);
4228
4229				if (isInBounds && (isAllDay || dayDelta || minuteDelta)) { // changed!
4230					eventDrop(this, event, dayDelta, isAllDay ? 0 : minuteDelta, isAllDay, ev, ui);
4231				}
4232				else { // either no change or out-of-bounds (draggable has already reverted)
4233
4234					// reset states for next time, and for updateUI()
4235					isInBounds = true;
4236					isAllDay = false;
4237					colDelta = 0;
4238					dayDelta = 0;
4239					minuteDelta = 0;
4240
4241					updateUI();
4242					eventElement.css('filter', ''); // clear IE opacity side-effects
4243
4244					// sometimes fast drags make event revert to wrong position, so reset.
4245					// also, if we dragged the element out of the area because of snapping,
4246					// but the *mouse* is still in bounds, we need to reset the position.
4247					eventElement.css(origPosition);
4248
4249					showEvents(event, eventElement);
4250				}
4251			}
4252		});
4253
4254		function updateUI() {
4255			clearOverlays();
4256			if (isInBounds) {
4257				if (isAllDay) {
4258					timeElement.hide();
4259					eventElement.draggable('option', 'grid', null); // disable grid snapping
4260					renderDayOverlay(
4261						addDays(cloneDate(event.start), dayDelta),
4262						addDays(exclEndDay(event), dayDelta)
4263					);
4264				}
4265				else {
4266					updateTimeText(minuteDelta);
4267					timeElement.css('display', ''); // show() was causing display=inline
4268					eventElement.draggable('option', 'grid', [colWidth, snapHeight]); // re-enable grid snapping
4269				}
4270			}
4271		}
4272
4273		function updateTimeText(minuteDelta) {
4274			var newStart = addMinutes(cloneDate(event.start), minuteDelta);
4275			var newEnd;
4276			if (event.end) {
4277				newEnd = addMinutes(cloneDate(event.end), minuteDelta);
4278			}
4279			timeElement.text(formatDates(newStart, newEnd, opt('timeFormat')));
4280		}
4281
4282	}
4283	
4284	
4285	
4286	/* Resizing
4287	--------------------------------------------------------------------------------------*/
4288	
4289	
4290	function resizableSlotEvent(event, eventElement, timeElement) {
4291		var snapDelta, prevSnapDelta;
4292		var snapHeight = getSnapHeight();
4293		var snapMinutes = getSnapMinutes();
4294		eventElement.resizable({
4295			handles: {
4296				s: '.ui-resizable-handle'
4297			},
4298			grid: snapHeight,
4299			start: function(ev, ui) {
4300				snapDelta = prevSnapDelta = 0;
4301				hideEvents(event, eventElement);
4302				trigger('eventResizeStart', this, event, ev, ui);
4303			},
4304			resize: function(ev, ui) {
4305				// don't rely on ui.size.height, doesn't take grid into account
4306				snapDelta = Math.round((Math.max(snapHeight, eventElement.height()) - ui.originalSize.height) / snapHeight);
4307				if (snapDelta != prevSnapDelta) {
4308					timeElement.text(
4309						formatDates(
4310							event.start,
4311							(!snapDelta && !event.end) ? null : // no change, so don't display time range
4312								addMinutes(eventEnd(event), snapMinutes*snapDelta),
4313							opt('timeFormat')
4314						)
4315					);
4316					prevSnapDelta = snapDelta;
4317				}
4318			},
4319			stop: function(ev, ui) {
4320				trigger('eventResizeStop', this, event, ev, ui);
4321				if (snapDelta) {
4322					eventResize(this, event, 0, snapMinutes*snapDelta, ev, ui);
4323				}else{
4324					showEvents(event, eventElement);
4325					// BUG: if event was really short, need to put title back in span
4326				}
4327			}
4328		});
4329	}
4330	
4331
4332}
4333
4334
4335
4336/* Agenda Event Segment Utilities
4337-----------------------------------------------------------------------------*/
4338
4339
4340// Sets the seg.backwardCoord and seg.forwardCoord on each segment and returns a new
4341// list in the order they should be placed into the DOM (an implicit z-index).
4342function placeSlotSegs(segs) {
4343	var levels = buildSlotSegLevels(segs);
4344	var level0 = levels[0];
4345	var i;
4346
4347	computeForwardSlotSegs(levels);
4348
4349	if (level0) {
4350
4351		for (i=0; i<level0.length; i++) {
4352			computeSlotSegPressures(level0[i]);
4353		}
4354
4355		for (i=0; i<level0.length; i++) {
4356			computeSlotSegCoords(level0[i], 0, 0);
4357		}
4358	}
4359
4360	return flattenSlotSegLevels(levels);
4361}
4362
4363
4364// Builds an array of segments "levels". The first level will be the leftmost tier of segments
4365// if the calendar is left-to-right, or the rightmost if the calendar is right-to-left.
4366function buildSlotSegLevels(segs) {
4367	var levels = [];
4368	var i, seg;
4369	var j;
4370
4371	for (i=0; i<segs.length; i++) {
4372		seg = segs[i];
4373
4374		// go through all the levels and stop on the first level where there are no collisions
4375		for (j=0; j<levels.length; j++) {
4376			if (!computeSlotSegCollisions(seg, levels[j]).length) {
4377				break;
4378			}
4379		}
4380
4381		(levels[j] || (levels[j] = [])).push(seg);
4382	}
4383
4384	return levels;
4385}
4386
4387
4388// For every segment, figure out the other segments that are in subsequent
4389// levels that also occupy the same vertical space. Accumulate in seg.forwardSegs
4390function computeForwardSlotSegs(levels) {
4391	var i, level;
4392	var j, seg;
4393	var k;
4394
4395	for (i=0; i<levels.length; i++) {
4396		level = levels[i];
4397
4398		for (j=0; j<level.length; j++) {
4399			seg = level[j];
4400
4401			seg.forwardSegs = [];
4402			for (k=i+1; k<levels.length; k++) {
4403				computeSlotSegCollisions(seg, levels[k], seg.forwardSegs);
4404			}
4405		}
4406	}
4407}
4408
4409
4410// Figure out which path forward (via seg.forwardSegs) results in the longest path until
4411// the furthest edge is reached. The number of segments in this path will be seg.forwardPressure
4412function computeSlotSegPressures(seg) {
4413	var forwardSegs = seg.forwardSegs;
4414	var forwardPressure = 0;
4415	var i, forwardSeg;
4416
4417	if (seg.forwardPressure === undefined) { // not already computed
4418
4419		for (i=0; i<forwardSegs.length; i++) {
4420			forwardSeg = forwardSegs[i];
4421
4422			// figure out the child's maximum forward path
4423			computeSlotSegPressures(forwardSeg);
4424
4425			// either use the existing maximum, or use the child's forward pressure
4426			// plus one (for the forwardSeg itself)
4427			forwardPressure = Math.max(
4428				forwardPressure,
4429				1 + forwardSeg.forwardPressure
4430			);
4431		}
4432
4433		seg.forwardPressure = forwardPressure;
4434	}
4435}
4436
4437
4438// Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
4439// from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
4440// seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
4441//
4442// The segment might be part of a "series", which means consecutive segments with the same pressure
4443// who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
4444// segments behind this one in the current series, and `seriesBackwardCoord` is the starting
4445// coordinate of the first segment in the series.
4446function computeSlotSegCoords(seg, seriesBackwardPressure, seriesBackwardCoord) {
4447	var forwardSegs = seg.forwardSegs;
4448	var i;
4449
4450	if (seg.forwardCoord === undefined) { // not already computed
4451
4452		if (!forwardSegs.length) {
4453
4454			// if there are no forward segments, this segment should butt up against the edge
4455			seg.forwardCoord = 1;
4456		}
4457		else {
4458
4459			// sort highest pressure first
4460			forwardSegs.sort(compareForwardSlotSegs);
4461
4462			// this segment's forwardCoord will be calculated from the backwardCoord of the
4463			// highest-pressure forward segment.
4464			computeSlotSegCoords(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord);
4465			seg.forwardCoord = forwardSegs[0].backwardCoord;
4466		}
4467
4468		// calculate the backwardCoord from the forwardCoord. consider the series
4469		seg.backwardCoord = seg.forwardCoord -
4470			(seg.forwardCoord - seriesBackwardCoord) / // available width for series
4471			(seriesBackwardPressure + 1); // # of segments in the series
4472
4473		// use this segment's coordinates to computed the coordinates of the less-pressurized
4474		// forward segments
4475		for (i=0; i<forwardSegs.length; i++) {
4476			computeSlotSegCoords(forwardSegs[i], 0, seg.forwardCoord);
4477		}
4478	}
4479}
4480
4481
4482// Outputs a flat array of segments, from lowest to highest level
4483function flattenSlotSegLevels(levels) {
4484	var segs = [];
4485	var i, level;
4486	var j;
4487
4488	for (i=0; i<levels.length; i++) {
4489		level = levels[i];
4490
4491		for (j=0; j<level.length; j++) {
4492			segs.push(level[j]);
4493		}
4494	}
4495
4496	return segs;
4497}
4498
4499
4500// Find all the segments in `otherSegs` that vertically collide with `seg`.
4501// Append into an optionally-supplied `results` array and return.
4502function computeSlotSegCollisions(seg, otherSegs, results) {
4503	results = results || [];
4504
4505	for (var i=0; i<otherSegs.length; i++) {
4506		if (isSlotSegCollision(seg, otherSegs[i])) {
4507			results.push(otherSegs[i]);
4508		}
4509	}
4510
4511	return results;
4512}
4513
4514
4515// Do these segments occupy the same vertical space?
4516function isSlotSegCollision(seg1, seg2) {
4517	return seg1.end > seg2.start && seg1.start < seg2.end;
4518}
4519
4520
4521// A cmp function for determining which forward segment to rely on more when computing coordinates.
4522function compareForwardSlotSegs(seg1, seg2) {
4523	// put higher-pressure first
4524	return seg2.forwardPressure - seg1.forwardPressure ||
4525		// put segments that are closer to initial edge first (and favor ones with no coords yet)
4526		(seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) ||
4527		// do normal sorting...
4528		compareSlotSegs(seg1, seg2);
4529}
4530
4531
4532// A cmp function for determining which segment should be closer to the initial edge
4533// (the left edge on a left-to-right calendar).
4534function compareSlotSegs(seg1, seg2) {
4535	return seg1.start - seg2.start || // earlier start time goes first
4536		(seg2.end - seg2.start) - (seg1.end - seg1.start) || // tie? longer-duration goes first
4537		(seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title
4538}
4539
4540
4541;;
4542
4543
4544function View(element, calendar, viewName) {
4545	var t = this;
4546	
4547	
4548	// exports
4549	t.element = element;
4550	t.calendar = calendar;
4551	t.name = viewName;
4552	t.opt = opt;
4553	t.trigger = trigger;
4554	t.isEventDraggable = isEventDraggable;
4555	t.isEventResizable = isEventResizable;
4556	t.setEventData = setEventData;
4557	t.clearEventData = clearEventData;
4558	t.eventEnd = eventEnd;
4559	t.reportEventElement = reportEventElement;
4560	t.triggerEventDestroy = triggerEventDestroy;
4561	t.eventElementHandlers = eventElementHandlers;
4562	t.showEvents = showEvents;
4563	t.hideEvents = hideEvents;
4564	t.eventDrop = eventDrop;
4565	t.eventResize = eventResize;
4566	// t.title
4567	// t.start, t.end
4568	// t.visStart, t.visEnd
4569	
4570	
4571	// imports
4572	var defaultEventEnd = t.defaultEventEnd;
4573	var normalizeEvent = calendar.normalizeEvent; // in EventManager
4574	var reportEventChange = calendar.reportEventChange;
4575	
4576	
4577	// locals
4578	var eventsByID = {}; // eventID mapped to array of events (there can be multiple b/c of repeating events)
4579	var eventElementsByID = {}; // eventID mapped to array of jQuery elements
4580	var eventElementCouples = []; // array of objects, { event, element } // TODO: unify with segment system
4581	var options = calendar.options;
4582	
4583	
4584	
4585	function opt(name, viewNameOverride) {
4586		var v = options[name];
4587		if ($.isPlainObject(v)) {
4588			return smartProperty(v, viewNameOverride || viewName);
4589		}
4590		return v;
4591	}
4592
4593	
4594	function trigger(name, thisObj) {
4595		return calendar.trigger.apply(
4596			calendar,
4597			[name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t])
4598		);
4599	}
4600	
4601
4602
4603	/* Event Editable Boolean Calculations
4604	------------------------------------------------------------------------------*/
4605
4606	
4607	function isEventDraggable(event) {
4608		var source = event.source || {};
4609		return firstDefined(
4610				event.startEditable,
4611				source.startEditable,
4612				opt('eventStartEditable'),
4613				event.editable,
4614				source.editable,
4615				opt('editable')
4616			)
4617			&& !opt('disableDragging'); // deprecated
4618	}
4619	
4620	
4621	function isEventResizable(event) { // but also need to make sure the seg.isEnd == true
4622		var source = event.source || {};
4623		return firstDefined(
4624				event.durationEditable,
4625				source.durationEditable,
4626				opt('eventDurationEditable'),
4627				event.editable,
4628				source.editable,
4629				opt('editable')
4630			)
4631			&& !opt('disableResizing'); // deprecated
4632	}
4633	
4634	
4635	
4636	/* Event Data
4637	------------------------------------------------------------------------------*/
4638	
4639	
4640	function setEventData(events) { // events are already normalized at this point
4641		eventsByID = {};
4642		var i, len=events.length, event;
4643		for (i=0; i<len; i++) {
4644			event = events[i];
4645			if (eventsByID[event._id]) {
4646				eventsByID[event._id].push(event);
4647			}else{
4648				eventsByID[event._id] = [event];
4649			}
4650		}
4651	}
4652
4653
4654	function clearEventData() {
4655		eventsByID = {};
4656		eventElementsByID = {};
4657		eventElementCouples = [];
4658	}
4659	
4660	
4661	// returns a Date object for an event's end
4662	function eventEnd(event) {
4663		return event.end ? cloneDate(event.end) : defaultEventEnd(event);
4664	}
4665	
4666	
4667	
4668	/* Event Elements
4669	------------------------------------------------------------------------------*/
4670	
4671	
4672	// report when view creates an element for an event
4673	function reportEventElement(event, element) {
4674		eventElementCouples.push({ event: event, element: element });
4675		if (eventElementsByID[event._id]) {
4676			eventElementsByID[event._id].push(element);
4677		}else{
4678			eventElementsByID[event._id] = [element];
4679		}
4680	}
4681
4682
4683	function triggerEventDestroy() {
4684		$.each(eventElementCouples, function(i, couple) {
4685			t.trigger('eventDestroy', couple.event, couple.event, couple.element);
4686		});
4687	}
4688	
4689	
4690	// attaches eventClick, eventMouseover, eventMouseout
4691	function eventElementHandlers(event, eventElement) {
4692		eventElement
4693			.click(function(ev) {
4694				if (!eventElement.hasClass('ui-draggable-dragging') &&
4695					!eventElement.hasClass('ui-resizable-resizing')) {
4696						return trigger('eventClick', this, event, ev);
4697					}
4698			})
4699			.hover(
4700				function(ev) {
4701					trigger('eventMouseover', this, event, ev);
4702				},
4703				function(ev) {
4704					trigger('eventMouseout', this, event, ev);
4705				}
4706			);
4707		// TODO: don't fire eventMouseover/eventMouseout *while* dragging is occuring (on subject element)
4708		// TODO: same for resizing
4709	}
4710	
4711	
4712	function showEvents(event, exceptElement) {
4713		eachEventElement(event, exceptElement, 'show');
4714	}
4715	
4716	
4717	function hideEvents(event, exceptElement) {
4718		eachEventElement(event, exceptElement, 'hide');
4719	}
4720	
4721	
4722	function eachEventElement(event, exceptElement, funcName) {
4723		// NOTE: there may be multiple events per ID (repeating events)
4724		// and multiple segments per event
4725		var elements = eventElementsByID[event._id],
4726			i, len = elements.length;
4727		for (i=0; i<len; i++) {
4728			if (!exceptElement || elements[i][0] != exceptElement[0]) {
4729				elements[i][funcName]();
4730			}
4731		}
4732	}
4733	
4734	
4735	
4736	/* Event Modification Reporting
4737	---------------------------------------------------------------------------------*/
4738	
4739	
4740	function eventDrop(e, event, dayDelta, minuteDelta, allDay, ev, ui) {
4741		var oldAllDay = event.allDay;
4742		var eventId = event._id;
4743		moveEvents(eventsByID[eventId], dayDelta, minuteDelta, allDay);
4744		trigger(
4745			'eventDrop',
4746			e,
4747			event,
4748			dayDelta,
4749			minuteDelta,
4750			allDay,
4751			function() {
4752				// TODO: investigate cases where this inverse technique might not work
4753				moveEvents(eventsByID[eventId], -dayDelta, -minuteDelta, oldAllDay);
4754				reportEventChange(eventId);
4755			},
4756			ev,
4757			ui
4758		);
4759		reportEventChange(eventId);
4760	}
4761	
4762	
4763	function eventResize(e, event, dayDelta, minuteDelta, ev, ui) {
4764		var eventId = event._id;
4765		elongateEvents(eventsByID[eventId], dayDelta, minuteDelta);
4766		trigger(
4767			'eventResize',
4768			e,
4769			event,
4770			dayDelta,
4771			minuteDelta,
4772			function() {
4773				// TODO: investigate cases where this inverse technique might not work
4774				elongateEvents(eventsByID[eventId], -dayDelta, -minuteDelta);
4775				reportEventChange(eventId);
4776			},
4777			ev,
4778			ui
4779		);
4780		reportEventChange(eventId);
4781	}
4782	
4783	
4784	
4785	/* Event Modification Math
4786	---------------------------------------------------------------------------------*/
4787	
4788	
4789	function moveEvents(events, dayDelta, minuteDelta, allDay) {
4790		minuteDelta = minuteDelta || 0;
4791		for (var e, len=events.length, i=0; i<len; i++) {
4792			e = events[i];
4793			if (allDay !== undefined) {
4794				e.allDay = allDay;
4795			}
4796			addMinutes(addDays(e.start, dayDelta, true), minuteDelta);
4797			if (e.end) {
4798				e.end = addMinutes(addDays(e.end, dayDelta, true), minuteDelta);
4799			}
4800			normalizeEvent(e, options);
4801		}
4802	}
4803	
4804	
4805	function elongateEvents(events, dayDelta, minuteDelta) {
4806		minuteDelta = minuteDelta || 0;
4807		for (var e, len=events.length, i=0; i<len; i++) {
4808			e = events[i];
4809			e.end = addMinutes(addDays(eventEnd(e), dayDelta, true), minuteDelta);
4810			normalizeEvent(e, options);
4811		}
4812	}
4813
4814
4815
4816	// ====================================================================================================
4817	// Utilities for day "cells"
4818	// ====================================================================================================
4819	// The "basic" views are completely made up of day cells.
4820	// The "agenda" views have day cells at the top "all day" slot.
4821	// This was the obvious common place to put these utilities, but they should be abstracted out into
4822	// a more meaningful class (like DayEventRenderer).
4823	// ====================================================================================================
4824
4825
4826	// For determining how a given "cell" translates into a "date":
4827	//
4828	// 1. Convert the "cell" (row and column) into a "cell offset" (the # of the cell, cronologically from the first).
4829	//    Keep in mind that column indices are inverted with isRTL. This is taken into account.
4830	//
4831	// 2. Convert the "cell offset" to a "day offset" (the # of days since the first visible day in the view).
4832	//
4833	// 3. Convert the "day offset" into a "date" (a JavaScript Date object).
4834	//
4835	// The reverse transformation happens when transforming a date into a cell.
4836
4837
4838	// exports
4839	t.isHiddenDay = isHiddenDay;
4840	t.skipHiddenDays = skipHiddenDays;
4841	t.getCellsPerWeek = getCellsPerWeek;
4842	t.dateToCell = dateToCell;
4843	t.dateToDayOffset = dateToDayOffset;
4844	t.dayOffsetToCellOffset = dayOffsetToCellOffset;
4845	t.cellOffsetToCell = cellOffsetToCell;
4846	t.cellToDate = cellToDate;
4847	t.cellToCellOffset = cellToCellOffset;
4848	t.cellOffsetToDayOffset = cellOffsetToDayOffset;
4849	t.dayOffsetToDate = dayOffsetToDate;
4850	t.rangeToSegments = rangeToSegments;
4851
4852
4853	// internals
4854	var hiddenDays = opt('hiddenDays') || []; // array of day-of-week indices that are hidden
4855	var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
4856	var cellsPerWeek;
4857	var dayToCellMap = []; // hash from dayIndex -> cellIndex, for one week
4858	var cellToDayMap = []; // hash from cellIndex -> dayIndex, for one week
4859	var isRTL = opt('isRTL');
4860
4861
4862	// initialize important internal variables
4863	(function() {
4864
4865		if (opt('weekends') === false) {
4866			hiddenDays.push(0, 6); // 0=sunday, 6=saturday
4867		}
4868
4869		// Loop through a hypothetical week and determine which
4870		// days-of-week are hidden. Record in both hashes (one is the reverse of the other).
4871		for (var dayIndex=0, cellIndex=0; dayIndex<7; dayIndex++) {
4872			dayToCellMap[dayIndex] = cellIndex;
4873			isHiddenDayHash[dayIndex] = $.inArray(dayIndex, hiddenDays) != -1;
4874			if (!isHiddenDayHash[dayIndex]) {
4875				cellToDayMap[cellIndex] = dayIndex;
4876				cellIndex++;
4877			}
4878		}
4879
4880		cellsPerWeek = cellIndex;
4881		if (!cellsPerWeek) {
4882			throw 'invalid hiddenDays'; // all days were hidden? bad.
4883		}
4884
4885	})();
4886
4887
4888	// Is the current day hidden?
4889	// `day` is a day-of-week index (0-6), or a Date object
4890	function isHiddenDay(day) {
4891		if (typeof day == 'object') {
4892			day = day.getDay();
4893		}
4894		return isHiddenDayHash[day];
4895	}
4896
4897
4898	function getCellsPerWeek() {
4899		return cellsPerWeek;
4900	}
4901
4902
4903	// Keep incrementing the current day until it is no longer a hidden day.
4904	// If the initial value of `date` is not a hidden day, don't do anything.
4905	// Pass `isExclusive` as `true` if you are dealing with an end date.
4906	// `inc` defaults to `1` (increment one day forward each time)
4907	function skipHiddenDays(date, inc, isExclusive) {
4908		inc = inc || 1;
4909		while (
4910			isHiddenDayHash[ ( date.getDay() + (isExclusive ? inc : 0) + 7 ) % 7 ]
4911		) {
4912			addDays(date, inc);
4913		}
4914	}
4915
4916
4917	//
4918	// TRANSFORMATIONS: cell -> cell offset -> day offset -> date
4919	//
4920
4921	// cell -> date (combines all transformations)
4922	// Possible arguments:
4923	// - row, col
4924	// - { row:#, col: # }
4925	function cellToDate() {
4926		var cellOffset = cellToCellOffset.apply(null, arguments);
4927		var dayOffset = cellOffsetToDayOffset(cellOffset);
4928		var date = dayOffsetToDate(dayOffset);
4929		return date;
4930	}
4931
4932	// cell -> cell offset
4933	// Possible arguments:
4934	// - row, col
4935	// - { row:#, col:# }
4936	function cellToCellOffset(row, col) {
4937		var colCnt = t.getColCnt();
4938
4939		// rtl variables. wish we could pre-populate these. but where?
4940		var dis = isRTL ? -1 : 1;
4941		var dit = isRTL ? colCnt - 1 : 0;
4942
4943		if (typeof row == 'object') {
4944			col = row.col;
4945			row = row.row;
4946		}
4947		var cellOffset = row * colCnt + (col * dis + dit); // column, adjusted for RTL (dis & dit)
4948
4949		return cellOffset;
4950	}
4951
4952	// cell offset -> day offset
4953	function cellOffsetToDayOffset(cellOffset) {
4954		var day0 = t.visStart.getDay(); // first date's day of week
4955		cellOffset += dayToCellMap[day0]; // normlize cellOffset to beginning-of-week
4956		return Math.floor(cellOffset / cellsPerWeek) * 7 // # of days from full weeks
4957			+ cellToDayMap[ // # of days from partial last week
4958				(cellOffset % cellsPerWeek + cellsPerWeek) % cellsPerWeek // crazy math to handle negative cellOffsets
4959			]
4960			- day0; // adjustment for beginning-of-week normalization
4961	}
4962
4963	// day offset -> date (JavaScript Date object)
4964	function dayOffsetToDate(dayOffset) {
4965		var date = cloneDate(t.visStart);
4966		addDays(date, dayOffset);
4967		return date;
4968	}
4969
4970
4971	//
4972	// TRANSFORMATIONS: date -> day offset -> cell offset -> cell
4973	//
4974
4975	// date -> cell (combines all transformations)
4976	function dateToCell(date) {
4977		var dayOffset = dateToDayOffset(date);
4978		var cellOffset = dayOffsetToCellOffset(dayOffset);
4979		var cell = cellOffsetToCell(cellOffset);
4980		return cell;
4981	}
4982
4983	// date -> day offset
4984	function dateToDayOffset(date) {
4985		return dayDiff(date, t.visStart);
4986	}
4987
4988	// day offset -> cell offset
4989	function dayOffsetToCellOffset(dayOffset) {
4990		var day0 = t.visStart.getDay(); // first date's day of week
4991		dayOffset += day0; // normalize dayOffset to beginning-of-week
4992		return Math.floor(dayOffset / 7) * cellsPerWeek // # of cells from full weeks
4993			+ dayToCellMap[ // # of cells from partial last week
4994				(dayOffset % 7 + 7) % 7 // crazy math to handle negative dayOffsets
4995			]
4996			- dayToCellMap[day0]; // adjustment for beginning-of-week normalization
4997	}
4998
4999	// cell offset -> cell (object with row & col keys)
5000	function cellOffsetToCell(cellOffset) {
5001		var colCnt = t.getColCnt();
5002
5003		// rtl variables. wish we could pre-populate these. but where?
5004		var dis = isRTL ? -1 : 1;
5005		var dit = isRTL ? colCnt - 1 : 0;
5006
5007		var row = Math.floor(cellOffset / colCnt);
5008		var col = ((cellOffset % colCnt + colCnt) % colCnt) * dis + dit;
5008 // column, adjusted for RTL (dis & dit)
5009		return {
5010			row: row,
5011			col: col
5012		};
5013	}
5014
5015
5016	//
5017	// Converts a date range into an array of segment objects.
5018	// "Segments" are horizontal stretches of time, sliced up by row.
5019	// A segment object has the following properties:
5020	// - row
5021	// - cols
5022	// - isStart
5023	// - isEnd
5024	//
5025	function rangeToSegments(startDate, endDate) {
5026		var rowCnt = t.getRowCnt();
5027		var colCnt = t.getColCnt();
5028		var segments = []; // array of segments to return
5029
5030		// day offset for given date range
5031		var rangeDayOffsetStart = dateToDayOffset(startDate);
5032		var rangeDayOffsetEnd = dateToDayOffset(endDate); // exclusive
5033
5034		// first and last cell offset for the given date range
5035		// "last" implies inclusivity
5036		var rangeCellOffsetFirst = dayOffsetToCellOffset(rangeDayOffsetStart);
5037		var rangeCellOffsetLast = dayOffsetToCellOffset(rangeDayOffsetEnd) - 1;
5038
5039		// loop through all the rows in the view
5040		for (var row=0; row<rowCnt; row++) {
5041
5042			// first and last cell offset for the row
5043			var rowCellOffsetFirst = row * colCnt;
5044			var rowCellOffsetLast = rowCellOffsetFirst + colCnt - 1;
5045
5046			// get the segment's cell offsets by constraining the range's cell offsets to the bounds of the row
5047			var segmentCellOffsetFirst = Math.max(rangeCellOffsetFirst, rowCellOffsetFirst);
5048			var segmentCellOffsetLast = Math.min(rangeCellOffsetLast, rowCellOffsetLast);
5049
5050			// make sure segment's offsets are valid and in view
5051			if (segmentCellOffsetFirst <= segmentCellOffsetLast) {
5052
5053				// translate to cells
5054				var segmentCellFirst = cellOffsetToCell(segmentCellOffsetFirst);
5055				var segmentCellLast = cellOffsetToCell(segmentCellOffsetLast);
5056
5057				// view might be RTL, so order by leftmost column
5058				var cols = [ segmentCellFirst.col, segmentCellLast.col ].sort();
5059
5060				// Determine if segment's first/last cell is the beginning/end of the date range.
5061				// We need to compare "day offset" because "cell offsets" are often ambiguous and
5062				// can translate to multiple days, and an edge case reveals itself when we the
5063				// range's first cell is hidden (we don't want isStart to be true).
5064				var isStart = cellOffsetToDayOffset(segmentCellOffsetFirst) == rangeDayOffsetStart;
5065				var isEnd = cellOffsetToDayOffset(segmentCellOffsetLast) + 1 == rangeDayOffsetEnd; // +1 for comparing exclusively
5066
5067				segments.push({
5068					row: row,
5069					leftCol: cols[0],
5070					rightCol: cols[1],
5071					isStart: isStart,
5072					isEnd: isEnd
5073				});
5074			}
5075		}
5076
5077		return segments;
5078	}
5079	
5080
5081}
5082
5083;;
5084
5085function DayEventRenderer() {
5086	var t = this;
5087
5088	
5089	// exports
5090	t.renderDayEvents = renderDayEvents;
5091	t.draggableDayEvent = draggableDayEvent; // made public so that subclasses can override
5092	t.resizableDayEvent = resizableDayEvent; // "
5093	
5094	
5095	// imports
5096	var opt = t.opt;
5097	var trigger = t.trigger;
5098	var isEventDraggable = t.isEventDraggable;
5099	var isEventResizable = t.isEventResizable;
5100	var eventEnd = t.eventEnd;
5101	var reportEventElement = t.reportEventElement;
5102	var eventElementHandlers = t.eventElementHandlers;
5103	var showEvents = t.showEvents;
5104	var hideEvents = t.hideEvents;
5105	var eventDrop = t.eventDrop;
5106	var eventResize = t.eventResize;
5107	var getRowCnt = t.getRowCnt;
5108	var getColCnt = t.getColCnt;
5109	var getColWidth = t.getColWidth;
5110	var allDayRow = t.allDayRow; // TODO: rename
5111	var colLeft = t.colLeft;
5112	var colRight = t.colRight;
5113	var colContentLeft = t.colContentLeft;
5114	var colContentRight = t.colContentRight;
5115	var dateToCell = t.dateToCell;
5116	var getDaySegmentContainer = t.getDaySegmentContainer;
5117	var formatDates = t.calendar.formatDates;
5118	var renderDayOverlay = t.renderDayOverlay;
5119	var clearOverlays = t.clearOverlays;
5120	var clearSelection = t.clearSelection;
5121	var getHoverListener = t.getHoverListener;
5122	var rangeToSegments = t.rangeToSegments;
5123	var cellToDate = t.cellToDate;
5124	var cellToCellOffset = t.cellToCellOffset;
5125	var cellOffsetToDayOffset = t.cellOffsetToDayOffset;
5126	var dateToDayOffset = t.dateToDayOffset;
5127	var dayOffsetToCellOffset = t.dayOffsetToCellOffset;
5128
5129
5130	// Render `events` onto the calendar, attach mouse event handlers, and call the `eventAfterRender` callback for each.
5131	// Mouse event will be lazily applied, except if the event has an ID of `modifiedEventId`.
5132	// Can only be called when the event container is empty (because it wipes out all innerHTML).
5133	function renderDayEvents(events, modifiedEventId) {
5134
5135		// do the actual rendering. Receive the intermediate "segment" data structures.
5136		var segments = _renderDayEvents(
5137			events,
5138			false, // don't append event elements
5139			true // set the heights of the rows
5140		);
5141
5142		// report the elements to the View, for general drag/resize utilities
5143		segmentElementEach(segments, function(segment, element) {
5144			reportEventElement(segment.event, element);
5145		});
5146
5147		// attach mouse handlers
5148		attachHandlers(segments, modifiedEventId);
5149
5150		// call `eventAfterRender` callback for each event
5151		segmentElementEach(segments, function(segment, element) {
5152			trigger('eventAfterRender', segment.event, segment.event, element);
5153		});
5154	}
5155
5156
5157	// Render an event on the calendar, but don't report them anywhere, and don't attach mouse handlers.
5158	// Append this event element to the event container, which might already be populated with events.
5159	// If an event's segment will have row equal to `adjustRow`, then explicitly set its top coordinate to `adjustTop`.
5160	// This hack is used to maintain continuity when user is manually resizing an event.
5161	// Returns an array of DOM elements for the event.
5162	function renderTempDayEvent(event, adjustRow, adjustTop) {
5163
5164		// actually render the event. `true` for appending element to container.
5165		// Recieve the intermediate "segment" data structures.
5166		var segments = _renderDayEvents(
5167			[ event ],
5168			true, // append event elements
5169			false // don't set the heights of the rows
5170		);
5171
5172		var elements = [];
5173
5174		// Adjust certain elements' top coordinates
5175		segmentElementEach(segments, function(segment, element) {
5176			if (segment.row === adjustRow) {
5177				element.css('top', adjustTop);
5178			}
5179			elements.push(element[0]); // accumulate DOM nodes
5180		});
5181
5182		return elements;
5183	}
5184
5185
5186	// Render events onto the calendar. Only responsible for the VISUAL aspect.
5187	// Not responsible for attaching handlers or calling callbacks.
5188	// Set `doAppend` to `true` for rendering elements without clearing the existing container.
5189	// Set `doRowHeights` to allow setting the height of each row, to compensate for vertical event overflow.
5190	function _renderDayEvents(events, doAppend, doRowHeights) {
5191
5192		// where the DOM nodes will eventually end up
5193		var finalContainer = getDaySegmentContainer();
5194
5195		// the container where the initial HTML will be rendered.
5196		// If `doAppend`==true, uses a temporary container.
5197		var renderContainer = doAppend ? $("<div/>") : finalContainer;
5198
5199		var segments = buildSegments(events);
5200		var html;
5201		var elements;
5202
5203		// calculate the desired `left` and `width` properties on each segment object
5204		calculateHorizontals(segments);
5205
5206		// build the HTML string. relies on `left` property
5207		html = buildHTML(segments);
5208
5209		// render the HTML. innerHTML is considerably faster than jQuery's .html()
5210		renderContainer[0].innerHTML = html;
5211
5212		// retrieve the individual elements
5213		elements = renderContainer.children();
5214
5215		// if we were appending, and thus using a temporary container,
5216		// re-attach elements to the real container.
5217		if (doAppend) {
5218			finalContainer.append(elements);
5219		}
5220
5221		// assigns each element to `segment.event`, after filtering them through user callbacks
5222		resolveElements(segments, elements);
5223
5224		// Calculate the left and right padding+margin for each element.
5225		// We need this for setting each element's desired outer width, because of the W3C box model.
5226		// It's important we do this in a separate pass from acually setting the width on the DOM elements
5227		// because alternating reading/writing dimensions causes reflow for every iteration.
5228		segmentElementEach(segments, function(segment, element) {
5229			segment.hsides = hsides(element, true); // include margins = `true`
5230		});
5231
5232		// Set the width of each element
5233		segmentElementEach(segments, function(segment, element) {
5234			element.width(
5235				Math.max(0, segment.outerWidth - segment.hsides)
5236			);
5237		});
5238
5239		// Grab each element's outerHeight (setVerticals uses this).
5240		// To get an accurate reading, it's important to have each element's width explicitly set already.
5241		segmentElementEach(segments, function(segment, element) {
5242			segment.outerHeight = element.outerHeight(true); // include margins = `true`
5243		});
5244
5245		// Set the top coordinate on each element (requires segment.outerHeight)
5246		setVerticals(segments, doRowHeights);
5247
5248		return segments;
5249	}
5250
5251
5252	// Generate an array of "segments" for all events.
5253	function buildSegments(events) {
5254		var segments = [];
5255		for (var i=0; i<events.length; i++) {
5256			var eventSegments = buildSegmentsForEvent(events[i]);
5257			segments.push.apply(segments, eventSegments); // append an array to an array
5258		}
5259		return segments;
5260	}
5261
5262
5263	// Generate an array of segments for a single event.
5264	// A "segment" is the same data structure that View.rangeToSegments produces,
5265	// with the addition of the `event` property being set to reference the original event.
5266	function buildSegmentsForEvent(event) {
5267		var startDate = event.start;
5268		var endDate = exclEndDay(event);
5269		var segments = rangeToSegments(startDate, endDate);
5270		for (var i=0; i<segments.length; i++) {
5271			segments[i].event = event;
5272		}
5273		return segments;
5274	}
5275
5276
5277	// Sets the `left` and `outerWidth` property of each segment.
5278	// These values are the desired dimensions for the eventual DOM elements.
5279	function calculateHorizontals(segments) {
5280		var isRTL = opt('isRTL');
5281		for (var i=0; i<segments.length; i++) {
5282			var segment = segments[i];
5283
5284			// Determine functions used for calulating the elements left/right coordinates,
5285			// depending on whether the view is RTL or not.
5286			// NOTE:
5287			// colLeft/colRight returns the coordinate butting up the edge of the cell.
5288			// colContentLeft/colContentRight is indented a little bit from the edge.
5289			var leftFunc = (isRTL ? segment.isEnd : segment.isStart) ? colContentLeft : colLeft;
5290			var rightFunc = (isRTL ? segment.isStart : segment.isEnd) ? colContentRight : colRight;
5291
5292			var left = leftFunc(segment.leftCol);
5293			var right = rightFunc(segment.rightCol);
5294			segment.left = left;
5295			segment.outerWidth = right - left;
5296		}
5297	}
5298
5299
5300	// Build a concatenated HTML string for an array of segments
5301	function buildHTML(segments) {
5302		var html = '';
5303		for (var i=0; i<segments.length; i++) {
5304			html += buildHTMLForSegment(segments[i]);
5305		}
5306		return html;
5307	}
5308
5309
5310	// Build an HTML string for a single segment.
5311	// Relies on the following properties:
5312	// - `segment.event` (from `buildSegmentsForEvent`)
5313	// - `segment.left` (from `calculateHorizontals`)
5314	function buildHTMLForSegment(segment) {
5315		var html = '';
5316		var isRTL = opt('isRTL');
5317		var event = segment.event;
5318		var url = event.url;
5319		var ariaLabel = event.ariaLabel;
5320
5321		// generate the list of CSS classNames
5322		var classNames = [ 'fc-event', 'fc-event-hori' ];
5323		if (isEventDraggable(event)) {
5324			classNames.push('fc-event-draggable');
5325		}
5326		if (segment.isStart) {
5327			classNames.push('fc-event-start');
5328		}
5329		if (segment.isEnd) {
5330			classNames.push('fc-event-end');
5331		}
5332		// use the event's configured classNames
5333		// guaranteed to be an array via `normalizeEvent`
5334		classNames = classNames.concat(event.className);
5335		if (event.source) {
5336			// use the event's source's classNames, if specified
5337			classNames = classNames.concat(event.source.className || []);
5338		}
5339
5340		// generate a semicolon delimited CSS string for any of the "skin" properties
5341		// of the event object (`backgroundColor`, `borderColor` and such)
5342		var skinCss = getSkinCss(event, opt);
5343
5344		if (url) {
5345			html += "<a href='" + htmlEscape(url) + "'";
5346			 if (ariaLabel){
5347                html += " aria-label='" + htmlEscape(ariaLabel) + "'";
5348           }
5349		}else{
5350			html += "<div";
5351		}
5352		html +=
5353			" class='" + classNames.join(' ') + "'" +
5354			" style=" +
5355				"'" +
5356				"position:absolute;" +
5357				"left:" + segment.left + "px;" +
5358				skinCss +
5359				"'" +
5360			">" +
5361			"<div class='fc-event-inner'>";
5362		if (!event.allDay && segment.isStart) {
5363			html +=
5364				"<span class='fc-event-time'>" +
5365				htmlEscape(
5366					formatDates(event.start, event.end, opt('timeFormat'))
5367				) +
5368				"</span>";
5369		}
5370		html +=
5371			"<span class='fc-event-title'>" +
5372			htmlEscape(event.title || '') +
5373			"</span>" +
5374			"</div>";
5375		if (segment.isEnd && isEventResizable(event)) {
5376			html +=
5377				"<div class='ui-resizable-handle ui-resizable-" + (isRTL ? 'w' : 'e') + "'>" +
5378				"&nbsp;&nbsp;&nbsp;" + // makes hit area a lot better for IE6/7
5379				"</div>";
5380		}
5381		html += "</" + (url ? "a" : "div") + ">";
5382
5383		// TODO:
5384		// When these elements are initially rendered, they will be briefly visibile on the screen,
5385		// even though their widths/heights are not set.
5386		// SOLUTION: initially set them as visibility:hidden ?
5387
5388		return html;
5389	}
5390
5391
5392	// Associate each segment (an object) with an element (a jQuery object),
5393	// by setting each `segment.element`.
5394	// Run each element through the `eventRender` filter, which allows developers to
5395	// modify an existing element, supply a new one, or cancel rendering.
5396	function resolveElements(segments, elements) {
5397		for (var i=0; i<segments.length; i++) {
5398			var segment = segments[i];
5399			var event = segment.event;
5400			var element = elements.eq(i);
5401
5402			// call the trigger with the original element
5403			var triggerRes = trigger('eventRender', event, event, element);
5404
5405			if (triggerRes === false) {
5406				// if `false`, remove the event from the DOM and don't assign it to `segment.event`
5407				element.remove();
5408			}
5409			else {
5410				if (triggerRes && triggerRes !== true) {
5411					// the trigger returned a new element, but not `true` (which means keep the existing element)
5412
5413					// re-assign the important CSS dimension properties that were already assigned in `buildHTMLForSegment`
5414					triggerRes = $(triggerRes)
5415						.css({
5416							position: 'absolute',
5417							left: segment.left
5418						});
5419
5420					element.replaceWith(triggerRes);
5421					element = triggerRes;
5422				}
5423
5424				segment.element = element;
5425			}
5426		}
5427	}
5428
5429
5430
5431	/* Top-coordinate Methods
5432	-------------------------------------------------------------------------------------------------*/
5433
5434
5435	// Sets the "top" CSS property for each element.
5436	// If `doRowHeights` is `true`, also sets each row's first cell to an explicit height,
5437	// so that if elements vertically overflow, the cell expands vertically to compensate.
5438	function setVerticals(segments, doRowHeights) {
5439		var rowContentHeights = calculateVerticals(segments); // also sets segment.top
5440		var rowContentElements = getRowContentElements(); // returns 1 inner div per row
5441		var rowContentTops = [];
5442
5443		// Set each row's height by setting height of first inner div
5444		if (doRowHeights) {
5445			for (var i=0; i<rowContentElements.length; i++) {
5446				rowContentElements[i].height(rowContentHeights[i]);
5447			}
5448		}
5449
5450		// Get each row's top, relative to the views's origin.
5451		// Important to do this after setting each row's height.
5452		for (var i=0; i<rowContentElements.length; i++) {
5453			rowContentTops.push(
5454				rowContentElements[i].position().top
5455			);
5456		}
5457
5458		// Set each segment element's CSS "top" property.
5459		// Each segment object has a "top" property, which is relative to the row's top, but...
5460		segmentElementEach(segments, function(segment, element) {
5461			element.css(
5462				'top',
5463				rowContentTops[segment.row] + segment.top // ...now, relative to views's origin
5464			);
5465		});
5466	}
5467
5468
5469	// Calculate the "top" coordinate for each segment, relative to the "top" of the row.
5470	// Also, return an array that contains the "content" height for each row
5471	// (the height displaced by the vertically stacked events in the row).
5472	// Requires segments to have their `outerHeight` property already set.
5473	function calculateVerticals(segments) {
5474		var rowCnt = getRowCnt();
5475		var colCnt = getColCnt();
5476		var rowContentHeights = []; // content height for each row
5477		var segmentRows = buildSegmentRows(segments); // an array of segment arrays, one for each row
5478
5479		for (var rowI=0; rowI<rowCnt; rowI++) {
5480			var segmentRow = segmentRows[rowI];
5481
5482			// an array of running total heights for each column.
5483			// initialize with all zeros.
5484			var colHeights = [];
5485			for (var colI=0; colI<colCnt; colI++) {
5486				colHeights.push(0);
5487			}
5488
5489			// loop through every segment
5490			for (var segmentI=0; segmentI<segmentRow.length; segmentI++) {
5491				var segment = segmentRow[segmentI];
5492
5493				// find the segment's top coordinate by looking at the max height
5494				// of all the columns the segment will be in.
5495				segment.top = arrayMax(
5496					colHeights.slice(
5497						segment.leftCol,
5498						segment.rightCol + 1 // make exclusive for slice
5499					)
5500				);
5501
5502				// adjust the columns to account for the segment's height
5503				for (var colI=segment.leftCol; colI<=segment.rightCol; colI++) {
5504					colHeights[colI] = segment.top + segment.outerHeight;
5505				}
5506			}
5507
5508			// the tallest column in the row should be the "content height"
5509			rowContentHeights.push(arrayMax(colHeights));
5510		}
5511
5512		return rowContentHeights;
5513	}
5514
5515
5516	// Build an array of segment arrays, each representing the segments that will
5517	// be in a row of the grid, sorted by which event should be closest to the top.
5518	function buildSegmentRows(segments) {
5519		var rowCnt = getRowCnt();
5520		var segmentRows = [];
5521		var segmentI;
5522		var segment;
5523		var rowI;
5524
5525		// group segments by row
5526		for (segmentI=0; segmentI<segments.length; segmentI++) {
5527			segment = segments[segmentI];
5528			rowI = segment.row;
5529			if (segment.element) { // was rendered?
5530				if (segmentRows[rowI]) {
5531					// already other segments. append to array
5532					segmentRows[rowI].push(segment);
5533				}
5534				else {
5535					// first segment in row. create new array
5536					segmentRows[rowI] = [ segment ];
5537				}
5538			}
5539		}
5540
5541		// sort each row
5542		for (rowI=0; rowI<rowCnt; rowI++) {
5543			segmentRows[rowI] = sortSegmentRow(
5544				segmentRows[rowI] || [] // guarantee an array, even if no segments
5545			);
5546		}
5547
5548		return segmentRows;
5549	}
5550
5551
5552	// Sort an array of segments according to which segment should appear closest to the top
5553	function sortSegmentRow(segments) {
5554		var sortedSegments = [];
5555
5556		// build the subrow array
5557		var subrows = buildSegmentSubrows(segments);
5558
5559		// flatten it
5560		for (var i=0; i<subrows.length; i++) {
5561			sortedSegments.push.apply(sortedSegments, subrows[i]); // append an array to an array
5562		}
5563
5564		return sortedSegments;
5565	}
5566
5567
5568	// Take an array of segments, which are all assumed to be in the same row,
5569	// and sort into subrows.
5570	function buildSegmentSubrows(segments) {
5571
5572		// Give preference to elements with certain criteria, so they have
5573		// a chance to be closer to the top.
5574		segments.sort(compareDaySegments);
5575
5576		var subrows = [];
5577		for (var i=0; i<segments.length; i++) {
5578			var segment = segments[i];
5579
5580			// loop through subrows, starting with the topmost, until the segment
5581			// doesn't collide with other segments.
5582			for (var j=0; j<subrows.length; j++) {
5583				if (!isDaySegmentCollision(segment, subrows[j])) {
5584					break;
5585				}
5586			}
5587			// `j` now holds the desired subrow index
5588			if (subrows[j]) {
5589				subrows[j].push(segment);
5590			}
5591			else {
5592				subrows[j] = [ segment ];
5593			}
5594		}
5595
5596		return subrows;
5597	}
5598
5599
5600	// Return an array of jQuery objects for the placeholder content containers of each row.
5601	// The content containers don't actually contain anything, but their dimensions should match
5602	// the events that are overlaid on top.
5603	function getRowContentElements() {
5604		var i;
5605		var rowCnt = getRowCnt();
5606		var rowDivs = [];
5607		for (i=0; i<rowCnt; i++) {
5608			rowDivs[i] = allDayRow(i)
5609				.find('div.fc-day-content > div');
5610		}
5611		return rowDivs;
5612	}
5613
5614
5615
5616	/* Mouse Handlers
5617	---------------------------------------------------------------------------------------------------*/
5618	// TODO: better documentation!
5619
5620
5621	function attachHandlers(segments, modifiedEventId) {
5622		var segmentContainer = getDaySegmentContainer();
5623
5624		segmentElementEach(segments, function(segment, element, i) {
5625			var event = segment.event;
5626			if (event._id === modifiedEventId) {
5627				bindDaySeg(event, element, segment);
5628			}else{
5629				element[0]._fci = i; // for lazySegBind
5630			}
5631		});
5632
5633		lazySegBind(segmentContainer, segments, bindDaySeg);
5634	}
5635
5636
5637	function bindDaySeg(event, eventElement, segment) {
5638
5639		if (isEventDraggable(event)) {
5640			t.draggableDayEvent(event, eventElement, segment); // use `t` so subclasses can override
5641		}
5642
5643		if (
5644			segment.isEnd && // only allow resizing on the final segment for an event
5645			isEventResizable(event)
5646		) {
5647			t.resizableDayEvent(event, eventElement, segment); // use `t` so subclasses can override
5648		}
5649
5650		// attach all other handlers.
5651		// needs to be after, because resizableDayEvent might stopImmediatePropagation on click
5652		eventElementHandlers(event, eventElement);
5653	}
5654
5655	
5656	function draggableDayEvent(event, eventElement) {
5657		var hoverListener = getHoverListener();
5658		var dayDelta;
5659		eventElement.draggable({
5660			delay: 50,
5661			opacity: opt('dragOpacity'),
5662			revertDuration: opt('dragRevertDuration'),
5663			start: function(ev, ui) {
5664				trigger('eventDragStart', eventElement, event, ev, ui);
5665				hideEvents(event, eventElement);
5666				hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
5667					eventElement.draggable('option', 'revert', !cell || !rowDelta && !colDelta);
5668					clearOverlays();
5669					if (cell) {
5670						var origDate = cellToDate(origCell);
5671						var date = cellToDate(cell);
5672						dayDelta = dayDiff(date, origDate);
5673						renderDayOverlay(
5674							addDays(cloneDate(event.start), dayDelta),
5675							addDays(exclEndDay(event), dayDelta)
5676						);
5677					}else{
5678						dayDelta = 0;
5679					}
5680				}, ev, 'drag');
5681			},
5682			stop: function(ev, ui) {
5683				hoverListener.stop();
5684				clearOverlays();
5685				trigger('eventDragStop', eventElement, event, ev, ui);
5686				if (dayDelta) {
5687					eventDrop(this, event, dayDelta, 0, event.allDay, ev, ui);
5688				}else{
5689					eventElement.css('filter', ''); // clear IE opacity side-effects
5690					showEvents(event, eventElement);
5691				}
5692			}
5693		});
5694	}
5695
5696	
5697	function resizableDayEvent(event, element, segment) {
5698		var isRTL = opt('isRTL');
5699		var direction = isRTL ? 'w' : 'e';
5700		var handle = element.find('.ui-resizable-' + direction); // TODO: stop using this class because we aren't using jqui for this
5701		var isResizing = false;
5702		
5703		// TODO: look into using jquery-ui mouse widget for this stuff
5704		disableTextSelection(element); // prevent native <a> selection for IE
5705		element
5706			.mousedown(function(ev) { // prevent native <a> selection for others
5707				ev.preventDefault();
5708			})
5709			.click(function(ev) {
5710				if (isResizing) {
5711					ev.preventDefault(); // prevent link from being visited (only method that worked in IE6)
5712					ev.stopImmediatePropagation(); // prevent fullcalendar eventClick handler from being called
5713					                               // (eventElementHandlers needs to be bound after resizableDayEvent)
5714				}
5715			});
5716		
5717		handle.mousedown(function(ev) {
5718			if (ev.which != 1) {
5719				return; // needs to be left mouse button
5720			}
5721			isResizing = true;
5722			var hoverListener = getHoverListener();
5723			var rowCnt = getRowCnt();
5724			var colCnt = getColCnt();
5725			var elementTop = element.css('top');
5726			var dayDelta;
5727			var helpers;
5728			var eventCopy = $.extend({}, event);
5729			var minCellOffset = dayOffsetToCellOffset( dateToDayOffset(event.start) );
5730			clearSelection();
5731			$('body')
5732				.css('cursor', direction + '-resize')
5733				.one('mouseup', mouseup);
5734			trigger('eventResizeStart', this, event, ev);
5735			hoverListener.start(function(cell, origCell) {
5736				if (cell) {
5737
5738					var origCellOffset = cellToCellOffset(origCell);
5739					var cellOffset = cellToCellOffset(cell);
5740
5741					// don't let resizing move earlier than start date cell
5742					cellOffset = Math.max(cellOffset, minCellOffset);
5743
5744					dayDelta =
5745						cellOffsetToDayOffset(cellOffset) -
5746						cellOffsetToDayOffset(origCellOffset);
5747
5748					if (dayDelta) {
5749						eventCopy.end = addDays(eventEnd(event), dayDelta, true);
5750						var oldHelpers = helpers;
5751
5752						helpers = renderTempDayEvent(eventCopy, segment.row, elementTop);
5753						helpers = $(helpers); // turn array into a jQuery object
5754
5755						helpers.find('*').css('cursor', direction + '-resize');
5756						if (oldHelpers) {
5757							oldHelpers.remove();
5758						}
5759
5760						hideEvents(event);
5761					}
5762					else {
5763						if (helpers) {
5764							showEvents(event);
5765							helpers.remove();
5766							helpers = null;
5767						}
5768					}
5769					clearOverlays();
5770					renderDayOverlay( // coordinate grid already rebuilt with hoverListener.start()
5771						event.start,
5772						addDays( exclEndDay(event), dayDelta )
5773						// TODO: instead of calling renderDayOverlay() with dates,
5774						// call _renderDayOverlay (or whatever) with cell offsets.
5775					);
5776				}
5777			}, ev);
5778			
5779			function mouseup(ev) {
5780				trigger('eventResizeStop', this, event, ev);
5781				$('body').css('cursor', '');
5782				hoverListener.stop();
5783				clearOverlays();
5784				if (dayDelta) {
5785					eventResize(this, event, dayDelta, 0, ev);
5786					// event redraw will clear helpers
5787				}
5788				// otherwise, the drag handler already restored the old events
5789				
5790				setTimeout(function() { // make this happen after the element's click event
5791					isResizing = false;
5792				},0);
5793			}
5794		});
5795	}
5796	
5797
5798}
5799
5800
5801
5802/* Generalized Segment Utilities
5803-------------------------------------------------------------------------------------------------*/
5804
5805
5806function isDaySegmentCollision(segment, otherSegments) {
5807	for (var i=0; i<otherSegments.length; i++) {
5808		var otherSegment = otherSegments[i];
5809		if (
5810			otherSegment.leftCol <= segment.rightCol &&
5811			otherSegment.rightCol >= segment.leftCol
5812		) {
5813			return true;
5814		}
5815	}
5816	return false;
5817}
5818
5819
5820function segmentElementEach(segments, callback) { // TODO: use in AgendaView?
5821	for (var i=0; i<segments.length; i++) {
5822		var segment = segments[i];
5823		var element = segment.element;
5824		if (element) {
5825			callback(segment, element, i);
5826		}
5827	}
5828}
5829
5830
5831// A cmp function for determining which segments should appear higher up
5832function compareDaySegments(a, b) {
5833	return (b.rightCol - b.leftCol) - (a.rightCol - a.leftCol) || // put wider events first
5834		b.event.allDay - a.event.allDay || // if tie, put all-day events first (booleans cast to 0/1)
5835		a.event.start - b.event.start || // if a tie, sort by event start date
5836		(a.event.title || '').localeCompare(b.event.title) // if a tie, sort by event title
5837}
5838
5839
5840;;
5841
5842//BUG: unselect needs to be triggered when events are dragged+dropped
5843
5844function SelectionManager() {
5845	var t = this;
5846	
5847	
5848	// exports
5849	t.select = select;
5850	t.unselect = unselect;
5851	t.reportSelection = reportSelection;
5852	t.daySelectionMousedown = daySelectionMousedown;
5853	
5854	
5855	// imports
5856	var opt = t.opt;
5857	var trigger = t.trigger;
5858	var defaultSelectionEnd = t.defaultSelectionEnd;
5859	var renderSelection = t.renderSelection;
5860	var clearSelection = t.clearSelection;
5861	
5862	
5863	// locals
5864	var selected = false;
5865
5866
5867
5868	// unselectAuto
5869	if (opt('selectable') && opt('unselectAuto')) {
5870		$(document).mousedown(function(ev) {
5871			var ignore = opt('unselectCancel');
5872			if (ignore) {
5873				if ($(ev.target).parents(ignore).length) { // could be optimized to stop after first match
5874					return;
5875				}
5876			}
5877			unselect(ev);
5878		});
5879	}
5880	
5881
5882	function select(startDate, endDate, allDay) {
5883		unselect();
5884		if (!endDate) {
5885			endDate = defaultSelectionEnd(startDate, allDay);
5886		}
5887		renderSelection(startDate, endDate, allDay);
5888		reportSelection(startDate, endDate, allDay);
5889	}
5890	
5891	
5892	function unselect(ev) {
5893		if (selected) {
5894			selected = false;
5895			clearSelection();
5896			trigger('unselect', null, ev);
5897		}
5898	}
5899	
5900	
5901	function reportSelection(startDate, endDate, allDay, ev) {
5902		selected = true;
5903		trigger('select', null, startDate, endDate, allDay, ev);
5904	}
5905	
5906	
5907	function daySelectionMousedown(ev) { // not really a generic manager method, oh well
5908		var cellToDate = t.cellToDate;
5909		var getIsCellAllDay = t.getIsCellAllDay;
5910		var hoverListener = t.getHoverListener();
5911		var reportDayClick = t.reportDayClick; // this is hacky and sort of weird
5912		if (ev.which == 1 && opt('selectable')) { // which==1 means left mouse button
5913			unselect(ev);
5914			var _mousedownElement = this;
5915			var dates;
5916			hoverListener.start(function(cell, origCell) { // TODO: maybe put cellToDate/getIsCellAllDay info in cell
5917				clearSelection();
5918				if (cell && getIsCellAllDay(cell)) {
5919					dates = [ cellToDate(origCell), cellToDate(cell) ].sort(dateCompare);
5920					renderSelection(dates[0], dates[1], true);
5921				}else{
5922					dates = null;
5923				}
5924			}, ev);
5925			$(document).one('mouseup', function(ev) {
5926				hoverListener.stop();
5927				if (dates) {
5928					if (+dates[0] == +dates[1]) {
5929						reportDayClick(dates[0], true, ev);
5930					}
5931					reportSelection(dates[0], dates[1], true, ev);
5932				}
5933			});
5934		}
5935	}
5936
5937
5938}
5939
5940;;
5941 
5942function OverlayManager() {
5943	var t = this;
5944	
5945	
5946	// exports
5947	t.renderOverlay = renderOverlay;
5948	t.clearOverlays = clearOverlays;
5949	
5950	
5951	// locals
5952	var usedOverlays = [];
5953	var unusedOverlays = [];
5954	
5955	
5956	function renderOverlay(rect, parent) {
5957		var e = unusedOverlays.shift();
5958		if (!e) {
5959			e = $("<div class='fc-cell-overlay' style='position:absolute;z-index:3'/>");
5960		}
5961		if (e[0].parentNode != parent[0]) {
5962			e.appendTo(parent);
5963		}
5964		usedOverlays.push(e.css(rect).show());
5965		return e;
5966	}
5967	
5968
5969	function clearOverlays() {
5970		var e;
5971		while (e = usedOverlays.shift()) {
5972			unusedOverlays.push(e.hide().unbind());
5973		}
5974	}
5975
5976
5977}
5978
5979;;
5980
5981function CoordinateGrid(buildFunc) {
5982
5983	var t = this;
5984	var rows;
5985	var cols;
5986	
5987	
5988	t.build = function() {
5989		rows = [];
5990		cols = [];
5991		buildFunc(rows, cols);
5992	};
5993	
5994	
5995	t.cell = function(x, y) {
5996		var rowCnt = rows.length;
5997		var colCnt = cols.length;
5998		var i, r=-1, c=-1;
5999		for (i=0; i<rowCnt; i++) {
6000			if (y >= rows[i][0] && y < rows[i][1]) {
6001				r = i;
6002				break;
6003			}
6004		}
6005		for (i=0; i<colCnt; i++) {
6006			if (x >= cols[i][0] && x < cols[i][1]) {
6007				c = i;
6008				break;
6009			}
6010		}
6011		return (r>=0 && c>=0) ? { row:r, col:c } : null;
6012	};
6013	
6014	
6015	t.rect = function(row0, col0, row1, col1, originElement) { // row1,col1 is inclusive
6016		var origin = originElement.offset();
6017		return {
6018			top: rows[row0][0] - origin.top,
6019			left: cols[col0][0] - origin.left,
6020			width: cols[col1][1] - cols[col0][0],
6021			height: rows[row1][1] - rows[row0][0]
6022		};
6023	};
6024
6025}
6026
6027;;
6028
6029function HoverListener(coordinateGrid) {
6030
6031
6032	var t = this;
6033	var bindType;
6034	var change;
6035	var firstCell;
6036	var cell;
6037	
6038	
6039	t.start = function(_change, ev, _bindType) {
6040		change = _change;
6041		firstCell = cell = null;
6042		coordinateGrid.build();
6043		mouse(ev);
6044		bindType = _bindType || 'mousemove';
6045		$(document).bind(bindType, mouse);
6046	};
6047	
6048	
6049	function mouse(ev) {
6050		_fixUIEvent(ev); // see below
6051		var newCell = coordinateGrid.cell(ev.pageX, ev.pageY);
6052		if (!newCell != !cell || newCell && (newCell.row != cell.row || newCell.col != cell.col)) {
6053			if (newCell) {
6054				if (!firstCell) {
6055					firstCell = newCell;
6056				}
6057				change(newCell, firstCell, newCell.row-firstCell.row, newCell.col-firstCell.col);
6058			}else{
6059				change(newCell, firstCell);
6060			}
6061			cell = newCell;
6062		}
6063	}
6064	
6065	
6066	t.stop = function() {
6067		$(document).unbind(bindType, mouse);
6068		return cell;
6069	};
6070	
6071	
6072}
6073
6074
6075
6076// this fix was only necessary for jQuery UI 1.8.16 (and jQuery 1.7 or 1.7.1)
6077// upgrading to jQuery UI 1.8.17 (and using either jQuery 1.7 or 1.7.1) fixed the problem
6078// but keep this in here for 1.8.16 users
6079// and maybe remove it down the line
6080
6081function _fixUIEvent(event) { // for issue 1168
6082	if (event.pageX === undefined) {
6083		event.pageX = event.originalEvent.pageX;
6084		event.pageY = event.originalEvent.pageY;
6085	}
6086}
6087;;
6088
6089function HorizontalPositionCache(getElement) {
6090
6091	var t = this,
6092		elements = {},
6093		lefts = {},
6094		rights = {};
6095		
6096	function e(i) {
6097		return elements[i] = elements[i] || getElement(i);
6098	}
6099	
6100	t.left = function(i) {
6101		return lefts[i] = lefts[i] === undefined ? e(i).position().left : lefts[i];
6102	};
6103	
6104	t.right = function(i) {
6105		return rights[i] = rights[i] === undefined ? t.left(i) + e(i).width() : rights[i];
6106	};
6107	
6108	t.clear = function() {
6109		elements = {};
6110		lefts = {};
6111		rights = {};
6112	};
6113	
6114}
6115
6116;;
6117
6118})(jQuery);

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.