Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
21627 kshitij.so 1
/**
2
 * @preserve
3
 * FullCalendar v1.5.4
4
 * http://arshaw.com/fullcalendar/
5
 *
6
 * Use fullcalendar.css for basic styling.
7
 * For event drag & drop, requires jQuery UI draggable.
8
 * For event resizing, requires jQuery UI resizable.
9
 *
10
 * Copyright (c) 2011 Adam Shaw
11
 * Dual licensed under the MIT and GPL licenses, located in
12
 * MIT-LICENSE.txt and GPL-LICENSE.txt respectively.
13
 *
14
 * Date: Tue Sep 4 23:38:33 2012 -0700
15
 *
16
 */
17
 
18
(function($, undefined) {
19
 
20
 
21
var defaults = {
22
 
23
	// display
24
	defaultView: 'month',
25
	aspectRatio: 1.35,
26
	header: {
27
		left: 'title',
28
		center: '',
29
		right: 'today prev,next'
30
	},
31
	weekends: true,
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: ' ◄ ',
70
		next: ' ► ',
71
		prevYear: ' << ',
72
		nextYear: ' >> ',
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
};
92
 
93
// right-to-left defaults
94
var rtlDefaults = {
95
	header: {
96
		left: 'next,prev today',
97
		center: '',
98
		right: 'title'
99
	},
100
	buttonText: {
101
		prev: ' ► ',
102
		next: ' ◄ ',
103
		prevYear: ' >> ',
104
		nextYear: ' << '
105
	},
106
	buttonIcons: {
107
		prev: 'circle-triangle-e',
108
		next: 'circle-triangle-w'
109
	}
110
};
111
 
112
 
113
 
114
var fc = $.fullCalendar = { version: "1.5.4" };
115
var fcViews = fc.views = {};
116
 
117
 
118
$.fn.fullCalendar = function(options) {
119
 
120
 
121
	// method calling
122
	if (typeof options == 'string') {
123
		var args = Array.prototype.slice.call(arguments, 1);
124
		var res;
125
		this.each(function() {
126
			var calendar = $.data(this, 'fullCalendar');
127
			if (calendar && $.isFunction(calendar[options])) {
128
				var r = calendar[options].apply(calendar, args);
129
				if (res === undefined) {
130
					res = r;
131
				}
132
				if (options == 'destroy') {
133
					$.removeData(this, 'fullCalendar');
134
				}
135
			}
136
		});
137
		if (res !== undefined) {
138
			return res;
139
		}
140
		return this;
141
	}
142
 
143
 
144
	// would like to have this logic in EventManager, but needs to happen before options are recursively extended
145
	var eventSources = options.eventSources || [];
146
	delete options.eventSources;
147
	if (options.events) {
148
		eventSources.push(options.events);
149
		delete options.events;
150
	}
151
 
152
 
153
	options = $.extend(true, {},
154
		defaults,
155
		(options.isRTL || options.isRTL===undefined && defaults.isRTL) ? rtlDefaults : {},
156
		options
157
	);
158
 
159
 
160
	this.each(function(i, _element) {
161
		var element = $(_element);
162
		var calendar = new Calendar(element, options, eventSources);
163
		element.data('fullCalendar', calendar); // TODO: look into memory leak implications
164
		calendar.render();
165
	});
166
 
167
 
168
	return this;
169
 
170
};
171
 
172
 
173
// function for adding/overriding defaults
174
function setDefaults(d) {
175
	$.extend(true, defaults, d);
176
}
177
 
178
 
179
 
180
 
181
function Calendar(element, options, eventSources) {
182
	var t = this;
183
 
184
 
185
	// exports
186
	t.options = options;
187
	t.render = render;
188
	t.destroy = destroy;
189
	t.refetchEvents = refetchEvents;
190
	t.reportEvents = reportEvents;
191
	t.reportEventChange = reportEventChange;
192
	t.rerenderEvents = rerenderEvents;
193
	t.changeView = changeView;
194
	t.select = select;
195
	t.unselect = unselect;
196
	t.prev = prev;
197
	t.next = next;
198
	t.prevYear = prevYear;
199
	t.nextYear = nextYear;
200
	t.today = today;
201
	t.gotoDate = gotoDate;
202
	t.incrementDate = incrementDate;
203
	t.formatDate = function(format, date) { return formatDate(format, date, options) };
204
	t.formatDates = function(format, date1, date2) { return formatDates(format, date1, date2, options) };
205
	t.getDate = getDate;
206
	t.getView = getView;
207
	t.option = option;
208
	t.trigger = trigger;
209
 
210
 
211
	// imports
212
	EventManager.call(t, options, eventSources);
213
	var isFetchNeeded = t.isFetchNeeded;
214
	var fetchEvents = t.fetchEvents;
215
 
216
 
217
	// locals
218
	var _element = element[0];
219
	var header;
220
	var headerElement;
221
	var content;
222
	var tm; // for making theme classes
223
	var currentView;
224
	var viewInstances = {};
225
	var elementOuterWidth;
226
	var suggestedViewHeight;
227
	var absoluteViewElement;
228
	var resizeUID = 0;
229
	var ignoreWindowResize = 0;
230
	var date = new Date();
231
	var events = [];
232
	var _dragElement;
233
 
234
 
235
	/* Main Rendering
236
	-----------------------------------------------------------------------------*/
237
 
238
 
239
	setYMD(date, options.year, options.month, options.date);
240
 
241
 
242
	function render(inc) {
243
		if (!content) {
244
			initialRender();
245
		}else{
246
			calcSize();
247
			markSizesDirty();
248
			markEventsDirty();
249
			renderView(inc);
250
		}
251
	}
252
 
253
 
254
	function initialRender() {
255
		tm = options.theme ? 'ui' : 'fc';
256
		element.addClass('fc');
257
		if (options.isRTL) {
258
			element.addClass('fc-rtl');
259
		}
260
		if (options.theme) {
261
			element.addClass('ui-widget');
262
		}
263
		content = $("<div class='fc-content' style='position:relative'/>")
264
			.prependTo(element);
265
		header = new Header(t, options);
266
		headerElement = header.render();
267
		if (headerElement) {
268
			element.prepend(headerElement);
269
		}
270
		changeView(options.defaultView);
271
		$(window).resize(windowResize);
272
		// needed for IE in a 0x0 iframe, b/c when it is resized, never triggers a windowResize
273
		if (!bodyVisible()) {
274
			lateRender();
275
		}
276
	}
277
 
278
 
279
	// called when we know the calendar couldn't be rendered when it was initialized,
280
	// but we think it's ready now
281
	function lateRender() {
282
		setTimeout(function() { // IE7 needs this so dimensions are calculated correctly
283
			if (!currentView.start && bodyVisible()) { // !currentView.start makes sure this never happens more than once
284
				renderView();
285
			}
286
		},0);
287
	}
288
 
289
 
290
	function destroy() {
291
		$(window).unbind('resize', windowResize);
292
		header.destroy();
293
		content.remove();
294
		element.removeClass('fc fc-rtl ui-widget');
295
	}
296
 
297
 
298
 
299
	function elementVisible() {
300
		return _element.offsetWidth !== 0;
301
	}
302
 
303
 
304
	function bodyVisible() {
305
		return $('body')[0].offsetWidth !== 0;
306
	}
307
 
308
 
309
 
310
	/* View Rendering
311
	-----------------------------------------------------------------------------*/
312
 
313
	// TODO: improve view switching (still weird transition in IE, and FF has whiteout problem)
314
 
315
	function changeView(newViewName) {
316
		if (!currentView || newViewName != currentView.name) {
317
			ignoreWindowResize++; // because setMinHeight might change the height before render (and subsequently setSize) is reached
318
 
319
			unselect();
320
 
321
			var oldView = currentView;
322
			var newViewElement;
323
 
324
			if (oldView) {
325
				(oldView.beforeHide || noop)(); // called before changing min-height. if called after, scroll state is reset (in Opera)
326
				setMinHeight(content, content.height());
327
				oldView.element.hide();
328
			}else{
329
				setMinHeight(content, 1); // needs to be 1 (not 0) for IE7, or else view dimensions miscalculated
330
			}
331
			content.css('overflow', 'hidden');
332
 
333
			currentView = viewInstances[newViewName];
334
			if (currentView) {
335
				currentView.element.show();
336
			}else{
337
				currentView = viewInstances[newViewName] = new fcViews[newViewName](
338
					newViewElement = absoluteViewElement =
339
						$("<div class='fc-view fc-view-" + newViewName + "' style='position:absolute'/>")
340
							.appendTo(content),
341
					t // the calendar object
342
				);
343
			}
344
 
345
			if (oldView) {
346
				header.deactivateButton(oldView.name);
347
			}
348
			header.activateButton(newViewName);
349
 
350
			renderView(); // after height has been set, will make absoluteViewElement's position=relative, then set to null
351
 
352
			content.css('overflow', '');
353
			if (oldView) {
354
				setMinHeight(content, 1);
355
			}
356
 
357
			if (!newViewElement) {
358
				(currentView.afterShow || noop)(); // called after setting min-height/overflow, so in final scroll state (for Opera)
359
			}
360
 
361
			ignoreWindowResize--;
362
		}
363
	}
364
 
365
 
366
 
367
	function renderView(inc) {
368
		if (elementVisible()) {
369
			ignoreWindowResize++; // because renderEvents might temporarily change the height before setSize is reached
370
 
371
			unselect();
372
 
373
			if (suggestedViewHeight === undefined) {
374
				calcSize();
375
			}
376
 
377
			var forceEventRender = false;
378
			if (!currentView.start || inc || date < currentView.start || date >= currentView.end) {
379
				// view must render an entire new date range (and refetch/render events)
380
				currentView.render(date, inc || 0); // responsible for clearing events
381
				setSize(true);
382
				forceEventRender = true;
383
			}
384
			else if (currentView.sizeDirty) {
385
				// view must resize (and rerender events)
386
				currentView.clearEvents();
387
				setSize();
388
				forceEventRender = true;
389
			}
390
			else if (currentView.eventsDirty) {
391
				currentView.clearEvents();
392
				forceEventRender = true;
393
			}
394
			currentView.sizeDirty = false;
395
			currentView.eventsDirty = false;
396
			updateEvents(forceEventRender);
397
 
398
			elementOuterWidth = element.outerWidth();
399
 
400
			header.updateTitle(currentView.title);
401
			var today = new Date();
402
			if (today >= currentView.start && today < currentView.end) {
403
				header.disableButton('today');
404
			}else{
405
				header.enableButton('today');
406
			}
407
 
408
			ignoreWindowResize--;
409
			currentView.trigger('viewDisplay', _element);
410
		}
411
	}
412
 
413
 
414
 
415
	/* Resizing
416
	-----------------------------------------------------------------------------*/
417
 
418
 
419
	function updateSize() {
420
		markSizesDirty();
421
		if (elementVisible()) {
422
			calcSize();
423
			setSize();
424
			unselect();
425
			currentView.clearEvents();
426
			currentView.renderEvents(events);
427
			currentView.sizeDirty = false;
428
		}
429
	}
430
 
431
 
432
	function markSizesDirty() {
433
		$.each(viewInstances, function(i, inst) {
434
			inst.sizeDirty = true;
435
		});
436
	}
437
 
438
 
439
	function calcSize() {
440
		if (options.contentHeight) {
441
			suggestedViewHeight = options.contentHeight;
442
		}
443
		else if (options.height) {
444
			suggestedViewHeight = options.height - (headerElement ? headerElement.height() : 0) - vsides(content);
445
		}
446
		else {
447
			suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5));
448
		}
449
	}
450
 
451
 
452
	function setSize(dateChanged) { // todo: dateChanged?
453
		ignoreWindowResize++;
454
		currentView.setHeight(suggestedViewHeight, dateChanged);
455
		if (absoluteViewElement) {
456
			absoluteViewElement.css('position', 'relative');
457
			absoluteViewElement = null;
458
		}
459
		currentView.setWidth(content.width(), dateChanged);
460
		ignoreWindowResize--;
461
	}
462
 
463
 
464
	function windowResize() {
465
		if (!ignoreWindowResize) {
466
			if (currentView.start) { // view has already been rendered
467
				var uid = ++resizeUID;
468
				setTimeout(function() { // add a delay
469
					if (uid == resizeUID && !ignoreWindowResize && elementVisible()) {
470
						if (elementOuterWidth != (elementOuterWidth = element.outerWidth())) {
471
							ignoreWindowResize++; // in case the windowResize callback changes the height
472
							updateSize();
473
							currentView.trigger('windowResize', _element);
474
							ignoreWindowResize--;
475
						}
476
					}
477
				}, 200);
478
			}else{
479
				// calendar must have been initialized in a 0x0 iframe that has just been resized
480
				lateRender();
481
			}
482
		}
483
	}
484
 
485
 
486
 
487
	/* Event Fetching/Rendering
488
	-----------------------------------------------------------------------------*/
489
 
490
 
491
	// fetches events if necessary, rerenders events if necessary (or if forced)
492
	function updateEvents(forceRender) {
493
		if (!options.lazyFetching || isFetchNeeded(currentView.visStart, currentView.visEnd)) {
494
			refetchEvents();
495
		}
496
		else if (forceRender) {
497
			rerenderEvents();
498
		}
499
	}
500
 
501
 
502
	function refetchEvents() {
503
		fetchEvents(currentView.visStart, currentView.visEnd); // will call reportEvents
504
	}
505
 
506
 
507
	// called when event data arrives
508
	function reportEvents(_events) {
509
		events = _events;
510
		rerenderEvents();
511
	}
512
 
513
 
514
	// called when a single event's data has been changed
515
	function reportEventChange(eventID) {
516
		rerenderEvents(eventID);
517
	}
518
 
519
 
520
	// attempts to rerenderEvents
521
	function rerenderEvents(modifiedEventID) {
522
		markEventsDirty();
523
		if (elementVisible()) {
524
			currentView.clearEvents();
525
			currentView.renderEvents(events, modifiedEventID);
526
			currentView.eventsDirty = false;
527
		}
528
	}
529
 
530
 
531
	function markEventsDirty() {
532
		$.each(viewInstances, function(i, inst) {
533
			inst.eventsDirty = true;
534
		});
535
	}
536
 
537
 
538
 
539
	/* Selection
540
	-----------------------------------------------------------------------------*/
541
 
542
 
543
	function select(start, end, allDay) {
544
		currentView.select(start, end, allDay===undefined ? true : allDay);
545
	}
546
 
547
 
548
	function unselect() { // safe to be called before renderView
549
		if (currentView) {
550
			currentView.unselect();
551
		}
552
	}
553
 
554
 
555
 
556
	/* Date
557
	-----------------------------------------------------------------------------*/
558
 
559
 
560
	function prev() {
561
		renderView(-1);
562
	}
563
 
564
 
565
	function next() {
566
		renderView(1);
567
	}
568
 
569
 
570
	function prevYear() {
571
		addYears(date, -1);
572
		renderView();
573
	}
574
 
575
 
576
	function nextYear() {
577
		addYears(date, 1);
578
		renderView();
579
	}
580
 
581
 
582
	function today() {
583
		date = new Date();
584
		renderView();
585
	}
586
 
587
 
588
	function gotoDate(year, month, dateOfMonth) {
589
		if (year instanceof Date) {
590
			date = cloneDate(year); // provided 1 argument, a Date
591
		}else{
592
			setYMD(date, year, month, dateOfMonth);
593
		}
594
		renderView();
595
	}
596
 
597
 
598
	function incrementDate(years, months, days) {
599
		if (years !== undefined) {
600
			addYears(date, years);
601
		}
602
		if (months !== undefined) {
603
			addMonths(date, months);
604
		}
605
		if (days !== undefined) {
606
			addDays(date, days);
607
		}
608
		renderView();
609
	}
610
 
611
 
612
	function getDate() {
613
		return cloneDate(date);
614
	}
615
 
616
 
617
 
618
	/* Misc
619
	-----------------------------------------------------------------------------*/
620
 
621
 
622
	function getView() {
623
		return currentView;
624
	}
625
 
626
 
627
	function option(name, value) {
628
		if (value === undefined) {
629
			return options[name];
630
		}
631
		if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') {
632
			options[name] = value;
633
			updateSize();
634
		}
635
	}
636
 
637
 
638
	function trigger(name, thisObj) {
639
		if (options[name]) {
640
			return options[name].apply(
641
				thisObj || _element,
642
				Array.prototype.slice.call(arguments, 2)
643
			);
644
		}
645
	}
646
 
647
 
648
 
649
	/* External Dragging
650
	------------------------------------------------------------------------*/
651
 
652
	if (options.droppable) {
653
		$(document)
654
			.bind('dragstart', function(ev, ui) {
655
				var _e = ev.target;
656
				var e = $(_e);
657
				if (!e.parents('.fc').length) { // not already inside a calendar
658
					var accept = options.dropAccept;
659
					if ($.isFunction(accept) ? accept.call(_e, e) : e.is(accept)) {
660
						_dragElement = _e;
661
						currentView.dragStart(_dragElement, ev, ui);
662
					}
663
				}
664
			})
665
			.bind('dragstop', function(ev, ui) {
666
				if (_dragElement) {
667
					currentView.dragStop(_dragElement, ev, ui);
668
					_dragElement = null;
669
				}
670
			});
671
	}
672
 
673
 
674
}
675
 
676
function Header(calendar, options) {
677
	var t = this;
678
 
679
 
680
	// exports
681
	t.render = render;
682
	t.destroy = destroy;
683
	t.updateTitle = updateTitle;
684
	t.activateButton = activateButton;
685
	t.deactivateButton = deactivateButton;
686
	t.disableButton = disableButton;
687
	t.enableButton = enableButton;
688
 
689
 
690
	// locals
691
	var element = $([]);
692
	var tm;
693
 
694
 
695
 
696
	function render() {
697
		tm = options.theme ? 'ui' : 'fc';
698
		var sections = options.header;
699
		if (sections) {
700
			element = $("<table class='fc-header' style='width:100%'/>")
701
				.append(
702
					$("<tr/>")
703
						.append(renderSection('left'))
704
						.append(renderSection('center'))
705
						.append(renderSection('right'))
706
				);
707
			return element;
708
		}
709
	}
710
 
711
 
712
	function destroy() {
713
		element.remove();
714
	}
715
 
716
 
717
	function renderSection(position) {
718
		var e = $("<td class='fc-header-" + position + "'/>");
719
		var buttonStr = options.header[position];
720
		if (buttonStr) {
721
			$.each(buttonStr.split(' '), function(i) {
722
				if (i > 0) {
723
					e.append("<span class='fc-header-space'/>");
724
				}
725
				var prevButton;
726
				$.each(this.split(','), function(j, buttonName) {
727
					if (buttonName == 'title') {
728
						e.append("<span class='fc-header-title'><h2>&nbsp;</h2></span>");
729
						if (prevButton) {
730
							prevButton.addClass(tm + '-corner-right');
731
						}
732
						prevButton = null;
733
					}else{
734
						var buttonClick;
735
						if (calendar[buttonName]) {
736
							buttonClick = calendar[buttonName]; // calendar method
737
						}
738
						else if (fcViews[buttonName]) {
739
							buttonClick = function() {
740
								button.removeClass(tm + '-state-hover'); // forget why
741
								calendar.changeView(buttonName);
742
							};
743
						}
744
						if (buttonClick) {
745
							var icon = options.theme ? smartProperty(options.buttonIcons, buttonName) : null; // why are we using smartProperty here?
746
							var text = smartProperty(options.buttonText, buttonName); // why are we using smartProperty here?
747
							var button = $(
748
								"<span class='fc-button fc-button-" + buttonName + " " + tm + "-state-default'>" +
749
									"<span class='fc-button-inner'>" +
750
										"<span class='fc-button-content'>" +
751
											(icon ?
752
												"<span class='fc-icon-wrap'>" +
753
													"<span class='ui-icon ui-icon-" + icon + "'/>" +
754
												"</span>" :
755
												text
756
												) +
757
										"</span>" +
758
										"<span class='fc-button-effect'><span></span></span>" +
759
									"</span>" +
760
								"</span>"
761
							);
762
							if (button) {
763
								button
764
									.click(function() {
765
										if (!button.hasClass(tm + '-state-disabled')) {
766
											buttonClick();
767
										}
768
									})
769
									.mousedown(function() {
770
										button
771
											.not('.' + tm + '-state-active')
772
											.not('.' + tm + '-state-disabled')
773
											.addClass(tm + '-state-down');
774
									})
775
									.mouseup(function() {
776
										button.removeClass(tm + '-state-down');
777
									})
778
									.hover(
779
										function() {
780
											button
781
												.not('.' + tm + '-state-active')
782
												.not('.' + tm + '-state-disabled')
783
												.addClass(tm + '-state-hover');
784
										},
785
										function() {
786
											button
787
												.removeClass(tm + '-state-hover')
788
												.removeClass(tm + '-state-down');
789
										}
790
									)
791
									.appendTo(e);
792
								if (!prevButton) {
793
									button.addClass(tm + '-corner-left');
794
								}
795
								prevButton = button;
796
							}
797
						}
798
					}
799
				});
800
				if (prevButton) {
801
					prevButton.addClass(tm + '-corner-right');
802
				}
803
			});
804
		}
805
		return e;
806
	}
807
 
808
 
809
	function updateTitle(html) {
810
		element.find('h2')
811
			.html(html);
812
	}
813
 
814
 
815
	function activateButton(buttonName) {
816
		element.find('span.fc-button-' + buttonName)
817
			.addClass(tm + '-state-active');
818
	}
819
 
820
 
821
	function deactivateButton(buttonName) {
822
		element.find('span.fc-button-' + buttonName)
823
			.removeClass(tm + '-state-active');
824
	}
825
 
826
 
827
	function disableButton(buttonName) {
828
		element.find('span.fc-button-' + buttonName)
829
			.addClass(tm + '-state-disabled');
830
	}
831
 
832
 
833
	function enableButton(buttonName) {
834
		element.find('span.fc-button-' + buttonName)
835
			.removeClass(tm + '-state-disabled');
836
	}
837
 
838
 
839
}
840
 
841
fc.sourceNormalizers = [];
842
fc.sourceFetchers = [];
843
 
844
var ajaxDefaults = {
845
	dataType: 'json',
846
	cache: false
847
};
848
 
849
var eventGUID = 1;
850
 
851
 
852
function EventManager(options, _sources) {
853
	var t = this;
854
 
855
 
856
	// exports
857
	t.isFetchNeeded = isFetchNeeded;
858
	t.fetchEvents = fetchEvents;
859
	t.addEventSource = addEventSource;
860
	t.removeEventSource = removeEventSource;
861
	t.updateEvent = updateEvent;
862
	t.renderEvent = renderEvent;
863
	t.removeEvents = removeEvents;
864
	t.clientEvents = clientEvents;
865
	t.normalizeEvent = normalizeEvent;
866
 
867
 
868
	// imports
869
	var trigger = t.trigger;
870
	var getView = t.getView;
871
	var reportEvents = t.reportEvents;
872
 
873
 
874
	// locals
875
	var stickySource = { events: [] };
876
	var sources = [ stickySource ];
877
	var rangeStart, rangeEnd;
878
	var currentFetchID = 0;
879
	var pendingSourceCnt = 0;
880
	var loadingLevel = 0;
881
	var cache = [];
882
 
883
 
884
	for (var i=0; i<_sources.length; i++) {
885
		_addEventSource(_sources[i]);
886
	}
887
 
888
 
889
 
890
	/* Fetching
891
	-----------------------------------------------------------------------------*/
892
 
893
 
894
	function isFetchNeeded(start, end) {
895
		return !rangeStart || start < rangeStart || end > rangeEnd;
896
	}
897
 
898
 
899
	function fetchEvents(start, end) {
900
		rangeStart = start;
901
		rangeEnd = end;
902
		cache = [];
903
		var fetchID = ++currentFetchID;
904
		var len = sources.length;
905
		pendingSourceCnt = len;
906
		for (var i=0; i<len; i++) {
907
			fetchEventSource(sources[i], fetchID);
908
		}
909
	}
910
 
911
 
912
	function fetchEventSource(source, fetchID) {
913
		_fetchEventSource(source, function(events) {
914
			if (fetchID == currentFetchID) {
915
				if (events) {
916
					for (var i=0; i<events.length; i++) {
917
						events[i].source = source;
918
						normalizeEvent(events[i]);
919
					}
920
					cache = cache.concat(events);
921
				}
922
				pendingSourceCnt--;
923
				if (!pendingSourceCnt) {
924
					reportEvents(cache);
925
				}
926
			}
927
		});
928
	}
929
 
930
 
931
	function _fetchEventSource(source, callback) {
932
		var i;
933
		var fetchers = fc.sourceFetchers;
934
		var res;
935
		for (i=0; i<fetchers.length; i++) {
936
			res = fetchers[i](source, rangeStart, rangeEnd, callback);
937
			if (res === true) {
938
				// the fetcher is in charge. made its own async request
939
				return;
940
			}
941
			else if (typeof res == 'object') {
942
				// the fetcher returned a new source. process it
943
				_fetchEventSource(res, callback);
944
				return;
945
			}
946
		}
947
		var events = source.events;
948
		if (events) {
949
			if ($.isFunction(events)) {
950
				pushLoading();
951
				events(cloneDate(rangeStart), cloneDate(rangeEnd), function(events) {
952
					callback(events);
953
					popLoading();
954
				});
955
			}
956
			else if ($.isArray(events)) {
957
				callback(events);
958
			}
959
			else {
960
				callback();
961
			}
962
		}else{
963
			var url = source.url;
964
			if (url) {
965
				var success = source.success;
966
				var error = source.error;
967
				var complete = source.complete;
968
				var data = $.extend({}, source.data || {});
969
				var startParam = firstDefined(source.startParam, options.startParam);
970
				var endParam = firstDefined(source.endParam, options.endParam);
971
				if (startParam) {
972
					data[startParam] = Math.round(+rangeStart / 1000);
973
				}
974
				if (endParam) {
975
					data[endParam] = Math.round(+rangeEnd / 1000);
976
				}
977
				pushLoading();
978
				$.ajax($.extend({}, ajaxDefaults, source, {
979
					data: data,
980
					success: function(events) {
981
						events = events || [];
982
						var res = applyAll(success, this, arguments);
983
						if ($.isArray(res)) {
984
							events = res;
985
						}
986
						callback(events);
987
					},
988
					error: function() {
989
						applyAll(error, this, arguments);
990
						callback();
991
					},
992
					complete: function() {
993
						applyAll(complete, this, arguments);
994
						popLoading();
995
					}
996
				}));
997
			}else{
998
				callback();
999
			}
1000
		}
1001
	}
1002
 
1003
 
1004
 
1005
	/* Sources
1006
	-----------------------------------------------------------------------------*/
1007
 
1008
 
1009
	function addEventSource(source) {
1010
		source = _addEventSource(source);
1011
		if (source) {
1012
			pendingSourceCnt++;
1013
			fetchEventSource(source, currentFetchID); // will eventually call reportEvents
1014
		}
1015
	}
1016
 
1017
 
1018
	function _addEventSource(source) {
1019
		if ($.isFunction(source) || $.isArray(source)) {
1020
			source = { events: source };
1021
		}
1022
		else if (typeof source == 'string') {
1023
			source = { url: source };
1024
		}
1025
		if (typeof source == 'object') {
1026
			normalizeSource(source);
1027
			sources.push(source);
1028
			return source;
1029
		}
1030
	}
1031
 
1032
 
1033
	function removeEventSource(source) {
1034
		sources = $.grep(sources, function(src) {
1035
			return !isSourcesEqual(src, source);
1036
		});
1037
		// remove all client events from that source
1038
		cache = $.grep(cache, function(e) {
1039
			return !isSourcesEqual(e.source, source);
1040
		});
1041
		reportEvents(cache);
1042
	}
1043
 
1044
 
1045
 
1046
	/* Manipulation
1047
	-----------------------------------------------------------------------------*/
1048
 
1049
 
1050
	function updateEvent(event) { // update an existing event
1051
		var i, len = cache.length, e,
1052
			defaultEventEnd = getView().defaultEventEnd, // getView???
1053
			startDelta = event.start - event._start,
1054
			endDelta = event.end ?
1055
				(event.end - (event._end || defaultEventEnd(event))) // event._end would be null if event.end
1056
				: 0;                                                      // was null and event was just resized
1057
		for (i=0; i<len; i++) {
1058
			e = cache[i];
1059
			if (e._id == event._id && e != event) {
1060
				e.start = new Date(+e.start + startDelta);
1061
				if (event.end) {
1062
					if (e.end) {
1063
						e.end = new Date(+e.end + endDelta);
1064
					}else{
1065
						e.end = new Date(+defaultEventEnd(e) + endDelta);
1066
					}
1067
				}else{
1068
					e.end = null;
1069
				}
1070
				e.title = event.title;
1071
				e.url = event.url;
1072
				e.allDay = event.allDay;
1073
				e.className = event.className;
1074
				e.editable = event.editable;
1075
				e.color = event.color;
1076
				e.backgroudColor = event.backgroudColor;
1077
				e.borderColor = event.borderColor;
1078
				e.textColor = event.textColor;
1079
				normalizeEvent(e);
1080
			}
1081
		}
1082
		normalizeEvent(event);
1083
		reportEvents(cache);
1084
	}
1085
 
1086
 
1087
	function renderEvent(event, stick) {
1088
		normalizeEvent(event);
1089
		if (!event.source) {
1090
			if (stick) {
1091
				stickySource.events.push(event);
1092
				event.source = stickySource;
1093
			}
1094
			cache.push(event);
1095
		}
1096
		reportEvents(cache);
1097
	}
1098
 
1099
 
1100
	function removeEvents(filter) {
1101
		if (!filter) { // remove all
1102
			cache = [];
1103
			// clear all array sources
1104
			for (var i=0; i<sources.length; i++) {
1105
				if ($.isArray(sources[i].events)) {
1106
					sources[i].events = [];
1107
				}
1108
			}
1109
		}else{
1110
			if (!$.isFunction(filter)) { // an event ID
1111
				var id = filter + '';
1112
				filter = function(e) {
1113
					return e._id == id;
1114
				};
1115
			}
1116
			cache = $.grep(cache, filter, true);
1117
			// remove events from array sources
1118
			for (var i=0; i<sources.length; i++) {
1119
				if ($.isArray(sources[i].events)) {
1120
					sources[i].events = $.grep(sources[i].events, filter, true);
1121
				}
1122
			}
1123
		}
1124
		reportEvents(cache);
1125
	}
1126
 
1127
 
1128
	function clientEvents(filter) {
1129
		if ($.isFunction(filter)) {
1130
			return $.grep(cache, filter);
1131
		}
1132
		else if (filter) { // an event ID
1133
			filter += '';
1134
			return $.grep(cache, function(e) {
1135
				return e._id == filter;
1136
			});
1137
		}
1138
		return cache; // else, return all
1139
	}
1140
 
1141
 
1142
 
1143
	/* Loading State
1144
	-----------------------------------------------------------------------------*/
1145
 
1146
 
1147
	function pushLoading() {
1148
		if (!loadingLevel++) {
1149
			trigger('loading', null, true);
1150
		}
1151
	}
1152
 
1153
 
1154
	function popLoading() {
1155
		if (!--loadingLevel) {
1156
			trigger('loading', null, false);
1157
		}
1158
	}
1159
 
1160
 
1161
 
1162
	/* Event Normalization
1163
	-----------------------------------------------------------------------------*/
1164
 
1165
 
1166
	function normalizeEvent(event) {
1167
		var source = event.source || {};
1168
		var ignoreTimezone = firstDefined(source.ignoreTimezone, options.ignoreTimezone);
1169
		event._id = event._id || (event.id === undefined ? '_fc' + eventGUID++ : event.id + '');
1170
		if (event.date) {
1171
			if (!event.start) {
1172
				event.start = event.date;
1173
			}
1174
			delete event.date;
1175
		}
1176
		event._start = cloneDate(event.start = parseDate(event.start, ignoreTimezone));
1177
		event.end = parseDate(event.end, ignoreTimezone);
1178
		if (event.end && event.end <= event.start) {
1179
			event.end = null;
1180
		}
1181
		event._end = event.end ? cloneDate(event.end) : null;
1182
		if (event.allDay === undefined) {
1183
			event.allDay = firstDefined(source.allDayDefault, options.allDayDefault);
1184
		}
1185
		if (event.className) {
1186
			if (typeof event.className == 'string') {
1187
				event.className = event.className.split(/\s+/);
1188
			}
1189
		}else{
1190
			event.className = [];
1191
		}
1192
		// TODO: if there is no start date, return false to indicate an invalid event
1193
	}
1194
 
1195
 
1196
 
1197
	/* Utils
1198
	------------------------------------------------------------------------------*/
1199
 
1200
 
1201
	function normalizeSource(source) {
1202
		if (source.className) {
1203
			// TODO: repeat code, same code for event classNames
1204
			if (typeof source.className == 'string') {
1205
				source.className = source.className.split(/\s+/);
1206
			}
1207
		}else{
1208
			source.className = [];
1209
		}
1210
		var normalizers = fc.sourceNormalizers;
1211
		for (var i=0; i<normalizers.length; i++) {
1212
			normalizers[i](source);
1213
		}
1214
	}
1215
 
1216
 
1217
	function isSourcesEqual(source1, source2) {
1218
		return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2);
1219
	}
1220
 
1221
 
1222
	function getSourcePrimitive(source) {
1223
		return ((typeof source == 'object') ? (source.events || source.url) : '') || source;
1224
	}
1225
 
1226
 
1227
}
1228
 
1229
 
1230
fc.addDays = addDays;
1231
fc.cloneDate = cloneDate;
1232
fc.parseDate = parseDate;
1233
fc.parseISO8601 = parseISO8601;
1234
fc.parseTime = parseTime;
1235
fc.formatDate = formatDate;
1236
fc.formatDates = formatDates;
1237
 
1238
 
1239
 
1240
/* Date Math
1241
-----------------------------------------------------------------------------*/
1242
 
1243
var dayIDs = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'],
1244
	DAY_MS = 86400000,
1245
	HOUR_MS = 3600000,
1246
	MINUTE_MS = 60000;
1247
 
1248
 
1249
function addYears(d, n, keepTime) {
1250
	d.setFullYear(d.getFullYear() + n);
1251
	if (!keepTime) {
1252
		clearTime(d);
1253
	}
1254
	return d;
1255
}
1256
 
1257
 
1258
function addMonths(d, n, keepTime) { // prevents day overflow/underflow
1259
	if (+d) { // prevent infinite looping on invalid dates
1260
		var m = d.getMonth() + n,
1261
			check = cloneDate(d);
1262
		check.setDate(1);
1263
		check.setMonth(m);
1264
		d.setMonth(m);
1265
		if (!keepTime) {
1266
			clearTime(d);
1267
		}
1268
		while (d.getMonth() != check.getMonth()) {
1269
			d.setDate(d.getDate() + (d < check ? 1 : -1));
1270
		}
1271
	}
1272
	return d;
1273
}
1274
 
1275
 
1276
function addDays(d, n, keepTime) { // deals with daylight savings
1277
	if (+d) {
1278
		var dd = d.getDate() + n,
1279
			check = cloneDate(d);
1280
		check.setHours(9); // set to middle of day
1281
		check.setDate(dd);
1282
		d.setDate(dd);
1283
		if (!keepTime) {
1284
			clearTime(d);
1285
		}
1286
		fixDate(d, check);
1287
	}
1288
	return d;
1289
}
1290
 
1291
 
1292
function fixDate(d, check) { // force d to be on check's YMD, for daylight savings purposes
1293
	if (+d) { // prevent infinite looping on invalid dates
1294
		while (d.getDate() != check.getDate()) {
1295
			d.setTime(+d + (d < check ? 1 : -1) * HOUR_MS);
1296
		}
1297
	}
1298
}
1299
 
1300
 
1301
function addMinutes(d, n) {
1302
	d.setMinutes(d.getMinutes() + n);
1303
	return d;
1304
}
1305
 
1306
 
1307
function clearTime(d) {
1308
	d.setHours(0);
1309
	d.setMinutes(0);
1310
	d.setSeconds(0); 
1311
	d.setMilliseconds(0);
1312
	return d;
1313
}
1314
 
1315
 
1316
function cloneDate(d, dontKeepTime) {
1317
	if (dontKeepTime) {
1318
		return clearTime(new Date(+d));
1319
	}
1320
	return new Date(+d);
1321
}
1322
 
1323
 
1324
function zeroDate() { // returns a Date with time 00:00:00 and dateOfMonth=1
1325
	var i=0, d;
1326
	do {
1327
		d = new Date(1970, i++, 1);
1328
	} while (d.getHours()); // != 0
1329
	return d;
1330
}
1331
 
1332
 
1333
function skipWeekend(date, inc, excl) {
1334
	inc = inc || 1;
1335
	while (!date.getDay() || (excl && date.getDay()==1 || !excl && date.getDay()==6)) {
1336
		addDays(date, inc);
1337
	}
1338
	return date;
1339
}
1340
 
1341
 
1342
function dayDiff(d1, d2) { // d1 - d2
1343
	return Math.round((cloneDate(d1, true) - cloneDate(d2, true)) / DAY_MS);
1344
}
1345
 
1346
 
1347
function setYMD(date, y, m, d) {
1348
	if (y !== undefined && y != date.getFullYear()) {
1349
		date.setDate(1);
1350
		date.setMonth(0);
1351
		date.setFullYear(y);
1352
	}
1353
	if (m !== undefined && m != date.getMonth()) {
1354
		date.setDate(1);
1355
		date.setMonth(m);
1356
	}
1357
	if (d !== undefined) {
1358
		date.setDate(d);
1359
	}
1360
}
1361
 
1362
 
1363
 
1364
/* Date Parsing
1365
-----------------------------------------------------------------------------*/
1366
 
1367
 
1368
function parseDate(s, ignoreTimezone) { // ignoreTimezone defaults to true
1369
	if (typeof s == 'object') { // already a Date object
1370
		return s;
1371
	}
1372
	if (typeof s == 'number') { // a UNIX timestamp
1373
		return new Date(s * 1000);
1374
	}
1375
	if (typeof s == 'string') {
1376
		if (s.match(/^\d+(\.\d+)?$/)) { // a UNIX timestamp
1377
			return new Date(parseFloat(s) * 1000);
1378
		}
1379
		if (ignoreTimezone === undefined) {
1380
			ignoreTimezone = true;
1381
		}
1382
		return parseISO8601(s, ignoreTimezone) || (s ? new Date(s) : null);
1383
	}
1384
	// TODO: never return invalid dates (like from new Date(<string>)), return null instead
1385
	return null;
1386
}
1387
 
1388
 
1389
function parseISO8601(s, ignoreTimezone) { // ignoreTimezone defaults to false
1390
	// derived from http://delete.me.uk/2005/03/iso8601.html
1391
	// TODO: for a know glitch/feature, read tests/issue_206_parseDate_dst.html
1392
	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}))?))?)?)?)?$/);
1393
	if (!m) {
1394
		return null;
1395
	}
1396
	var date = new Date(m[1], 0, 1);
1397
	if (ignoreTimezone || !m[13]) {
1398
		var check = new Date(m[1], 0, 1, 9, 0);
1399
		if (m[3]) {
1400
			date.setMonth(m[3] - 1);
1401
			check.setMonth(m[3] - 1);
1402
		}
1403
		if (m[5]) {
1404
			date.setDate(m[5]);
1405
			check.setDate(m[5]);
1406
		}
1407
		fixDate(date, check);
1408
		if (m[7]) {
1409
			date.setHours(m[7]);
1410
		}
1411
		if (m[8]) {
1412
			date.setMinutes(m[8]);
1413
		}
1414
		if (m[10]) {
1415
			date.setSeconds(m[10]);
1416
		}
1417
		if (m[12]) {
1418
			date.setMilliseconds(Number("0." + m[12]) * 1000);
1419
		}
1420
		fixDate(date, check);
1421
	}else{
1422
		date.setUTCFullYear(
1423
			m[1],
1424
			m[3] ? m[3] - 1 : 0,
1425
			m[5] || 1
1426
		);
1427
		date.setUTCHours(
1428
			m[7] || 0,
1429
			m[8] || 0,
1430
			m[10] || 0,
1431
			m[12] ? Number("0." + m[12]) * 1000 : 0
1432
		);
1433
		if (m[14]) {
1434
			var offset = Number(m[16]) * 60 + (m[18] ? Number(m[18]) : 0);
1435
			offset *= m[15] == '-' ? 1 : -1;
1436
			date = new Date(+date + (offset * 60 * 1000));
1437
		}
1438
	}
1439
	return date;
1440
}
1441
 
1442
 
1443
function parseTime(s) { // returns minutes since start of day
1444
	if (typeof s == 'number') { // an hour
1445
		return s * 60;
1446
	}
1447
	if (typeof s == 'object') { // a Date object
1448
		return s.getHours() * 60 + s.getMinutes();
1449
	}
1450
	var m = s.match(/(\d+)(?::(\d+))?\s*(\w+)?/);
1451
	if (m) {
1452
		var h = parseInt(m[1], 10);
1453
		if (m[3]) {
1454
			h %= 12;
1455
			if (m[3].toLowerCase().charAt(0) == 'p') {
1456
				h += 12;
1457
			}
1458
		}
1459
		return h * 60 + (m[2] ? parseInt(m[2], 10) : 0);
1460
	}
1461
}
1462
 
1463
 
1464
 
1465
/* Date Formatting
1466
-----------------------------------------------------------------------------*/
1467
// TODO: use same function formatDate(date, [date2], format, [options])
1468
 
1469
 
1470
function formatDate(date, format, options) {
1471
	return formatDates(date, null, format, options);
1472
}
1473
 
1474
 
1475
function formatDates(date1, date2, format, options) {
1476
	options = options || defaults;
1477
	var date = date1,
1478
		otherDate = date2,
1479
		i, len = format.length, c,
1480
		i2, formatter,
1481
		res = '';
1482
	for (i=0; i<len; i++) {
1483
		c = format.charAt(i);
1484
		if (c == "'") {
1485
			for (i2=i+1; i2<len; i2++) {
1486
				if (format.charAt(i2) == "'") {
1487
					if (date) {
1488
						if (i2 == i+1) {
1489
							res += "'";
1490
						}else{
1491
							res += format.substring(i+1, i2);
1492
						}
1493
						i = i2;
1494
					}
1495
					break;
1496
				}
1497
			}
1498
		}
1499
		else if (c == '(') {
1500
			for (i2=i+1; i2<len; i2++) {
1501
				if (format.charAt(i2) == ')') {
1502
					var subres = formatDate(date, format.substring(i+1, i2), options);
1503
					if (parseInt(subres.replace(/\D/, ''), 10)) {
1504
						res += subres;
1505
					}
1506
					i = i2;
1507
					break;
1508
				}
1509
			}
1510
		}
1511
		else if (c == '[') {
1512
			for (i2=i+1; i2<len; i2++) {
1513
				if (format.charAt(i2) == ']') {
1514
					var subformat = format.substring(i+1, i2);
1515
					var subres = formatDate(date, subformat, options);
1516
					if (subres != formatDate(otherDate, subformat, options)) {
1517
						res += subres;
1518
					}
1519
					i = i2;
1520
					break;
1521
				}
1522
			}
1523
		}
1524
		else if (c == '{') {
1525
			date = date2;
1526
			otherDate = date1;
1527
		}
1528
		else if (c == '}') {
1529
			date = date1;
1530
			otherDate = date2;
1531
		}
1532
		else {
1533
			for (i2=len; i2>i; i2--) {
1534
				if (formatter = dateFormatters[format.substring(i, i2)]) {
1535
					if (date) {
1536
						res += formatter(date, options);
1537
					}
1538
					i = i2 - 1;
1539
					break;
1540
				}
1541
			}
1542
			if (i2 == i) {
1543
				if (date) {
1544
					res += c;
1545
				}
1546
			}
1547
		}
1548
	}
1549
	return res;
1550
};
1551
 
1552
 
1553
var dateFormatters = {
1554
	s	: function(d)	{ return d.getSeconds() },
1555
	ss	: function(d)	{ return zeroPad(d.getSeconds()) },
1556
	m	: function(d)	{ return d.getMinutes() },
1557
	mm	: function(d)	{ return zeroPad(d.getMinutes()) },
1558
	h	: function(d)	{ return d.getHours() % 12 || 12 },
1559
	hh	: function(d)	{ return zeroPad(d.getHours() % 12 || 12) },
1560
	H	: function(d)	{ return d.getHours() },
1561
	HH	: function(d)	{ return zeroPad(d.getHours()) },
1562
	d	: function(d)	{ return d.getDate() },
1563
	dd	: function(d)	{ return zeroPad(d.getDate()) },
1564
	ddd	: function(d,o)	{ return o.dayNamesShort[d.getDay()] },
1565
	dddd: function(d,o)	{ return o.dayNames[d.getDay()] },
1566
	M	: function(d)	{ return d.getMonth() + 1 },
1567
	MM	: function(d)	{ return zeroPad(d.getMonth() + 1) },
1568
	MMM	: function(d,o)	{ return o.monthNamesShort[d.getMonth()] },
1569
	MMMM: function(d,o)	{ return o.monthNames[d.getMonth()] },
1570
	yy	: function(d)	{ return (d.getFullYear()+'').substring(2) },
1571
	yyyy: function(d)	{ return d.getFullYear() },
1572
	t	: function(d)	{ return d.getHours() < 12 ? 'a' : 'p' },
1573
	tt	: function(d)	{ return d.getHours() < 12 ? 'am' : 'pm' },
1574
	T	: function(d)	{ return d.getHours() < 12 ? 'A' : 'P' },
1575
	TT	: function(d)	{ return d.getHours() < 12 ? 'AM' : 'PM' },
1576
	u	: function(d)	{ return formatDate(d, "yyyy-MM-dd'T'HH:mm:ss'Z'") },
1577
	S	: function(d)	{
1578
		var date = d.getDate();
1579
		if (date > 10 && date < 20) {
1580
			return 'th';
1581
		}
1582
		return ['st', 'nd', 'rd'][date%10-1] || 'th';
1583
	}
1584
};
1585
 
1586
 
1587
 
1588
fc.applyAll = applyAll;
1589
 
1590
 
1591
/* Event Date Math
1592
-----------------------------------------------------------------------------*/
1593
 
1594
 
1595
function exclEndDay(event) {
1596
	if (event.end) {
1597
		return _exclEndDay(event.end, event.allDay);
1598
	}else{
1599
		return addDays(cloneDate(event.start), 1);
1600
	}
1601
}
1602
 
1603
 
1604
function _exclEndDay(end, allDay) {
1605
	end = cloneDate(end);
1606
	return allDay || end.getHours() || end.getMinutes() ? addDays(end, 1) : clearTime(end);
1607
}
1608
 
1609
 
1610
function segCmp(a, b) {
1611
	return (b.msLength - a.msLength) * 100 + (a.event.start - b.event.start);
1612
}
1613
 
1614
 
1615
function segsCollide(seg1, seg2) {
1616
	return seg1.end > seg2.start && seg1.start < seg2.end;
1617
}
1618
 
1619
 
1620
 
1621
/* Event Sorting
1622
-----------------------------------------------------------------------------*/
1623
 
1624
 
1625
// event rendering utilities
1626
function sliceSegs(events, visEventEnds, start, end) {
1627
	var segs = [],
1628
		i, len=events.length, event,
1629
		eventStart, eventEnd,
1630
		segStart, segEnd,
1631
		isStart, isEnd;
1632
	for (i=0; i<len; i++) {
1633
		event = events[i];
1634
		eventStart = event.start;
1635
		eventEnd = visEventEnds[i];
1636
		if (eventEnd > start && eventStart < end) {
1637
			if (eventStart < start) {
1638
				segStart = cloneDate(start);
1639
				isStart = false;
1640
			}else{
1641
				segStart = eventStart;
1642
				isStart = true;
1643
			}
1644
			if (eventEnd > end) {
1645
				segEnd = cloneDate(end);
1646
				isEnd = false;
1647
			}else{
1648
				segEnd = eventEnd;
1649
				isEnd = true;
1650
			}
1651
			segs.push({
1652
				event: event,
1653
				start: segStart,
1654
				end: segEnd,
1655
				isStart: isStart,
1656
				isEnd: isEnd,
1657
				msLength: segEnd - segStart
1658
			});
1659
		}
1660
	}
1661
	return segs.sort(segCmp);
1662
}
1663
 
1664
 
1665
// event rendering calculation utilities
1666
function stackSegs(segs) {
1667
	var levels = [],
1668
		i, len = segs.length, seg,
1669
		j, collide, k;
1670
	for (i=0; i<len; i++) {
1671
		seg = segs[i];
1672
		j = 0; // the level index where seg should belong
1673
		while (true) {
1674
			collide = false;
1675
			if (levels[j]) {
1676
				for (k=0; k<levels[j].length; k++) {
1677
					if (segsCollide(levels[j][k], seg)) {
1678
						collide = true;
1679
						break;
1680
					}
1681
				}
1682
			}
1683
			if (collide) {
1684
				j++;
1685
			}else{
1686
				break;
1687
			}
1688
		}
1689
		if (levels[j]) {
1690
			levels[j].push(seg);
1691
		}else{
1692
			levels[j] = [seg];
1693
		}
1694
	}
1695
	return levels;
1696
}
1697
 
1698
 
1699
 
1700
/* Event Element Binding
1701
-----------------------------------------------------------------------------*/
1702
 
1703
 
1704
function lazySegBind(container, segs, bindHandlers) {
1705
	container.unbind('mouseover').mouseover(function(ev) {
1706
		var parent=ev.target, e,
1707
			i, seg;
1708
		while (parent != this) {
1709
			e = parent;
1710
			parent = parent.parentNode;
1711
		}
1712
		if ((i = e._fci) !== undefined) {
1713
			e._fci = undefined;
1714
			seg = segs[i];
1715
			bindHandlers(seg.event, seg.element, seg);
1716
			$(ev.target).trigger(ev);
1717
		}
1718
		ev.stopPropagation();
1719
	});
1720
}
1721
 
1722
 
1723
 
1724
/* Element Dimensions
1725
-----------------------------------------------------------------------------*/
1726
 
1727
 
1728
function setOuterWidth(element, width, includeMargins) {
1729
	for (var i=0, e; i<element.length; i++) {
1730
		e = $(element[i]);
1731
		e.width(Math.max(0, width - hsides(e, includeMargins)));
1732
	}
1733
}
1734
 
1735
 
1736
function setOuterHeight(element, height, includeMargins) {
1737
	for (var i=0, e; i<element.length; i++) {
1738
		e = $(element[i]);
1739
		e.height(Math.max(0, height - vsides(e, includeMargins)));
1740
	}
1741
}
1742
 
1743
 
1744
function hsides(element, includeMargins) {
1745
	return hpadding(element) + hborders(element) + (includeMargins ? hmargins(element) : 0);
1746
}
1747
 
1748
 
1749
function hpadding(element) {
1750
	return (parseFloat($.css(element[0], 'paddingLeft', true)) || 0) +
1751
	       (parseFloat($.css(element[0], 'paddingRight', true)) || 0);
1752
}
1753
 
1754
 
1755
function hmargins(element) {
1756
	return (parseFloat($.css(element[0], 'marginLeft', true)) || 0) +
1757
	       (parseFloat($.css(element[0], 'marginRight', true)) || 0);
1758
}
1759
 
1760
 
1761
function hborders(element) {
1762
	return (parseFloat($.css(element[0], 'borderLeftWidth', true)) || 0) +
1763
	       (parseFloat($.css(element[0], 'borderRightWidth', true)) || 0);
1764
}
1765
 
1766
 
1767
function vsides(element, includeMargins) {
1768
	return vpadding(element) +  vborders(element) + (includeMargins ? vmargins(element) : 0);
1769
}
1770
 
1771
 
1772
function vpadding(element) {
1773
	return (parseFloat($.css(element[0], 'paddingTop', true)) || 0) +
1774
	       (parseFloat($.css(element[0], 'paddingBottom', true)) || 0);
1775
}
1776
 
1777
 
1778
function vmargins(element) {
1779
	return (parseFloat($.css(element[0], 'marginTop', true)) || 0) +
1780
	       (parseFloat($.css(element[0], 'marginBottom', true)) || 0);
1781
}
1782
 
1783
 
1784
function vborders(element) {
1785
	return (parseFloat($.css(element[0], 'borderTopWidth', true)) || 0) +
1786
	       (parseFloat($.css(element[0], 'borderBottomWidth', true)) || 0);
1787
}
1788
 
1789
 
1790
function setMinHeight(element, height) {
1791
	height = (typeof height == 'number' ? height + 'px' : height);
1792
	element.each(function(i, _element) {
1793
		_element.style.cssText += ';min-height:' + height + ';_height:' + height;
1794
		// why can't we just use .css() ? i forget
1795
	});
1796
}
1797
 
1798
 
1799
 
1800
/* Misc Utils
1801
-----------------------------------------------------------------------------*/
1802
 
1803
 
1804
//TODO: arraySlice
1805
//TODO: isFunction, grep ?
1806
 
1807
 
1808
function noop() { }
1809
 
1810
 
1811
function cmp(a, b) {
1812
	return a - b;
1813
}
1814
 
1815
 
1816
function arrayMax(a) {
1817
	return Math.max.apply(Math, a);
1818
}
1819
 
1820
 
1821
function zeroPad(n) {
1822
	return (n < 10 ? '0' : '') + n;
1823
}
1824
 
1825
 
1826
function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object
1827
	if (obj[name] !== undefined) {
1828
		return obj[name];
1829
	}
1830
	var parts = name.split(/(?=[A-Z])/),
1831
		i=parts.length-1, res;
1832
	for (; i>=0; i--) {
1833
		res = obj[parts[i].toLowerCase()];
1834
		if (res !== undefined) {
1835
			return res;
1836
		}
1837
	}
1838
	return obj[''];
1839
}
1840
 
1841
 
1842
function htmlEscape(s) {
1843
	return s.replace(/&/g, '&amp;')
1844
		.replace(/</g, '&lt;')
1845
		.replace(/>/g, '&gt;')
1846
		.replace(/'/g, '&#039;')
1847
		.replace(/"/g, '&quot;')
1848
		.replace(/\n/g, '<br />');
1849
}
1850
 
1851
 
1852
function cssKey(_element) {
1853
	return _element.id + '/' + _element.className + '/' + _element.style.cssText.replace(/(^|;)\s*(top|left|width|height)\s*:[^;]*/ig, '');
1854
}
1855
 
1856
 
1857
function disableTextSelection(element) {
1858
	element
1859
		.attr('unselectable', 'on')
1860
		.css('MozUserSelect', 'none')
1861
		.bind('selectstart.ui', function() { return false; });
1862
}
1863
 
1864
 
1865
/*
1866
function enableTextSelection(element) {
1867
	element
1868
		.attr('unselectable', 'off')
1869
		.css('MozUserSelect', '')
1870
		.unbind('selectstart.ui');
1871
}
1872
*/
1873
 
1874
 
1875
function markFirstLast(e) {
1876
	e.children()
1877
		.removeClass('fc-first fc-last')
1878
		.filter(':first-child')
1879
			.addClass('fc-first')
1880
		.end()
1881
		.filter(':last-child')
1882
			.addClass('fc-last');
1883
}
1884
 
1885
 
1886
function setDayID(cell, date) {
1887
	cell.each(function(i, _cell) {
1888
		_cell.className = _cell.className.replace(/^fc-\w*/, 'fc-' + dayIDs[date.getDay()]);
1889
		// TODO: make a way that doesn't rely on order of classes
1890
	});
1891
}
1892
 
1893
 
1894
function getSkinCss(event, opt) {
1895
	var source = event.source || {};
1896
	var eventColor = event.color;
1897
	var sourceColor = source.color;
1898
	var optionColor = opt('eventColor');
1899
	var backgroundColor =
1900
		event.backgroundColor ||
1901
		eventColor ||
1902
		source.backgroundColor ||
1903
		sourceColor ||
1904
		opt('eventBackgroundColor') ||
1905
		optionColor;
1906
	var borderColor =
1907
		event.borderColor ||
1908
		eventColor ||
1909
		source.borderColor ||
1910
		sourceColor ||
1911
		opt('eventBorderColor') ||
1912
		optionColor;
1913
	var textColor =
1914
		event.textColor ||
1915
		source.textColor ||
1916
		opt('eventTextColor');
1917
	var statements = [];
1918
	if (backgroundColor) {
1919
		statements.push('background-color:' + backgroundColor);
1920
	}
1921
	if (borderColor) {
1922
		statements.push('border-color:' + borderColor);
1923
	}
1924
	if (textColor) {
1925
		statements.push('color:' + textColor);
1926
	}
1927
	return statements.join(';');
1928
}
1929
 
1930
 
1931
function applyAll(functions, thisObj, args) {
1932
	if ($.isFunction(functions)) {
1933
		functions = [ functions ];
1934
	}
1935
	if (functions) {
1936
		var i;
1937
		var ret;
1938
		for (i=0; i<functions.length; i++) {
1939
			ret = functions[i].apply(thisObj, args) || ret;
1940
		}
1941
		return ret;
1942
	}
1943
}
1944
 
1945
 
1946
function firstDefined() {
1947
	for (var i=0; i<arguments.length; i++) {
1948
		if (arguments[i] !== undefined) {
1949
			return arguments[i];
1950
		}
1951
	}
1952
}
1953
 
1954
 
1955
fcViews.month = MonthView;
1956
 
1957
function MonthView(element, calendar) {
1958
	var t = this;
1959
 
1960
 
1961
	// exports
1962
	t.render = render;
1963
 
1964
 
1965
	// imports
1966
	BasicView.call(t, element, calendar, 'month');
1967
	var opt = t.opt;
1968
	var renderBasic = t.renderBasic;
1969
	var formatDate = calendar.formatDate;
1970
 
1971
 
1972
 
1973
	function render(date, delta) {
1974
		if (delta) {
1975
			addMonths(date, delta);
1976
			date.setDate(1);
1977
		}
1978
		var start = cloneDate(date, true);
1979
		start.setDate(1);
1980
		var end = addMonths(cloneDate(start), 1);
1981
		var visStart = cloneDate(start);
1982
		var visEnd = cloneDate(end);
1983
		var firstDay = opt('firstDay');
1984
		var nwe = opt('weekends') ? 0 : 1;
1985
		if (nwe) {
1986
			skipWeekend(visStart);
1987
			skipWeekend(visEnd, -1, true);
1988
		}
1989
		addDays(visStart, -((visStart.getDay() - Math.max(firstDay, nwe) + 7) % 7));
1990
		addDays(visEnd, (7 - visEnd.getDay() + Math.max(firstDay, nwe)) % 7);
1991
		var rowCnt = Math.round((visEnd - visStart) / (DAY_MS * 7));
1992
		if (opt('weekMode') == 'fixed') {
1993
			addDays(visEnd, (6 - rowCnt) * 7);
1994
			rowCnt = 6;
1995
		}
1996
		t.title = formatDate(start, opt('titleFormat'));
1997
		t.start = start;
1998
		t.end = end;
1999
		t.visStart = visStart;
2000
		t.visEnd = visEnd;
2001
		renderBasic(6, rowCnt, nwe ? 5 : 7, true);
2002
	}
2003
 
2004
 
2005
}
2006
 
2007
fcViews.basicWeek = BasicWeekView;
2008
 
2009
function BasicWeekView(element, calendar) {
2010
	var t = this;
2011
 
2012
 
2013
	// exports
2014
	t.render = render;
2015
 
2016
 
2017
	// imports
2018
	BasicView.call(t, element, calendar, 'basicWeek');
2019
	var opt = t.opt;
2020
	var renderBasic = t.renderBasic;
2021
	var formatDates = calendar.formatDates;
2022
 
2023
 
2024
 
2025
	function render(date, delta) {
2026
		if (delta) {
2027
			addDays(date, delta * 7);
2028
		}
2029
		var start = addDays(cloneDate(date), -((date.getDay() - opt('firstDay') + 7) % 7));
2030
		var end = addDays(cloneDate(start), 7);
2031
		var visStart = cloneDate(start);
2032
		var visEnd = cloneDate(end);
2033
		var weekends = opt('weekends');
2034
		if (!weekends) {
2035
			skipWeekend(visStart);
2036
			skipWeekend(visEnd, -1, true);
2037
		}
2038
		t.title = formatDates(
2039
			visStart,
2040
			addDays(cloneDate(visEnd), -1),
2041
			opt('titleFormat')
2042
		);
2043
		t.start = start;
2044
		t.end = end;
2045
		t.visStart = visStart;
2046
		t.visEnd = visEnd;
2047
		renderBasic(1, 1, weekends ? 7 : 5, false);
2048
	}
2049
 
2050
 
2051
}
2052
 
2053
fcViews.basicDay = BasicDayView;
2054
 
2055
//TODO: when calendar's date starts out on a weekend, shouldn't happen
2056
 
2057
 
2058
function BasicDayView(element, calendar) {
2059
	var t = this;
2060
 
2061
 
2062
	// exports
2063
	t.render = render;
2064
 
2065
 
2066
	// imports
2067
	BasicView.call(t, element, calendar, 'basicDay');
2068
	var opt = t.opt;
2069
	var renderBasic = t.renderBasic;
2070
	var formatDate = calendar.formatDate;
2071
 
2072
 
2073
 
2074
	function render(date, delta) {
2075
		if (delta) {
2076
			addDays(date, delta);
2077
			if (!opt('weekends')) {
2078
				skipWeekend(date, delta < 0 ? -1 : 1);
2079
			}
2080
		}
2081
		t.title = formatDate(date, opt('titleFormat'));
2082
		t.start = t.visStart = cloneDate(date, true);
2083
		t.end = t.visEnd = addDays(cloneDate(t.start), 1);
2084
		renderBasic(1, 1, 1, false);
2085
	}
2086
 
2087
 
2088
}
2089
 
2090
setDefaults({
2091
	weekMode: 'fixed'
2092
});
2093
 
2094
 
2095
function BasicView(element, calendar, viewName) {
2096
	var t = this;
2097
 
2098
 
2099
	// exports
2100
	t.renderBasic = renderBasic;
2101
	t.setHeight = setHeight;
2102
	t.setWidth = setWidth;
2103
	t.renderDayOverlay = renderDayOverlay;
2104
	t.defaultSelectionEnd = defaultSelectionEnd;
2105
	t.renderSelection = renderSelection;
2106
	t.clearSelection = clearSelection;
2107
	t.reportDayClick = reportDayClick; // for selection (kinda hacky)
2108
	t.dragStart = dragStart;
2109
	t.dragStop = dragStop;
2110
	t.defaultEventEnd = defaultEventEnd;
2111
	t.getHoverListener = function() { return hoverListener };
2112
	t.colContentLeft = colContentLeft;
2113
	t.colContentRight = colContentRight;
2114
	t.dayOfWeekCol = dayOfWeekCol;
2115
	t.dateCell = dateCell;
2116
	t.cellDate = cellDate;
2117
	t.cellIsAllDay = function() { return true };
2118
	t.allDayRow = allDayRow;
2119
	t.allDayBounds = allDayBounds;
2120
	t.getRowCnt = function() { return rowCnt };
2121
	t.getColCnt = function() { return colCnt };
2122
	t.getColWidth = function() { return colWidth };
2123
	t.getDaySegmentContainer = function() { return daySegmentContainer };
2124
 
2125
 
2126
	// imports
2127
	View.call(t, element, calendar, viewName);
2128
	OverlayManager.call(t);
2129
	SelectionManager.call(t);
2130
	BasicEventRenderer.call(t);
2131
	var opt = t.opt;
2132
	var trigger = t.trigger;
2133
	var clearEvents = t.clearEvents;
2134
	var renderOverlay = t.renderOverlay;
2135
	var clearOverlays = t.clearOverlays;
2136
	var daySelectionMousedown = t.daySelectionMousedown;
2137
	var formatDate = calendar.formatDate;
2138
 
2139
 
2140
	// locals
2141
 
2142
	var head;
2143
	var headCells;
2144
	var body;
2145
	var bodyRows;
2146
	var bodyCells;
2147
	var bodyFirstCells;
2148
	var bodyCellTopInners;
2149
	var daySegmentContainer;
2150
 
2151
	var viewWidth;
2152
	var viewHeight;
2153
	var colWidth;
2154
 
2155
	var rowCnt, colCnt;
2156
	var coordinateGrid;
2157
	var hoverListener;
2158
	var colContentPositions;
2159
 
2160
	var rtl, dis, dit;
2161
	var firstDay;
2162
	var nwe;
2163
	var tm;
2164
	var colFormat;
2165
 
2166
 
2167
 
2168
	/* Rendering
2169
	------------------------------------------------------------*/
2170
 
2171
 
2172
	disableTextSelection(element.addClass('fc-grid'));
2173
 
2174
 
2175
	function renderBasic(maxr, r, c, showNumbers) {
2176
		rowCnt = r;
2177
		colCnt = c;
2178
		updateOptions();
2179
		var firstTime = !body;
2180
		if (firstTime) {
2181
			buildSkeleton(maxr, showNumbers);
2182
		}else{
2183
			clearEvents();
2184
		}
2185
		updateCells(firstTime);
2186
	}
2187
 
2188
 
2189
 
2190
	function updateOptions() {
2191
		rtl = opt('isRTL');
2192
		if (rtl) {
2193
			dis = -1;
2194
			dit = colCnt - 1;
2195
		}else{
2196
			dis = 1;
2197
			dit = 0;
2198
		}
2199
		firstDay = opt('firstDay');
2200
		nwe = opt('weekends') ? 0 : 1;
2201
		tm = opt('theme') ? 'ui' : 'fc';
2202
		colFormat = opt('columnFormat');
2203
	}
2204
 
2205
 
2206
 
2207
	function buildSkeleton(maxRowCnt, showNumbers) {
2208
		var s;
2209
		var headerClass = tm + "-widget-header";
2210
		var contentClass = tm + "-widget-content";
2211
		var i, j;
2212
		var table;
2213
 
2214
		s =
2215
			"<table class='fc-border-separate' style='width:100%' cellspacing='0'>" +
2216
			"<thead>" +
2217
			"<tr>";
2218
		for (i=0; i<colCnt; i++) {
2219
			s +=
2220
				"<th class='fc- " + headerClass + "'/>"; // need fc- for setDayID
2221
		}
2222
		s +=
2223
			"</tr>" +
2224
			"</thead>" +
2225
			"<tbody>";
2226
		for (i=0; i<maxRowCnt; i++) {
2227
			s +=
2228
				"<tr class='fc-week" + i + "'>";
2229
			for (j=0; j<colCnt; j++) {
2230
				s +=
2231
					"<td class='fc- " + contentClass + " fc-day" + (i*colCnt+j) + "'>" + // need fc- for setDayID
2232
					"<div>" +
2233
					(showNumbers ?
2234
						"<div class='fc-day-number'/>" :
2235
						''
2236
						) +
2237
					"<div class='fc-day-content'>" +
2238
					"<div style='position:relative'>&nbsp;</div>" +
2239
					"</div>" +
2240
					"</div>" +
2241
					"</td>";
2242
			}
2243
			s +=
2244
				"</tr>";
2245
		}
2246
		s +=
2247
			"</tbody>" +
2248
			"</table>";
2249
		table = $(s).appendTo(element);
2250
 
2251
		head = table.find('thead');
2252
		headCells = head.find('th');
2253
		body = table.find('tbody');
2254
		bodyRows = body.find('tr');
2255
		bodyCells = body.find('td');
2256
		bodyFirstCells = bodyCells.filter(':first-child');
2257
		bodyCellTopInners = bodyRows.eq(0).find('div.fc-day-content div');
2258
 
2259
		markFirstLast(head.add(head.find('tr'))); // marks first+last tr/th's
2260
		markFirstLast(bodyRows); // marks first+last td's
2261
		bodyRows.eq(0).addClass('fc-first'); // fc-last is done in updateCells
2262
 
2263
		dayBind(bodyCells);
2264
 
2265
		daySegmentContainer =
2266
			$("<div style='position:absolute;z-index:8;top:0;left:0'/>")
2267
				.appendTo(element);
2268
	}
2269
 
2270
 
2271
 
2272
	function updateCells(firstTime) {
2273
		var dowDirty = firstTime || rowCnt == 1; // could the cells' day-of-weeks need updating?
2274
		var month = t.start.getMonth();
2275
		var today = clearTime(new Date());
2276
		var cell;
2277
		var date;
2278
		var row;
2279
 
2280
		if (dowDirty) {
2281
			headCells.each(function(i, _cell) {
2282
				cell = $(_cell);
2283
				date = indexDate(i);
2284
				cell.html(formatDate(date, colFormat));
2285
				setDayID(cell, date);
2286
			});
2287
		}
2288
 
2289
		bodyCells.each(function(i, _cell) {
2290
			cell = $(_cell);
2291
			date = indexDate(i);
2292
			if (date.getMonth() == month) {
2293
				cell.removeClass('fc-other-month');
2294
			}else{
2295
				cell.addClass('fc-other-month');
2296
			}
2297
			if (+date == +today) {
2298
				cell.addClass(tm + '-state-highlight fc-today');
2299
			}else{
2300
				cell.removeClass(tm + '-state-highlight fc-today');
2301
			}
2302
			cell.find('div.fc-day-number').text(date.getDate());
2303
			if (dowDirty) {
2304
				setDayID(cell, date);
2305
			}
2306
		});
2307
 
2308
		bodyRows.each(function(i, _row) {
2309
			row = $(_row);
2310
			if (i < rowCnt) {
2311
				row.show();
2312
				if (i == rowCnt-1) {
2313
					row.addClass('fc-last');
2314
				}else{
2315
					row.removeClass('fc-last');
2316
				}
2317
			}else{
2318
				row.hide();
2319
			}
2320
		});
2321
	}
2322
 
2323
 
2324
 
2325
	function setHeight(height) {
2326
		viewHeight = height;
2327
 
2328
		var bodyHeight = viewHeight - head.height();
2329
		var rowHeight;
2330
		var rowHeightLast;
2331
		var cell;
2332
 
2333
		if (opt('weekMode') == 'variable') {
2334
			rowHeight = rowHeightLast = Math.floor(bodyHeight / (rowCnt==1 ? 2 : 6));
2335
		}else{
2336
			rowHeight = Math.floor(bodyHeight / rowCnt);
2337
			rowHeightLast = bodyHeight - rowHeight * (rowCnt-1);
2338
		}
2339
 
2340
		bodyFirstCells.each(function(i, _cell) {
2341
			if (i < rowCnt) {
2342
				cell = $(_cell);
2343
				setMinHeight(
2344
					cell.find('> div'),
2345
					(i==rowCnt-1 ? rowHeightLast : rowHeight) - vsides(cell)
2346
				);
2347
			}
2348
		});
2349
 
2350
	}
2351
 
2352
 
2353
	function setWidth(width) {
2354
		viewWidth = width;
2355
		colContentPositions.clear();
2356
		colWidth = Math.floor(viewWidth / colCnt);
2357
		setOuterWidth(headCells.slice(0, -1), colWidth);
2358
	}
2359
 
2360
 
2361
 
2362
	/* Day clicking and binding
2363
	-----------------------------------------------------------*/
2364
 
2365
 
2366
	function dayBind(days) {
2367
		days.click(dayClick)
2368
			.mousedown(daySelectionMousedown);
2369
	}
2370
 
2371
 
2372
	function dayClick(ev) {
2373
		if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick
2374
			var index = parseInt(this.className.match(/fc\-day(\d+)/)[1]); // TODO: maybe use .data
2375
			var date = indexDate(index);
2376
			trigger('dayClick', this, date, true, ev);
2377
		}
2378
	}
2379
 
2380
 
2381
 
2382
	/* Semi-transparent Overlay Helpers
2383
	------------------------------------------------------*/
2384
 
2385
 
2386
	function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive
2387
		if (refreshCoordinateGrid) {
2388
			coordinateGrid.build();
2389
		}
2390
		var rowStart = cloneDate(t.visStart);
2391
		var rowEnd = addDays(cloneDate(rowStart), colCnt);
2392
		for (var i=0; i<rowCnt; i++) {
2393
			var stretchStart = new Date(Math.max(rowStart, overlayStart));
2394
			var stretchEnd = new Date(Math.min(rowEnd, overlayEnd));
2395
			if (stretchStart < stretchEnd) {
2396
				var colStart, colEnd;
2397
				if (rtl) {
2398
					colStart = dayDiff(stretchEnd, rowStart)*dis+dit+1;
2399
					colEnd = dayDiff(stretchStart, rowStart)*dis+dit+1;
2400
				}else{
2401
					colStart = dayDiff(stretchStart, rowStart);
2402
					colEnd = dayDiff(stretchEnd, rowStart);
2403
				}
2404
				dayBind(
2405
					renderCellOverlay(i, colStart, i, colEnd-1)
2406
				);
2407
			}
2408
			addDays(rowStart, 7);
2409
			addDays(rowEnd, 7);
2410
		}
2411
	}
2412
 
2413
 
2414
	function renderCellOverlay(row0, col0, row1, col1) { // row1,col1 is inclusive
2415
		var rect = coordinateGrid.rect(row0, col0, row1, col1, element);
2416
		return renderOverlay(rect, element);
2417
	}
2418
 
2419
 
2420
 
2421
	/* Selection
2422
	-----------------------------------------------------------------------*/
2423
 
2424
 
2425
	function defaultSelectionEnd(startDate, allDay) {
2426
		return cloneDate(startDate);
2427
	}
2428
 
2429
 
2430
	function renderSelection(startDate, endDate, allDay) {
2431
		renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true); // rebuild every time???
2432
	}
2433
 
2434
 
2435
	function clearSelection() {
2436
		clearOverlays();
2437
	}
2438
 
2439
 
2440
	function reportDayClick(date, allDay, ev) {
2441
		var cell = dateCell(date);
2442
		var _element = bodyCells[cell.row*colCnt + cell.col];
2443
		trigger('dayClick', _element, date, allDay, ev);
2444
	}
2445
 
2446
 
2447
 
2448
	/* External Dragging
2449
	-----------------------------------------------------------------------*/
2450
 
2451
 
2452
	function dragStart(_dragElement, ev, ui) {
2453
		hoverListener.start(function(cell) {
2454
			clearOverlays();
2455
			if (cell) {
2456
				renderCellOverlay(cell.row, cell.col, cell.row, cell.col);
2457
			}
2458
		}, ev);
2459
	}
2460
 
2461
 
2462
	function dragStop(_dragElement, ev, ui) {
2463
		var cell = hoverListener.stop();
2464
		clearOverlays();
2465
		if (cell) {
2466
			var d = cellDate(cell);
2467
			trigger('drop', _dragElement, d, true, ev, ui);
2468
		}
2469
	}
2470
 
2471
 
2472
 
2473
	/* Utilities
2474
	--------------------------------------------------------*/
2475
 
2476
 
2477
	function defaultEventEnd(event) {
2478
		return cloneDate(event.start);
2479
	}
2480
 
2481
 
2482
	coordinateGrid = new CoordinateGrid(function(rows, cols) {
2483
		var e, n, p;
2484
		headCells.each(function(i, _e) {
2485
			e = $(_e);
2486
			n = e.offset().left;
2487
			if (i) {
2488
				p[1] = n;
2489
			}
2490
			p = [n];
2491
			cols[i] = p;
2492
		});
2493
		p[1] = n + e.outerWidth();
2494
		bodyRows.each(function(i, _e) {
2495
			if (i < rowCnt) {
2496
				e = $(_e);
2497
				n = e.offset().top;
2498
				if (i) {
2499
					p[1] = n;
2500
				}
2501
				p = [n];
2502
				rows[i] = p;
2503
			}
2504
		});
2505
		p[1] = n + e.outerHeight();
2506
	});
2507
 
2508
 
2509
	hoverListener = new HoverListener(coordinateGrid);
2510
 
2511
 
2512
	colContentPositions = new HorizontalPositionCache(function(col) {
2513
		return bodyCellTopInners.eq(col);
2514
	});
2515
 
2516
 
2517
	function colContentLeft(col) {
2518
		return colContentPositions.left(col);
2519
	}
2520
 
2521
 
2522
	function colContentRight(col) {
2523
		return colContentPositions.right(col);
2524
	}
2525
 
2526
 
2527
 
2528
 
2529
	function dateCell(date) {
2530
		return {
2531
			row: Math.floor(dayDiff(date, t.visStart) / 7),
2532
			col: dayOfWeekCol(date.getDay())
2533
		};
2534
	}
2535
 
2536
 
2537
	function cellDate(cell) {
2538
		return _cellDate(cell.row, cell.col);
2539
	}
2540
 
2541
 
2542
	function _cellDate(row, col) {
2543
		return addDays(cloneDate(t.visStart), row*7 + col*dis+dit);
2544
		// what about weekends in middle of week?
2545
	}
2546
 
2547
 
2548
	function indexDate(index) {
2549
		return _cellDate(Math.floor(index/colCnt), index%colCnt);
2550
	}
2551
 
2552
 
2553
	function dayOfWeekCol(dayOfWeek) {
2554
		return ((dayOfWeek - Math.max(firstDay, nwe) + colCnt) % colCnt) * dis + dit;
2555
	}
2556
 
2557
 
2558
 
2559
 
2560
	function allDayRow(i) {
2561
		return bodyRows.eq(i);
2562
	}
2563
 
2564
 
2565
	function allDayBounds(i) {
2566
		return {
2567
			left: 0,
2568
			right: viewWidth
2569
		};
2570
	}
2571
 
2572
 
2573
}
2574
 
2575
function BasicEventRenderer() {
2576
	var t = this;
2577
 
2578
 
2579
	// exports
2580
	t.renderEvents = renderEvents;
2581
	t.compileDaySegs = compileSegs; // for DayEventRenderer
2582
	t.clearEvents = clearEvents;
2583
	t.bindDaySeg = bindDaySeg;
2584
 
2585
 
2586
	// imports
2587
	DayEventRenderer.call(t);
2588
	var opt = t.opt;
2589
	var trigger = t.trigger;
2590
	//var setOverflowHidden = t.setOverflowHidden;
2591
	var isEventDraggable = t.isEventDraggable;
2592
	var isEventResizable = t.isEventResizable;
2593
	var reportEvents = t.reportEvents;
2594
	var reportEventClear = t.reportEventClear;
2595
	var eventElementHandlers = t.eventElementHandlers;
2596
	var showEvents = t.showEvents;
2597
	var hideEvents = t.hideEvents;
2598
	var eventDrop = t.eventDrop;
2599
	var getDaySegmentContainer = t.getDaySegmentContainer;
2600
	var getHoverListener = t.getHoverListener;
2601
	var renderDayOverlay = t.renderDayOverlay;
2602
	var clearOverlays = t.clearOverlays;
2603
	var getRowCnt = t.getRowCnt;
2604
	var getColCnt = t.getColCnt;
2605
	var renderDaySegs = t.renderDaySegs;
2606
	var resizableDayEvent = t.resizableDayEvent;
2607
 
2608
 
2609
 
2610
	/* Rendering
2611
	--------------------------------------------------------------------*/
2612
 
2613
 
2614
	function renderEvents(events, modifiedEventId) {
2615
		reportEvents(events);
2616
		renderDaySegs(compileSegs(events), modifiedEventId);
2617
	}
2618
 
2619
 
2620
	function clearEvents() {
2621
		reportEventClear();
2622
		getDaySegmentContainer().empty();
2623
	}
2624
 
2625
 
2626
	function compileSegs(events) {
2627
		var rowCnt = getRowCnt(),
2628
			colCnt = getColCnt(),
2629
			d1 = cloneDate(t.visStart),
2630
			d2 = addDays(cloneDate(d1), colCnt),
2631
			visEventsEnds = $.map(events, exclEndDay),
2632
			i, row,
2633
			j, level,
2634
			k, seg,
2635
			segs=[];
2636
		for (i=0; i<rowCnt; i++) {
2637
			row = stackSegs(sliceSegs(events, visEventsEnds, d1, d2));
2638
			for (j=0; j<row.length; j++) {
2639
				level = row[j];
2640
				for (k=0; k<level.length; k++) {
2641
					seg = level[k];
2642
					seg.row = i;
2643
					seg.level = j; // not needed anymore
2644
					segs.push(seg);
2645
				}
2646
			}
2647
			addDays(d1, 7);
2648
			addDays(d2, 7);
2649
		}
2650
		return segs;
2651
	}
2652
 
2653
 
2654
	function bindDaySeg(event, eventElement, seg) {
2655
		if (isEventDraggable(event)) {
2656
			draggableDayEvent(event, eventElement);
2657
		}
2658
		if (seg.isEnd && isEventResizable(event)) {
2659
			resizableDayEvent(event, eventElement, seg);
2660
		}
2661
		eventElementHandlers(event, eventElement);
2662
			// needs to be after, because resizableDayEvent might stopImmediatePropagation on click
2663
	}
2664
 
2665
 
2666
 
2667
	/* Dragging
2668
	----------------------------------------------------------------------------*/
2669
 
2670
 
2671
	function draggableDayEvent(event, eventElement) {
2672
		var hoverListener = getHoverListener();
2673
		var dayDelta;
2674
		eventElement.draggable({
2675
			zIndex: 9,
2676
			delay: 50,
2677
			opacity: opt('dragOpacity'),
2678
			revertDuration: opt('dragRevertDuration'),
2679
			start: function(ev, ui) {
2680
				trigger('eventDragStart', eventElement, event, ev, ui);
2681
				hideEvents(event, eventElement);
2682
				hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
2683
					eventElement.draggable('option', 'revert', !cell || !rowDelta && !colDelta);
2684
					clearOverlays();
2685
					if (cell) {
2686
						//setOverflowHidden(true);
2687
						dayDelta = rowDelta*7 + colDelta * (opt('isRTL') ? -1 : 1);
2688
						renderDayOverlay(
2689
							addDays(cloneDate(event.start), dayDelta),
2690
							addDays(exclEndDay(event), dayDelta)
2691
						);
2692
					}else{
2693
						//setOverflowHidden(false);
2694
						dayDelta = 0;
2695
					}
2696
				}, ev, 'drag');
2697
			},
2698
			stop: function(ev, ui) {
2699
				hoverListener.stop();
2700
				clearOverlays();
2701
				trigger('eventDragStop', eventElement, event, ev, ui);
2702
				if (dayDelta) {
2703
					eventDrop(this, event, dayDelta, 0, event.allDay, ev, ui);
2704
				}else{
2705
					eventElement.css('filter', ''); // clear IE opacity side-effects
2706
					showEvents(event, eventElement);
2707
				}
2708
				//setOverflowHidden(false);
2709
			}
2710
		});
2711
	}
2712
 
2713
 
2714
}
2715
 
2716
fcViews.agendaWeek = AgendaWeekView;
2717
 
2718
function AgendaWeekView(element, calendar) {
2719
	var t = this;
2720
 
2721
 
2722
	// exports
2723
	t.render = render;
2724
 
2725
 
2726
	// imports
2727
	AgendaView.call(t, element, calendar, 'agendaWeek');
2728
	var opt = t.opt;
2729
	var renderAgenda = t.renderAgenda;
2730
	var formatDates = calendar.formatDates;
2731
 
2732
 
2733
 
2734
	function render(date, delta) {
2735
		if (delta) {
2736
			addDays(date, delta * 7);
2737
		}
2738
		var start = addDays(cloneDate(date), -((date.getDay() - opt('firstDay') + 7) % 7));
2739
		var end = addDays(cloneDate(start), 7);
2740
		var visStart = cloneDate(start);
2741
		var visEnd = cloneDate(end);
2742
		var weekends = opt('weekends');
2743
		if (!weekends) {
2744
			skipWeekend(visStart);
2745
			skipWeekend(visEnd, -1, true);
2746
		}
2747
		t.title = formatDates(
2748
			visStart,
2749
			addDays(cloneDate(visEnd), -1),
2750
			opt('titleFormat')
2751
		);
2752
		t.start = start;
2753
		t.end = end;
2754
		t.visStart = visStart;
2755
		t.visEnd = visEnd;
2756
		renderAgenda(weekends ? 7 : 5);
2757
	}
2758
 
2759
 
2760
}
2761
 
2762
fcViews.agendaDay = AgendaDayView;
2763
 
2764
function AgendaDayView(element, calendar) {
2765
	var t = this;
2766
 
2767
 
2768
	// exports
2769
	t.render = render;
2770
 
2771
 
2772
	// imports
2773
	AgendaView.call(t, element, calendar, 'agendaDay');
2774
	var opt = t.opt;
2775
	var renderAgenda = t.renderAgenda;
2776
	var formatDate = calendar.formatDate;
2777
 
2778
 
2779
 
2780
	function render(date, delta) {
2781
		if (delta) {
2782
			addDays(date, delta);
2783
			if (!opt('weekends')) {
2784
				skipWeekend(date, delta < 0 ? -1 : 1);
2785
			}
2786
		}
2787
		var start = cloneDate(date, true);
2788
		var end = addDays(cloneDate(start), 1);
2789
		t.title = formatDate(date, opt('titleFormat'));
2790
		t.start = t.visStart = start;
2791
		t.end = t.visEnd = end;
2792
		renderAgenda(1);
2793
	}
2794
 
2795
 
2796
}
2797
 
2798
setDefaults({
2799
	allDaySlot: true,
2800
	allDayText: 'all-day',
2801
	firstHour: 6,
2802
	slotMinutes: 30,
2803
	defaultEventMinutes: 120,
2804
	axisFormat: 'h(:mm)tt',
2805
	timeFormat: {
2806
		agenda: 'h:mm{ - h:mm}'
2807
	},
2808
	dragOpacity: {
2809
		agenda: .5
2810
	},
2811
	minTime: 0,
2812
	maxTime: 24
2813
});
2814
 
2815
 
2816
// TODO: make it work in quirks mode (event corners, all-day height)
2817
// TODO: test liquid width, especially in IE6
2818
 
2819
 
2820
function AgendaView(element, calendar, viewName) {
2821
	var t = this;
2822
 
2823
 
2824
	// exports
2825
	t.renderAgenda = renderAgenda;
2826
	t.setWidth = setWidth;
2827
	t.setHeight = setHeight;
2828
	t.beforeHide = beforeHide;
2829
	t.afterShow = afterShow;
2830
	t.defaultEventEnd = defaultEventEnd;
2831
	t.timePosition = timePosition;
2832
	t.dayOfWeekCol = dayOfWeekCol;
2833
	t.dateCell = dateCell;
2834
	t.cellDate = cellDate;
2835
	t.cellIsAllDay = cellIsAllDay;
2836
	t.allDayRow = getAllDayRow;
2837
	t.allDayBounds = allDayBounds;
2838
	t.getHoverListener = function() { return hoverListener };
2839
	t.colContentLeft = colContentLeft;
2840
	t.colContentRight = colContentRight;
2841
	t.getDaySegmentContainer = function() { return daySegmentContainer };
2842
	t.getSlotSegmentContainer = function() { return slotSegmentContainer };
2843
	t.getMinMinute = function() { return minMinute };
2844
	t.getMaxMinute = function() { return maxMinute };
2845
	t.getBodyContent = function() { return slotContent }; // !!??
2846
	t.getRowCnt = function() { return 1 };
2847
	t.getColCnt = function() { return colCnt };
2848
	t.getColWidth = function() { return colWidth };
2849
	t.getSlotHeight = function() { return slotHeight };
2850
	t.defaultSelectionEnd = defaultSelectionEnd;
2851
	t.renderDayOverlay = renderDayOverlay;
2852
	t.renderSelection = renderSelection;
2853
	t.clearSelection = clearSelection;
2854
	t.reportDayClick = reportDayClick; // selection mousedown hack
2855
	t.dragStart = dragStart;
2856
	t.dragStop = dragStop;
2857
 
2858
 
2859
	// imports
2860
	View.call(t, element, calendar, viewName);
2861
	OverlayManager.call(t);
2862
	SelectionManager.call(t);
2863
	AgendaEventRenderer.call(t);
2864
	var opt = t.opt;
2865
	var trigger = t.trigger;
2866
	var clearEvents = t.clearEvents;
2867
	var renderOverlay = t.renderOverlay;
2868
	var clearOverlays = t.clearOverlays;
2869
	var reportSelection = t.reportSelection;
2870
	var unselect = t.unselect;
2871
	var daySelectionMousedown = t.daySelectionMousedown;
2872
	var slotSegHtml = t.slotSegHtml;
2873
	var formatDate = calendar.formatDate;
2874
 
2875
 
2876
	// locals
2877
 
2878
	var dayTable;
2879
	var dayHead;
2880
	var dayHeadCells;
2881
	var dayBody;
2882
	var dayBodyCells;
2883
	var dayBodyCellInners;
2884
	var dayBodyFirstCell;
2885
	var dayBodyFirstCellStretcher;
2886
	var slotLayer;
2887
	var daySegmentContainer;
2888
	var allDayTable;
2889
	var allDayRow;
2890
	var slotScroller;
2891
	var slotContent;
2892
	var slotSegmentContainer;
2893
	var slotTable;
2894
	var slotTableFirstInner;
2895
	var axisFirstCells;
2896
	var gutterCells;
2897
	var selectionHelper;
2898
 
2899
	var viewWidth;
2900
	var viewHeight;
2901
	var axisWidth;
2902
	var colWidth;
2903
	var gutterWidth;
2904
	var slotHeight; // TODO: what if slotHeight changes? (see issue 650)
2905
	var savedScrollTop;
2906
 
2907
	var colCnt;
2908
	var slotCnt;
2909
	var coordinateGrid;
2910
	var hoverListener;
2911
	var colContentPositions;
2912
	var slotTopCache = {};
2913
 
2914
	var tm;
2915
	var firstDay;
2916
	var nwe;            // no weekends (int)
2917
	var rtl, dis, dit;  // day index sign / translate
2918
	var minMinute, maxMinute;
2919
	var colFormat;
2920
 
2921
 
2922
 
2923
	/* Rendering
2924
	-----------------------------------------------------------------------------*/
2925
 
2926
 
2927
	disableTextSelection(element.addClass('fc-agenda'));
2928
 
2929
 
2930
	function renderAgenda(c) {
2931
		colCnt = c;
2932
		updateOptions();
2933
		if (!dayTable) {
2934
			buildSkeleton();
2935
		}else{
2936
			clearEvents();
2937
		}
2938
		updateCells();
2939
	}
2940
 
2941
 
2942
 
2943
	function updateOptions() {
2944
		tm = opt('theme') ? 'ui' : 'fc';
2945
		nwe = opt('weekends') ? 0 : 1;
2946
		firstDay = opt('firstDay');
2947
		if (rtl = opt('isRTL')) {
2948
			dis = -1;
2949
			dit = colCnt - 1;
2950
		}else{
2951
			dis = 1;
2952
			dit = 0;
2953
		}
2954
		minMinute = parseTime(opt('minTime'));
2955
		maxMinute = parseTime(opt('maxTime'));
2956
		colFormat = opt('columnFormat');
2957
	}
2958
 
2959
 
2960
 
2961
	function buildSkeleton() {
2962
		var headerClass = tm + "-widget-header";
2963
		var contentClass = tm + "-widget-content";
2964
		var s;
2965
		var i;
2966
		var d;
2967
		var maxd;
2968
		var minutes;
2969
		var slotNormal = opt('slotMinutes') % 15 == 0;
2970
 
2971
		s =
2972
			"<table style='width:100%' class='fc-agenda-days fc-border-separate' cellspacing='0'>" +
2973
			"<thead>" +
2974
			"<tr>" +
2975
			"<th class='fc-agenda-axis " + headerClass + "'>&nbsp;</th>";
2976
		for (i=0; i<colCnt; i++) {
2977
			s +=
2978
				"<th class='fc- fc-col" + i + ' ' + headerClass + "'/>"; // fc- needed for setDayID
2979
		}
2980
		s +=
2981
			"<th class='fc-agenda-gutter " + headerClass + "'>&nbsp;</th>" +
2982
			"</tr>" +
2983
			"</thead>" +
2984
			"<tbody>" +
2985
			"<tr>" +
2986
			"<th class='fc-agenda-axis " + headerClass + "'>&nbsp;</th>";
2987
		for (i=0; i<colCnt; i++) {
2988
			s +=
2989
				"<td class='fc- fc-col" + i + ' ' + contentClass + "'>" + // fc- needed for setDayID
2990
				"<div>" +
2991
				"<div class='fc-day-content'>" +
2992
				"<div style='position:relative'>&nbsp;</div>" +
2993
				"</div>" +
2994
				"</div>" +
2995
				"</td>";
2996
		}
2997
		s +=
2998
			"<td class='fc-agenda-gutter " + contentClass + "'>&nbsp;</td>" +
2999
			"</tr>" +
3000
			"</tbody>" +
3001
			"</table>";
3002
		dayTable = $(s).appendTo(element);
3003
		dayHead = dayTable.find('thead');
3004
		dayHeadCells = dayHead.find('th').slice(1, -1);
3005
		dayBody = dayTable.find('tbody');
3006
		dayBodyCells = dayBody.find('td').slice(0, -1);
3007
		dayBodyCellInners = dayBodyCells.find('div.fc-day-content div');
3008
		dayBodyFirstCell = dayBodyCells.eq(0);
3009
		dayBodyFirstCellStretcher = dayBodyFirstCell.find('> div');
3010
 
3011
		markFirstLast(dayHead.add(dayHead.find('tr')));
3012
		markFirstLast(dayBody.add(dayBody.find('tr')));
3013
 
3014
		axisFirstCells = dayHead.find('th:first');
3015
		gutterCells = dayTable.find('.fc-agenda-gutter');
3016
 
3017
		slotLayer =
3018
			$("<div style='position:absolute;z-index:2;left:0;width:100%'/>")
3019
				.appendTo(element);
3020
 
3021
		if (opt('allDaySlot')) {
3022
 
3023
			daySegmentContainer =
3024
				$("<div style='position:absolute;z-index:8;top:0;left:0'/>")
3025
					.appendTo(slotLayer);
3026
 
3027
			s =
3028
				"<table style='width:100%' class='fc-agenda-allday' cellspacing='0'>" +
3029
				"<tr>" +
3030
				"<th class='" + headerClass + " fc-agenda-axis'>" + opt('allDayText') + "</th>" +
3031
				"<td>" +
3032
				"<div class='fc-day-content'><div style='position:relative'/></div>" +
3033
				"</td>" +
3034
				"<th class='" + headerClass + " fc-agenda-gutter'>&nbsp;</th>" +
3035
				"</tr>" +
3036
				"</table>";
3037
			allDayTable = $(s).appendTo(slotLayer);
3038
			allDayRow = allDayTable.find('tr');
3039
 
3040
			dayBind(allDayRow.find('td'));
3041
 
3042
			axisFirstCells = axisFirstCells.add(allDayTable.find('th:first'));
3043
			gutterCells = gutterCells.add(allDayTable.find('th.fc-agenda-gutter'));
3044
 
3045
			slotLayer.append(
3046
				"<div class='fc-agenda-divider " + headerClass + "'>" +
3047
				"<div class='fc-agenda-divider-inner'/>" +
3048
				"</div>"
3049
			);
3050
 
3051
		}else{
3052
 
3053
			daySegmentContainer = $([]); // in jQuery 1.4, we can just do $()
3054
 
3055
		}
3056
 
3057
		slotScroller =
3058
			$("<div style='position:absolute;width:100%;overflow-x:hidden;overflow-y:auto'/>")
3059
				.appendTo(slotLayer);
3060
 
3061
		slotContent =
3062
			$("<div style='position:relative;width:100%;overflow:hidden'/>")
3063
				.appendTo(slotScroller);
3064
 
3065
		slotSegmentContainer =
3066
			$("<div style='position:absolute;z-index:8;top:0;left:0'/>")
3067
				.appendTo(slotContent);
3068
 
3069
		s =
3070
			"<table class='fc-agenda-slots' style='width:100%' cellspacing='0'>" +
3071
			"<tbody>";
3072
		d = zeroDate();
3073
		maxd = addMinutes(cloneDate(d), maxMinute);
3074
		addMinutes(d, minMinute);
3075
		slotCnt = 0;
3076
		for (i=0; d < maxd; i++) {
3077
			minutes = d.getMinutes();
3078
			s +=
3079
				"<tr class='fc-slot" + i + ' ' + (!minutes ? '' : 'fc-minor') + "'>" +
3080
				"<th class='fc-agenda-axis " + headerClass + "'>" +
3081
				((!slotNormal || !minutes) ? formatDate(d, opt('axisFormat')) : '&nbsp;') +
3082
				"</th>" +
3083
				"<td class='" + contentClass + "'>" +
3084
				"<div style='position:relative'>&nbsp;</div>" +
3085
				"</td>" +
3086
				"</tr>";
3087
			addMinutes(d, opt('slotMinutes'));
3088
			slotCnt++;
3089
		}
3090
		s +=
3091
			"</tbody>" +
3092
			"</table>";
3093
		slotTable = $(s).appendTo(slotContent);
3094
		slotTableFirstInner = slotTable.find('div:first');
3095
 
3096
		slotBind(slotTable.find('td'));
3097
 
3098
		axisFirstCells = axisFirstCells.add(slotTable.find('th:first'));
3099
	}
3100
 
3101
 
3102
 
3103
	function updateCells() {
3104
		var i;
3105
		var headCell;
3106
		var bodyCell;
3107
		var date;
3108
		var today = clearTime(new Date());
3109
		for (i=0; i<colCnt; i++) {
3110
			date = colDate(i);
3111
			headCell = dayHeadCells.eq(i);
3112
			headCell.html(formatDate(date, colFormat));
3113
			bodyCell = dayBodyCells.eq(i);
3114
			if (+date == +today) {
3115
				bodyCell.addClass(tm + '-state-highlight fc-today');
3116
			}else{
3117
				bodyCell.removeClass(tm + '-state-highlight fc-today');
3118
			}
3119
			setDayID(headCell.add(bodyCell), date);
3120
		}
3121
	}
3122
 
3123
 
3124
 
3125
	function setHeight(height, dateChanged) {
3126
		if (height === undefined) {
3127
			height = viewHeight;
3128
		}
3129
		viewHeight = height;
3130
		slotTopCache = {};
3131
 
3132
		var headHeight = dayBody.position().top;
3133
		var allDayHeight = slotScroller.position().top; // including divider
3134
		var bodyHeight = Math.min( // total body height, including borders
3135
			height - headHeight,   // when scrollbars
3136
			slotTable.height() + allDayHeight + 1 // when no scrollbars. +1 for bottom border
3137
		);
3138
 
3139
		dayBodyFirstCellStretcher
3140
			.height(bodyHeight - vsides(dayBodyFirstCell));
3141
 
3142
		slotLayer.css('top', headHeight);
3143
 
3144
		slotScroller.height(bodyHeight - allDayHeight - 1);
3145
 
3146
		slotHeight = slotTableFirstInner.height() + 1; // +1 for border
3147
 
3148
		if (dateChanged) {
3149
			resetScroll();
3150
		}
3151
	}
3152
 
3153
 
3154
 
3155
	function setWidth(width) {
3156
		viewWidth = width;
3157
		colContentPositions.clear();
3158
 
3159
		axisWidth = 0;
3160
		setOuterWidth(
3161
			axisFirstCells
3162
				.width('')
3163
				.each(function(i, _cell) {
3164
					axisWidth = Math.max(axisWidth, $(_cell).outerWidth());
3165
				}),
3166
			axisWidth
3167
		);
3168
 
3169
		var slotTableWidth = slotScroller[0].clientWidth; // needs to be done after axisWidth (for IE7)
3170
		//slotTable.width(slotTableWidth);
3171
 
3172
		gutterWidth = slotScroller.width() - slotTableWidth;
3173
		if (gutterWidth) {
3174
			setOuterWidth(gutterCells, gutterWidth);
3175
			gutterCells
3176
				.show()
3177
				.prev()
3178
				.removeClass('fc-last');
3179
		}else{
3180
			gutterCells
3181
				.hide()
3182
				.prev()
3183
				.addClass('fc-last');
3184
		}
3185
 
3186
		colWidth = Math.floor((slotTableWidth - axisWidth) / colCnt);
3187
		setOuterWidth(dayHeadCells.slice(0, -1), colWidth);
3188
	}
3189
 
3190
 
3191
 
3192
	function resetScroll() {
3193
		var d0 = zeroDate();
3194
		var scrollDate = cloneDate(d0);
3195
		scrollDate.setHours(opt('firstHour'));
3196
		var top = timePosition(d0, scrollDate) + 1; // +1 for the border
3197
		function scroll() {
3198
			slotScroller.scrollTop(top);
3199
		}
3200
		scroll();
3201
		setTimeout(scroll, 0); // overrides any previous scroll state made by the browser
3202
	}
3203
 
3204
 
3205
	function beforeHide() {
3206
		savedScrollTop = slotScroller.scrollTop();
3207
	}
3208
 
3209
 
3210
	function afterShow() {
3211
		slotScroller.scrollTop(savedScrollTop);
3212
	}
3213
 
3214
 
3215
 
3216
	/* Slot/Day clicking and binding
3217
	-----------------------------------------------------------------------*/
3218
 
3219
 
3220
	function dayBind(cells) {
3221
		cells.click(slotClick)
3222
			.mousedown(daySelectionMousedown);
3223
	}
3224
 
3225
 
3226
	function slotBind(cells) {
3227
		cells.click(slotClick)
3228
			.mousedown(slotSelectionMousedown);
3229
	}
3230
 
3231
 
3232
	function slotClick(ev) {
3233
		if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick
3234
			var col = Math.min(colCnt-1, Math.floor((ev.pageX - dayTable.offset().left - axisWidth) / colWidth));
3235
			var date = colDate(col);
3236
			var rowMatch = this.parentNode.className.match(/fc-slot(\d+)/); // TODO: maybe use data
3237
			if (rowMatch) {
3238
				var mins = parseInt(rowMatch[1]) * opt('slotMinutes');
3239
				var hours = Math.floor(mins/60);
3240
				date.setHours(hours);
3241
				date.setMinutes(mins%60 + minMinute);
3242
				trigger('dayClick', dayBodyCells[col], date, false, ev);
3243
			}else{
3244
				trigger('dayClick', dayBodyCells[col], date, true, ev);
3245
			}
3246
		}
3247
	}
3248
 
3249
 
3250
 
3251
	/* Semi-transparent Overlay Helpers
3252
	-----------------------------------------------------*/
3253
 
3254
 
3255
	function renderDayOverlay(startDate, endDate, refreshCoordinateGrid) { // endDate is exclusive
3256
		if (refreshCoordinateGrid) {
3257
			coordinateGrid.build();
3258
		}
3259
		var visStart = cloneDate(t.visStart);
3260
		var startCol, endCol;
3261
		if (rtl) {
3262
			startCol = dayDiff(endDate, visStart)*dis+dit+1;
3263
			endCol = dayDiff(startDate, visStart)*dis+dit+1;
3264
		}else{
3265
			startCol = dayDiff(startDate, visStart);
3266
			endCol = dayDiff(endDate, visStart);
3267
		}
3268
		startCol = Math.max(0, startCol);
3269
		endCol = Math.min(colCnt, endCol);
3270
		if (startCol < endCol) {
3271
			dayBind(
3272
				renderCellOverlay(0, startCol, 0, endCol-1)
3273
			);
3274
		}
3275
	}
3276
 
3277
 
3278
	function renderCellOverlay(row0, col0, row1, col1) { // only for all-day?
3279
		var rect = coordinateGrid.rect(row0, col0, row1, col1, slotLayer);
3280
		return renderOverlay(rect, slotLayer);
3281
	}
3282
 
3283
 
3284
	function renderSlotOverlay(overlayStart, overlayEnd) {
3285
		var dayStart = cloneDate(t.visStart);
3286
		var dayEnd = addDays(cloneDate(dayStart), 1);
3287
		for (var i=0; i<colCnt; i++) {
3288
			var stretchStart = new Date(Math.max(dayStart, overlayStart));
3289
			var stretchEnd = new Date(Math.min(dayEnd, overlayEnd));
3290
			if (stretchStart < stretchEnd) {
3291
				var col = i*dis+dit;
3292
				var rect = coordinateGrid.rect(0, col, 0, col, slotContent); // only use it for horizontal coords
3293
				var top = timePosition(dayStart, stretchStart);
3294
				var bottom = timePosition(dayStart, stretchEnd);
3295
				rect.top = top;
3296
				rect.height = bottom - top;
3297
				slotBind(
3298
					renderOverlay(rect, slotContent)
3299
				);
3300
			}
3301
			addDays(dayStart, 1);
3302
			addDays(dayEnd, 1);
3303
		}
3304
	}
3305
 
3306
 
3307
 
3308
	/* Coordinate Utilities
3309
	-----------------------------------------------------------------------------*/
3310
 
3311
 
3312
	coordinateGrid = new CoordinateGrid(function(rows, cols) {
3313
		var e, n, p;
3314
		dayHeadCells.each(function(i, _e) {
3315
			e = $(_e);
3316
			n = e.offset().left;
3317
			if (i) {
3318
				p[1] = n;
3319
			}
3320
			p = [n];
3321
			cols[i] = p;
3322
		});
3323
		p[1] = n + e.outerWidth();
3324
		if (opt('allDaySlot')) {
3325
			e = allDayRow;
3326
			n = e.offset().top;
3327
			rows[0] = [n, n+e.outerHeight()];
3328
		}
3329
		var slotTableTop = slotContent.offset().top;
3330
		var slotScrollerTop = slotScroller.offset().top;
3331
		var slotScrollerBottom = slotScrollerTop + slotScroller.outerHeight();
3332
		function constrain(n) {
3333
			return Math.max(slotScrollerTop, Math.min(slotScrollerBottom, n));
3334
		}
3335
		for (var i=0; i<slotCnt; i++) {
3336
			rows.push([
3337
				constrain(slotTableTop + slotHeight*i),
3338
				constrain(slotTableTop + slotHeight*(i+1))
3339
			]);
3340
		}
3341
	});
3342
 
3343
 
3344
	hoverListener = new HoverListener(coordinateGrid);
3345
 
3346
 
3347
	colContentPositions = new HorizontalPositionCache(function(col) {
3348
		return dayBodyCellInners.eq(col);
3349
	});
3350
 
3351
 
3352
	function colContentLeft(col) {
3353
		return colContentPositions.left(col);
3354
	}
3355
 
3356
 
3357
	function colContentRight(col) {
3358
		return colContentPositions.right(col);
3359
	}
3360
 
3361
 
3362
 
3363
 
3364
	function dateCell(date) { // "cell" terminology is now confusing
3365
		return {
3366
			row: Math.floor(dayDiff(date, t.visStart) / 7),
3367
			col: dayOfWeekCol(date.getDay())
3368
		};
3369
	}
3370
 
3371
 
3372
	function cellDate(cell) {
3373
		var d = colDate(cell.col);
3374
		var slotIndex = cell.row;
3375
		if (opt('allDaySlot')) {
3376
			slotIndex--;
3377
		}
3378
		if (slotIndex >= 0) {
3379
			addMinutes(d, minMinute + slotIndex * opt('slotMinutes'));
3380
		}
3381
		return d;
3382
	}
3383
 
3384
 
3385
	function colDate(col) { // returns dates with 00:00:00
3386
		return addDays(cloneDate(t.visStart), col*dis+dit);
3387
	}
3388
 
3389
 
3390
	function cellIsAllDay(cell) {
3391
		return opt('allDaySlot') && !cell.row;
3392
	}
3393
 
3394
 
3395
	function dayOfWeekCol(dayOfWeek) {
3396
		return ((dayOfWeek - Math.max(firstDay, nwe) + colCnt) % colCnt)*dis+dit;
3397
	}
3398
 
3399
 
3400
 
3401
 
3402
	// get the Y coordinate of the given time on the given day (both Date objects)
3403
	function timePosition(day, time) { // both date objects. day holds 00:00 of current day
3404
		day = cloneDate(day, true);
3405
		if (time < addMinutes(cloneDate(day), minMinute)) {
3406
			return 0;
3407
		}
3408
		if (time >= addMinutes(cloneDate(day), maxMinute)) {
3409
			return slotTable.height();
3410
		}
3411
		var slotMinutes = opt('slotMinutes'),
3412
			minutes = time.getHours()*60 + time.getMinutes() - minMinute,
3413
			slotI = Math.floor(minutes / slotMinutes),
3414
			slotTop = slotTopCache[slotI];
3415
		if (slotTop === undefined) {
3416
			slotTop = slotTopCache[slotI] = slotTable.find('tr:eq(' + slotI + ') td div')[0].offsetTop; //.position().top; // need this optimization???
3417
		}
3418
		return Math.max(0, Math.round(
3419
			slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes)
3420
		));
3421
	}
3422
 
3423
 
3424
	function allDayBounds() {
3425
		return {
3426
			left: axisWidth,
3427
			right: viewWidth - gutterWidth
3428
		}
3429
	}
3430
 
3431
 
3432
	function getAllDayRow(index) {
3433
		return allDayRow;
3434
	}
3435
 
3436
 
3437
	function defaultEventEnd(event) {
3438
		var start = cloneDate(event.start);
3439
		if (event.allDay) {
3440
			return start;
3441
		}
3442
		return addMinutes(start, opt('defaultEventMinutes'));
3443
	}
3444
 
3445
 
3446
 
3447
	/* Selection
3448
	---------------------------------------------------------------------------------*/
3449
 
3450
 
3451
	function defaultSelectionEnd(startDate, allDay) {
3452
		if (allDay) {
3453
			return cloneDate(startDate);
3454
		}
3455
		return addMinutes(cloneDate(startDate), opt('slotMinutes'));
3456
	}
3457
 
3458
 
3459
	function renderSelection(startDate, endDate, allDay) { // only for all-day
3460
		if (allDay) {
3461
			if (opt('allDaySlot')) {
3462
				renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true);
3463
			}
3464
		}else{
3465
			renderSlotSelection(startDate, endDate);
3466
		}
3467
	}
3468
 
3469
 
3470
	function renderSlotSelection(startDate, endDate) {
3471
		var helperOption = opt('selectHelper');
3472
		coordinateGrid.build();
3473
		if (helperOption) {
3474
			var col = dayDiff(startDate, t.visStart) * dis + dit;
3475
			if (col >= 0 && col < colCnt) { // only works when times are on same day
3476
				var rect = coordinateGrid.rect(0, col, 0, col, slotContent); // only for horizontal coords
3477
				var top = timePosition(startDate, startDate);
3478
				var bottom = timePosition(startDate, endDate);
3479
				if (bottom > top) { // protect against selections that are entirely before or after visible range
3480
					rect.top = top;
3481
					rect.height = bottom - top;
3482
					rect.left += 2;
3483
					rect.width -= 5;
3484
					if ($.isFunction(helperOption)) {
3485
						var helperRes = helperOption(startDate, endDate);
3486
						if (helperRes) {
3487
							rect.position = 'absolute';
3488
							rect.zIndex = 8;
3489
							selectionHelper = $(helperRes)
3490
								.css(rect)
3491
								.appendTo(slotContent);
3492
						}
3493
					}else{
3494
						rect.isStart = true; // conside rect a "seg" now
3495
						rect.isEnd = true;   //
3496
						selectionHelper = $(slotSegHtml(
3497
							{
3498
								title: '',
3499
								start: startDate,
3500
								end: endDate,
3501
								className: ['fc-select-helper'],
3502
								editable: false
3503
							},
3504
							rect
3505
						));
3506
						selectionHelper.css('opacity', opt('dragOpacity'));
3507
					}
3508
					if (selectionHelper) {
3509
						slotBind(selectionHelper);
3510
						slotContent.append(selectionHelper);
3511
						setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended
3512
						setOuterHeight(selectionHelper, rect.height, true);
3513
					}
3514
				}
3515
			}
3516
		}else{
3517
			renderSlotOverlay(startDate, endDate);
3518
		}
3519
	}
3520
 
3521
 
3522
	function clearSelection() {
3523
		clearOverlays();
3524
		if (selectionHelper) {
3525
			selectionHelper.remove();
3526
			selectionHelper = null;
3527
		}
3528
	}
3529
 
3530
 
3531
	function slotSelectionMousedown(ev) {
3532
		if (ev.which == 1 && opt('selectable')) { // ev.which==1 means left mouse button
3533
			unselect(ev);
3534
			var dates;
3535
			hoverListener.start(function(cell, origCell) {
3536
				clearSelection();
3537
				if (cell && cell.col == origCell.col && !cellIsAllDay(cell)) {
3538
					var d1 = cellDate(origCell);
3539
					var d2 = cellDate(cell);
3540
					dates = [
3541
						d1,
3542
						addMinutes(cloneDate(d1), opt('slotMinutes')),
3543
						d2,
3544
						addMinutes(cloneDate(d2), opt('slotMinutes'))
3545
					].sort(cmp);
3546
					renderSlotSelection(dates[0], dates[3]);
3547
				}else{
3548
					dates = null;
3549
				}
3550
			}, ev);
3551
			$(document).one('mouseup', function(ev) {
3552
				hoverListener.stop();
3553
				if (dates) {
3554
					if (+dates[0] == +dates[1]) {
3555
						reportDayClick(dates[0], false, ev);
3556
					}
3557
					reportSelection(dates[0], dates[3], false, ev);
3558
				}
3559
			});
3560
		}
3561
	}
3562
 
3563
 
3564
	function reportDayClick(date, allDay, ev) {
3565
		trigger('dayClick', dayBodyCells[dayOfWeekCol(date.getDay())], date, allDay, ev);
3566
	}
3567
 
3568
 
3569
 
3570
	/* External Dragging
3571
	--------------------------------------------------------------------------------*/
3572
 
3573
 
3574
	function dragStart(_dragElement, ev, ui) {
3575
		hoverListener.start(function(cell) {
3576
			clearOverlays();
3577
			if (cell) {
3578
				if (cellIsAllDay(cell)) {
3579
					renderCellOverlay(cell.row, cell.col, cell.row, cell.col);
3580
				}else{
3581
					var d1 = cellDate(cell);
3582
					var d2 = addMinutes(cloneDate(d1), opt('defaultEventMinutes'));
3583
					renderSlotOverlay(d1, d2);
3584
				}
3585
			}
3586
		}, ev);
3587
	}
3588
 
3589
 
3590
	function dragStop(_dragElement, ev, ui) {
3591
		var cell = hoverListener.stop();
3592
		clearOverlays();
3593
		if (cell) {
3594
			trigger('drop', _dragElement, cellDate(cell), cellIsAllDay(cell), ev, ui);
3595
		}
3596
	}
3597
 
3598
 
3599
}
3600
 
3601
function AgendaEventRenderer() {
3602
	var t = this;
3603
 
3604
 
3605
	// exports
3606
	t.renderEvents = renderEvents;
3607
	t.compileDaySegs = compileDaySegs; // for DayEventRenderer
3608
	t.clearEvents = clearEvents;
3609
	t.slotSegHtml = slotSegHtml;
3610
	t.bindDaySeg = bindDaySeg;
3611
 
3612
 
3613
	// imports
3614
	DayEventRenderer.call(t);
3615
	var opt = t.opt;
3616
	var trigger = t.trigger;
3617
	//var setOverflowHidden = t.setOverflowHidden;
3618
	var isEventDraggable = t.isEventDraggable;
3619
	var isEventResizable = t.isEventResizable;
3620
	var eventEnd = t.eventEnd;
3621
	var reportEvents = t.reportEvents;
3622
	var reportEventClear = t.reportEventClear;
3623
	var eventElementHandlers = t.eventElementHandlers;
3624
	var setHeight = t.setHeight;
3625
	var getDaySegmentContainer = t.getDaySegmentContainer;
3626
	var getSlotSegmentContainer = t.getSlotSegmentContainer;
3627
	var getHoverListener = t.getHoverListener;
3628
	var getMaxMinute = t.getMaxMinute;
3629
	var getMinMinute = t.getMinMinute;
3630
	var timePosition = t.timePosition;
3631
	var colContentLeft = t.colContentLeft;
3632
	var colContentRight = t.colContentRight;
3633
	var renderDaySegs = t.renderDaySegs;
3634
	var resizableDayEvent = t.resizableDayEvent; // TODO: streamline binding architecture
3635
	var getColCnt = t.getColCnt;
3636
	var getColWidth = t.getColWidth;
3637
	var getSlotHeight = t.getSlotHeight;
3638
	var getBodyContent = t.getBodyContent;
3639
	var reportEventElement = t.reportEventElement;
3640
	var showEvents = t.showEvents;
3641
	var hideEvents = t.hideEvents;
3642
	var eventDrop = t.eventDrop;
3643
	var eventResize = t.eventResize;
3644
	var renderDayOverlay = t.renderDayOverlay;
3645
	var clearOverlays = t.clearOverlays;
3646
	var calendar = t.calendar;
3647
	var formatDate = calendar.formatDate;
3648
	var formatDates = calendar.formatDates;
3649
 
3650
 
3651
 
3652
	/* Rendering
3653
	----------------------------------------------------------------------------*/
3654
 
3655
 
3656
	function renderEvents(events, modifiedEventId) {
3657
		reportEvents(events);
3658
		var i, len=events.length,
3659
			dayEvents=[],
3660
			slotEvents=[];
3661
		for (i=0; i<len; i++) {
3662
			if (events[i].allDay) {
3663
				dayEvents.push(events[i]);
3664
			}else{
3665
				slotEvents.push(events[i]);
3666
			}
3667
		}
3668
		if (opt('allDaySlot')) {
3669
			renderDaySegs(compileDaySegs(dayEvents), modifiedEventId);
3670
			setHeight(); // no params means set to viewHeight
3671
		}
3672
		renderSlotSegs(compileSlotSegs(slotEvents), modifiedEventId);
3673
	}
3674
 
3675
 
3676
	function clearEvents() {
3677
		reportEventClear();
3678
		getDaySegmentContainer().empty();
3679
		getSlotSegmentContainer().empty();
3680
	}
3681
 
3682
 
3683
	function compileDaySegs(events) {
3684
		var levels = stackSegs(sliceSegs(events, $.map(events, exclEndDay), t.visStart, t.visEnd)),
3685
			i, levelCnt=levels.length, level,
3686
			j, seg,
3687
			segs=[];
3688
		for (i=0; i<levelCnt; i++) {
3689
			level = levels[i];
3690
			for (j=0; j<level.length; j++) {
3691
				seg = level[j];
3692
				seg.row = 0;
3693
				seg.level = i; // not needed anymore
3694
				segs.push(seg);
3695
			}
3696
		}
3697
		return segs;
3698
	}
3699
 
3700
 
3701
	function compileSlotSegs(events) {
3702
		var colCnt = getColCnt(),
3703
			minMinute = getMinMinute(),
3704
			maxMinute = getMaxMinute(),
3705
			d = addMinutes(cloneDate(t.visStart), minMinute),
3706
			visEventEnds = $.map(events, slotEventEnd),
3707
			i, col,
3708
			j, level,
3709
			k, seg,
3710
			segs=[];
3711
		for (i=0; i<colCnt; i++) {
3712
			col = stackSegs(sliceSegs(events, visEventEnds, d, addMinutes(cloneDate(d), maxMinute-minMinute)));
3713
			countForwardSegs(col);
3714
			for (j=0; j<col.length; j++) {
3715
				level = col[j];
3716
				for (k=0; k<level.length; k++) {
3717
					seg = level[k];
3718
					seg.col = i;
3719
					seg.level = j;
3720
					segs.push(seg);
3721
				}
3722
			}
3723
			addDays(d, 1, true);
3724
		}
3725
		return segs;
3726
	}
3727
 
3728
 
3729
	function slotEventEnd(event) {
3730
		if (event.end) {
3731
			return cloneDate(event.end);
3732
		}else{
3733
			return addMinutes(cloneDate(event.start), opt('defaultEventMinutes'));
3734
		}
3735
	}
3736
 
3737
 
3738
	// renders events in the 'time slots' at the bottom
3739
 
3740
	function renderSlotSegs(segs, modifiedEventId) {
3741
 
3742
		var i, segCnt=segs.length, seg,
3743
			event,
3744
			classes,
3745
			top, bottom,
3746
			colI, levelI, forward,
3747
			leftmost,
3748
			availWidth,
3749
			outerWidth,
3750
			left,
3751
			html='',
3752
			eventElements,
3753
			eventElement,
3754
			triggerRes,
3755
			vsideCache={},
3756
			hsideCache={},
3757
			key, val,
3758
			contentElement,
3759
			height,
3760
			slotSegmentContainer = getSlotSegmentContainer(),
3761
			rtl, dis, dit,
3762
			colCnt = getColCnt();
3763
 
3764
		if (rtl = opt('isRTL')) {
3765
			dis = -1;
3766
			dit = colCnt - 1;
3767
		}else{
3768
			dis = 1;
3769
			dit = 0;
3770
		}
3771
 
3772
		// calculate position/dimensions, create html
3773
		for (i=0; i<segCnt; i++) {
3774
			seg = segs[i];
3775
			event = seg.event;
3776
			top = timePosition(seg.start, seg.start);
3777
			bottom = timePosition(seg.start, seg.end);
3778
			colI = seg.col;
3779
			levelI = seg.level;
3780
			forward = seg.forward || 0;
3781
			leftmost = colContentLeft(colI*dis + dit);
3782
			availWidth = colContentRight(colI*dis + dit) - leftmost;
3783
			availWidth = Math.min(availWidth-6, availWidth*.95); // TODO: move this to CSS
3784
			if (levelI) {
3785
				// indented and thin
3786
				outerWidth = availWidth / (levelI + forward + 1);
3787
			}else{
3788
				if (forward) {
3789
					// moderately wide, aligned left still
3790
					outerWidth = ((availWidth / (forward + 1)) - (12/2)) * 2; // 12 is the predicted width of resizer =
3791
				}else{
3792
					// can be entire width, aligned left
3793
					outerWidth = availWidth;
3794
				}
3795
			}
3796
			left = leftmost +                                  // leftmost possible
3797
				(availWidth / (levelI + forward + 1) * levelI) // indentation
3798
				* dis + (rtl ? availWidth - outerWidth : 0);   // rtl
3799
			seg.top = top;
3800
			seg.left = left;
3801
			seg.outerWidth = outerWidth;
3802
			seg.outerHeight = bottom - top;
3803
			html += slotSegHtml(event, seg);
3804
		}
3805
		slotSegmentContainer[0].innerHTML = html; // faster than html()
3806
		eventElements = slotSegmentContainer.children();
3807
 
3808
		// retrieve elements, run through eventRender callback, bind event handlers
3809
		for (i=0; i<segCnt; i++) {
3810
			seg = segs[i];
3811
			event = seg.event;
3812
			eventElement = $(eventElements[i]); // faster than eq()
3813
			triggerRes = trigger('eventRender', event, event, eventElement);
3814
			if (triggerRes === false) {
3815
				eventElement.remove();
3816
			}else{
3817
				if (triggerRes && triggerRes !== true) {
3818
					eventElement.remove();
3819
					eventElement = $(triggerRes)
3820
						.css({
3821
							position: 'absolute',
3822
							top: seg.top,
3823
							left: seg.left
3824
						})
3825
						.appendTo(slotSegmentContainer);
3826
				}
3827
				seg.element = eventElement;
3828
				if (event._id === modifiedEventId) {
3829
					bindSlotSeg(event, eventElement, seg);
3830
				}else{
3831
					eventElement[0]._fci = i; // for lazySegBind
3832
				}
3833
				reportEventElement(event, eventElement);
3834
			}
3835
		}
3836
 
3837
		lazySegBind(slotSegmentContainer, segs, bindSlotSeg);
3838
 
3839
		// record event sides and title positions
3840
		for (i=0; i<segCnt; i++) {
3841
			seg = segs[i];
3842
			if (eventElement = seg.element) {
3843
				val = vsideCache[key = seg.key = cssKey(eventElement[0])];
3844
				seg.vsides = val === undefined ? (vsideCache[key] = vsides(eventElement, true)) : val;
3845
				val = hsideCache[key];
3846
				seg.hsides = val === undefined ? (hsideCache[key] = hsides(eventElement, true)) : val;
3847
				contentElement = eventElement.find('div.fc-event-content');
3848
				if (contentElement.length) {
3849
					seg.contentTop = contentElement[0].offsetTop;
3850
				}
3851
			}
3852
		}
3853
 
3854
		// set all positions/dimensions at once
3855
		for (i=0; i<segCnt; i++) {
3856
			seg = segs[i];
3857
			if (eventElement = seg.element) {
3858
				eventElement[0].style.width = Math.max(0, seg.outerWidth - seg.hsides) + 'px';
3859
				height = Math.max(0, seg.outerHeight - seg.vsides);
3860
				eventElement[0].style.height = height + 'px';
3861
				event = seg.event;
3862
				if (seg.contentTop !== undefined && height - seg.contentTop < 10) {
3863
					// not enough room for title, put it in the time header
3864
					eventElement.find('div.fc-event-time')
3865
						.text(formatDate(event.start, opt('timeFormat')) + ' - ' + event.title);
3866
					eventElement.find('div.fc-event-title')
3867
						.remove();
3868
				}
3869
				trigger('eventAfterRender', event, event, eventElement);
3870
			}
3871
		}
3872
 
3873
	}
3874
 
3875
 
3876
	function slotSegHtml(event, seg) {
3877
		var html = "<";
3878
		var url = event.url;
3879
		var skinCss = getSkinCss(event, opt);
3880
		var skinCssAttr = (skinCss ? " style='" + skinCss + "'" : '');
3881
		var classes = ['fc-event', 'fc-event-skin', 'fc-event-vert'];
3882
		if (isEventDraggable(event)) {
3883
			classes.push('fc-event-draggable');
3884
		}
3885
		if (seg.isStart) {
3886
			classes.push('fc-corner-top');
3887
		}
3888
		if (seg.isEnd) {
3889
			classes.push('fc-corner-bottom');
3890
		}
3891
		classes = classes.concat(event.className);
3892
		if (event.source) {
3893
			classes = classes.concat(event.source.className || []);
3894
		}
3895
		if (url) {
3896
			html += "a href='" + htmlEscape(event.url) + "'";
3897
		}else{
3898
			html += "div";
3899
		}
3900
		html +=
3901
			" class='" + classes.join(' ') + "'" +
3902
			" style='position:absolute;z-index:8;top:" + seg.top + "px;left:" + seg.left + "px;" + skinCss + "'" +
3903
			">" +
3904
			"<div class='fc-event-inner fc-event-skin'" + skinCssAttr + ">" +
3905
			"<div class='fc-event-head fc-event-skin'" + skinCssAttr + ">" +
3906
			"<div class='fc-event-time'>" +
3907
			htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) +
3908
			"</div>" +
3909
			"</div>" +
3910
			"<div class='fc-event-content'>" +
3911
			"<div class='fc-event-title'>" +
3912
			htmlEscape(event.title) +
3913
			"</div>" +
3914
			"</div>" +
3915
			"<div class='fc-event-bg'></div>" +
3916
			"</div>"; // close inner
3917
		if (seg.isEnd && isEventResizable(event)) {
3918
			html +=
3919
				"<div class='ui-resizable-handle ui-resizable-s'>=</div>";
3920
		}
3921
		html +=
3922
			"</" + (url ? "a" : "div") + ">";
3923
		return html;
3924
	}
3925
 
3926
 
3927
	function bindDaySeg(event, eventElement, seg) {
3928
		if (isEventDraggable(event)) {
3929
			draggableDayEvent(event, eventElement, seg.isStart);
3930
		}
3931
		if (seg.isEnd && isEventResizable(event)) {
3932
			resizableDayEvent(event, eventElement, seg);
3933
		}
3934
		eventElementHandlers(event, eventElement);
3935
			// needs to be after, because resizableDayEvent might stopImmediatePropagation on click
3936
	}
3937
 
3938
 
3939
	function bindSlotSeg(event, eventElement, seg) {
3940
		var timeElement = eventElement.find('div.fc-event-time');
3941
		if (isEventDraggable(event)) {
3942
			draggableSlotEvent(event, eventElement, timeElement);
3943
		}
3944
		if (seg.isEnd && isEventResizable(event)) {
3945
			resizableSlotEvent(event, eventElement, timeElement);
3946
		}
3947
		eventElementHandlers(event, eventElement);
3948
	}
3949
 
3950
 
3951
 
3952
	/* Dragging
3953
	-----------------------------------------------------------------------------------*/
3954
 
3955
 
3956
	// when event starts out FULL-DAY
3957
 
3958
	function draggableDayEvent(event, eventElement, isStart) {
3959
		var origWidth;
3960
		var revert;
3961
		var allDay=true;
3962
		var dayDelta;
3963
		var dis = opt('isRTL') ? -1 : 1;
3964
		var hoverListener = getHoverListener();
3965
		var colWidth = getColWidth();
3966
		var slotHeight = getSlotHeight();
3967
		var minMinute = getMinMinute();
3968
		eventElement.draggable({
3969
			zIndex: 9,
3970
			opacity: opt('dragOpacity', 'month'), // use whatever the month view was using
3971
			revertDuration: opt('dragRevertDuration'),
3972
			start: function(ev, ui) {
3973
				trigger('eventDragStart', eventElement, event, ev, ui);
3974
				hideEvents(event, eventElement);
3975
				origWidth = eventElement.width();
3976
				hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
3977
					clearOverlays();
3978
					if (cell) {
3979
						//setOverflowHidden(true);
3980
						revert = false;
3981
						dayDelta = colDelta * dis;
3982
						if (!cell.row) {
3983
							// on full-days
3984
							renderDayOverlay(
3985
								addDays(cloneDate(event.start), dayDelta),
3986
								addDays(exclEndDay(event), dayDelta)
3987
							);
3988
							resetElement();
3989
						}else{
3990
							// mouse is over bottom slots
3991
							if (isStart) {
3992
								if (allDay) {
3993
									// convert event to temporary slot-event
3994
									eventElement.width(colWidth - 10); // don't use entire width
3995
									setOuterHeight(
3996
										eventElement,
3997
										slotHeight * Math.round(
3998
											(event.end ? ((event.end - event.start) / MINUTE_MS) : opt('defaultEventMinutes'))
3999
											/ opt('slotMinutes')
4000
										)
4001
									);
4002
									eventElement.draggable('option', 'grid', [colWidth, 1]);
4003
									allDay = false;
4004
								}
4005
							}else{
4006
								revert = true;
4007
							}
4008
						}
4009
						revert = revert || (allDay && !dayDelta);
4010
					}else{
4011
						resetElement();
4012
						//setOverflowHidden(false);
4013
						revert = true;
4014
					}
4015
					eventElement.draggable('option', 'revert', revert);
4016
				}, ev, 'drag');
4017
			},
4018
			stop: function(ev, ui) {
4019
				hoverListener.stop();
4020
				clearOverlays();
4021
				trigger('eventDragStop', eventElement, event, ev, ui);
4022
				if (revert) {
4023
					// hasn't moved or is out of bounds (draggable has already reverted)
4024
					resetElement();
4025
					eventElement.css('filter', ''); // clear IE opacity side-effects
4026
					showEvents(event, eventElement);
4027
				}else{
4028
					// changed!
4029
					var minuteDelta = 0;
4030
					if (!allDay) {
4031
						minuteDelta = Math.round((eventElement.offset().top - getBodyContent().offset().top) / slotHeight)
4032
							* opt('slotMinutes')
4033
							+ minMinute
4034
							- (event.start.getHours() * 60 + event.start.getMinutes());
4035
					}
4036
					eventDrop(this, event, dayDelta, minuteDelta, allDay, ev, ui);
4037
				}
4038
				//setOverflowHidden(false);
4039
			}
4040
		});
4041
		function resetElement() {
4042
			if (!allDay) {
4043
				eventElement
4044
					.width(origWidth)
4045
					.height('')
4046
					.draggable('option', 'grid', null);
4047
				allDay = true;
4048
			}
4049
		}
4050
	}
4051
 
4052
 
4053
	// when event starts out IN TIMESLOTS
4054
 
4055
	function draggableSlotEvent(event, eventElement, timeElement) {
4056
		var origPosition;
4057
		var allDay=false;
4058
		var dayDelta;
4059
		var minuteDelta;
4060
		var prevMinuteDelta;
4061
		var dis = opt('isRTL') ? -1 : 1;
4062
		var hoverListener = getHoverListener();
4063
		var colCnt = getColCnt();
4064
		var colWidth = getColWidth();
4065
		var slotHeight = getSlotHeight();
4066
		eventElement.draggable({
4067
			zIndex: 9,
4068
			scroll: false,
4069
			grid: [colWidth, slotHeight],
4070
			axis: colCnt==1 ? 'y' : false,
4071
			opacity: opt('dragOpacity'),
4072
			revertDuration: opt('dragRevertDuration'),
4073
			start: function(ev, ui) {
4074
				trigger('eventDragStart', eventElement, event, ev, ui);
4075
				hideEvents(event, eventElement);
4076
				origPosition = eventElement.position();
4077
				minuteDelta = prevMinuteDelta = 0;
4078
				hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
4079
					eventElement.draggable('option', 'revert', !cell);
4080
					clearOverlays();
4081
					if (cell) {
4082
						dayDelta = colDelta * dis;
4083
						if (opt('allDaySlot') && !cell.row) {
4084
							// over full days
4085
							if (!allDay) {
4086
								// convert to temporary all-day event
4087
								allDay = true;
4088
								timeElement.hide();
4089
								eventElement.draggable('option', 'grid', null);
4090
							}
4091
							renderDayOverlay(
4092
								addDays(cloneDate(event.start), dayDelta),
4093
								addDays(exclEndDay(event), dayDelta)
4094
							);
4095
						}else{
4096
							// on slots
4097
							resetElement();
4098
						}
4099
					}
4100
				}, ev, 'drag');
4101
			},
4102
			drag: function(ev, ui) {
4103
				minuteDelta = Math.round((ui.position.top - origPosition.top) / slotHeight) * opt('slotMinutes');
4104
				if (minuteDelta != prevMinuteDelta) {
4105
					if (!allDay) {
4106
						updateTimeText(minuteDelta);
4107
					}
4108
					prevMinuteDelta = minuteDelta;
4109
				}
4110
			},
4111
			stop: function(ev, ui) {
4112
				var cell = hoverListener.stop();
4113
				clearOverlays();
4114
				trigger('eventDragStop', eventElement, event, ev, ui);
4115
				if (cell && (dayDelta || minuteDelta || allDay)) {
4116
					// changed!
4117
					eventDrop(this, event, dayDelta, allDay ? 0 : minuteDelta, allDay, ev, ui);
4118
				}else{
4119
					// either no change or out-of-bounds (draggable has already reverted)
4120
					resetElement();
4121
					eventElement.css('filter', ''); // clear IE opacity side-effects
4122
					eventElement.css(origPosition); // sometimes fast drags make event revert to wrong position
4123
					updateTimeText(0);
4124
					showEvents(event, eventElement);
4125
				}
4126
			}
4127
		});
4128
		function updateTimeText(minuteDelta) {
4129
			var newStart = addMinutes(cloneDate(event.start), minuteDelta);
4130
			var newEnd;
4131
			if (event.end) {
4132
				newEnd = addMinutes(cloneDate(event.end), minuteDelta);
4133
			}
4134
			timeElement.text(formatDates(newStart, newEnd, opt('timeFormat')));
4135
		}
4136
		function resetElement() {
4137
			// convert back to original slot-event
4138
			if (allDay) {
4139
				timeElement.css('display', ''); // show() was causing display=inline
4140
				eventElement.draggable('option', 'grid', [colWidth, slotHeight]);
4141
				allDay = false;
4142
			}
4143
		}
4144
	}
4145
 
4146
 
4147
 
4148
	/* Resizing
4149
	--------------------------------------------------------------------------------------*/
4150
 
4151
 
4152
	function resizableSlotEvent(event, eventElement, timeElement) {
4153
		var slotDelta, prevSlotDelta;
4154
		var slotHeight = getSlotHeight();
4155
		eventElement.resizable({
4156
			handles: {
4157
				s: 'div.ui-resizable-s'
4158
			},
4159
			grid: slotHeight,
4160
			start: function(ev, ui) {
4161
				slotDelta = prevSlotDelta = 0;
4162
				hideEvents(event, eventElement);
4163
				eventElement.css('z-index', 9);
4164
				trigger('eventResizeStart', this, event, ev, ui);
4165
			},
4166
			resize: function(ev, ui) {
4167
				// don't rely on ui.size.height, doesn't take grid into account
4168
				slotDelta = Math.round((Math.max(slotHeight, eventElement.height()) - ui.originalSize.height) / slotHeight);
4169
				if (slotDelta != prevSlotDelta) {
4170
					timeElement.text(
4171
						formatDates(
4172
							event.start,
4173
							(!slotDelta && !event.end) ? null : // no change, so don't display time range
4174
								addMinutes(eventEnd(event), opt('slotMinutes')*slotDelta),
4175
							opt('timeFormat')
4176
						)
4177
					);
4178
					prevSlotDelta = slotDelta;
4179
				}
4180
			},
4181
			stop: function(ev, ui) {
4182
				trigger('eventResizeStop', this, event, ev, ui);
4183
				if (slotDelta) {
4184
					eventResize(this, event, 0, opt('slotMinutes')*slotDelta, ev, ui);
4185
				}else{
4186
					eventElement.css('z-index', 8);
4187
					showEvents(event, eventElement);
4188
					// BUG: if event was really short, need to put title back in span
4189
				}
4190
			}
4191
		});
4192
	}
4193
 
4194
 
4195
}
4196
 
4197
 
4198
function countForwardSegs(levels) {
4199
	var i, j, k, level, segForward, segBack;
4200
	for (i=levels.length-1; i>0; i--) {
4201
		level = levels[i];
4202
		for (j=0; j<level.length; j++) {
4203
			segForward = level[j];
4204
			for (k=0; k<levels[i-1].length; k++) {
4205
				segBack = levels[i-1][k];
4206
				if (segsCollide(segForward, segBack)) {
4207
					segBack.forward = Math.max(segBack.forward||0, (segForward.forward||0)+1);
4208
				}
4209
			}
4210
		}
4211
	}
4212
}
4213
 
4214
 
4215
 
4216
 
4217
function View(element, calendar, viewName) {
4218
	var t = this;
4219
 
4220
 
4221
	// exports
4222
	t.element = element;
4223
	t.calendar = calendar;
4224
	t.name = viewName;
4225
	t.opt = opt;
4226
	t.trigger = trigger;
4227
	//t.setOverflowHidden = setOverflowHidden;
4228
	t.isEventDraggable = isEventDraggable;
4229
	t.isEventResizable = isEventResizable;
4230
	t.reportEvents = reportEvents;
4231
	t.eventEnd = eventEnd;
4232
	t.reportEventElement = reportEventElement;
4233
	t.reportEventClear = reportEventClear;
4234
	t.eventElementHandlers = eventElementHandlers;
4235
	t.showEvents = showEvents;
4236
	t.hideEvents = hideEvents;
4237
	t.eventDrop = eventDrop;
4238
	t.eventResize = eventResize;
4239
	// t.title
4240
	// t.start, t.end
4241
	// t.visStart, t.visEnd
4242
 
4243
 
4244
	// imports
4245
	var defaultEventEnd = t.defaultEventEnd;
4246
	var normalizeEvent = calendar.normalizeEvent; // in EventManager
4247
	var reportEventChange = calendar.reportEventChange;
4248
 
4249
 
4250
	// locals
4251
	var eventsByID = {};
4252
	var eventElements = [];
4253
	var eventElementsByID = {};
4254
	var options = calendar.options;
4255
 
4256
 
4257
 
4258
	function opt(name, viewNameOverride) {
4259
		var v = options[name];
4260
		if (typeof v == 'object') {
4261
			return smartProperty(v, viewNameOverride || viewName);
4262
		}
4263
		return v;
4264
	}
4265
 
4266
 
4267
	function trigger(name, thisObj) {
4268
		return calendar.trigger.apply(
4269
			calendar,
4270
			[name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t])
4271
		);
4272
	}
4273
 
4274
 
4275
	/*
4276
	function setOverflowHidden(bool) {
4277
		element.css('overflow', bool ? 'hidden' : '');
4278
	}
4279
	*/
4280
 
4281
 
4282
	function isEventDraggable(event) {
4283
		return isEventEditable(event) && !opt('disableDragging');
4284
	}
4285
 
4286
 
4287
	function isEventResizable(event) { // but also need to make sure the seg.isEnd == true
4288
		return isEventEditable(event) && !opt('disableResizing');
4289
	}
4290
 
4291
 
4292
	function isEventEditable(event) {
4293
		return firstDefined(event.editable, (event.source || {}).editable, opt('editable'));
4294
	}
4295
 
4296
 
4297
 
4298
	/* Event Data
4299
	------------------------------------------------------------------------------*/
4300
 
4301
 
4302
	// report when view receives new events
4303
	function reportEvents(events) { // events are already normalized at this point
4304
		eventsByID = {};
4305
		var i, len=events.length, event;
4306
		for (i=0; i<len; i++) {
4307
			event = events[i];
4308
			if (eventsByID[event._id]) {
4309
				eventsByID[event._id].push(event);
4310
			}else{
4311
				eventsByID[event._id] = [event];
4312
			}
4313
		}
4314
	}
4315
 
4316
 
4317
	// returns a Date object for an event's end
4318
	function eventEnd(event) {
4319
		return event.end ? cloneDate(event.end) : defaultEventEnd(event);
4320
	}
4321
 
4322
 
4323
 
4324
	/* Event Elements
4325
	------------------------------------------------------------------------------*/
4326
 
4327
 
4328
	// report when view creates an element for an event
4329
	function reportEventElement(event, element) {
4330
		eventElements.push(element);
4331
		if (eventElementsByID[event._id]) {
4332
			eventElementsByID[event._id].push(element);
4333
		}else{
4334
			eventElementsByID[event._id] = [element];
4335
		}
4336
	}
4337
 
4338
 
4339
	function reportEventClear() {
4340
		eventElements = [];
4341
		eventElementsByID = {};
4342
	}
4343
 
4344
 
4345
	// attaches eventClick, eventMouseover, eventMouseout
4346
	function eventElementHandlers(event, eventElement) {
4347
		eventElement
4348
			.click(function(ev) {
4349
				if (!eventElement.hasClass('ui-draggable-dragging') &&
4350
					!eventElement.hasClass('ui-resizable-resizing')) {
4351
						return trigger('eventClick', this, event, ev);
4352
					}
4353
			})
4354
			.hover(
4355
				function(ev) {
4356
					trigger('eventMouseover', this, event, ev);
4357
				},
4358
				function(ev) {
4359
					trigger('eventMouseout', this, event, ev);
4360
				}
4361
			);
4362
		// TODO: don't fire eventMouseover/eventMouseout *while* dragging is occuring (on subject element)
4363
		// TODO: same for resizing
4364
	}
4365
 
4366
 
4367
	function showEvents(event, exceptElement) {
4368
		eachEventElement(event, exceptElement, 'show');
4369
	}
4370
 
4371
 
4372
	function hideEvents(event, exceptElement) {
4373
		eachEventElement(event, exceptElement, 'hide');
4374
	}
4375
 
4376
 
4377
	function eachEventElement(event, exceptElement, funcName) {
4378
		var elements = eventElementsByID[event._id],
4379
			i, len = elements.length;
4380
		for (i=0; i<len; i++) {
4381
			if (!exceptElement || elements[i][0] != exceptElement[0]) {
4382
				elements[i][funcName]();
4383
			}
4384
		}
4385
	}
4386
 
4387
 
4388
 
4389
	/* Event Modification Reporting
4390
	---------------------------------------------------------------------------------*/
4391
 
4392
 
4393
	function eventDrop(e, event, dayDelta, minuteDelta, allDay, ev, ui) {
4394
		var oldAllDay = event.allDay;
4395
		var eventId = event._id;
4396
		moveEvents(eventsByID[eventId], dayDelta, minuteDelta, allDay);
4397
		trigger(
4398
			'eventDrop',
4399
			e,
4400
			event,
4401
			dayDelta,
4402
			minuteDelta,
4403
			allDay,
4404
			function() {
4405
				// TODO: investigate cases where this inverse technique might not work
4406
				moveEvents(eventsByID[eventId], -dayDelta, -minuteDelta, oldAllDay);
4407
				reportEventChange(eventId);
4408
			},
4409
			ev,
4410
			ui
4411
		);
4412
		reportEventChange(eventId);
4413
	}
4414
 
4415
 
4416
	function eventResize(e, event, dayDelta, minuteDelta, ev, ui) {
4417
		var eventId = event._id;
4418
		elongateEvents(eventsByID[eventId], dayDelta, minuteDelta);
4419
		trigger(
4420
			'eventResize',
4421
			e,
4422
			event,
4423
			dayDelta,
4424
			minuteDelta,
4425
			function() {
4426
				// TODO: investigate cases where this inverse technique might not work
4427
				elongateEvents(eventsByID[eventId], -dayDelta, -minuteDelta);
4428
				reportEventChange(eventId);
4429
			},
4430
			ev,
4431
			ui
4432
		);
4433
		reportEventChange(eventId);
4434
	}
4435
 
4436
 
4437
 
4438
	/* Event Modification Math
4439
	---------------------------------------------------------------------------------*/
4440
 
4441
 
4442
	function moveEvents(events, dayDelta, minuteDelta, allDay) {
4443
		minuteDelta = minuteDelta || 0;
4444
		for (var e, len=events.length, i=0; i<len; i++) {
4445
			e = events[i];
4446
			if (allDay !== undefined) {
4447
				e.allDay = allDay;
4448
			}
4449
			addMinutes(addDays(e.start, dayDelta, true), minuteDelta);
4450
			if (e.end) {
4451
				e.end = addMinutes(addDays(e.end, dayDelta, true), minuteDelta);
4452
			}
4453
			normalizeEvent(e, options);
4454
		}
4455
	}
4456
 
4457
 
4458
	function elongateEvents(events, dayDelta, minuteDelta) {
4459
		minuteDelta = minuteDelta || 0;
4460
		for (var e, len=events.length, i=0; i<len; i++) {
4461
			e = events[i];
4462
			e.end = addMinutes(addDays(eventEnd(e), dayDelta, true), minuteDelta);
4463
			normalizeEvent(e, options);
4464
		}
4465
	}
4466
 
4467
 
4468
}
4469
 
4470
function DayEventRenderer() {
4471
	var t = this;
4472
 
4473
 
4474
	// exports
4475
	t.renderDaySegs = renderDaySegs;
4476
	t.resizableDayEvent = resizableDayEvent;
4477
 
4478
 
4479
	// imports
4480
	var opt = t.opt;
4481
	var trigger = t.trigger;
4482
	var isEventDraggable = t.isEventDraggable;
4483
	var isEventResizable = t.isEventResizable;
4484
	var eventEnd = t.eventEnd;
4485
	var reportEventElement = t.reportEventElement;
4486
	var showEvents = t.showEvents;
4487
	var hideEvents = t.hideEvents;
4488
	var eventResize = t.eventResize;
4489
	var getRowCnt = t.getRowCnt;
4490
	var getColCnt = t.getColCnt;
4491
	var getColWidth = t.getColWidth;
4492
	var allDayRow = t.allDayRow;
4493
	var allDayBounds = t.allDayBounds;
4494
	var colContentLeft = t.colContentLeft;
4495
	var colContentRight = t.colContentRight;
4496
	var dayOfWeekCol = t.dayOfWeekCol;
4497
	var dateCell = t.dateCell;
4498
	var compileDaySegs = t.compileDaySegs;
4499
	var getDaySegmentContainer = t.getDaySegmentContainer;
4500
	var bindDaySeg = t.bindDaySeg; //TODO: streamline this
4501
	var formatDates = t.calendar.formatDates;
4502
	var renderDayOverlay = t.renderDayOverlay;
4503
	var clearOverlays = t.clearOverlays;
4504
	var clearSelection = t.clearSelection;
4505
 
4506
 
4507
 
4508
	/* Rendering
4509
	-----------------------------------------------------------------------------*/
4510
 
4511
 
4512
	function renderDaySegs(segs, modifiedEventId) {
4513
		var segmentContainer = getDaySegmentContainer();
4514
		var rowDivs;
4515
		var rowCnt = getRowCnt();
4516
		var colCnt = getColCnt();
4517
		var i = 0;
4518
		var rowI;
4519
		var levelI;
4520
		var colHeights;
4521
		var j;
4522
		var segCnt = segs.length;
4523
		var seg;
4524
		var top;
4525
		var k;
4526
		segmentContainer[0].innerHTML = daySegHTML(segs); // faster than .html()
4527
		daySegElementResolve(segs, segmentContainer.children());
4528
		daySegElementReport(segs);
4529
		daySegHandlers(segs, segmentContainer, modifiedEventId);
4530
		daySegCalcHSides(segs);
4531
		daySegSetWidths(segs);
4532
		daySegCalcHeights(segs);
4533
		rowDivs = getRowDivs();
4534
		// set row heights, calculate event tops (in relation to row top)
4535
		for (rowI=0; rowI<rowCnt; rowI++) {
4536
			levelI = 0;
4537
			colHeights = [];
4538
			for (j=0; j<colCnt; j++) {
4539
				colHeights[j] = 0;
4540
			}
4541
			while (i<segCnt && (seg = segs[i]).row == rowI) {
4542
				// loop through segs in a row
4543
				top = arrayMax(colHeights.slice(seg.startCol, seg.endCol));
4544
				seg.top = top;
4545
				top += seg.outerHeight;
4546
				for (k=seg.startCol; k<seg.endCol; k++) {
4547
					colHeights[k] = top;
4548
				}
4549
				i++;
4550
			}
4551
			rowDivs[rowI].height(arrayMax(colHeights));
4552
		}
4553
		daySegSetTops(segs, getRowTops(rowDivs));
4554
	}
4555
 
4556
 
4557
	function renderTempDaySegs(segs, adjustRow, adjustTop) {
4558
		var tempContainer = $("<div/>");
4559
		var elements;
4560
		var segmentContainer = getDaySegmentContainer();
4561
		var i;
4562
		var segCnt = segs.length;
4563
		var element;
4564
		tempContainer[0].innerHTML = daySegHTML(segs); // faster than .html()
4565
		elements = tempContainer.children();
4566
		segmentContainer.append(elements);
4567
		daySegElementResolve(segs, elements);
4568
		daySegCalcHSides(segs);
4569
		daySegSetWidths(segs);
4570
		daySegCalcHeights(segs);
4571
		daySegSetTops(segs, getRowTops(getRowDivs()));
4572
		elements = [];
4573
		for (i=0; i<segCnt; i++) {
4574
			element = segs[i].element;
4575
			if (element) {
4576
				if (segs[i].row === adjustRow) {
4577
					element.css('top', adjustTop);
4578
				}
4579
				elements.push(element[0]);
4580
			}
4581
		}
4582
		return $(elements);
4583
	}
4584
 
4585
 
4586
	function daySegHTML(segs) { // also sets seg.left and seg.outerWidth
4587
		var rtl = opt('isRTL');
4588
		var i;
4589
		var segCnt=segs.length;
4590
		var seg;
4591
		var event;
4592
		var url;
4593
		var classes;
4594
		var bounds = allDayBounds();
4595
		var minLeft = bounds.left;
4596
		var maxLeft = bounds.right;
4597
		var leftCol;
4598
		var rightCol;
4599
		var left;
4600
		var right;
4601
		var skinCss;
4602
		var html = '';
4603
		// calculate desired position/dimensions, create html
4604
		for (i=0; i<segCnt; i++) {
4605
			seg = segs[i];
4606
			event = seg.event;
4607
			classes = ['fc-event', 'fc-event-skin', 'fc-event-hori'];
4608
			if (isEventDraggable(event)) {
4609
				classes.push('fc-event-draggable');
4610
			}
4611
			if (rtl) {
4612
				if (seg.isStart) {
4613
					classes.push('fc-corner-right');
4614
				}
4615
				if (seg.isEnd) {
4616
					classes.push('fc-corner-left');
4617
				}
4618
				leftCol = dayOfWeekCol(seg.end.getDay()-1);
4619
				rightCol = dayOfWeekCol(seg.start.getDay());
4620
				left = seg.isEnd ? colContentLeft(leftCol) : minLeft;
4621
				right = seg.isStart ? colContentRight(rightCol) : maxLeft;
4622
			}else{
4623
				if (seg.isStart) {
4624
					classes.push('fc-corner-left');
4625
				}
4626
				if (seg.isEnd) {
4627
					classes.push('fc-corner-right');
4628
				}
4629
				leftCol = dayOfWeekCol(seg.start.getDay());
4630
				rightCol = dayOfWeekCol(seg.end.getDay()-1);
4631
				left = seg.isStart ? colContentLeft(leftCol) : minLeft;
4632
				right = seg.isEnd ? colContentRight(rightCol) : maxLeft;
4633
			}
4634
			classes = classes.concat(event.className);
4635
			if (event.source) {
4636
				classes = classes.concat(event.source.className || []);
4637
			}
4638
			url = event.url;
4639
			skinCss = getSkinCss(event, opt);
4640
			if (url) {
4641
				html += "<a href='" + htmlEscape(url) + "'";
4642
			}else{
4643
				html += "<div";
4644
			}
4645
			html +=
4646
				" class='" + classes.join(' ') + "'" +
4647
				" style='position:absolute;z-index:8;left:"+left+"px;" + skinCss + "'" +
4648
				">" +
4649
				"<div" +
4650
				" class='fc-event-inner fc-event-skin'" +
4651
				(skinCss ? " style='" + skinCss + "'" : '') +
4652
				">";
4653
			if (!event.allDay && seg.isStart) {
4654
				html +=
4655
					"<span class='fc-event-time'>" +
4656
					htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) +
4657
					"</span>";
4658
			}
4659
			html +=
4660
				"<span class='fc-event-title'>" + htmlEscape(event.title) + "</span>" +
4661
				"</div>";
4662
			if (seg.isEnd && isEventResizable(event)) {
4663
				html +=
4664
					"<div class='ui-resizable-handle ui-resizable-" + (rtl ? 'w' : 'e') + "'>" +
4665
					"&nbsp;&nbsp;&nbsp;" + // makes hit area a lot better for IE6/7
4666
					"</div>";
4667
			}
4668
			html +=
4669
				"</" + (url ? "a" : "div" ) + ">";
4670
			seg.left = left;
4671
			seg.outerWidth = right - left;
4672
			seg.startCol = leftCol;
4673
			seg.endCol = rightCol + 1; // needs to be exclusive
4674
		}
4675
		return html;
4676
	}
4677
 
4678
 
4679
	function daySegElementResolve(segs, elements) { // sets seg.element
4680
		var i;
4681
		var segCnt = segs.length;
4682
		var seg;
4683
		var event;
4684
		var element;
4685
		var triggerRes;
4686
		for (i=0; i<segCnt; i++) {
4687
			seg = segs[i];
4688
			event = seg.event;
4689
			element = $(elements[i]); // faster than .eq()
4690
			triggerRes = trigger('eventRender', event, event, element);
4691
			if (triggerRes === false) {
4692
				element.remove();
4693
			}else{
4694
				if (triggerRes && triggerRes !== true) {
4695
					triggerRes = $(triggerRes)
4696
						.css({
4697
							position: 'absolute',
4698
							left: seg.left
4699
						});
4700
					element.replaceWith(triggerRes);
4701
					element = triggerRes;
4702
				}
4703
				seg.element = element;
4704
			}
4705
		}
4706
	}
4707
 
4708
 
4709
	function daySegElementReport(segs) {
4710
		var i;
4711
		var segCnt = segs.length;
4712
		var seg;
4713
		var element;
4714
		for (i=0; i<segCnt; i++) {
4715
			seg = segs[i];
4716
			element = seg.element;
4717
			if (element) {
4718
				reportEventElement(seg.event, element);
4719
			}
4720
		}
4721
	}
4722
 
4723
 
4724
	function daySegHandlers(segs, segmentContainer, modifiedEventId) {
4725
		var i;
4726
		var segCnt = segs.length;
4727
		var seg;
4728
		var element;
4729
		var event;
4730
		// retrieve elements, run through eventRender callback, bind handlers
4731
		for (i=0; i<segCnt; i++) {
4732
			seg = segs[i];
4733
			element = seg.element;
4734
			if (element) {
4735
				event = seg.event;
4736
				if (event._id === modifiedEventId) {
4737
					bindDaySeg(event, element, seg);
4738
				}else{
4739
					element[0]._fci = i; // for lazySegBind
4740
				}
4741
			}
4742
		}
4743
		lazySegBind(segmentContainer, segs, bindDaySeg);
4744
	}
4745
 
4746
 
4747
	function daySegCalcHSides(segs) { // also sets seg.key
4748
		var i;
4749
		var segCnt = segs.length;
4750
		var seg;
4751
		var element;
4752
		var key, val;
4753
		var hsideCache = {};
4754
		// record event horizontal sides
4755
		for (i=0; i<segCnt; i++) {
4756
			seg = segs[i];
4757
			element = seg.element;
4758
			if (element) {
4759
				key = seg.key = cssKey(element[0]);
4760
				val = hsideCache[key];
4761
				if (val === undefined) {
4762
					val = hsideCache[key] = hsides(element, true);
4763
				}
4764
				seg.hsides = val;
4765
			}
4766
		}
4767
	}
4768
 
4769
 
4770
	function daySegSetWidths(segs) {
4771
		var i;
4772
		var segCnt = segs.length;
4773
		var seg;
4774
		var element;
4775
		for (i=0; i<segCnt; i++) {
4776
			seg = segs[i];
4777
			element = seg.element;
4778
			if (element) {
4779
				element[0].style.width = Math.max(0, seg.outerWidth - seg.hsides) + 'px';
4780
			}
4781
		}
4782
	}
4783
 
4784
 
4785
	function daySegCalcHeights(segs) {
4786
		var i;
4787
		var segCnt = segs.length;
4788
		var seg;
4789
		var element;
4790
		var key, val;
4791
		var vmarginCache = {};
4792
		// record event heights
4793
		for (i=0; i<segCnt; i++) {
4794
			seg = segs[i];
4795
			element = seg.element;
4796
			if (element) {
4797
				key = seg.key; // created in daySegCalcHSides
4798
				val = vmarginCache[key];
4799
				if (val === undefined) {
4800
					val = vmarginCache[key] = vmargins(element);
4801
				}
4802
				seg.outerHeight = element[0].offsetHeight + val;
4803
			}
4804
		}
4805
	}
4806
 
4807
 
4808
	function getRowDivs() {
4809
		var i;
4810
		var rowCnt = getRowCnt();
4811
		var rowDivs = [];
4812
		for (i=0; i<rowCnt; i++) {
4813
			rowDivs[i] = allDayRow(i)
4814
				.find('td:first div.fc-day-content > div'); // optimal selector?
4815
		}
4816
		return rowDivs;
4817
	}
4818
 
4819
 
4820
	function getRowTops(rowDivs) {
4821
		var i;
4822
		var rowCnt = rowDivs.length;
4823
		var tops = [];
4824
		for (i=0; i<rowCnt; i++) {
4825
			tops[i] = rowDivs[i][0].offsetTop; // !!?? but this means the element needs position:relative if in a table cell!!!!
4826
		}
4827
		return tops;
4828
	}
4829
 
4830
 
4831
	function daySegSetTops(segs, rowTops) { // also triggers eventAfterRender
4832
		var i;
4833
		var segCnt = segs.length;
4834
		var seg;
4835
		var element;
4836
		var event;
4837
		for (i=0; i<segCnt; i++) {
4838
			seg = segs[i];
4839
			element = seg.element;
4840
			if (element) {
4841
				element[0].style.top = rowTops[seg.row] + (seg.top||0) + 'px';
4842
				event = seg.event;
4843
				trigger('eventAfterRender', event, event, element);
4844
			}
4845
		}
4846
	}
4847
 
4848
 
4849
 
4850
	/* Resizing
4851
	-----------------------------------------------------------------------------------*/
4852
 
4853
 
4854
	function resizableDayEvent(event, element, seg) {
4855
		var rtl = opt('isRTL');
4856
		var direction = rtl ? 'w' : 'e';
4857
		var handle = element.find('div.ui-resizable-' + direction);
4858
		var isResizing = false;
4859
 
4860
		// TODO: look into using jquery-ui mouse widget for this stuff
4861
		disableTextSelection(element); // prevent native <a> selection for IE
4862
		element
4863
			.mousedown(function(ev) { // prevent native <a> selection for others
4864
				ev.preventDefault();
4865
			})
4866
			.click(function(ev) {
4867
				if (isResizing) {
4868
					ev.preventDefault(); // prevent link from being visited (only method that worked in IE6)
4869
					ev.stopImmediatePropagation(); // prevent fullcalendar eventClick handler from being called
4870
					                               // (eventElementHandlers needs to be bound after resizableDayEvent)
4871
				}
4872
			});
4873
 
4874
		handle.mousedown(function(ev) {
4875
			if (ev.which != 1) {
4876
				return; // needs to be left mouse button
4877
			}
4878
			isResizing = true;
4879
			var hoverListener = t.getHoverListener();
4880
			var rowCnt = getRowCnt();
4881
			var colCnt = getColCnt();
4882
			var dis = rtl ? -1 : 1;
4883
			var dit = rtl ? colCnt-1 : 0;
4884
			var elementTop = element.css('top');
4885
			var dayDelta;
4886
			var helpers;
4887
			var eventCopy = $.extend({}, event);
4888
			var minCell = dateCell(event.start);
4889
			clearSelection();
4890
			$('body')
4891
				.css('cursor', direction + '-resize')
4892
				.one('mouseup', mouseup);
4893
			trigger('eventResizeStart', this, event, ev);
4894
			hoverListener.start(function(cell, origCell) {
4895
				if (cell) {
4896
					var r = Math.max(minCell.row, cell.row);
4897
					var c = cell.col;
4898
					if (rowCnt == 1) {
4899
						r = 0; // hack for all-day area in agenda views
4900
					}
4901
					if (r == minCell.row) {
4902
						if (rtl) {
4903
							c = Math.min(minCell.col, c);
4904
						}else{
4905
							c = Math.max(minCell.col, c);
4906
						}
4907
					}
4908
					dayDelta = (r*7 + c*dis+dit) - (origCell.row*7 + origCell.col*dis+dit);
4909
					var newEnd = addDays(eventEnd(event), dayDelta, true);
4910
					if (dayDelta) {
4911
						eventCopy.end = newEnd;
4912
						var oldHelpers = helpers;
4913
						helpers = renderTempDaySegs(compileDaySegs([eventCopy]), seg.row, elementTop);
4914
						helpers.find('*').css('cursor', direction + '-resize');
4915
						if (oldHelpers) {
4916
							oldHelpers.remove();
4917
						}
4918
						hideEvents(event);
4919
					}else{
4920
						if (helpers) {
4921
							showEvents(event);
4922
							helpers.remove();
4923
							helpers = null;
4924
						}
4925
					}
4926
					clearOverlays();
4927
					renderDayOverlay(event.start, addDays(cloneDate(newEnd), 1)); // coordinate grid already rebuild at hoverListener.start
4928
				}
4929
			}, ev);
4930
 
4931
			function mouseup(ev) {
4932
				trigger('eventResizeStop', this, event, ev);
4933
				$('body').css('cursor', '');
4934
				hoverListener.stop();
4935
				clearOverlays();
4936
				if (dayDelta) {
4937
					eventResize(this, event, dayDelta, 0, ev);
4938
					// event redraw will clear helpers
4939
				}
4940
				// otherwise, the drag handler already restored the old events
4941
 
4942
				setTimeout(function() { // make this happen after the element's click event
4943
					isResizing = false;
4944
				},0);
4945
			}
4946
 
4947
		});
4948
	}
4949
 
4950
 
4951
}
4952
 
4953
//BUG: unselect needs to be triggered when events are dragged+dropped
4954
 
4955
function SelectionManager() {
4956
	var t = this;
4957
 
4958
 
4959
	// exports
4960
	t.select = select;
4961
	t.unselect = unselect;
4962
	t.reportSelection = reportSelection;
4963
	t.daySelectionMousedown = daySelectionMousedown;
4964
 
4965
 
4966
	// imports
4967
	var opt = t.opt;
4968
	var trigger = t.trigger;
4969
	var defaultSelectionEnd = t.defaultSelectionEnd;
4970
	var renderSelection = t.renderSelection;
4971
	var clearSelection = t.clearSelection;
4972
 
4973
 
4974
	// locals
4975
	var selected = false;
4976
 
4977
 
4978
 
4979
	// unselectAuto
4980
	if (opt('selectable') && opt('unselectAuto')) {
4981
		$(document).mousedown(function(ev) {
4982
			var ignore = opt('unselectCancel');
4983
			if (ignore) {
4984
				if ($(ev.target).parents(ignore).length) { // could be optimized to stop after first match
4985
					return;
4986
				}
4987
			}
4988
			unselect(ev);
4989
		});
4990
	}
4991
 
4992
 
4993
	function select(startDate, endDate, allDay) {
4994
		unselect();
4995
		if (!endDate) {
4996
			endDate = defaultSelectionEnd(startDate, allDay);
4997
		}
4998
		renderSelection(startDate, endDate, allDay);
4999
		reportSelection(startDate, endDate, allDay);
5000
	}
5001
 
5002
 
5003
	function unselect(ev) {
5004
		if (selected) {
5005
			selected = false;
5006
			clearSelection();
5007
			trigger('unselect', null, ev);
5008
		}
5009
	}
5010
 
5011
 
5012
	function reportSelection(startDate, endDate, allDay, ev) {
5013
		selected = true;
5014
		trigger('select', null, startDate, endDate, allDay, ev);
5015
	}
5016
 
5017
 
5018
	function daySelectionMousedown(ev) { // not really a generic manager method, oh well
5019
		var cellDate = t.cellDate;
5020
		var cellIsAllDay = t.cellIsAllDay;
5021
		var hoverListener = t.getHoverListener();
5022
		var reportDayClick = t.reportDayClick; // this is hacky and sort of weird
5023
		if (ev.which == 1 && opt('selectable')) { // which==1 means left mouse button
5024
			unselect(ev);
5025
			var _mousedownElement = this;
5026
			var dates;
5027
			hoverListener.start(function(cell, origCell) { // TODO: maybe put cellDate/cellIsAllDay info in cell
5028
				clearSelection();
5029
				if (cell && cellIsAllDay(cell)) {
5030
					dates = [ cellDate(origCell), cellDate(cell) ].sort(cmp);
5031
					renderSelection(dates[0], dates[1], true);
5032
				}else{
5033
					dates = null;
5034
				}
5035
			}, ev);
5036
			$(document).one('mouseup', function(ev) {
5037
				hoverListener.stop();
5038
				if (dates) {
5039
					if (+dates[0] == +dates[1]) {
5040
						reportDayClick(dates[0], true, ev);
5041
					}
5042
					reportSelection(dates[0], dates[1], true, ev);
5043
				}
5044
			});
5045
		}
5046
	}
5047
 
5048
 
5049
}
5050
 
5051
function OverlayManager() {
5052
	var t = this;
5053
 
5054
 
5055
	// exports
5056
	t.renderOverlay = renderOverlay;
5057
	t.clearOverlays = clearOverlays;
5058
 
5059
 
5060
	// locals
5061
	var usedOverlays = [];
5062
	var unusedOverlays = [];
5063
 
5064
 
5065
	function renderOverlay(rect, parent) {
5066
		var e = unusedOverlays.shift();
5067
		if (!e) {
5068
			e = $("<div class='fc-cell-overlay' style='position:absolute;z-index:3'/>");
5069
		}
5070
		if (e[0].parentNode != parent[0]) {
5071
			e.appendTo(parent);
5072
		}
5073
		usedOverlays.push(e.css(rect).show());
5074
		return e;
5075
	}
5076
 
5077
 
5078
	function clearOverlays() {
5079
		var e;
5080
		while (e = usedOverlays.shift()) {
5081
			unusedOverlays.push(e.hide().unbind());
5082
		}
5083
	}
5084
 
5085
 
5086
}
5087
 
5088
function CoordinateGrid(buildFunc) {
5089
 
5090
	var t = this;
5091
	var rows;
5092
	var cols;
5093
 
5094
 
5095
	t.build = function() {
5096
		rows = [];
5097
		cols = [];
5098
		buildFunc(rows, cols);
5099
	};
5100
 
5101
 
5102
	t.cell = function(x, y) {
5103
		var rowCnt = rows.length;
5104
		var colCnt = cols.length;
5105
		var i, r=-1, c=-1;
5106
		for (i=0; i<rowCnt; i++) {
5107
			if (y >= rows[i][0] && y < rows[i][1]) {
5108
				r = i;
5109
				break;
5110
			}
5111
		}
5112
		for (i=0; i<colCnt; i++) {
5113
			if (x >= cols[i][0] && x < cols[i][1]) {
5114
				c = i;
5115
				break;
5116
			}
5117
		}
5118
		return (r>=0 && c>=0) ? { row:r, col:c } : null;
5119
	};
5120
 
5121
 
5122
	t.rect = function(row0, col0, row1, col1, originElement) { // row1,col1 is inclusive
5123
		var origin = originElement.offset();
5124
		return {
5125
			top: rows[row0][0] - origin.top,
5126
			left: cols[col0][0] - origin.left,
5127
			width: cols[col1][1] - cols[col0][0],
5128
			height: rows[row1][1] - rows[row0][0]
5129
		};
5130
	};
5131
 
5132
}
5133
 
5134
function HoverListener(coordinateGrid) {
5135
 
5136
 
5137
	var t = this;
5138
	var bindType;
5139
	var change;
5140
	var firstCell;
5141
	var cell;
5142
 
5143
 
5144
	t.start = function(_change, ev, _bindType) {
5145
		change = _change;
5146
		firstCell = cell = null;
5147
		coordinateGrid.build();
5148
		mouse(ev);
5149
		bindType = _bindType || 'mousemove';
5150
		$(document).bind(bindType, mouse);
5151
	};
5152
 
5153
 
5154
	function mouse(ev) {
5155
		_fixUIEvent(ev); // see below
5156
		var newCell = coordinateGrid.cell(ev.pageX, ev.pageY);
5157
		if (!newCell != !cell || newCell && (newCell.row != cell.row || newCell.col != cell.col)) {
5158
			if (newCell) {
5159
				if (!firstCell) {
5160
					firstCell = newCell;
5161
				}
5162
				change(newCell, firstCell, newCell.row-firstCell.row, newCell.col-firstCell.col);
5163
			}else{
5164
				change(newCell, firstCell);
5165
			}
5166
			cell = newCell;
5167
		}
5168
	}
5169
 
5170
 
5171
	t.stop = function() {
5172
		$(document).unbind(bindType, mouse);
5173
		return cell;
5174
	};
5175
 
5176
 
5177
}
5178
 
5179
 
5180
 
5181
// this fix was only necessary for jQuery UI 1.8.16 (and jQuery 1.7 or 1.7.1)
5182
// upgrading to jQuery UI 1.8.17 (and using either jQuery 1.7 or 1.7.1) fixed the problem
5183
// but keep this in here for 1.8.16 users
5184
// and maybe remove it down the line
5185
 
5186
function _fixUIEvent(event) { // for issue 1168
5187
	if (event.pageX === undefined) {
5188
		event.pageX = event.originalEvent.pageX;
5189
		event.pageY = event.originalEvent.pageY;
5190
	}
5191
}
5192
function HorizontalPositionCache(getElement) {
5193
 
5194
	var t = this,
5195
		elements = {},
5196
		lefts = {},
5197
		rights = {};
5198
 
5199
	function e(i) {
5200
		return elements[i] = elements[i] || getElement(i);
5201
	}
5202
 
5203
	t.left = function(i) {
5204
		return lefts[i] = lefts[i] === undefined ? e(i).position().left : lefts[i];
5205
	};
5206
 
5207
	t.right = function(i) {
5208
		return rights[i] = rights[i] === undefined ? t.left(i) + e(i).width() : rights[i];
5209
	};
5210
 
5211
	t.clear = function() {
5212
		elements = {};
5213
		lefts = {};
5214
		rights = {};
5215
	};
5216
 
5217
}
5218
 
5219
})(jQuery);