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