source: trunk/prototype/plugins/fullcalendar/fullcalendar.js @ 5529

Revision 5529, 139.7 KB checked in by acoutinho, 12 years ago (diff)

Ticket #2434 - Correcoes de bugs e atualizicao do fullcalendar

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