source: trunk/prototype/app/plugins/fullcalendar/fullcalendar.js @ 5341

Revision 5341, 139.1 KB checked in by wmerlotto, 12 years ago (diff)

Ticket #2434 - Commit inicial do novo módulo de agenda do Expresso - expressoCalendar

Line 
1/**
2 * @preserve
3 * FullCalendar v1.5
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: Sat Mar 19 18:59:37 2011 -0700
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" };
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;
751                                                        var text = smartProperty(options.buttonText, buttonName);
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+$/)) { // a UNIX timestamp
1382                        return new Date(parseInt(s, 10) * 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[14]) {
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                var offset = Number(m[16]) * 60 + Number(m[17]);
1439                offset *= m[15] == '-' ? 1 : -1;
1440                date = new Date(+date + (offset * 60 * 1000));
1441        }
1442        return date;
1443}
1444
1445
1446function parseTime(s) { // returns minutes since start of day
1447        if (typeof s == 'number') { // an hour
1448                return s * 60;
1449        }
1450        if (typeof s == 'object') { // a Date object
1451                return s.getHours() * 60 + s.getMinutes();
1452        }
1453        var m = s.match(/(\d+)(?::(\d+))?\s*(\w+)?/);
1454        if (m) {
1455                var h = parseInt(m[1], 10);
1456                if (m[3]) {
1457                        h %= 12;
1458                        if (m[3].toLowerCase().charAt(0) == 'p') {
1459                                h += 12;
1460                        }
1461                }
1462                return h * 60 + (m[2] ? parseInt(m[2], 10) : 0);
1463        }
1464}
1465
1466
1467
1468/* Date Formatting
1469-----------------------------------------------------------------------------*/
1470// TODO: use same function formatDate(date, [date2], format, [options])
1471
1472
1473function formatDate(date, format, options) {
1474        return formatDates(date, null, format, options);
1475}
1476
1477
1478function formatDates(date1, date2, format, options) {
1479        options = options || defaults;
1480        var date = date1,
1481                otherDate = date2,
1482                i, len = format.length, c,
1483                i2, formatter,
1484                res = '';
1485        for (i=0; i<len; i++) {
1486                c = format.charAt(i);
1487                if (c == "'") {
1488                        for (i2=i+1; i2<len; i2++) {
1489                                if (format.charAt(i2) == "'") {
1490                                        if (date) {
1491                                                if (i2 == i+1) {
1492                                                        res += "'";
1493                                                }else{
1494                                                        res += format.substring(i+1, i2);
1495                                                }
1496                                                i = i2;
1497                                        }
1498                                        break;
1499                                }
1500                        }
1501                }
1502                else if (c == '(') {
1503                        for (i2=i+1; i2<len; i2++) {
1504                                if (format.charAt(i2) == ')') {
1505                                        var subres = formatDate(date, format.substring(i+1, i2), options);
1506                                        if (parseInt(subres.replace(/\D/, ''), 10)) {
1507                                                res += subres;
1508                                        }
1509                                        i = i2;
1510                                        break;
1511                                }
1512                        }
1513                }
1514                else if (c == '[') {
1515                        for (i2=i+1; i2<len; i2++) {
1516                                if (format.charAt(i2) == ']') {
1517                                        var subformat = format.substring(i+1, i2);
1518                                        var subres = formatDate(date, subformat, options);
1519                                        if (subres != formatDate(otherDate, subformat, options)) {
1520                                                res += subres;
1521                                        }
1522                                        i = i2;
1523                                        break;
1524                                }
1525                        }
1526                }
1527                else if (c == '{') {
1528                        date = date2;
1529                        otherDate = date1;
1530                }
1531                else if (c == '}') {
1532                        date = date1;
1533                        otherDate = date2;
1534                }
1535                else {
1536                        for (i2=len; i2>i; i2--) {
1537                                if (formatter = dateFormatters[format.substring(i, i2)]) {
1538                                        if (date) {
1539                                                res += formatter(date, options);
1540                                        }
1541                                        i = i2 - 1;
1542                                        break;
1543                                }
1544                        }
1545                        if (i2 == i) {
1546                                if (date) {
1547                                        res += c;
1548                                }
1549                        }
1550                }
1551        }
1552        return res;
1553};
1554
1555
1556var dateFormatters = {
1557        s       : function(d)   { return d.getSeconds() },
1558        ss      : function(d)   { return zeroPad(d.getSeconds()) },
1559        m       : function(d)   { return d.getMinutes() },
1560        mm      : function(d)   { return zeroPad(d.getMinutes()) },
1561        h       : function(d)   { return d.getHours() % 12 || 12 },
1562        hh      : function(d)   { return zeroPad(d.getHours() % 12 || 12) },
1563        H       : function(d)   { return d.getHours() },
1564        HH      : function(d)   { return zeroPad(d.getHours()) },
1565        d       : function(d)   { return d.getDate() },
1566        dd      : function(d)   { return zeroPad(d.getDate()) },
1567        ddd     : function(d,o) { return o.dayNamesShort[d.getDay()] },
1568        e   : function(d,o)     { return o.dayNamesShortest[d.getDay()] }, //add by Kebin ----> 5
1569        dddd: function(d,o)     { return o.dayNames[d.getDay()] },
1570        M       : function(d)   { return d.getMonth() + 1 },
1571        MM      : function(d)   { return zeroPad(d.getMonth() + 1) },
1572        MMM     : function(d,o) { return o.monthNamesShort[d.getMonth()] },
1573        MMMM: function(d,o)     { return o.monthNames[d.getMonth()] },
1574        yy      : function(d)   { return (d.getFullYear()+'').substring(2) },
1575        yyyy: function(d)       { return d.getFullYear() },
1576        t       : function(d)   { return d.getHours() < 12 ? 'a' : 'p' },
1577        tt      : function(d)   { return d.getHours() < 12 ? 'am' : 'pm' },
1578        T       : function(d)   { return d.getHours() < 12 ? 'A' : 'P' },
1579        TT      : function(d)   { return d.getHours() < 12 ? 'AM' : 'PM' },
1580        u       : function(d)   { return formatDate(d, "yyyy-MM-dd'T'HH:mm:ss'Z'") },
1581        S       : function(d)   {
1582                var date = d.getDate();
1583                if (date > 10 && date < 20) {
1584                        return 'th';
1585                }
1586                return ['st', 'nd', 'rd'][date%10-1] || 'th';
1587        }
1588};
1589
1590
1591
1592fc.applyAll = applyAll;
1593
1594
1595/* Event Date Math
1596-----------------------------------------------------------------------------*/
1597
1598
1599function exclEndDay(event) {
1600        if (event.end) {
1601                return _exclEndDay(event.end, event.allDay);
1602        }else{
1603                return addDays(cloneDate(event.start), 1);
1604        }
1605}
1606
1607
1608function _exclEndDay(end, allDay) {
1609        end = cloneDate(end);
1610        return allDay || end.getHours() || end.getMinutes() ? addDays(end, 1) : clearTime(end);
1611}
1612
1613
1614function segCmp(a, b) {
1615        return (b.msLength - a.msLength) * 100 + (a.event.start - b.event.start);
1616}
1617
1618
1619function segsCollide(seg1, seg2) {
1620        return seg1.end > seg2.start && seg1.start < seg2.end;
1621}
1622
1623
1624
1625/* Event Sorting
1626-----------------------------------------------------------------------------*/
1627
1628
1629// event rendering utilities
1630function sliceSegs(events, visEventEnds, start, end) {
1631        var segs = [],
1632                i, len=events.length, event,
1633                eventStart, eventEnd,
1634                segStart, segEnd,
1635                isStart, isEnd;
1636        for (i=0; i<len; i++) {
1637                event = events[i];
1638                eventStart = event.start;
1639                eventEnd = visEventEnds[i];
1640                if (eventEnd > start && eventStart < end) {
1641                        if (eventStart < start) {
1642                                segStart = cloneDate(start);
1643                                isStart = false;
1644                        }else{
1645                                segStart = eventStart;
1646                                isStart = true;
1647                        }
1648                        if (eventEnd > end) {
1649                                segEnd = cloneDate(end);
1650                                isEnd = false;
1651                        }else{
1652                                segEnd = eventEnd;
1653                                isEnd = true;
1654                        }
1655                        segs.push({
1656                                event: event,
1657                                start: segStart,
1658                                end: segEnd,
1659                                isStart: isStart,
1660                                isEnd: isEnd,
1661                                msLength: segEnd - segStart
1662                        });
1663                }
1664        }
1665        return segs.sort(segCmp);
1666}
1667
1668
1669// event rendering calculation utilities
1670function stackSegs(segs) {
1671        var levels = [],
1672                i, len = segs.length, seg,
1673                j, collide, k;
1674        for (i=0; i<len; i++) {
1675                seg = segs[i];
1676                j = 0; // the level index where seg should belong
1677                while (true) {
1678                        collide = false;
1679                        if (levels[j]) {
1680                                for (k=0; k<levels[j].length; k++) {
1681                                        if (segsCollide(levels[j][k], seg)) {
1682                                                collide = true;
1683                                                break;
1684                                        }
1685                                }
1686                        }
1687                        if (collide) {
1688                                j++;
1689                        }else{
1690                                break;
1691                        }
1692                }
1693                if (levels[j]) {
1694                        levels[j].push(seg);
1695                }else{
1696                        levels[j] = [seg];
1697                }
1698        }
1699        return levels;
1700}
1701
1702
1703
1704/* Event Element Binding
1705-----------------------------------------------------------------------------*/
1706
1707
1708function lazySegBind(container, segs, bindHandlers) {
1709        container.unbind('mouseover').mouseover(function(ev) {
1710                var parent=ev.target, e,
1711                        i, seg;
1712                while (parent != this) {
1713                        e = parent;
1714                        parent = parent.parentNode;
1715                }
1716                if ((i = e._fci) !== undefined) {
1717                        e._fci = undefined;
1718                        seg = segs[i];
1719                        bindHandlers(seg.event, seg.element, seg);
1720                        $(ev.target).trigger(ev);
1721                }
1722                ev.stopPropagation();
1723        });
1724}
1725
1726
1727
1728/* Element Dimensions
1729-----------------------------------------------------------------------------*/
1730
1731
1732function setOuterWidth(element, width, includeMargins) {
1733        for (var i=0, e; i<element.length; i++) {
1734                e = $(element[i]);
1735                e.width(Math.max(0, width - hsides(e, includeMargins)));
1736        }
1737}
1738
1739
1740function setOuterHeight(element, height, includeMargins) {
1741        for (var i=0, e; i<element.length; i++) {
1742                e = $(element[i]);
1743                e.height(Math.max(0, height - vsides(e, includeMargins)));
1744        }
1745}
1746
1747
1748// TODO: curCSS has been deprecated (jQuery 1.4.3 - 10/16/2010)
1749
1750
1751function hsides(element, includeMargins) {
1752        return hpadding(element) + hborders(element) + (includeMargins ? hmargins(element) : 0);
1753}
1754
1755
1756function hpadding(element) {
1757        return (parseFloat($.curCSS(element[0], 'paddingLeft', true)) || 0) +
1758               (parseFloat($.curCSS(element[0], 'paddingRight', true)) || 0);
1759}
1760
1761
1762function hmargins(element) {
1763        return (parseFloat($.curCSS(element[0], 'marginLeft', true)) || 0) +
1764               (parseFloat($.curCSS(element[0], 'marginRight', true)) || 0);
1765}
1766
1767
1768function hborders(element) {
1769        return (parseFloat($.curCSS(element[0], 'borderLeftWidth', true)) || 0) +
1770               (parseFloat($.curCSS(element[0], 'borderRightWidth', true)) || 0);
1771}
1772
1773
1774function vsides(element, includeMargins) {
1775        return vpadding(element) +  vborders(element) + (includeMargins ? vmargins(element) : 0);
1776}
1777
1778
1779function vpadding(element) {
1780        return (parseFloat($.curCSS(element[0], 'paddingTop', true)) || 0) +
1781               (parseFloat($.curCSS(element[0], 'paddingBottom', true)) || 0);
1782}
1783
1784
1785function vmargins(element) {
1786        return (parseFloat($.curCSS(element[0], 'marginTop', true)) || 0) +
1787               (parseFloat($.curCSS(element[0], 'marginBottom', true)) || 0);
1788}
1789
1790
1791function vborders(element) {
1792        return (parseFloat($.curCSS(element[0], 'borderTopWidth', true)) || 0) +
1793               (parseFloat($.curCSS(element[0], 'borderBottomWidth', true)) || 0);
1794}
1795
1796
1797function setMinHeight(element, height) {
1798        height = (typeof height == 'number' ? height + 'px' : height);
1799        element.each(function(i, _element) {
1800                _element.style.cssText += ';min-height:' + height + ';_height:' + height;
1801                // why can't we just use .css() ? i forget
1802        });
1803}
1804
1805
1806
1807/* Misc Utils
1808-----------------------------------------------------------------------------*/
1809
1810
1811//TODO: arraySlice
1812//TODO: isFunction, grep ?
1813
1814
1815function noop() { }
1816
1817
1818function cmp(a, b) {
1819        return a - b;
1820}
1821
1822
1823function arrayMax(a) {
1824        return Math.max.apply(Math, a);
1825}
1826
1827
1828function zeroPad(n) {
1829        return (n < 10 ? '0' : '') + n;
1830}
1831
1832
1833function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object
1834        if (obj[name] !== undefined) {
1835                return obj[name];
1836        }
1837        var parts = name.split(/(?=[A-Z])/),
1838                i=parts.length-1, res;
1839        for (; i>=0; i--) {
1840                res = obj[parts[i].toLowerCase()];
1841                if (res !== undefined) {
1842                        return res;
1843                }
1844        }
1845        return obj[''];
1846}
1847
1848
1849function htmlEscape(s) {
1850        return s.replace(/&/g, '&amp;')
1851                .replace(/</g, '&lt;')
1852                .replace(/>/g, '&gt;')
1853                .replace(/'/g, '&#039;')
1854                .replace(/"/g, '&quot;')
1855                .replace(/\n/g, '<br />');
1856}
1857
1858
1859function cssKey(_element) {
1860        return _element.id + '/' + _element.className + '/' + _element.style.cssText.replace(/(^|;)\s*(top|left|width|height)\s*:[^;]*/ig, '');
1861}
1862
1863
1864function disableTextSelection(element) {
1865        element
1866                .attr('unselectable', 'on')
1867                .css('MozUserSelect', 'none')
1868                .bind('selectstart.ui', function() { return false; });
1869}
1870
1871
1872/*
1873function enableTextSelection(element) {
1874        element
1875                .attr('unselectable', 'off')
1876                .css('MozUserSelect', '')
1877                .unbind('selectstart.ui');
1878}
1879*/
1880
1881
1882function markFirstLast(e) {
1883        e.children()
1884                .removeClass('fc-first fc-last')
1885                .filter(':first-child')
1886                        .addClass('fc-first')
1887                .end()
1888                .filter(':last-child')
1889                        .addClass('fc-last');
1890}
1891
1892
1893function setDayID(cell, date) {
1894        cell.each(function(i, _cell) {
1895                _cell.className = _cell.className.replace(/^fc-\w*/, 'fc-' + dayIDs[date.getDay()]);
1896                // TODO: make a way that doesn't rely on order of classes
1897        });
1898}
1899
1900
1901function getSkinCss(event, opt) {
1902        var source = event.source || {};
1903        var eventColor = event.color;
1904        var sourceColor = source.color;
1905        var optionColor = opt('eventColor');
1906        var backgroundColor =
1907                event.backgroundColor ||
1908                eventColor ||
1909                source.backgroundColor ||
1910                sourceColor ||
1911                opt('eventBackgroundColor') ||
1912                optionColor;
1913        var borderColor =
1914                event.borderColor ||
1915                eventColor ||
1916                source.borderColor ||
1917                sourceColor ||
1918                opt('eventBorderColor') ||
1919                optionColor;
1920        var textColor =
1921                event.textColor ||
1922                source.textColor ||
1923                opt('eventTextColor');
1924        var statements = [];
1925        if (backgroundColor) {
1926                statements.push('background-color:' + backgroundColor);
1927        }
1928        if (borderColor) {
1929                statements.push('border-color:' + borderColor);
1930        }
1931        if (textColor) {
1932                statements.push('color:' + textColor);
1933        }
1934        return statements.join(';');
1935}
1936
1937
1938function applyAll(functions, thisObj, args) {
1939        if ($.isFunction(functions)) {
1940                functions = [ functions ];
1941        }
1942        if (functions) {
1943                var i;
1944                var ret;
1945                for (i=0; i<functions.length; i++) {
1946                        ret = functions[i].apply(thisObj, args) || ret;
1947                }
1948                return ret;
1949        }
1950}
1951
1952
1953function firstDefined() {
1954        for (var i=0; i<arguments.length; i++) {
1955                if (arguments[i] !== undefined) {
1956                        return arguments[i];
1957                }
1958        }
1959}
1960
1961//Year View Start ----------------------------------------------------------------------------------
1962//add by kebin --> 6
1963fcViews.year = YearView;
1964
1965function YearView(element, calendar) {
1966        var t = this;
1967        // exports
1968        t.render = render;
1969       
1970        // imports
1971        BasicYearView.call(t, element, calendar, 'year');
1972        var opt = t.opt;
1973        var renderYear = t.renderYear;
1974        var formatDate = calendar.formatDate;
1975       
1976        function render(date, delta) {
1977                if (delta) {
1978                        t.curYear = addYears(date, delta);
1979                }
1980                var start = cloneDate(date, true);             
1981                //start.setDate(1);
1982                start.setFullYear(start.getFullYear(),0,1);
1983                var end = cloneDate(date);
1984                end.setFullYear(end.getFullYear(), 11,31);
1985               
1986                var visStart = cloneDate(start); //set startDay
1987                var visEnd = cloneDate(end);
1988                var nwe = opt('weekends') ? 0 : 1;
1989                colAndRow = '4x3'; //'2x6', '3x4', '4x3' 3 types
1990                t.title = formatDate(start, opt('titleFormat'));
1991                t.start = start;
1992                t.end = end;
1993                t.visStart = visStart;
1994                t.visEnd = visEnd;
1995                renderYear(colAndRow, 6, nwe ? 5 : 7, true);
1996        }
1997}
1998
1999function BasicYearView(element, calendar, viewName) {
2000        var t = this;
2001       
2002        // exports
2003        t.renderYear = renderYear;
2004        t.setHeight = setHeight;
2005        t.setWidth = setWidth;
2006        t.renderDayOverlay = renderDayOverlay;
2007        t.defaultSelectionEnd = defaultSelectionEnd;
2008        t.renderSelection = renderSelection;
2009        t.clearSelection = clearSelection;
2010        t.reportDayClick = reportDayClick; // for selection (kinda hacky)
2011        t.dragStart = dragStart;
2012        t.dragStop = dragStop;
2013        t.defaultEventEnd = defaultEventEnd;
2014        t.getHoverListener = function() { return hoverListener };
2015        t.colContentLeft = colContentLeft;
2016        t.colContentRight = colContentRight;
2017        t.dayOfWeekCol = dayOfWeekCol;
2018        t.dateCell = dateCell;
2019        t.cellDate = cellDate;
2020        t.cellIsAllDay = function() { return true };
2021        t.allDayRow = allDayRow;
2022        t.allDayBounds = allDayBounds;
2023        t.getRowCnt = function() {return rowCnt };
2024        t.getColCnt = function() { return colCnt };
2025        t.getColWidth = function() { return colWidth };
2026        t.getBodyRows = function(){return bodyRows};
2027        t.getDaySegmentContainer = function() { return daySegmentContainer };
2028       
2029       
2030        // imports
2031        View.call(t, element, calendar, viewName);
2032        OverlayManager.call(t);
2033        SelectionManager.call(t);
2034        BasicYearEventRenderer.call(t);
2035        var opt = t.opt;
2036        var trigger = t.trigger;
2037        var clearEvents = t.clearEvents;
2038        var renderOverlay = t.renderOverlay;
2039        var clearOverlays = t.clearOverlays;
2040        var daySelectionMousedown = t.daySelectionMousedown;
2041        var formatDate = calendar.formatDate;
2042       
2043       
2044        // locals
2045        var table;
2046        //var head;
2047        //var headCells;
2048        var body;
2049        var bodyRows;
2050       
2051        var mainBody;
2052        var subTables;
2053       
2054        var bodyCells;
2055        //var bodyFirstCells;
2056        //var bodyCellTopInners;
2057        var daySegmentContainer;
2058       
2059        var viewWidth;
2060        var viewHeight;
2061        var colWidth;
2062       
2063        var rowCnt, colCnt;
2064        var coordinateGrid;
2065        var hoverListener;
2066        var colContentPositions;
2067       
2068        var rtl, dis, dit;
2069        var firstDay;
2070        var nwe;
2071        var tm;
2072        var colFormat;
2073       
2074       
2075       
2076        /* Rendering
2077        ------------------------------------------------------------*/
2078       
2079       
2080        disableTextSelection(element.addClass('fc-grid'));
2081       
2082       
2083        function renderYear(maxr, r, c, showNumbers) {
2084                rowCnt = r;
2085                colCnt = c;
2086                updateOptions();
2087                var firstTime = !table;
2088                if (firstTime) {
2089                        buildSkeleton(maxr, showNumbers);
2090                }else{
2091                        clearEvents();
2092                }
2093                        updateCells(firstTime);
2094        }
2095       
2096       
2097       
2098        function updateOptions() {
2099                rtl = opt('isRTL');
2100                if (rtl) {
2101                        dis = -1;
2102                        dit = colCnt - 1;
2103                }else{
2104                        dis = 1;
2105                        dit = 0;
2106                }
2107                firstDay = opt('firstDay');
2108                nwe = opt('weekends') ? 0 : 1;
2109                tm = opt('theme') ? 'ui' : 'fc';
2110                colFormat = opt('columnFormat');
2111        }
2112       
2113       
2114       
2115        function buildSkeleton(maxRowCnt, showNumbers) {
2116                var s;
2117                var headerClass = tm + "-widget-header";
2118                var contentClass = tm + "-widget-content";
2119                var i, j;
2120                var dayStr;
2121                var di = cloneDate(t.start);
2122               
2123                s ="<table class='fc-border-separate fc-year-main-table' style='width:100%'><tr >";
2124                for(var mi=0; mi<12; mi++){
2125                        di.setFullYear(di.getFullYear(),mi,1);
2126                        di.setFullYear(di.getFullYear(),mi,1-di.getDay());
2127                        if(mi%4==0 && mi > 0) s+="</tr><tr>";
2128                       
2129                        s +="<td class='fc-year-monthly-td'>";
2130                        s+="<table class='fc-border-separate' style='width:100%' cellspacing='0'>"+
2131                                "<thead>"+
2132                                "<tr><th colspan='7' class='fc-year-monthly-header' >"+opt('monthNames', mi)+"</th></tr>"+ // !k
2133                                "<tr>";
2134                for (i=0; i<colCnt; i++) {
2135                        s +="<th class='fc-year-month-weekly-head'>"+ opt('dayNamesShortest', i) +"</th>"; // need fc- for setDayID
2136                }
2137                s +="</tr>" +
2138                        "</thead>" +
2139                        "<tbody>";
2140                for (i=0; i<6; i++) {
2141                        s +="<tr class='fc-week" + i + "'>";
2142                        for (j=0; j<colCnt; j++) {
2143                                if(di.getMonth()== mi){
2144                                        dayStr=formatDate(di, '-yyyy-MM-dd');
2145                                }else{
2146                                        dayStr="";
2147                                }
2148                                s +="<td class='fc- " + contentClass + " fc-day" + dayStr + "'>" + // need fc- for setDayID
2149                                        "<div>" +
2150                                        (showNumbers ?
2151                                                "<div class='fc-day-number'/>" :
2152                                                ''
2153                                                ) +
2154                                        "<div class='fc-day-content'>" +
2155                                        "</div>" +
2156                                        "</div>" +
2157                                        "</td>";
2158                                addDays(di, 1);
2159                        }
2160                        s +="</tr>";
2161                       
2162                }
2163                s +="</tbody>" +
2164                        "</table>";
2165                s+="</td>";
2166                }
2167                s+="</tr></table>";
2168                table = $(s).appendTo(element);
2169                //head = table.find('thead');
2170                //headCells = head.find('th');
2171                //mainBody = table.find('tbody table');
2172                subTables = table.find('table');
2173                //subTables.each( function(x, _sub){
2174                //   });
2175               
2176               
2177                bodyRows = table.find('table tr td');
2178                //bodyCells = body.find('td');
2179                //bodyFirstCells = bodyCells.filter(':first-child');
2180                //bodyCellTopInners = bodyRows.eq(0).find('div.fc-day-content div');
2181               
2182                //markFirstLast(head.add(head.find('tr'))); // marks first+last tr/th's
2183                //markFirstLast(bodyRows); // marks first+last td's
2184                //bodyRows.eq(0).addClass('fc-first'); // fc-last is done in updateCells
2185               
2186                //dayBind(bodyCells);
2187                daySegmentContainer =$("<div style='position:absolute;z-index:8;top:0;left:0'/>").appendTo(element);
2188        }
2189       
2190       
2191       
2192        function updateCells(firstTime) {
2193                var startYear = t.start.getFullYear();
2194                var today = clearTime(new Date());
2195                var cell;
2196                var date;
2197                var row;
2198                subTables.each(function(i, _sub){
2199                var d = cloneDate(t.start);
2200               
2201                if ( !t.curYear ) t.curYear = t.start;
2202                var d = cloneDate(t.curYear);
2203               
2204                d.setFullYear(d.getFullYear(),i,1);
2205                d.setFullYear(d.getFullYear(),i,1-d.getDay());
2206                $(_sub).find('td').each(function(ii, _cell) {
2207                       
2208                        var dayStr;
2209                        cell = $(_cell);
2210                        if (d.getMonth() == i) {
2211                                cell.removeClass('fc-other-month');
2212                                dayStr=formatDate(d, '-yyyy-MM-dd');
2213                        }else{
2214                                cell.addClass('fc-other-month');
2215                                dayStr="";
2216                        }
2217                        if (+d == +today && d.getMonth() == i) {
2218                                cell.addClass(tm + '-state-highlight fc-today');
2219                        }else{
2220                                cell.removeClass(tm + '-state-highlight fc-today');
2221                        }
2222                        var $div = cell.find('div.fc-day-number');
2223                        $div.text(d.getDate());
2224                        $div.parent().parent().attr('class', "fc-widget-content fc-day" + dayStr);
2225                        addDays(d, 1);
2226                });
2227                });
2228                bodyRows.filter('.fc-year-have-event').removeClass('fc-year-have-event');
2229        }
2230       
2231       
2232       
2233        function setHeight(height) {
2234                /*
2235                viewHeight = height;
2236               
2237                var bodyHeight = viewHeight - head.height();
2238                var rowHeight;
2239                var rowHeightLast;
2240                var cell;
2241                       
2242                if (opt('weekMode') == 'variable') {
2243                        rowHeight = rowHeightLast = Math.floor(bodyHeight / (rowCnt==1 ? 2 : 6));
2244                }else{
2245                        rowHeight = Math.floor(bodyHeight / rowCnt);
2246                        rowHeightLast = bodyHeight - rowHeight * (rowCnt-1);
2247                }
2248               
2249                bodyFirstCells.each(function(i, _cell) {
2250                        if (i < rowCnt) {
2251                                cell = $(_cell);
2252                                setMinHeight(
2253                                        cell.find('> div'),
2254                                        (i==rowCnt-1 ? rowHeightLast : rowHeight) - vsides(cell)
2255                                );
2256                        }
2257                });
2258                */
2259        }
2260       
2261       
2262        function setWidth(width) {
2263                /*
2264                viewWidth = width;
2265                colContentPositions.clear();
2266                colWidth = Math.floor(viewWidth / colCnt);
2267                setOuterWidth(headCells.slice(0, -1), colWidth);
2268                */
2269        }
2270       
2271       
2272       
2273        /* Day clicking and binding
2274        -----------------------------------------------------------*/
2275       
2276       
2277        function dayBind(days) {
2278                days.click(dayClick).mousedown(daySelectionMousedown);
2279        }
2280       
2281       
2282        function dayClick(ev) {
2283                if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick
2284                        var index = parseInt(this.className.match(/fc\-day(\d+)/)[1]); // TODO: maybe use .data
2285                        var date = indexDate(index);
2286                        trigger('dayClick', this, date, true, ev);
2287                }
2288        }
2289       
2290       
2291       
2292        /* Semi-transparent Overlay Helpers
2293        ------------------------------------------------------*/
2294       
2295       
2296        function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive
2297                if (refreshCoordinateGrid) {
2298                        coordinateGrid.build();
2299                }
2300                var rowStart = cloneDate(t.visStart);
2301                var rowEnd = addDays(cloneDate(rowStart), colCnt);
2302                for (var i=0; i<rowCnt; i++) {
2303                        var stretchStart = new Date(Math.max(rowStart, overlayStart));
2304                        var stretchEnd = new Date(Math.min(rowEnd, overlayEnd));
2305                        if (stretchStart < stretchEnd) {
2306                                var colStart, colEnd;
2307                                if (rtl) {
2308                                        colStart = dayDiff(stretchEnd, rowStart)*dis+dit+1;
2309                                        colEnd = dayDiff(stretchStart, rowStart)*dis+dit+1;
2310                                }else{
2311                                        colStart = dayDiff(stretchStart, rowStart);
2312                                        colEnd = dayDiff(stretchEnd, rowStart);
2313                                }
2314                                dayBind(
2315                                        renderCellOverlay(i, colStart, i, colEnd-1)
2316                                );
2317                        }
2318                        addDays(rowStart, 7);
2319                        addDays(rowEnd, 7);
2320                }
2321        }
2322       
2323       
2324        function renderCellOverlay(row0, col0, row1, col1) { // row1,col1 is inclusive
2325                var rect = coordinateGrid.rect(row0, col0, row1, col1, element);
2326                return renderOverlay(rect, element);
2327        }
2328       
2329       
2330        /* Selection
2331        -----------------------------------------------------------------------*/
2332       
2333       
2334        function defaultSelectionEnd(startDate, allDay) {
2335                return cloneDate(startDate);
2336        }
2337       
2338       
2339        function renderSelection(startDate, endDate, allDay) {
2340                renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true); // rebuild every time???
2341        }
2342       
2343       
2344        function clearSelection() {
2345                clearOverlays();
2346        }
2347       
2348       
2349        function reportDayClick(date, allDay, ev) {
2350                var cell = dateCell(date);
2351                var _element = bodyCells[cell.row*colCnt + cell.col];
2352                trigger('dayClick', _element, date, allDay, ev);
2353        }
2354       
2355       
2356       
2357        /* External Dragging
2358        -----------------------------------------------------------------------*/
2359       
2360       
2361        function dragStart(_dragElement, ev, ui) {
2362                hoverListener.start(function(cell) {
2363                        clearOverlays();
2364                        if (cell) {
2365                                renderCellOverlay(cell.row, cell.col, cell.row, cell.col);
2366                        }
2367                }, ev);
2368        }
2369       
2370       
2371        function dragStop(_dragElement, ev, ui) {
2372                var cell = hoverListener.stop();
2373                clearOverlays();
2374                if (cell) {
2375                        var d = cellDate(cell);
2376                        trigger('drop', _dragElement, d, true, ev, ui);
2377                }
2378        }
2379       
2380       
2381       
2382        /* Utilities
2383        --------------------------------------------------------*/
2384       
2385       
2386        function defaultEventEnd(event) {
2387                return cloneDate(event.start);
2388        }
2389       
2390       
2391        coordinateGrid = new CoordinateGrid(function(rows, cols) {
2392                var e, n, p;
2393                headCells.each(function(i, _e) {
2394                        e = $(_e);
2395                        n = e.offset().left;
2396                        if (i) {
2397                                p[1] = n;
2398                        }
2399                        p = [n];
2400                        cols[i] = p;
2401                });
2402                p[1] = n + e.outerWidth();
2403                bodyRows.each(function(i, _e) {
2404                        if (i < rowCnt) {
2405                                e = $(_e);
2406                                n = e.offset().top;
2407                                if (i) {
2408                                        p[1] = n;
2409                                }
2410                                p = [n];
2411                                rows[i] = p;
2412                        }
2413                });
2414                p[1] = n + e.outerHeight();
2415        });
2416       
2417       
2418        hoverListener = new HoverListener(coordinateGrid);
2419       
2420       
2421        colContentPositions = new HorizontalPositionCache(function(col) {
2422                return bodyCellTopInners.eq(col);
2423        });
2424       
2425       
2426        function colContentLeft(col) {
2427                return colContentPositions.left(col);
2428        }
2429       
2430       
2431        function colContentRight(col) {
2432                return colContentPositions.right(col);
2433        }
2434       
2435       
2436       
2437       
2438        function dateCell(date) {
2439                return {
2440                        row: Math.floor(dayDiff(date, t.visStart) / 7),
2441                        col: dayOfWeekCol(date.getDay())
2442                };
2443        }
2444       
2445       
2446        function cellDate(cell) {
2447                return _cellDate(cell.row, cell.col);
2448        }
2449       
2450       
2451        function _cellDate(row, col) {
2452                return addDays(cloneDate(t.visStart), row*7 + col*dis+dit);
2453                // what about weekends in middle of week?
2454        }
2455       
2456       
2457        function indexDate(index) {
2458                return _cellDate(Math.floor(index/colCnt), index%colCnt);
2459        }
2460       
2461       
2462        function dayOfWeekCol(dayOfWeek) {
2463                return ((dayOfWeek - Math.max(firstDay, nwe) + colCnt) % colCnt) * dis + dit;
2464        }
2465       
2466       
2467       
2468       
2469        function allDayRow(i) {
2470                return bodyRows.eq(i);
2471        }
2472       
2473       
2474        function allDayBounds(i) {
2475                return {
2476                        left: 0,
2477                        right: viewWidth
2478                };
2479        }
2480       
2481       
2482}
2483
2484function BasicYearEventRenderer() {
2485        var t = this;
2486       
2487        // exports
2488        t.renderEvents = renderEvents;
2489        t.compileDaySegs = compileSegs; // for DayEventRenderer
2490        t.clearEvents = clearEvents;
2491        t.bindDaySeg = bindDaySeg;
2492       
2493       
2494        // imports
2495        DayEventRenderer.call(t);
2496        var opt = t.opt;
2497        var trigger = t.trigger;
2498        //var setOverflowHidden = t.setOverflowHidden;
2499        var isEventDraggable = t.isEventDraggable;
2500        var isEventResizable = t.isEventResizable;
2501        var reportEvents = t.reportEvents;
2502        var reportEventClear = t.reportEventClear;
2503        var eventElementHandlers = t.eventElementHandlers;
2504        var showEvents = t.showEvents;
2505        var hideEvents = t.hideEvents;
2506        var eventDrop = t.eventDrop;
2507        var getDaySegmentContainer = t.getDaySegmentContainer;
2508        var getHoverListener = t.getHoverListener;
2509        var renderDayOverlay = t.renderDayOverlay;
2510        var clearOverlays = t.clearOverlays;
2511        var getRowCnt = t.getRowCnt;
2512        var getColCnt = t.getColCnt;
2513        var getBodyRows = t.getBodyRows;
2514        //var renderDaySegs = t.renderDaySegs;
2515        var resizableDayEvent = t.resizableDayEvent;
2516       
2517       
2518       
2519        /* Rendering
2520        --------------------------------------------------------------------*/
2521       
2522       
2523        function renderEvents(events, modifiedEventId) {
2524                reportEvents(events);
2525                renderDaySegs(compileSegs(events), modifiedEventId);
2526        }
2527       
2528       
2529        function clearEvents() {
2530                reportEventClear();
2531                getDaySegmentContainer().empty();
2532        }
2533       
2534       
2535        function compileSegs(events) {
2536                var rowCnt = getRowCnt(),
2537                        colCnt = getColCnt(),
2538                        d1 = cloneDate(t.visStart),
2539                        d2 = cloneDate(t.visEnd),
2540                        visEventsEnds = $.map(events, exclEndDay),
2541                        i, row,
2542                        j, level,
2543                        k, seg,
2544                        segs=[];
2545                        row = stackSegs(sliceSegs(events, visEventsEnds, d1, d2));
2546                        for (j=0; j<row.length; j++) {
2547                                level = row[j];
2548                                for (k=0; k<level.length; k++) {
2549                                        seg = level[k];
2550                                        seg.row = i;
2551                                        seg.level = j; // not needed anymore
2552                                        segs.push(seg);
2553                                }
2554                        }
2555                return segs;
2556        }
2557       
2558        function sliceSegs(events, visEventEnds, start, end) {
2559        var segs = [],
2560                i, len=events.length, event,
2561                eventStart, eventEnd,
2562                segStart, segEnd,
2563                isStart, isEnd;
2564        for (i=0; i<len; i++) {
2565                event = events[i];
2566                eventStart = event.start;
2567                eventEnd = visEventEnds[i];
2568                if (eventEnd > start && eventStart < end) {
2569                        if (eventStart < start) {
2570                                segStart = cloneDate(start);
2571                                isStart = false;
2572                        }else{
2573                                segStart = eventStart;
2574                                isStart = true;
2575                        }
2576                        if (eventEnd > end) {
2577                                segEnd = cloneDate(end);
2578                                isEnd = false;
2579                        }else{
2580                                segEnd = eventEnd;
2581                                isEnd = true;
2582                        }
2583                        segs.push({
2584                                event: event,
2585                                start: segStart,
2586                                end: segEnd,
2587                                isStart: isStart,
2588                                isEnd: isEnd,
2589                                msLength: segEnd - segStart
2590                        });
2591                }
2592        }
2593        return segs.sort(segCmp);
2594}
2595
2596
2597// event rendering calculation utilities
2598function stackSegs(segs) {
2599        var levels = [],
2600                i, len = segs.length, seg,
2601                j, collide, k;
2602        for (i=0; i<len; i++) {
2603                seg = segs[i];
2604                j = 0; // the level index where seg should belong
2605                while (true) {
2606                        collide = false;
2607                        if (levels[j]) {
2608                                for (k=0; k<levels[j].length; k++) {
2609                                        if (segsCollide(levels[j][k], seg)) {
2610                                                collide = true;
2611                                                break;
2612                                        }
2613                                }
2614                        }
2615                        if (collide) {
2616                                j++;
2617                        }else{
2618                                break;
2619                        }
2620                }
2621                if (levels[j]) {
2622                        levels[j].push(seg);
2623                }else{
2624                        levels[j] = [seg];
2625                }
2626        }
2627        return levels;
2628}
2629       
2630        function bindDaySeg(event, eventElement, seg) {
2631                if (isEventDraggable(event)) {
2632                        draggableDayEvent(event, eventElement);
2633                }
2634                if (seg.isEnd && isEventResizable(event)) {
2635                        resizableDayEvent(event, eventElement, seg);
2636                }
2637                eventElementHandlers(event, eventElement);
2638                        // needs to be after, because resizableDayEvent might stopImmediatePropagation on click
2639        }
2640       
2641        function renderDaySegs(segs, modifiedEventId) {
2642                try{
2643                        var segCnt = segs.length;
2644                        var rowsTd = getBodyRows();
2645                        //alert(rowsTd.find('td.fc-day-1-1'));
2646                        //rowsTd.find('td.fc-day-1-1').addClass('fc-year-have-event');
2647                        /*
2648                        rowsTd.each(function(i,_td){
2649                                alert(_td.className.match('\\d{4}-\\d{2}-\\d{2}'));
2650                                $(_td).css();
2651                        });
2652                        */
2653                        for(var i=0;i<segs.length;i++){
2654                                var sd = cloneDate(segs[i].start);
2655                                while(sd.getTime() < segs[i].end.getTime()){
2656                                        rowsTd.filter('.fc-day-'+formatDate(sd, 'yyyy-MM-dd')).addClass('fc-year-have-event');
2657                                        addDays(sd,1);
2658                                }
2659                        }
2660                       
2661                       
2662                }catch(e){
2663                        alert("MyrenderDaySegs:"+e);
2664                }
2665        }
2666       
2667        /* Dragging
2668        ----------------------------------------------------------------------------*/
2669       
2670       
2671        function draggableDayEvent(event, eventElement) {
2672                var hoverListener = getHoverListener();
2673                var dayDelta;
2674                eventElement.draggable({
2675                        zIndex: 9,
2676                        delay: 50,
2677                        opacity: opt('dragOpacity'),
2678                        revertDuration: opt('dragRevertDuration'),
2679                        start: function(ev, ui) {
2680                                trigger('eventDragStart', eventElement, event, ev, ui);
2681                                hideEvents(event, eventElement);
2682                                hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
2683                                        eventElement.draggable('option', 'revert', !cell || !rowDelta && !colDelta);
2684                                        clearOverlays();
2685                                        if (cell) {
2686                                                //setOverflowHidden(true);
2687                                                dayDelta = rowDelta*7 + colDelta * (opt('isRTL') ? -1 : 1);
2688                                                renderDayOverlay(
2689                                                        addDays(cloneDate(event.start), dayDelta),
2690                                                        addDays(exclEndDay(event), dayDelta)
2691                                                );
2692                                        }else{
2693                                                //setOverflowHidden(false);
2694                                                dayDelta = 0;
2695                                        }
2696                                }, ev, 'drag');
2697                        },
2698                        stop: function(ev, ui) {
2699                                hoverListener.stop();
2700                                clearOverlays();
2701                                trigger('eventDragStop', eventElement, event, ev, ui);
2702                                if (dayDelta) {
2703                                        eventDrop(this, event, dayDelta, 0, event.allDay, ev, ui);
2704                                }else{
2705                                        eventElement.css('filter', ''); // clear IE opacity side-effects
2706                                        showEvents(event, eventElement);
2707                                }
2708                                //setOverflowHidden(false);
2709                        }
2710                });
2711        }
2712
2713
2714}
2715
2716// Year View END ----------------------------------------------------------------------------------
2717
2718fcViews.month = MonthView;
2719
2720function MonthView(element, calendar) {
2721        var t = this;
2722       
2723       
2724        // exports
2725        t.render = render;
2726       
2727       
2728        // imports
2729        BasicView.call(t, element, calendar, 'month');
2730        var opt = t.opt;
2731        var renderBasic = t.renderBasic;
2732        var formatDate = calendar.formatDate;
2733       
2734       
2735       
2736        function render(date, delta) {
2737                if (delta) {
2738                        addMonths(date, delta);
2739                        date.setDate(1);
2740                }
2741                var start = cloneDate(date, true);
2742                start.setDate(1);
2743                var end = addMonths(cloneDate(start), 1);
2744                var visStart = cloneDate(start);
2745                var visEnd = cloneDate(end);
2746                var firstDay = opt('firstDay');
2747                var nwe = opt('weekends') ? 0 : 1;
2748                if (nwe) {
2749                        skipWeekend(visStart);
2750                        skipWeekend(visEnd, -1, true);
2751                }
2752                addDays(visStart, -((visStart.getDay() - Math.max(firstDay, nwe) + 7) % 7));
2753                addDays(visEnd, (7 - visEnd.getDay() + Math.max(firstDay, nwe)) % 7);
2754                var rowCnt = Math.round((visEnd - visStart) / (DAY_MS * 7));
2755                if (opt('weekMode') == 'fixed') {
2756                        addDays(visEnd, (6 - rowCnt) * 7);
2757                        rowCnt = 6;
2758                }
2759                t.title = formatDate(start, opt('titleFormat'));
2760                t.start = start;
2761                t.end = end;
2762                t.visStart = visStart;
2763                t.visEnd = visEnd;
2764                renderBasic(6, rowCnt, nwe ? 5 : 7, true);
2765        }
2766       
2767       
2768}
2769
2770fcViews.basicWeek = BasicWeekView;
2771
2772function BasicWeekView(element, calendar) {
2773        var t = this;
2774       
2775       
2776        // exports
2777        t.render = render;
2778       
2779       
2780        // imports
2781        BasicView.call(t, element, calendar, 'basicWeek');
2782        var opt = t.opt;
2783        var renderBasic = t.renderBasic;
2784        var formatDates = calendar.formatDates;
2785       
2786       
2787       
2788        function render(date, delta) {
2789                if (delta) {
2790                        addDays(date, delta * 7);
2791                }
2792                var start = addDays(cloneDate(date), -((date.getDay() - opt('firstDay') + 7) % 7));
2793                var end = addDays(cloneDate(start), 7);
2794                var visStart = cloneDate(start);
2795                var visEnd = cloneDate(end);
2796                var weekends = opt('weekends');
2797                if (!weekends) {
2798                        skipWeekend(visStart);
2799                        skipWeekend(visEnd, -1, true);
2800                }
2801                t.title = formatDates(
2802                        visStart,
2803                        addDays(cloneDate(visEnd), -1),
2804                        opt('titleFormat')
2805                );
2806                t.start = start;
2807                t.end = end;
2808                t.visStart = visStart;
2809                t.visEnd = visEnd;
2810                renderBasic(1, 1, weekends ? 7 : 5, false);
2811        }
2812       
2813       
2814}
2815
2816fcViews.basicDay = BasicDayView;
2817
2818//TODO: when calendar's date starts out on a weekend, shouldn't happen
2819
2820
2821function BasicDayView(element, calendar) {
2822        var t = this;
2823       
2824       
2825        // exports
2826        t.render = render;
2827       
2828       
2829        // imports
2830        BasicView.call(t, element, calendar, 'basicDay');
2831        var opt = t.opt;
2832        var renderBasic = t.renderBasic;
2833        var formatDate = calendar.formatDate;
2834       
2835       
2836       
2837        function render(date, delta) {
2838                if (delta) {
2839                        addDays(date, delta);
2840                        if (!opt('weekends')) {
2841                                skipWeekend(date, delta < 0 ? -1 : 1);
2842                        }
2843                }
2844                t.title = formatDate(date, opt('titleFormat'));
2845                t.start = t.visStart = cloneDate(date, true);
2846                t.end = t.visEnd = addDays(cloneDate(t.start), 1);
2847                renderBasic(1, 1, 1, false);
2848        }
2849       
2850       
2851}
2852
2853setDefaults({
2854        weekMode: 'fixed'
2855});
2856
2857
2858function BasicView(element, calendar, viewName) {
2859        var t = this;
2860       
2861       
2862        // exports
2863        t.renderBasic = renderBasic;
2864        t.setHeight = setHeight;
2865        t.setWidth = setWidth;
2866        t.renderDayOverlay = renderDayOverlay;
2867        t.defaultSelectionEnd = defaultSelectionEnd;
2868        t.renderSelection = renderSelection;
2869        t.clearSelection = clearSelection;
2870        t.reportDayClick = reportDayClick; // for selection (kinda hacky)
2871        t.dragStart = dragStart;
2872        t.dragStop = dragStop;
2873        t.defaultEventEnd = defaultEventEnd;
2874        t.getHoverListener = function() { return hoverListener };
2875        t.colContentLeft = colContentLeft;
2876        t.colContentRight = colContentRight;
2877        t.dayOfWeekCol = dayOfWeekCol;
2878        t.dateCell = dateCell;
2879        t.cellDate = cellDate;
2880        t.cellIsAllDay = function() { return true };
2881        t.allDayRow = allDayRow;
2882        t.allDayBounds = allDayBounds;
2883        t.getRowCnt = function() { return rowCnt };
2884        t.getColCnt = function() { return colCnt };
2885        t.getColWidth = function() { return colWidth };
2886        t.getDaySegmentContainer = function() { return daySegmentContainer };
2887       
2888       
2889        // imports
2890        View.call(t, element, calendar, viewName);
2891        OverlayManager.call(t);
2892        SelectionManager.call(t);
2893        BasicEventRenderer.call(t);
2894        var opt = t.opt;
2895        var trigger = t.trigger;
2896        var clearEvents = t.clearEvents;
2897        var renderOverlay = t.renderOverlay;
2898        var clearOverlays = t.clearOverlays;
2899        var daySelectionMousedown = t.daySelectionMousedown;
2900        var formatDate = calendar.formatDate;
2901       
2902       
2903        // locals
2904       
2905        var head;
2906        var headCells;
2907        var body;
2908        var bodyRows;
2909        var bodyCells;
2910        var bodyFirstCells;
2911        var bodyCellTopInners;
2912        var daySegmentContainer;
2913       
2914        var viewWidth;
2915        var viewHeight;
2916        var colWidth;
2917       
2918        var rowCnt, colCnt;
2919        var coordinateGrid;
2920        var hoverListener;
2921        var colContentPositions;
2922       
2923        var rtl, dis, dit;
2924        var firstDay;
2925        var nwe;
2926        var tm;
2927        var colFormat;
2928       
2929       
2930       
2931        /* Rendering
2932        ------------------------------------------------------------*/
2933       
2934       
2935        disableTextSelection(element.addClass('fc-grid'));
2936       
2937       
2938        function renderBasic(maxr, r, c, showNumbers) {
2939                rowCnt = r;
2940                colCnt = c;
2941                updateOptions();
2942                var firstTime = !body;
2943                if (firstTime) {
2944                        buildSkeleton(maxr, showNumbers);
2945                }else{
2946                        clearEvents();
2947                }
2948                updateCells(firstTime);
2949        }
2950       
2951       
2952       
2953        function updateOptions() {
2954                rtl = opt('isRTL');
2955                if (rtl) {
2956                        dis = -1;
2957                        dit = colCnt - 1;
2958                }else{
2959                        dis = 1;
2960                        dit = 0;
2961                }
2962                firstDay = opt('firstDay');
2963                nwe = opt('weekends') ? 0 : 1;
2964                tm = opt('theme') ? 'ui' : 'fc';
2965                colFormat = opt('columnFormat');
2966        }
2967       
2968       
2969       
2970        function buildSkeleton(maxRowCnt, showNumbers) {
2971                var s;
2972                var headerClass = tm + "-widget-header";
2973                var contentClass = tm + "-widget-content";
2974                var i, j;
2975                var table;
2976               
2977                s =
2978                        "<table class='fc-border-separate' style='width:100%' cellspacing='0'>" +
2979                        "<thead>" +
2980                        "<tr>";
2981                for (i=0; i<colCnt; i++) {
2982                        s +=
2983                                "<th class='fc- " + headerClass + "'/>"; // need fc- for setDayID
2984                }
2985                s +=
2986                        "</tr>" +
2987                        "</thead>" +
2988                        "<tbody>";
2989                for (i=0; i<maxRowCnt; i++) {
2990                        s +=
2991                                "<tr class='fc-week" + i + "'>";
2992                        for (j=0; j<colCnt; j++) {
2993                                s +=
2994                                        "<td class='fc- " + contentClass + " fc-day" + (i*colCnt+j) + "'>" + // need fc- for setDayID
2995                                        "<div>" +
2996                                        (showNumbers ?
2997                                                "<div class='fc-day-number'/>" :
2998                                                ''
2999                                                ) +
3000                                        "<div class='fc-day-content'>" +
3001                                        "<div style='position:relative'>&nbsp;</div>" +
3002                                        "</div>" +
3003                                        "</div>" +
3004                                        "</td>";
3005                        }
3006                        s +=
3007                                "</tr>";
3008                }
3009                s +=
3010                        "</tbody>" +
3011                        "</table>";
3012                table = $(s).appendTo(element);
3013               
3014                head = table.find('thead');
3015                headCells = head.find('th');
3016                body = table.find('tbody');
3017                bodyRows = body.find('tr');
3018                bodyCells = body.find('td');
3019                bodyFirstCells = bodyCells.filter(':first-child');
3020                bodyCellTopInners = bodyRows.eq(0).find('div.fc-day-content div');
3021               
3022                markFirstLast(head.add(head.find('tr'))); // marks first+last tr/th's
3023                markFirstLast(bodyRows); // marks first+last td's
3024                bodyRows.eq(0).addClass('fc-first'); // fc-last is done in updateCells
3025               
3026                dayBind(bodyCells);
3027               
3028                daySegmentContainer =
3029                        $("<div style='position:absolute;z-index:8;top:0;left:0'/>")
3030                                .appendTo(element);
3031        }
3032       
3033       
3034       
3035        function updateCells(firstTime) {
3036                var dowDirty = firstTime || rowCnt == 1; // could the cells' day-of-weeks need updating?
3037                var month = t.start.getMonth();
3038                var today = clearTime(new Date());
3039                var cell;
3040                var date;
3041                var row;
3042       
3043                if (dowDirty) {
3044                        headCells.each(function(i, _cell) {
3045                                cell = $(_cell);
3046                                date = indexDate(i);
3047                                cell.html(formatDate(date, colFormat));
3048                                setDayID(cell, date);
3049                        });
3050                }
3051               
3052                bodyCells.each(function(i, _cell) {
3053                        cell = $(_cell);
3054                        date = indexDate(i);
3055                        if (date.getMonth() == month) {
3056                                cell.removeClass('fc-other-month');
3057                        }else{
3058                                cell.addClass('fc-other-month');
3059                        }
3060                        if (+date == +today) {
3061                                cell.addClass(tm + '-state-highlight fc-today');
3062                        }else{
3063                                cell.removeClass(tm + '-state-highlight fc-today');
3064                        }
3065                        cell.find('div.fc-day-number').text(date.getDate());
3066                        if (dowDirty) {
3067                                setDayID(cell, date);
3068                        }
3069                });
3070               
3071                bodyRows.each(function(i, _row) {
3072                        row = $(_row);
3073                        if (i < rowCnt) {
3074                                row.show();
3075                                if (i == rowCnt-1) {
3076                                        row.addClass('fc-last');
3077                                }else{
3078                                        row.removeClass('fc-last');
3079                                }
3080                        }else{
3081                                row.hide();
3082                        }
3083                });
3084        }
3085       
3086       
3087       
3088        function setHeight(height) {
3089                viewHeight = height;
3090               
3091                var bodyHeight = viewHeight - head.height();
3092                var rowHeight;
3093                var rowHeightLast;
3094                var cell;
3095                       
3096                if (opt('weekMode') == 'variable') {
3097                        rowHeight = rowHeightLast = Math.floor(bodyHeight / (rowCnt==1 ? 2 : 6));
3098                }else{
3099                        rowHeight = Math.floor(bodyHeight / rowCnt);
3100                        rowHeightLast = bodyHeight - rowHeight * (rowCnt-1);
3101                }
3102               
3103                bodyFirstCells.each(function(i, _cell) {
3104                        if (i < rowCnt) {
3105                                cell = $(_cell);
3106                                setMinHeight(
3107                                        cell.find('> div'),
3108                                        (i==rowCnt-1 ? rowHeightLast : rowHeight) - vsides(cell)
3109                                );
3110                        }
3111                });
3112               
3113        }
3114       
3115       
3116        function setWidth(width) {
3117                viewWidth = width;
3118                colContentPositions.clear();
3119                colWidth = Math.floor(viewWidth / colCnt);
3120                setOuterWidth(headCells.slice(0, -1), colWidth);
3121        }
3122       
3123       
3124       
3125        /* Day clicking and binding
3126        -----------------------------------------------------------*/
3127       
3128       
3129        function dayBind(days) {
3130                days.click(dayClick)
3131                        .mousedown(daySelectionMousedown);
3132        }
3133       
3134       
3135        function dayClick(ev) {
3136                if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick
3137                        var index = parseInt(this.className.match(/fc\-day(\d+)/)[1]); // TODO: maybe use .data
3138                        var date = indexDate(index);
3139                        trigger('dayClick', this, date, true, ev);
3140                }
3141        }
3142       
3143       
3144       
3145        /* Semi-transparent Overlay Helpers
3146        ------------------------------------------------------*/
3147       
3148       
3149        function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive
3150                if (refreshCoordinateGrid) {
3151                        coordinateGrid.build();
3152                }
3153                var rowStart = cloneDate(t.visStart);
3154                var rowEnd = addDays(cloneDate(rowStart), colCnt);
3155                for (var i=0; i<rowCnt; i++) {
3156                        var stretchStart = new Date(Math.max(rowStart, overlayStart));
3157                        var stretchEnd = new Date(Math.min(rowEnd, overlayEnd));
3158                        if (stretchStart < stretchEnd) {
3159                                var colStart, colEnd;
3160                                if (rtl) {
3161                                        colStart = dayDiff(stretchEnd, rowStart)*dis+dit+1;
3162                                        colEnd = dayDiff(stretchStart, rowStart)*dis+dit+1;
3163                                }else{
3164                                        colStart = dayDiff(stretchStart, rowStart);
3165                                        colEnd = dayDiff(stretchEnd, rowStart);
3166                                }
3167                                dayBind(
3168                                        renderCellOverlay(i, colStart, i, colEnd-1)
3169                                );
3170                        }
3171                        addDays(rowStart, 7);
3172                        addDays(rowEnd, 7);
3173                }
3174        }
3175       
3176       
3177        function renderCellOverlay(row0, col0, row1, col1) { // row1,col1 is inclusive
3178                var rect = coordinateGrid.rect(row0, col0, row1, col1, element);
3179                return renderOverlay(rect, element);
3180        }
3181       
3182       
3183       
3184        /* Selection
3185        -----------------------------------------------------------------------*/
3186       
3187       
3188        function defaultSelectionEnd(startDate, allDay) {
3189                return cloneDate(startDate);
3190        }
3191       
3192       
3193        function renderSelection(startDate, endDate, allDay) {
3194                renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true); // rebuild every time???
3195        }
3196       
3197       
3198        function clearSelection() {
3199                clearOverlays();
3200        }
3201       
3202       
3203        function reportDayClick(date, allDay, ev) {
3204                var cell = dateCell(date);
3205                var _element = bodyCells[cell.row*colCnt + cell.col];
3206                trigger('dayClick', _element, date, allDay, ev);
3207        }
3208       
3209       
3210       
3211        /* External Dragging
3212        -----------------------------------------------------------------------*/
3213       
3214       
3215        function dragStart(_dragElement, ev, ui) {
3216                hoverListener.start(function(cell) {
3217                        clearOverlays();
3218                        if (cell) {
3219                                renderCellOverlay(cell.row, cell.col, cell.row, cell.col);
3220                        }
3221                }, ev);
3222        }
3223       
3224       
3225        function dragStop(_dragElement, ev, ui) {
3226                var cell = hoverListener.stop();
3227                clearOverlays();
3228                if (cell) {
3229                        var d = cellDate(cell);
3230                        trigger('drop', _dragElement, d, true, ev, ui);
3231                }
3232        }
3233       
3234       
3235       
3236        /* Utilities
3237        --------------------------------------------------------*/
3238       
3239       
3240        function defaultEventEnd(event) {
3241                return cloneDate(event.start);
3242        }
3243       
3244       
3245        coordinateGrid = new CoordinateGrid(function(rows, cols) {
3246                var e, n, p;
3247                headCells.each(function(i, _e) {
3248                        e = $(_e);
3249                        n = e.offset().left;
3250                        if (i) {
3251                                p[1] = n;
3252                        }
3253                        p = [n];
3254                        cols[i] = p;
3255                });
3256                p[1] = n + e.outerWidth();
3257                bodyRows.each(function(i, _e) {
3258                        if (i < rowCnt) {
3259                                e = $(_e);
3260                                n = e.offset().top;
3261                                if (i) {
3262                                        p[1] = n;
3263                                }
3264                                p = [n];
3265                                rows[i] = p;
3266                        }
3267                });
3268                p[1] = n + e.outerHeight();
3269        });
3270       
3271       
3272        hoverListener = new HoverListener(coordinateGrid);
3273       
3274       
3275        colContentPositions = new HorizontalPositionCache(function(col) {
3276                return bodyCellTopInners.eq(col);
3277        });
3278       
3279       
3280        function colContentLeft(col) {
3281                return colContentPositions.left(col);
3282        }
3283       
3284       
3285        function colContentRight(col) {
3286                return colContentPositions.right(col);
3287        }
3288       
3289       
3290       
3291       
3292        function dateCell(date) {
3293                return {
3294                        row: Math.floor(dayDiff(date, t.visStart) / 7),
3295                        col: dayOfWeekCol(date.getDay())
3296                };
3297        }
3298       
3299       
3300        function cellDate(cell) {
3301                return _cellDate(cell.row, cell.col);
3302        }
3303       
3304       
3305        function _cellDate(row, col) {
3306                return addDays(cloneDate(t.visStart), row*7 + col*dis+dit);
3307                // what about weekends in middle of week?
3308        }
3309       
3310       
3311        function indexDate(index) {
3312                return _cellDate(Math.floor(index/colCnt), index%colCnt);
3313        }
3314       
3315       
3316        function dayOfWeekCol(dayOfWeek) {
3317                return ((dayOfWeek - Math.max(firstDay, nwe) + colCnt) % colCnt) * dis + dit;
3318        }
3319       
3320       
3321       
3322       
3323        function allDayRow(i) {
3324                return bodyRows.eq(i);
3325        }
3326       
3327       
3328        function allDayBounds(i) {
3329                return {
3330                        left: 0,
3331                        right: viewWidth
3332                };
3333        }
3334       
3335       
3336}
3337
3338function BasicEventRenderer() {
3339        var t = this;
3340       
3341       
3342        // exports
3343        t.renderEvents = renderEvents;
3344        t.compileDaySegs = compileSegs; // for DayEventRenderer
3345        t.clearEvents = clearEvents;
3346        t.bindDaySeg = bindDaySeg;
3347       
3348       
3349        // imports
3350        DayEventRenderer.call(t);
3351        var opt = t.opt;
3352        var trigger = t.trigger;
3353        //var setOverflowHidden = t.setOverflowHidden;
3354        var isEventDraggable = t.isEventDraggable;
3355        var isEventResizable = t.isEventResizable;
3356        var reportEvents = t.reportEvents;
3357        var reportEventClear = t.reportEventClear;
3358        var eventElementHandlers = t.eventElementHandlers;
3359        var showEvents = t.showEvents;
3360        var hideEvents = t.hideEvents;
3361        var eventDrop = t.eventDrop;
3362        var getDaySegmentContainer = t.getDaySegmentContainer;
3363        var getHoverListener = t.getHoverListener;
3364        var renderDayOverlay = t.renderDayOverlay;
3365        var clearOverlays = t.clearOverlays;
3366        var getRowCnt = t.getRowCnt;
3367        var getColCnt = t.getColCnt;
3368        var renderDaySegs = t.renderDaySegs;
3369        var resizableDayEvent = t.resizableDayEvent;
3370       
3371       
3372       
3373        /* Rendering
3374        --------------------------------------------------------------------*/
3375       
3376       
3377        function renderEvents(events, modifiedEventId) {
3378                reportEvents(events);
3379                renderDaySegs(compileSegs(events), modifiedEventId);
3380        }
3381       
3382       
3383        function clearEvents() {
3384                reportEventClear();
3385                getDaySegmentContainer().empty();
3386        }
3387       
3388       
3389        function compileSegs(events) {
3390                var rowCnt = getRowCnt(),
3391                        colCnt = getColCnt(),
3392                        d1 = cloneDate(t.visStart),
3393                        d2 = addDays(cloneDate(d1), colCnt),
3394                        visEventsEnds = $.map(events, exclEndDay),
3395                        i, row,
3396                        j, level,
3397                        k, seg,
3398                        segs=[];
3399                for (i=0; i<rowCnt; i++) {
3400                        row = stackSegs(sliceSegs(events, visEventsEnds, d1, d2));
3401                        for (j=0; j<row.length; j++) {
3402                                level = row[j];
3403                                for (k=0; k<level.length; k++) {
3404                                        seg = level[k];
3405                                        seg.row = i;
3406                                        seg.level = j; // not needed anymore
3407                                        segs.push(seg);
3408                                }
3409                        }
3410                        addDays(d1, 7);
3411                        addDays(d2, 7);
3412                }
3413                return segs;
3414        }
3415       
3416       
3417        function bindDaySeg(event, eventElement, seg) {
3418                if (isEventDraggable(event)) {
3419                        draggableDayEvent(event, eventElement);
3420                }
3421                if (seg.isEnd && isEventResizable(event)) {
3422                        resizableDayEvent(event, eventElement, seg);
3423                }
3424                eventElementHandlers(event, eventElement);
3425                        // needs to be after, because resizableDayEvent might stopImmediatePropagation on click
3426        }
3427       
3428       
3429       
3430        /* Dragging
3431        ----------------------------------------------------------------------------*/
3432       
3433       
3434        function draggableDayEvent(event, eventElement) {
3435                var hoverListener = getHoverListener();
3436                var dayDelta;
3437                eventElement.draggable({
3438                        zIndex: 9,
3439                        delay: 50,
3440                        opacity: opt('dragOpacity'),
3441                        revertDuration: opt('dragRevertDuration'),
3442                        start: function(ev, ui) {
3443                                trigger('eventDragStart', eventElement, event, ev, ui);
3444                                hideEvents(event, eventElement);
3445                                hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
3446                                        eventElement.draggable('option', 'revert', !cell || !rowDelta && !colDelta);
3447                                        clearOverlays();
3448                                        if (cell) {
3449                                                //setOverflowHidden(true);
3450                                                dayDelta = rowDelta*7 + colDelta * (opt('isRTL') ? -1 : 1);
3451                                                renderDayOverlay(
3452                                                        addDays(cloneDate(event.start), dayDelta),
3453                                                        addDays(exclEndDay(event), dayDelta)
3454                                                );
3455                                        }else{
3456                                                //setOverflowHidden(false);
3457                                                dayDelta = 0;
3458                                        }
3459                                }, ev, 'drag');
3460                        },
3461                        stop: function(ev, ui) {
3462                                hoverListener.stop();
3463                                clearOverlays();
3464                                trigger('eventDragStop', eventElement, event, ev, ui);
3465                                if (dayDelta) {
3466                                        eventDrop(this, event, dayDelta, 0, event.allDay, ev, ui);
3467                                }else{
3468                                        eventElement.css('filter', ''); // clear IE opacity side-effects
3469                                        showEvents(event, eventElement);
3470                                }
3471                                //setOverflowHidden(false);
3472                        }
3473                });
3474        }
3475
3476
3477}
3478
3479fcViews.agendaWeek = AgendaWeekView;
3480
3481function AgendaWeekView(element, calendar) {
3482        var t = this;
3483       
3484       
3485        // exports
3486        t.render = render;
3487       
3488       
3489        // imports
3490        AgendaView.call(t, element, calendar, 'agendaWeek');
3491        var opt = t.opt;
3492        var renderAgenda = t.renderAgenda;
3493        var formatDates = calendar.formatDates;
3494       
3495       
3496       
3497        function render(date, delta) {
3498                if (delta) {
3499                        addDays(date, delta * 7);
3500                }
3501                var start = addDays(cloneDate(date), -((date.getDay() - opt('firstDay') + 7) % 7));
3502                var end = addDays(cloneDate(start), 7);
3503                var visStart = cloneDate(start);
3504                var visEnd = cloneDate(end);
3505                var weekends = opt('weekends');
3506                if (!weekends) {
3507                        skipWeekend(visStart);
3508                        skipWeekend(visEnd, -1, true);
3509                }
3510                t.title = formatDates(
3511                        visStart,
3512                        addDays(cloneDate(visEnd), -1),
3513                        opt('titleFormat')
3514                );
3515                t.start = start;
3516                t.end = end;
3517                t.visStart = visStart;
3518                t.visEnd = visEnd;
3519                renderAgenda(weekends ? 7 : 5);
3520        }
3521       
3522
3523}
3524
3525fcViews.agendaDay = AgendaDayView;
3526
3527function AgendaDayView(element, calendar) {
3528        var t = this;
3529       
3530       
3531        // exports
3532        t.render = render;
3533       
3534       
3535        // imports
3536        AgendaView.call(t, element, calendar, 'agendaDay');
3537        var opt = t.opt;
3538        var renderAgenda = t.renderAgenda;
3539        var formatDate = calendar.formatDate;
3540       
3541       
3542       
3543        function render(date, delta) {
3544                if (delta) {
3545                        addDays(date, delta);
3546                        if (!opt('weekends')) {
3547                                skipWeekend(date, delta < 0 ? -1 : 1);
3548                        }
3549                }
3550                var start = cloneDate(date, true);
3551                var end = addDays(cloneDate(start), 1);
3552                t.title = formatDate(date, opt('titleFormat'));
3553                t.start = t.visStart = start;
3554                t.end = t.visEnd = end;
3555                renderAgenda(1);
3556        }
3557       
3558
3559}
3560
3561setDefaults({
3562        allDaySlot: true,
3563        allDayText: 'all-day',
3564        firstHour: 6,
3565        slotMinutes: 30,
3566        defaultEventMinutes: 120,
3567        axisFormat: 'h(:mm)tt',
3568        timeFormat: {
3569                agenda: 'h:mm{ - h:mm}'
3570        },
3571        dragOpacity: {
3572                agenda: .5
3573        },
3574        minTime: 0,
3575        maxTime: 24
3576});
3577
3578
3579// TODO: make it work in quirks mode (event corners, all-day height)
3580// TODO: test liquid width, especially in IE6
3581
3582
3583function AgendaView(element, calendar, viewName) {
3584        var t = this;
3585       
3586       
3587        // exports
3588        t.renderAgenda = renderAgenda;
3589        t.setWidth = setWidth;
3590        t.setHeight = setHeight;
3591        t.beforeHide = beforeHide;
3592        t.afterShow = afterShow;
3593        t.defaultEventEnd = defaultEventEnd;
3594        t.timePosition = timePosition;
3595        t.dayOfWeekCol = dayOfWeekCol;
3596        t.dateCell = dateCell;
3597        t.cellDate = cellDate;
3598        t.cellIsAllDay = cellIsAllDay;
3599        t.allDayRow = getAllDayRow;
3600        t.allDayBounds = allDayBounds;
3601        t.getHoverListener = function() { return hoverListener };
3602        t.colContentLeft = colContentLeft;
3603        t.colContentRight = colContentRight;
3604        t.getDaySegmentContainer = function() { return daySegmentContainer };
3605        t.getSlotSegmentContainer = function() { return slotSegmentContainer };
3606        t.getMinMinute = function() { return minMinute };
3607        t.getMaxMinute = function() { return maxMinute };
3608        t.getBodyContent = function() { return slotContent }; // !!??
3609        t.getRowCnt = function() { return 1 };
3610        t.getColCnt = function() { return colCnt };
3611        t.getColWidth = function() { return colWidth };
3612        t.getSlotHeight = function() { return slotHeight };
3613        t.defaultSelectionEnd = defaultSelectionEnd;
3614        t.renderDayOverlay = renderDayOverlay;
3615        t.renderSelection = renderSelection;
3616        t.clearSelection = clearSelection;
3617        t.reportDayClick = reportDayClick; // selection mousedown hack
3618        t.dragStart = dragStart;
3619        t.dragStop = dragStop;
3620       
3621       
3622        // imports
3623        View.call(t, element, calendar, viewName);
3624        OverlayManager.call(t);
3625        SelectionManager.call(t);
3626        AgendaEventRenderer.call(t);
3627        var opt = t.opt;
3628        var trigger = t.trigger;
3629        var clearEvents = t.clearEvents;
3630        var renderOverlay = t.renderOverlay;
3631        var clearOverlays = t.clearOverlays;
3632        var reportSelection = t.reportSelection;
3633        var unselect = t.unselect;
3634        var daySelectionMousedown = t.daySelectionMousedown;
3635        var slotSegHtml = t.slotSegHtml;
3636        var formatDate = calendar.formatDate;
3637       
3638       
3639        // locals
3640       
3641        var dayTable;
3642        var dayHead;
3643        var dayHeadCells;
3644        var dayBody;
3645        var dayBodyCells;
3646        var dayBodyCellInners;
3647        var dayBodyFirstCell;
3648        var dayBodyFirstCellStretcher;
3649        var slotLayer;
3650        var daySegmentContainer;
3651        var allDayTable;
3652        var allDayRow;
3653        var slotScroller;
3654        var slotContent;
3655        var slotSegmentContainer;
3656        var slotTable;
3657        var slotTableFirstInner;
3658        var axisFirstCells;
3659        var gutterCells;
3660        var selectionHelper;
3661       
3662        var viewWidth;
3663        var viewHeight;
3664        var axisWidth;
3665        var colWidth;
3666        var gutterWidth;
3667        var slotHeight; // TODO: what if slotHeight changes? (see issue 650)
3668        var savedScrollTop;
3669       
3670        var colCnt;
3671        var slotCnt;
3672        var coordinateGrid;
3673        var hoverListener;
3674        var colContentPositions;
3675        var slotTopCache = {};
3676       
3677        var tm;
3678        var firstDay;
3679        var nwe;            // no weekends (int)
3680        var rtl, dis, dit;  // day index sign / translate
3681        var minMinute, maxMinute;
3682        var colFormat;
3683       
3684
3685       
3686        /* Rendering
3687        -----------------------------------------------------------------------------*/
3688       
3689       
3690        disableTextSelection(element.addClass('fc-agenda'));
3691       
3692       
3693        function renderAgenda(c) {
3694                colCnt = c;
3695                updateOptions();
3696                if (!dayTable) {
3697                        buildSkeleton();
3698                }else{
3699                        clearEvents();
3700                }
3701                updateCells();
3702        }
3703       
3704       
3705       
3706        function updateOptions() {
3707                tm = opt('theme') ? 'ui' : 'fc';
3708                nwe = opt('weekends') ? 0 : 1;
3709                firstDay = opt('firstDay');
3710                if (rtl = opt('isRTL')) {
3711                        dis = -1;
3712                        dit = colCnt - 1;
3713                }else{
3714                        dis = 1;
3715                        dit = 0;
3716                }
3717                minMinute = parseTime(opt('minTime'));
3718                maxMinute = parseTime(opt('maxTime'));
3719                colFormat = opt('columnFormat');
3720        }
3721       
3722       
3723       
3724        function buildSkeleton() {
3725                var headerClass = tm + "-widget-header";
3726                var contentClass = tm + "-widget-content";
3727                var s;
3728                var i;
3729                var d;
3730                var maxd;
3731                var minutes;
3732                var slotNormal = opt('slotMinutes') % 15 == 0;
3733               
3734                s =
3735                        "<table style='width:100%' class='fc-agenda-days fc-border-separate' cellspacing='0'>" +
3736                        "<thead>" +
3737                        "<tr>" +
3738                        "<th class='fc-agenda-axis " + headerClass + "'>&nbsp;</th>";
3739                for (i=0; i<colCnt; i++) {
3740                        s +=
3741                                "<th class='fc- fc-col" + i + ' ' + headerClass + "'/>"; // fc- needed for setDayID
3742                }
3743                s +=
3744                        "<th class='fc-agenda-gutter " + headerClass + "'>&nbsp;</th>" +
3745                        "</tr>" +
3746                        "</thead>" +
3747                        "<tbody>" +
3748                        "<tr>" +
3749                        "<th class='fc-agenda-axis " + headerClass + "'>&nbsp;</th>";
3750                for (i=0; i<colCnt; i++) {
3751                        s +=
3752                                "<td class='fc- fc-col" + i + ' ' + contentClass + "'>" + // fc- needed for setDayID
3753                                "<div>" +
3754                                "<div class='fc-day-content'>" +
3755                                "<div style='position:relative'>&nbsp;</div>" +
3756                                "</div>" +
3757                                "</div>" +
3758                                "</td>";
3759                }
3760                s +=
3761                        "<td class='fc-agenda-gutter " + contentClass + "'>&nbsp;</td>" +
3762                        "</tr>" +
3763                        "</tbody>" +
3764                        "</table>";
3765                dayTable = $(s).appendTo(element);
3766                dayHead = dayTable.find('thead');
3767                dayHeadCells = dayHead.find('th').slice(1, -1);
3768                dayBody = dayTable.find('tbody');
3769                dayBodyCells = dayBody.find('td').slice(0, -1);
3770                dayBodyCellInners = dayBodyCells.find('div.fc-day-content div');
3771                dayBodyFirstCell = dayBodyCells.eq(0);
3772                dayBodyFirstCellStretcher = dayBodyFirstCell.find('> div');
3773               
3774                markFirstLast(dayHead.add(dayHead.find('tr')));
3775                markFirstLast(dayBody.add(dayBody.find('tr')));
3776               
3777                axisFirstCells = dayHead.find('th:first');
3778                gutterCells = dayTable.find('.fc-agenda-gutter');
3779               
3780                slotLayer =
3781                        $("<div style='position:absolute;z-index:2;left:0;width:100%'/>")
3782                                .appendTo(element);
3783                               
3784                if (opt('allDaySlot')) {
3785               
3786                        daySegmentContainer =
3787                                $("<div style='position:absolute;z-index:8;top:0;left:0'/>")
3788                                        .appendTo(slotLayer);
3789               
3790                        s =
3791                                "<table style='width:100%' class='fc-agenda-allday' cellspacing='0'>" +
3792                                "<tr>" +
3793                                "<th class='" + headerClass + " fc-agenda-axis'>" + opt('allDayText') + "</th>" +
3794                                "<td>" +
3795                                "<div class='fc-day-content'><div style='position:relative'/></div>" +
3796                                "</td>" +
3797                                "<th class='" + headerClass + " fc-agenda-gutter'>&nbsp;</th>" +
3798                                "</tr>" +
3799                                "</table>";
3800                        allDayTable = $(s).appendTo(slotLayer);
3801                        allDayRow = allDayTable.find('tr');
3802                       
3803                        dayBind(allDayRow.find('td'));
3804                       
3805                        axisFirstCells = axisFirstCells.add(allDayTable.find('th:first'));
3806                        gutterCells = gutterCells.add(allDayTable.find('th.fc-agenda-gutter'));
3807                       
3808                        slotLayer.append(
3809                                "<div class='fc-agenda-divider " + headerClass + "'>" +
3810                                "<div class='fc-agenda-divider-inner'/>" +
3811                                "</div>"
3812                        );
3813                       
3814                }else{
3815               
3816                        daySegmentContainer = $([]); // in jQuery 1.4, we can just do $()
3817               
3818                }
3819               
3820                slotScroller =
3821                        $("<div style='position:absolute;width:100%;overflow-x:hidden;overflow-y:auto'/>")
3822                                .appendTo(slotLayer);
3823                               
3824                slotContent =
3825                        $("<div style='position:relative;width:100%;overflow:hidden'/>")
3826                                .appendTo(slotScroller);
3827                               
3828                slotSegmentContainer =
3829                        $("<div style='position:absolute;z-index:8;top:0;left:0'/>")
3830                                .appendTo(slotContent);
3831               
3832                s =
3833                        "<table class='fc-agenda-slots' style='width:100%' cellspacing='0'>" +
3834                        "<tbody>";
3835                d = zeroDate();
3836                maxd = addMinutes(cloneDate(d), maxMinute);
3837                addMinutes(d, minMinute);
3838                slotCnt = 0;
3839                for (i=0; d < maxd; i++) {
3840                        minutes = d.getMinutes();
3841                        s +=
3842                                "<tr class='fc-slot" + i + ' ' + (!minutes ? '' : 'fc-minor') + "'>" +
3843                                "<th class='fc-agenda-axis " + headerClass + "'>" +
3844                                ((!slotNormal || !minutes) ? formatDate(d, opt('axisFormat')) : '&nbsp;') +
3845                                "</th>" +
3846                                "<td class='" + contentClass + "'>" +
3847                                "<div style='position:relative'>&nbsp;</div>" +
3848                                "</td>" +
3849                                "</tr>";
3850                        addMinutes(d, opt('slotMinutes'));
3851                        slotCnt++;
3852                }
3853                s +=
3854                        "</tbody>" +
3855                        "</table>";
3856                slotTable = $(s).appendTo(slotContent);
3857                slotTableFirstInner = slotTable.find('div:first');
3858               
3859                slotBind(slotTable.find('td'));
3860               
3861                axisFirstCells = axisFirstCells.add(slotTable.find('th:first'));
3862        }
3863       
3864       
3865       
3866        function updateCells() {
3867                var i;
3868                var headCell;
3869                var bodyCell;
3870                var date;
3871                var today = clearTime(new Date());
3872                for (i=0; i<colCnt; i++) {
3873                        date = colDate(i);
3874                        headCell = dayHeadCells.eq(i);
3875                        headCell.html(formatDate(date, colFormat));
3876                        bodyCell = dayBodyCells.eq(i);
3877                        if (+date == +today) {
3878                                bodyCell.addClass(tm + '-state-highlight fc-today');
3879                        }else{
3880                                bodyCell.removeClass(tm + '-state-highlight fc-today');
3881                        }
3882                        setDayID(headCell.add(bodyCell), date);
3883                }
3884        }
3885       
3886       
3887       
3888        function setHeight(height, dateChanged) {
3889                if (height === undefined) {
3890                        height = viewHeight;
3891                }
3892                viewHeight = height;
3893                slotTopCache = {};
3894       
3895                var headHeight = dayBody.position().top;
3896                var allDayHeight = slotScroller.position().top; // including divider
3897                var bodyHeight = Math.min( // total body height, including borders
3898                        height - headHeight,   // when scrollbars
3899                        slotTable.height() + allDayHeight + 1 // when no scrollbars. +1 for bottom border
3900                );
3901               
3902                dayBodyFirstCellStretcher
3903                        .height(bodyHeight - vsides(dayBodyFirstCell));
3904               
3905                slotLayer.css('top', headHeight);
3906               
3907                slotScroller.height(bodyHeight - allDayHeight - 1);
3908               
3909                slotHeight = slotTableFirstInner.height() + 1; // +1 for border
3910               
3911                if (dateChanged) {
3912                        resetScroll();
3913                }
3914        }
3915       
3916       
3917       
3918        function setWidth(width) {
3919                viewWidth = width;
3920                colContentPositions.clear();
3921               
3922                axisWidth = 0;
3923                setOuterWidth(
3924                        axisFirstCells
3925                                .width('')
3926                                .each(function(i, _cell) {
3927                                        axisWidth = Math.max(axisWidth, $(_cell).outerWidth());
3928                                }),
3929                        axisWidth
3930                );
3931               
3932                var slotTableWidth = slotScroller[0].clientWidth; // needs to be done after axisWidth (for IE7)
3933                //slotTable.width(slotTableWidth);
3934               
3935                gutterWidth = slotScroller.width() - slotTableWidth;
3936                if (gutterWidth) {
3937                        setOuterWidth(gutterCells, gutterWidth);
3938                        gutterCells
3939                                .show()
3940                                .prev()
3941                                .removeClass('fc-last');
3942                }else{
3943                        gutterCells
3944                                .hide()
3945                                .prev()
3946                                .addClass('fc-last');
3947                }
3948               
3949                colWidth = Math.floor((slotTableWidth - axisWidth) / colCnt);
3950                setOuterWidth(dayHeadCells.slice(0, -1), colWidth);
3951        }
3952       
3953
3954
3955        function resetScroll() {
3956                var d0 = zeroDate();
3957                var scrollDate = cloneDate(d0);
3958                scrollDate.setHours(opt('firstHour'));
3959                var top = timePosition(d0, scrollDate) + 1; // +1 for the border
3960                function scroll() {
3961                        slotScroller.scrollTop(top);
3962                }
3963                scroll();
3964                setTimeout(scroll, 0); // overrides any previous scroll state made by the browser
3965        }
3966       
3967       
3968        function beforeHide() {
3969                savedScrollTop = slotScroller.scrollTop();
3970        }
3971       
3972       
3973        function afterShow() {
3974                slotScroller.scrollTop(savedScrollTop);
3975        }
3976       
3977       
3978       
3979        /* Slot/Day clicking and binding
3980        -----------------------------------------------------------------------*/
3981       
3982
3983        function dayBind(cells) {
3984                cells.click(slotClick)
3985                        .mousedown(daySelectionMousedown);
3986        }
3987
3988
3989        function slotBind(cells) {
3990                cells.click(slotClick)
3991                        .mousedown(slotSelectionMousedown);
3992        }
3993       
3994       
3995        function slotClick(ev) {
3996                if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick
3997                        var col = Math.min(colCnt-1, Math.floor((ev.pageX - dayTable.offset().left - axisWidth) / colWidth));
3998                        var date = colDate(col);
3999                        var rowMatch = this.parentNode.className.match(/fc-slot(\d+)/); // TODO: maybe use data
4000                        if (rowMatch) {
4001                                var mins = parseInt(rowMatch[1]) * opt('slotMinutes');
4002                                var hours = Math.floor(mins/60);
4003                                date.setHours(hours);
4004                                date.setMinutes(mins%60 + minMinute);
4005                                trigger('dayClick', dayBodyCells[col], date, false, ev);
4006                        }else{
4007                                trigger('dayClick', dayBodyCells[col], date, true, ev);
4008                        }
4009                }
4010        }
4011       
4012       
4013       
4014        /* Semi-transparent Overlay Helpers
4015        -----------------------------------------------------*/
4016       
4017
4018        function renderDayOverlay(startDate, endDate, refreshCoordinateGrid) { // endDate is exclusive
4019                if (refreshCoordinateGrid) {
4020                        coordinateGrid.build();
4021                }
4022                var visStart = cloneDate(t.visStart);
4023                var startCol, endCol;
4024                if (rtl) {
4025                        startCol = dayDiff(endDate, visStart)*dis+dit+1;
4026                        endCol = dayDiff(startDate, visStart)*dis+dit+1;
4027                }else{
4028                        startCol = dayDiff(startDate, visStart);
4029                        endCol = dayDiff(endDate, visStart);
4030                }
4031                startCol = Math.max(0, startCol);
4032                endCol = Math.min(colCnt, endCol);
4033                if (startCol < endCol) {
4034                        dayBind(
4035                                renderCellOverlay(0, startCol, 0, endCol-1)
4036                        );
4037                }
4038        }
4039       
4040       
4041        function renderCellOverlay(row0, col0, row1, col1) { // only for all-day?
4042                var rect = coordinateGrid.rect(row0, col0, row1, col1, slotLayer);
4043                return renderOverlay(rect, slotLayer);
4044        }
4045       
4046
4047        function renderSlotOverlay(overlayStart, overlayEnd) {
4048                var dayStart = cloneDate(t.visStart);
4049                var dayEnd = addDays(cloneDate(dayStart), 1);
4050                for (var i=0; i<colCnt; i++) {
4051                        var stretchStart = new Date(Math.max(dayStart, overlayStart));
4052                        var stretchEnd = new Date(Math.min(dayEnd, overlayEnd));
4053                        if (stretchStart < stretchEnd) {
4054                                var col = i*dis+dit;
4055                                var rect = coordinateGrid.rect(0, col, 0, col, slotContent); // only use it for horizontal coords
4056                                var top = timePosition(dayStart, stretchStart);
4057                                var bottom = timePosition(dayStart, stretchEnd);
4058                                rect.top = top;
4059                                rect.height = bottom - top;
4060                                slotBind(
4061                                        renderOverlay(rect, slotContent)
4062                                );
4063                        }
4064                        addDays(dayStart, 1);
4065                        addDays(dayEnd, 1);
4066                }
4067        }
4068       
4069       
4070       
4071        /* Coordinate Utilities
4072        -----------------------------------------------------------------------------*/
4073       
4074       
4075        coordinateGrid = new CoordinateGrid(function(rows, cols) {
4076                var e, n, p;
4077                dayHeadCells.each(function(i, _e) {
4078                        e = $(_e);
4079                        n = e.offset().left;
4080                        if (i) {
4081                                p[1] = n;
4082                        }
4083                        p = [n];
4084                        cols[i] = p;
4085                });
4086                p[1] = n + e.outerWidth();
4087                if (opt('allDaySlot')) {
4088                        e = allDayRow;
4089                        n = e.offset().top;
4090                        rows[0] = [n, n+e.outerHeight()];
4091                }
4092                var slotTableTop = slotContent.offset().top;
4093                var slotScrollerTop = slotScroller.offset().top;
4094                var slotScrollerBottom = slotScrollerTop + slotScroller.outerHeight();
4095                function constrain(n) {
4096                        return Math.max(slotScrollerTop, Math.min(slotScrollerBottom, n));
4097                }
4098                for (var i=0; i<slotCnt; i++) {
4099                        rows.push([
4100                                constrain(slotTableTop + slotHeight*i),
4101                                constrain(slotTableTop + slotHeight*(i+1))
4102                        ]);
4103                }
4104        });
4105       
4106       
4107        hoverListener = new HoverListener(coordinateGrid);
4108       
4109       
4110        colContentPositions = new HorizontalPositionCache(function(col) {
4111                return dayBodyCellInners.eq(col);
4112        });
4113       
4114       
4115        function colContentLeft(col) {
4116                return colContentPositions.left(col);
4117        }
4118       
4119       
4120        function colContentRight(col) {
4121                return colContentPositions.right(col);
4122        }
4123       
4124       
4125       
4126       
4127        function dateCell(date) { // "cell" terminology is now confusing
4128                return {
4129                        row: Math.floor(dayDiff(date, t.visStart) / 7),
4130                        col: dayOfWeekCol(date.getDay())
4131                };
4132        }
4133       
4134       
4135        function cellDate(cell) {
4136                var d = colDate(cell.col);
4137                var slotIndex = cell.row;
4138                if (opt('allDaySlot')) {
4139                        slotIndex--;
4140                }
4141                if (slotIndex >= 0) {
4142                        addMinutes(d, minMinute + slotIndex * opt('slotMinutes'));
4143                }
4144                return d;
4145        }
4146       
4147       
4148        function colDate(col) { // returns dates with 00:00:00
4149                return addDays(cloneDate(t.visStart), col*dis+dit);
4150        }
4151       
4152       
4153        function cellIsAllDay(cell) {
4154                return opt('allDaySlot') && !cell.row;
4155        }
4156       
4157       
4158        function dayOfWeekCol(dayOfWeek) {
4159                return ((dayOfWeek - Math.max(firstDay, nwe) + colCnt) % colCnt)*dis+dit;
4160        }
4161       
4162       
4163       
4164       
4165        // get the Y coordinate of the given time on the given day (both Date objects)
4166        function timePosition(day, time) { // both date objects. day holds 00:00 of current day
4167                day = cloneDate(day, true);
4168                if (time < addMinutes(cloneDate(day), minMinute)) {
4169                        return 0;
4170                }
4171                if (time >= addMinutes(cloneDate(day), maxMinute)) {
4172                        return slotTable.height();
4173                }
4174                var slotMinutes = opt('slotMinutes'),
4175                        minutes = time.getHours()*60 + time.getMinutes() - minMinute,
4176                        slotI = Math.floor(minutes / slotMinutes),
4177                        slotTop = slotTopCache[slotI];
4178                if (slotTop === undefined) {
4179                        slotTop = slotTopCache[slotI] = slotTable.find('tr:eq(' + slotI + ') td div')[0].offsetTop; //.position().top; // need this optimization???
4180                }
4181                return Math.max(0, Math.round(
4182                        slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes)
4183                ));
4184        }
4185       
4186       
4187        function allDayBounds() {
4188                return {
4189                        left: axisWidth,
4190                        right: viewWidth - gutterWidth
4191                }
4192        }
4193       
4194       
4195        function getAllDayRow(index) {
4196                return allDayRow;
4197        }
4198       
4199       
4200        function defaultEventEnd(event) {
4201                var start = cloneDate(event.start);
4202                if (event.allDay) {
4203                        return start;
4204                }
4205                return addMinutes(start, opt('defaultEventMinutes'));
4206        }
4207       
4208       
4209       
4210        /* Selection
4211        ---------------------------------------------------------------------------------*/
4212       
4213       
4214        function defaultSelectionEnd(startDate, allDay) {
4215                if (allDay) {
4216                        return cloneDate(startDate);
4217                }
4218                return addMinutes(cloneDate(startDate), opt('slotMinutes'));
4219        }
4220       
4221       
4222        function renderSelection(startDate, endDate, allDay) { // only for all-day
4223                if (allDay) {
4224                        if (opt('allDaySlot')) {
4225                                renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true);
4226                        }
4227                }else{
4228                        renderSlotSelection(startDate, endDate);
4229                }
4230        }
4231       
4232       
4233        function renderSlotSelection(startDate, endDate) {
4234                var helperOption = opt('selectHelper');
4235                coordinateGrid.build();
4236                if (helperOption) {
4237                        var col = dayDiff(startDate, t.visStart) * dis + dit;
4238                        if (col >= 0 && col < colCnt) { // only works when times are on same day
4239                                var rect = coordinateGrid.rect(0, col, 0, col, slotContent); // only for horizontal coords
4240                                var top = timePosition(startDate, startDate);
4241                                var bottom = timePosition(startDate, endDate);
4242                                if (bottom > top) { // protect against selections that are entirely before or after visible range
4243                                        rect.top = top;
4244                                        rect.height = bottom - top;
4245                                        rect.left += 2;
4246                                        rect.width -= 5;
4247                                        if ($.isFunction(helperOption)) {
4248                                                var helperRes = helperOption(startDate, endDate);
4249                                                if (helperRes) {
4250                                                        rect.position = 'absolute';
4251                                                        rect.zIndex = 8;
4252                                                        selectionHelper = $(helperRes)
4253                                                                .css(rect)
4254                                                                .appendTo(slotContent);
4255                                                }
4256                                        }else{
4257                                                rect.isStart = true; // conside rect a "seg" now
4258                                                rect.isEnd = true;   //
4259                                                selectionHelper = $(slotSegHtml(
4260                                                        {
4261                                                                title: '',
4262                                                                start: startDate,
4263                                                                end: endDate,
4264                                                                className: ['fc-select-helper'],
4265                                                                editable: false
4266                                                        },
4267                                                        rect
4268                                                ));
4269                                                selectionHelper.css('opacity', opt('dragOpacity'));
4270                                        }
4271                                        if (selectionHelper) {
4272                                                slotBind(selectionHelper);
4273                                                slotContent.append(selectionHelper);
4274                                                setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended
4275                                                setOuterHeight(selectionHelper, rect.height, true);
4276                                        }
4277                                }
4278                        }
4279                }else{
4280                        renderSlotOverlay(startDate, endDate);
4281                }
4282        }
4283       
4284       
4285        function clearSelection() {
4286                clearOverlays();
4287                if (selectionHelper) {
4288                        selectionHelper.remove();
4289                        selectionHelper = null;
4290                }
4291        }
4292       
4293       
4294        function slotSelectionMousedown(ev) {
4295                if (ev.which == 1 && opt('selectable')) { // ev.which==1 means left mouse button
4296                        unselect(ev);
4297                        var dates;
4298                        hoverListener.start(function(cell, origCell) {
4299                                clearSelection();
4300                                if (cell && cell.col == origCell.col && !cellIsAllDay(cell)) {
4301                                        var d1 = cellDate(origCell);
4302                                        var d2 = cellDate(cell);
4303                                        dates = [
4304                                                d1,
4305                                                addMinutes(cloneDate(d1), opt('slotMinutes')),
4306                                                d2,
4307                                                addMinutes(cloneDate(d2), opt('slotMinutes'))
4308                                        ].sort(cmp);
4309                                        renderSlotSelection(dates[0], dates[3]);
4310                                }else{
4311                                        dates = null;
4312                                }
4313                        }, ev);
4314                        $(document).one('mouseup', function(ev) {
4315                                hoverListener.stop();
4316                                if (dates) {
4317                                        if (+dates[0] == +dates[1]) {
4318                                                reportDayClick(dates[0], false, ev);
4319                                        }
4320                                        reportSelection(dates[0], dates[3], false, ev);
4321                                }
4322                        });
4323                }
4324        }
4325       
4326       
4327        function reportDayClick(date, allDay, ev) {
4328                trigger('dayClick', dayBodyCells[dayOfWeekCol(date.getDay())], date, allDay, ev);
4329        }
4330       
4331       
4332       
4333        /* External Dragging
4334        --------------------------------------------------------------------------------*/
4335       
4336       
4337        function dragStart(_dragElement, ev, ui) {
4338                hoverListener.start(function(cell) {
4339                        clearOverlays();
4340                        if (cell) {
4341                                if (cellIsAllDay(cell)) {
4342                                        renderCellOverlay(cell.row, cell.col, cell.row, cell.col);
4343                                }else{
4344                                        var d1 = cellDate(cell);
4345                                        var d2 = addMinutes(cloneDate(d1), opt('defaultEventMinutes'));
4346                                        renderSlotOverlay(d1, d2);
4347                                }
4348                        }
4349                }, ev);
4350        }
4351       
4352       
4353        function dragStop(_dragElement, ev, ui) {
4354                var cell = hoverListener.stop();
4355                clearOverlays();
4356                if (cell) {
4357                        trigger('drop', _dragElement, cellDate(cell), cellIsAllDay(cell), ev, ui);
4358                }
4359        }
4360
4361
4362}
4363
4364function AgendaEventRenderer() {
4365        var t = this;
4366       
4367       
4368        // exports
4369        t.renderEvents = renderEvents;
4370        t.compileDaySegs = compileDaySegs; // for DayEventRenderer
4371        t.clearEvents = clearEvents;
4372        t.slotSegHtml = slotSegHtml;
4373        t.bindDaySeg = bindDaySeg;
4374       
4375       
4376        // imports
4377        DayEventRenderer.call(t);
4378        var opt = t.opt;
4379        var trigger = t.trigger;
4380        //var setOverflowHidden = t.setOverflowHidden;
4381        var isEventDraggable = t.isEventDraggable;
4382        var isEventResizable = t.isEventResizable;
4383        var eventEnd = t.eventEnd;
4384        var reportEvents = t.reportEvents;
4385        var reportEventClear = t.reportEventClear;
4386        var eventElementHandlers = t.eventElementHandlers;
4387        var setHeight = t.setHeight;
4388        var getDaySegmentContainer = t.getDaySegmentContainer;
4389        var getSlotSegmentContainer = t.getSlotSegmentContainer;
4390        var getHoverListener = t.getHoverListener;
4391        var getMaxMinute = t.getMaxMinute;
4392        var getMinMinute = t.getMinMinute;
4393        var timePosition = t.timePosition;
4394        var colContentLeft = t.colContentLeft;
4395        var colContentRight = t.colContentRight;
4396        var renderDaySegs = t.renderDaySegs;
4397        var resizableDayEvent = t.resizableDayEvent; // TODO: streamline binding architecture
4398        var getColCnt = t.getColCnt;
4399        var getColWidth = t.getColWidth;
4400        var getSlotHeight = t.getSlotHeight;
4401        var getBodyContent = t.getBodyContent;
4402        var reportEventElement = t.reportEventElement;
4403        var showEvents = t.showEvents;
4404        var hideEvents = t.hideEvents;
4405        var eventDrop = t.eventDrop;
4406        var eventResize = t.eventResize;
4407        var renderDayOverlay = t.renderDayOverlay;
4408        var clearOverlays = t.clearOverlays;
4409        var calendar = t.calendar;
4410        var formatDate = calendar.formatDate;
4411        var formatDates = calendar.formatDates;
4412       
4413       
4414       
4415        /* Rendering
4416        ----------------------------------------------------------------------------*/
4417       
4418
4419        function renderEvents(events, modifiedEventId) {
4420                reportEvents(events);
4421                var i, len=events.length,
4422                        dayEvents=[],
4423                        slotEvents=[];
4424                for (i=0; i<len; i++) {
4425                        if (events[i].allDay) {
4426                                dayEvents.push(events[i]);
4427                        }else{
4428                                slotEvents.push(events[i]);
4429                        }
4430                }
4431                if (opt('allDaySlot')) {
4432                        renderDaySegs(compileDaySegs(dayEvents), modifiedEventId);
4433                        setHeight(); // no params means set to viewHeight
4434                }
4435                renderSlotSegs(compileSlotSegs(slotEvents), modifiedEventId);
4436        }
4437       
4438       
4439        function clearEvents() {
4440                reportEventClear();
4441                getDaySegmentContainer().empty();
4442                getSlotSegmentContainer().empty();
4443        }
4444       
4445       
4446        function compileDaySegs(events) {
4447                var levels = stackSegs(sliceSegs(events, $.map(events, exclEndDay), t.visStart, t.visEnd)),
4448                        i, levelCnt=levels.length, level,
4449                        j, seg,
4450                        segs=[];
4451                for (i=0; i<levelCnt; i++) {
4452                        level = levels[i];
4453                        for (j=0; j<level.length; j++) {
4454                                seg = level[j];
4455                                seg.row = 0;
4456                                seg.level = i; // not needed anymore
4457                                segs.push(seg);
4458                        }
4459                }
4460                return segs;
4461        }
4462       
4463       
4464        function compileSlotSegs(events) {
4465                var colCnt = getColCnt(),
4466                        minMinute = getMinMinute(),
4467                        maxMinute = getMaxMinute(),
4468                        d = addMinutes(cloneDate(t.visStart), minMinute),
4469                        visEventEnds = $.map(events, slotEventEnd),
4470                        i, col,
4471                        j, level,
4472                        k, seg,
4473                        segs=[];
4474                for (i=0; i<colCnt; i++) {
4475                        col = stackSegs(sliceSegs(events, visEventEnds, d, addMinutes(cloneDate(d), maxMinute-minMinute)));
4476                        countForwardSegs(col);
4477                        for (j=0; j<col.length; j++) {
4478                                level = col[j];
4479                                for (k=0; k<level.length; k++) {
4480                                        seg = level[k];
4481                                        seg.col = i;
4482                                        seg.level = j;
4483                                        segs.push(seg);
4484                                }
4485                        }
4486                        addDays(d, 1, true);
4487                }
4488                return segs;
4489        }
4490       
4491       
4492        function slotEventEnd(event) {
4493                if (event.end) {
4494                        return cloneDate(event.end);
4495                }else{
4496                        return addMinutes(cloneDate(event.start), opt('defaultEventMinutes'));
4497                }
4498        }
4499       
4500       
4501        // renders events in the 'time slots' at the bottom
4502       
4503        function renderSlotSegs(segs, modifiedEventId) {
4504       
4505                var i, segCnt=segs.length, seg,
4506                        event,
4507                        classes,
4508                        top, bottom,
4509                        colI, levelI, forward,
4510                        leftmost,
4511                        availWidth,
4512                        outerWidth,
4513                        left,
4514                        html='',
4515                        eventElements,
4516                        eventElement,
4517                        triggerRes,
4518                        vsideCache={},
4519                        hsideCache={},
4520                        key, val,
4521                        contentElement,
4522                        height,
4523                        slotSegmentContainer = getSlotSegmentContainer(),
4524                        rtl, dis, dit,
4525                        colCnt = getColCnt();
4526                       
4527                if (rtl = opt('isRTL')) {
4528                        dis = -1;
4529                        dit = colCnt - 1;
4530                }else{
4531                        dis = 1;
4532                        dit = 0;
4533                }
4534                       
4535                // calculate position/dimensions, create html
4536                for (i=0; i<segCnt; i++) {
4537                        seg = segs[i];
4538                        event = seg.event;
4539                        top = timePosition(seg.start, seg.start);
4540                        bottom = timePosition(seg.start, seg.end);
4541                        colI = seg.col;
4542                        levelI = seg.level;
4543                        forward = seg.forward || 0;
4544                        leftmost = colContentLeft(colI*dis + dit);
4545                        availWidth = colContentRight(colI*dis + dit) - leftmost;
4546                        availWidth = Math.min(availWidth-6, availWidth*.95); // TODO: move this to CSS
4547                        if (levelI) {
4548                                // indented and thin
4549                                outerWidth = availWidth / (levelI + forward + 1);
4550                        }else{
4551                                if (forward) {
4552                                        // moderately wide, aligned left still
4553                                        outerWidth = ((availWidth / (forward + 1)) - (12/2)) * 2; // 12 is the predicted width of resizer =
4554                                }else{
4555                                        // can be entire width, aligned left
4556                                        outerWidth = availWidth;
4557                                }
4558                        }
4559                        left = leftmost +                                  // leftmost possible
4560                                (availWidth / (levelI + forward + 1) * levelI) // indentation
4561                                * dis + (rtl ? availWidth - outerWidth : 0);   // rtl
4562                        seg.top = top;
4563                        seg.left = left;
4564                        seg.outerWidth = outerWidth;
4565                        seg.outerHeight = bottom - top;
4566                        html += slotSegHtml(event, seg);
4567                }
4568                slotSegmentContainer[0].innerHTML = html; // faster than html()
4569                eventElements = slotSegmentContainer.children();
4570               
4571                // retrieve elements, run through eventRender callback, bind event handlers
4572                for (i=0; i<segCnt; i++) {
4573                        seg = segs[i];
4574                        event = seg.event;
4575                        eventElement = $(eventElements[i]); // faster than eq()
4576                        triggerRes = trigger('eventRender', event, event, eventElement);
4577                        if (triggerRes === false) {
4578                                eventElement.remove();
4579                        }else{
4580                                if (triggerRes && triggerRes !== true) {
4581                                        eventElement.remove();
4582                                        eventElement = $(triggerRes)
4583                                                .css({
4584                                                        position: 'absolute',
4585                                                        top: seg.top,
4586                                                        left: seg.left
4587                                                })
4588                                                .appendTo(slotSegmentContainer);
4589                                }
4590                                seg.element = eventElement;
4591                                if (event._id === modifiedEventId) {
4592                                        bindSlotSeg(event, eventElement, seg);
4593                                }else{
4594                                        eventElement[0]._fci = i; // for lazySegBind
4595                                }
4596                                reportEventElement(event, eventElement);
4597                        }
4598                }
4599               
4600                lazySegBind(slotSegmentContainer, segs, bindSlotSeg);
4601               
4602                // record event sides and title positions
4603                for (i=0; i<segCnt; i++) {
4604                        seg = segs[i];
4605                        if (eventElement = seg.element) {
4606                                val = vsideCache[key = seg.key = cssKey(eventElement[0])];
4607                                seg.vsides = val === undefined ? (vsideCache[key] = vsides(eventElement, true)) : val;
4608                                val = hsideCache[key];
4609                                seg.hsides = val === undefined ? (hsideCache[key] = hsides(eventElement, true)) : val;
4610                                contentElement = eventElement.find('div.fc-event-content');
4611                                if (contentElement.length) {
4612                                        seg.contentTop = contentElement[0].offsetTop;
4613                                }
4614                        }
4615                }
4616               
4617                // set all positions/dimensions at once
4618                for (i=0; i<segCnt; i++) {
4619                        seg = segs[i];
4620                        if (eventElement = seg.element) {
4621                                eventElement[0].style.width = Math.max(0, seg.outerWidth - seg.hsides) + 'px';
4622                                height = Math.max(0, seg.outerHeight - seg.vsides);
4623                                eventElement[0].style.height = height + 'px';
4624                                event = seg.event;
4625                                if (seg.contentTop !== undefined && height - seg.contentTop < 10) {
4626                                        // not enough room for title, put it in the time header
4627                                        eventElement.find('div.fc-event-time')
4628                                                .text(formatDate(event.start, opt('timeFormat')) + ' - ' + event.title);
4629                                        eventElement.find('div.fc-event-title')
4630                                                .remove();
4631                                }
4632                                trigger('eventAfterRender', event, event, eventElement);
4633                        }
4634                }
4635                                       
4636        }
4637       
4638       
4639        function slotSegHtml(event, seg) {
4640                var html = "<";
4641                var url = event.url;
4642                var skinCss = getSkinCss(event, opt);
4643                var skinCssAttr = (skinCss ? " style='" + skinCss + "'" : '');
4644                var classes = ['fc-event', 'fc-event-skin', 'fc-event-vert'];
4645                if (isEventDraggable(event)) {
4646                        classes.push('fc-event-draggable');
4647                }
4648                if (seg.isStart) {
4649                        classes.push('fc-corner-top');
4650                }
4651                if (seg.isEnd) {
4652                        classes.push('fc-corner-bottom');
4653                }
4654                classes = classes.concat(event.className);
4655                if (event.source) {
4656                        classes = classes.concat(event.source.className || []);
4657                }
4658                if (url) {
4659                        html += "a href='" + htmlEscape(event.url) + "'";
4660                }else{
4661                        html += "div";
4662                }
4663                html +=
4664                        " class='" + classes.join(' ') + "'" +
4665                        " style='position:absolute;z-index:8;top:" + seg.top + "px;left:" + seg.left + "px;" + skinCss + "'" +
4666                        ">" +
4667                        "<div class='fc-event-inner fc-event-skin'" + skinCssAttr + ">" +
4668                        "<div class='fc-event-head fc-event-skin'" + skinCssAttr + ">" +
4669                        "<div class='fc-event-time'>" +
4670                        htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) +
4671                        "</div>" +
4672                        "</div>" +
4673                        "<div class='fc-event-content'>" +
4674                        "<div class='fc-event-title'>" +
4675                        htmlEscape(event.title) +
4676                        "</div>" +
4677                        "</div>" +
4678                        "<div class='fc-event-bg'></div>" +
4679                        "</div>"; // close inner
4680                if (seg.isEnd && isEventResizable(event)) {
4681                        html +=
4682                                "<div class='ui-resizable-handle ui-resizable-s'>=</div>";
4683                }
4684                html +=
4685                        "</" + (url ? "a" : "div") + ">";
4686                return html;
4687        }
4688       
4689       
4690        function bindDaySeg(event, eventElement, seg) {
4691                if (isEventDraggable(event)) {
4692                        draggableDayEvent(event, eventElement, seg.isStart);
4693                }
4694                if (seg.isEnd && isEventResizable(event)) {
4695                        resizableDayEvent(event, eventElement, seg);
4696                }
4697                eventElementHandlers(event, eventElement);
4698                        // needs to be after, because resizableDayEvent might stopImmediatePropagation on click
4699        }
4700       
4701       
4702        function bindSlotSeg(event, eventElement, seg) {
4703                var timeElement = eventElement.find('div.fc-event-time');
4704                if (isEventDraggable(event)) {
4705                        draggableSlotEvent(event, eventElement, timeElement);
4706                }
4707                if (seg.isEnd && isEventResizable(event)) {
4708                        resizableSlotEvent(event, eventElement, timeElement);
4709                }
4710                eventElementHandlers(event, eventElement);
4711        }
4712       
4713       
4714       
4715        /* Dragging
4716        -----------------------------------------------------------------------------------*/
4717       
4718       
4719        // when event starts out FULL-DAY
4720       
4721        function draggableDayEvent(event, eventElement, isStart) {
4722                var origWidth;
4723                var revert;
4724                var allDay=true;
4725                var dayDelta;
4726                var dis = opt('isRTL') ? -1 : 1;
4727                var hoverListener = getHoverListener();
4728                var colWidth = getColWidth();
4729                var slotHeight = getSlotHeight();
4730                var minMinute = getMinMinute();
4731                eventElement.draggable({
4732                        zIndex: 9,
4733                        opacity: opt('dragOpacity', 'month'), // use whatever the month view was using
4734                        revertDuration: opt('dragRevertDuration'),
4735                        start: function(ev, ui) {
4736                                trigger('eventDragStart', eventElement, event, ev, ui);
4737                                hideEvents(event, eventElement);
4738                                origWidth = eventElement.width();
4739                                hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
4740                                        clearOverlays();
4741                                        if (cell) {
4742                                                //setOverflowHidden(true);
4743                                                revert = false;
4744                                                dayDelta = colDelta * dis;
4745                                                if (!cell.row) {
4746                                                        // on full-days
4747                                                        renderDayOverlay(
4748                                                                addDays(cloneDate(event.start), dayDelta),
4749                                                                addDays(exclEndDay(event), dayDelta)
4750                                                        );
4751                                                        resetElement();
4752                                                }else{
4753                                                        // mouse is over bottom slots
4754                                                        if (isStart) {
4755                                                                if (allDay) {
4756                                                                        // convert event to temporary slot-event
4757                                                                        eventElement.width(colWidth - 10); // don't use entire width
4758                                                                        setOuterHeight(
4759                                                                                eventElement,
4760                                                                                slotHeight * Math.round(
4761                                                                                        (event.end ? ((event.end - event.start) / MINUTE_MS) : opt('defaultEventMinutes'))
4762                                                                                        / opt('slotMinutes')
4763                                                                                )
4764                                                                        );
4765                                                                        eventElement.draggable('option', 'grid', [colWidth, 1]);
4766                                                                        allDay = false;
4767                                                                }
4768                                                        }else{
4769                                                                revert = true;
4770                                                        }
4771                                                }
4772                                                revert = revert || (allDay && !dayDelta);
4773                                        }else{
4774                                                resetElement();
4775                                                //setOverflowHidden(false);
4776                                                revert = true;
4777                                        }
4778                                        eventElement.draggable('option', 'revert', revert);
4779                                }, ev, 'drag');
4780                        },
4781                        stop: function(ev, ui) {
4782                                hoverListener.stop();
4783                                clearOverlays();
4784                                trigger('eventDragStop', eventElement, event, ev, ui);
4785                                if (revert) {
4786                                        // hasn't moved or is out of bounds (draggable has already reverted)
4787                                        resetElement();
4788                                        eventElement.css('filter', ''); // clear IE opacity side-effects
4789                                        showEvents(event, eventElement);
4790                                }else{
4791                                        // changed!
4792                                        var minuteDelta = 0;
4793                                        if (!allDay) {
4794                                                minuteDelta = Math.round((eventElement.offset().top - getBodyContent().offset().top) / slotHeight)
4795                                                        * opt('slotMinutes')
4796                                                        + minMinute
4797                                                        - (event.start.getHours() * 60 + event.start.getMinutes());
4798                                        }
4799                                        eventDrop(this, event, dayDelta, minuteDelta, allDay, ev, ui);
4800                                }
4801                                //setOverflowHidden(false);
4802                        }
4803                });
4804                function resetElement() {
4805                        if (!allDay) {
4806                                eventElement
4807                                        .width(origWidth)
4808                                        .height('')
4809                                        .draggable('option', 'grid', null);
4810                                allDay = true;
4811                        }
4812                }
4813        }
4814       
4815       
4816        // when event starts out IN TIMESLOTS
4817       
4818        function draggableSlotEvent(event, eventElement, timeElement) {
4819                var origPosition;
4820                var allDay=false;
4821                var dayDelta;
4822                var minuteDelta;
4823                var prevMinuteDelta;
4824                var dis = opt('isRTL') ? -1 : 1;
4825                var hoverListener = getHoverListener();
4826                var colCnt = getColCnt();
4827                var colWidth = getColWidth();
4828                var slotHeight = getSlotHeight();
4829                eventElement.draggable({
4830                        zIndex: 9,
4831                        scroll: false,
4832                        grid: [colWidth, slotHeight],
4833                        axis: colCnt==1 ? 'y' : false,
4834                        opacity: opt('dragOpacity'),
4835                        revertDuration: opt('dragRevertDuration'),
4836                        start: function(ev, ui) {
4837                                trigger('eventDragStart', eventElement, event, ev, ui);
4838                                hideEvents(event, eventElement);
4839                                origPosition = eventElement.position();
4840                                minuteDelta = prevMinuteDelta = 0;
4841                                hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
4842                                        eventElement.draggable('option', 'revert', !cell);
4843                                        clearOverlays();
4844                                        if (cell) {
4845                                                dayDelta = colDelta * dis;
4846                                                if (opt('allDaySlot') && !cell.row) {
4847                                                        // over full days
4848                                                        if (!allDay) {
4849                                                                // convert to temporary all-day event
4850                                                                allDay = true;
4851                                                                timeElement.hide();
4852                                                                eventElement.draggable('option', 'grid', null);
4853                                                        }
4854                                                        renderDayOverlay(
4855                                                                addDays(cloneDate(event.start), dayDelta),
4856                                                                addDays(exclEndDay(event), dayDelta)
4857                                                        );
4858                                                }else{
4859                                                        // on slots
4860                                                        resetElement();
4861                                                }
4862                                        }
4863                                }, ev, 'drag');
4864                        },
4865                        drag: function(ev, ui) {
4866                                minuteDelta = Math.round((ui.position.top - origPosition.top) / slotHeight) * opt('slotMinutes');
4867                                if (minuteDelta != prevMinuteDelta) {
4868                                        if (!allDay) {
4869                                                updateTimeText(minuteDelta);
4870                                        }
4871                                        prevMinuteDelta = minuteDelta;
4872                                }
4873                        },
4874                        stop: function(ev, ui) {
4875                                var cell = hoverListener.stop();
4876                                clearOverlays();
4877                                trigger('eventDragStop', eventElement, event, ev, ui);
4878                                if (cell && (dayDelta || minuteDelta || allDay)) {
4879                                        // changed!
4880                                        eventDrop(this, event, dayDelta, allDay ? 0 : minuteDelta, allDay, ev, ui);
4881                                }else{
4882                                        // either no change or out-of-bounds (draggable has already reverted)
4883                                        resetElement();
4884                                        eventElement.css('filter', ''); // clear IE opacity side-effects
4885                                        eventElement.css(origPosition); // sometimes fast drags make event revert to wrong position
4886                                        updateTimeText(0);
4887                                        showEvents(event, eventElement);
4888                                }
4889                        }
4890                });
4891                function updateTimeText(minuteDelta) {
4892                        var newStart = addMinutes(cloneDate(event.start), minuteDelta);
4893                        var newEnd;
4894                        if (event.end) {
4895                                newEnd = addMinutes(cloneDate(event.end), minuteDelta);
4896                        }
4897                        timeElement.text(formatDates(newStart, newEnd, opt('timeFormat')));
4898                }
4899                function resetElement() {
4900                        // convert back to original slot-event
4901                        if (allDay) {
4902                                timeElement.css('display', ''); // show() was causing display=inline
4903                                eventElement.draggable('option', 'grid', [colWidth, slotHeight]);
4904                                allDay = false;
4905                        }
4906                }
4907        }
4908       
4909       
4910       
4911        /* Resizing
4912        --------------------------------------------------------------------------------------*/
4913       
4914       
4915        function resizableSlotEvent(event, eventElement, timeElement) {
4916                var slotDelta, prevSlotDelta;
4917                var slotHeight = getSlotHeight();
4918                eventElement.resizable({
4919                        handles: {
4920                                s: 'div.ui-resizable-s'
4921                        },
4922                        grid: slotHeight,
4923                        start: function(ev, ui) {
4924                                slotDelta = prevSlotDelta = 0;
4925                                hideEvents(event, eventElement);
4926                                eventElement.css('z-index', 9);
4927                                trigger('eventResizeStart', this, event, ev, ui);
4928                        },
4929                        resize: function(ev, ui) {
4930                                // don't rely on ui.size.height, doesn't take grid into account
4931                                slotDelta = Math.round((Math.max(slotHeight, eventElement.height()) - ui.originalSize.height) / slotHeight);
4932                                if (slotDelta != prevSlotDelta) {
4933                                        timeElement.text(
4934                                                formatDates(
4935                                                        event.start,
4936                                                        (!slotDelta && !event.end) ? null : // no change, so don't display time range
4937                                                                addMinutes(eventEnd(event), opt('slotMinutes')*slotDelta),
4938                                                        opt('timeFormat')
4939                                                )
4940                                        );
4941                                        prevSlotDelta = slotDelta;
4942                                }
4943                        },
4944                        stop: function(ev, ui) {
4945                                trigger('eventResizeStop', this, event, ev, ui);
4946                                if (slotDelta) {
4947                                        eventResize(this, event, 0, opt('slotMinutes')*slotDelta, ev, ui);
4948                                }else{
4949                                        eventElement.css('z-index', 8);
4950                                        showEvents(event, eventElement);
4951                                        // BUG: if event was really short, need to put title back in span
4952                                }
4953                        }
4954                });
4955        }
4956       
4957
4958}
4959
4960
4961function countForwardSegs(levels) {
4962        var i, j, k, level, segForward, segBack;
4963        for (i=levels.length-1; i>0; i--) {
4964                level = levels[i];
4965                for (j=0; j<level.length; j++) {
4966                        segForward = level[j];
4967                        for (k=0; k<levels[i-1].length; k++) {
4968                                segBack = levels[i-1][k];
4969                                if (segsCollide(segForward, segBack)) {
4970                                        segBack.forward = Math.max(segBack.forward||0, (segForward.forward||0)+1);
4971                                }
4972                        }
4973                }
4974        }
4975}
4976
4977
4978
4979
4980function View(element, calendar, viewName) {
4981        var t = this;
4982       
4983       
4984        // exports
4985        t.element = element;
4986        t.calendar = calendar;
4987        t.name = viewName;
4988        t.opt = opt;
4989        t.trigger = trigger;
4990        //t.setOverflowHidden = setOverflowHidden;
4991        t.isEventDraggable = isEventDraggable;
4992        t.isEventResizable = isEventResizable;
4993        t.reportEvents = reportEvents;
4994        t.eventEnd = eventEnd;
4995        t.reportEventElement = reportEventElement;
4996        t.reportEventClear = reportEventClear;
4997        t.eventElementHandlers = eventElementHandlers;
4998        t.showEvents = showEvents;
4999        t.hideEvents = hideEvents;
5000        t.eventDrop = eventDrop;
5001        t.eventResize = eventResize;
5002        // t.title
5003        // t.start, t.end
5004        // t.visStart, t.visEnd
5005       
5006       
5007        // imports
5008        var defaultEventEnd = t.defaultEventEnd;
5009        var normalizeEvent = calendar.normalizeEvent; // in EventManager
5010        var reportEventChange = calendar.reportEventChange;
5011       
5012       
5013        // locals
5014        var eventsByID = {};
5015        var eventElements = [];
5016        var eventElementsByID = {};
5017        var options = calendar.options;
5018       
5019       
5020       
5021        function opt(name, viewNameOverride) {
5022                var v = options[name];
5023                if (typeof v == 'object') {
5024                        if ((typeof viewNameOverride != 'undefined') || (viewNameOverride) || (viewNameOverride === 0))
5025                            return smartProperty(v, viewNameOverride);
5026                        else
5027                            return smartProperty(v, viewName);
5028                }
5029                return v;
5030        }
5031
5032       
5033        function trigger(name, thisObj) {
5034                return calendar.trigger.apply(
5035                        calendar,
5036                        [name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t])
5037                );
5038        }
5039       
5040       
5041        /*
5042        function setOverflowHidden(bool) {
5043                element.css('overflow', bool ? 'hidden' : '');
5044        }
5045        */
5046       
5047       
5048        function isEventDraggable(event) {
5049                return isEventEditable(event) && !opt('disableDragging');
5050        }
5051       
5052       
5053        function isEventResizable(event) { // but also need to make sure the seg.isEnd == true
5054                return isEventEditable(event) && !opt('disableResizing');
5055        }
5056       
5057       
5058        function isEventEditable(event) {
5059                return firstDefined(event.editable, (event.source || {}).editable, opt('editable'));
5060        }
5061       
5062       
5063       
5064        /* Event Data
5065        ------------------------------------------------------------------------------*/
5066       
5067       
5068        // report when view receives new events
5069        function reportEvents(events) { // events are already normalized at this point
5070                eventsByID = {};
5071                var i, len=events.length, event;
5072                for (i=0; i<len; i++) {
5073                        event = events[i];
5074                        if (eventsByID[event._id]) {
5075                                eventsByID[event._id].push(event);
5076                        }else{
5077                                eventsByID[event._id] = [event];
5078                        }
5079                }
5080        }
5081       
5082       
5083        // returns a Date object for an event's end
5084        function eventEnd(event) {
5085                return event.end ? cloneDate(event.end) : defaultEventEnd(event);
5086        }
5087       
5088       
5089       
5090        /* Event Elements
5091        ------------------------------------------------------------------------------*/
5092       
5093       
5094        // report when view creates an element for an event
5095        function reportEventElement(event, element) {
5096                eventElements.push(element);
5097                if (eventElementsByID[event._id]) {
5098                        eventElementsByID[event._id].push(element);
5099                }else{
5100                        eventElementsByID[event._id] = [element];
5101                }
5102        }
5103       
5104       
5105        function reportEventClear() {
5106                eventElements = [];
5107                eventElementsByID = {};
5108        }
5109       
5110       
5111        // attaches eventClick, eventMouseover, eventMouseout
5112        function eventElementHandlers(event, eventElement) {
5113                eventElement
5114                        .click(function(ev) {
5115                                if (!eventElement.hasClass('ui-draggable-dragging') &&
5116                                        !eventElement.hasClass('ui-resizable-resizing')) {
5117                                                return trigger('eventClick', this, event, ev);
5118                                        }
5119                        })
5120                        .hover(
5121                                function(ev) {
5122                                        trigger('eventMouseover', this, event, ev);
5123                                },
5124                                function(ev) {
5125                                        trigger('eventMouseout', this, event, ev);
5126                                }
5127                        );
5128                // TODO: don't fire eventMouseover/eventMouseout *while* dragging is occuring (on subject element)
5129                // TODO: same for resizing
5130        }
5131       
5132       
5133        function showEvents(event, exceptElement) {
5134                eachEventElement(event, exceptElement, 'show');
5135        }
5136       
5137       
5138        function hideEvents(event, exceptElement) {
5139                eachEventElement(event, exceptElement, 'hide');
5140        }
5141       
5142       
5143        function eachEventElement(event, exceptElement, funcName) {
5144                var elements = eventElementsByID[event._id],
5145                        i, len = elements.length;
5146                for (i=0; i<len; i++) {
5147                        if (!exceptElement || elements[i][0] != exceptElement[0]) {
5148                                elements[i][funcName]();
5149                        }
5150                }
5151        }
5152       
5153       
5154       
5155        /* Event Modification Reporting
5156        ---------------------------------------------------------------------------------*/
5157       
5158       
5159        function eventDrop(e, event, dayDelta, minuteDelta, allDay, ev, ui) {
5160                var oldAllDay = event.allDay;
5161                var eventId = event._id;
5162                moveEvents(eventsByID[eventId], dayDelta, minuteDelta, allDay);
5163                trigger(
5164                        'eventDrop',
5165                        e,
5166                        event,
5167                        dayDelta,
5168                        minuteDelta,
5169                        allDay,
5170                        function() {
5171                                // TODO: investigate cases where this inverse technique might not work
5172                                moveEvents(eventsByID[eventId], -dayDelta, -minuteDelta, oldAllDay);
5173                                reportEventChange(eventId);
5174                        },
5175                        ev,
5176                        ui
5177                );
5178                reportEventChange(eventId);
5179        }
5180       
5181       
5182        function eventResize(e, event, dayDelta, minuteDelta, ev, ui) {
5183                var eventId = event._id;
5184                elongateEvents(eventsByID[eventId], dayDelta, minuteDelta);
5185                trigger(
5186                        'eventResize',
5187                        e,
5188                        event,
5189                        dayDelta,
5190                        minuteDelta,
5191                        function() {
5192                                // TODO: investigate cases where this inverse technique might not work
5193                                elongateEvents(eventsByID[eventId], -dayDelta, -minuteDelta);
5194                                reportEventChange(eventId);
5195                        },
5196                        ev,
5197                        ui
5198                );
5199                reportEventChange(eventId);
5200        }
5201       
5202       
5203       
5204        /* Event Modification Math
5205        ---------------------------------------------------------------------------------*/
5206       
5207       
5208        function moveEvents(events, dayDelta, minuteDelta, allDay) {
5209                minuteDelta = minuteDelta || 0;
5210                for (var e, len=events.length, i=0; i<len; i++) {
5211                        e = events[i];
5212                        if (allDay !== undefined) {
5213                                e.allDay = allDay;
5214                        }
5215                        addMinutes(addDays(e.start, dayDelta, true), minuteDelta);
5216                        if (e.end) {
5217                                e.end = addMinutes(addDays(e.end, dayDelta, true), minuteDelta);
5218                        }
5219                        normalizeEvent(e, options);
5220                }
5221        }
5222       
5223       
5224        function elongateEvents(events, dayDelta, minuteDelta) {
5225                minuteDelta = minuteDelta || 0;
5226                for (var e, len=events.length, i=0; i<len; i++) {
5227                        e = events[i];
5228                        e.end = addMinutes(addDays(eventEnd(e), dayDelta, true), minuteDelta);
5229                        normalizeEvent(e, options);
5230                }
5231        }
5232       
5233
5234}
5235
5236function DayEventRenderer() {
5237        var t = this;
5238
5239       
5240        // exports
5241        t.renderDaySegs = renderDaySegs;
5242        t.resizableDayEvent = resizableDayEvent;
5243       
5244       
5245        // imports
5246        var opt = t.opt;
5247        var trigger = t.trigger;
5248        var isEventDraggable = t.isEventDraggable;
5249        var isEventResizable = t.isEventResizable;
5250        var eventEnd = t.eventEnd;
5251        var reportEventElement = t.reportEventElement;
5252        var showEvents = t.showEvents;
5253        var hideEvents = t.hideEvents;
5254        var eventResize = t.eventResize;
5255        var getRowCnt = t.getRowCnt;
5256        var getColCnt = t.getColCnt;
5257        var getColWidth = t.getColWidth;
5258        var allDayRow = t.allDayRow;
5259        var allDayBounds = t.allDayBounds;
5260        var colContentLeft = t.colContentLeft;
5261        var colContentRight = t.colContentRight;
5262        var dayOfWeekCol = t.dayOfWeekCol;
5263        var dateCell = t.dateCell;
5264        var compileDaySegs = t.compileDaySegs;
5265        var getDaySegmentContainer = t.getDaySegmentContainer;
5266        var bindDaySeg = t.bindDaySeg; //TODO: streamline this
5267        var formatDates = t.calendar.formatDates;
5268        var renderDayOverlay = t.renderDayOverlay;
5269        var clearOverlays = t.clearOverlays;
5270        var clearSelection = t.clearSelection;
5271       
5272       
5273       
5274        /* Rendering
5275        -----------------------------------------------------------------------------*/
5276       
5277       
5278        function renderDaySegs(segs, modifiedEventId) {
5279                var segmentContainer = getDaySegmentContainer();
5280                var rowDivs;
5281                var rowCnt = getRowCnt();
5282                var colCnt = getColCnt();
5283                var i = 0;
5284                var rowI;
5285                var levelI;
5286                var colHeights;
5287                var j;
5288                var segCnt = segs.length;
5289                var seg;
5290                var top;
5291                var k;
5292                segmentContainer[0].innerHTML = daySegHTML(segs); // faster than .html()
5293                daySegElementResolve(segs, segmentContainer.children());
5294                daySegElementReport(segs);
5295                daySegHandlers(segs, segmentContainer, modifiedEventId);
5296                daySegCalcHSides(segs);
5297                daySegSetWidths(segs);
5298                daySegCalcHeights(segs);
5299                rowDivs = getRowDivs();
5300                // set row heights, calculate event tops (in relation to row top)
5301                for (rowI=0; rowI<rowCnt; rowI++) {
5302                        levelI = 0;
5303                        colHeights = [];
5304                        for (j=0; j<colCnt; j++) {
5305                                colHeights[j] = 0;
5306                        }
5307                        while (i<segCnt && (seg = segs[i]).row == rowI) {
5308                                // loop through segs in a row
5309                                top = arrayMax(colHeights.slice(seg.startCol, seg.endCol));
5310                                seg.top = top;
5311                                top += seg.outerHeight;
5312                                for (k=seg.startCol; k<seg.endCol; k++) {
5313                                        colHeights[k] = top;
5314                                }
5315                                i++;
5316                        }
5317                        rowDivs[rowI].height(arrayMax(colHeights));
5318                }
5319                daySegSetTops(segs, getRowTops(rowDivs));
5320        }
5321       
5322       
5323        function renderTempDaySegs(segs, adjustRow, adjustTop) {
5324                var tempContainer = $("<div/>");
5325                var elements;
5326                var segmentContainer = getDaySegmentContainer();
5327                var i;
5328                var segCnt = segs.length;
5329                var element;
5330                tempContainer[0].innerHTML = daySegHTML(segs); // faster than .html()
5331                elements = tempContainer.children();
5332                segmentContainer.append(elements);
5333                daySegElementResolve(segs, elements);
5334                daySegCalcHSides(segs);
5335                daySegSetWidths(segs);
5336                daySegCalcHeights(segs);
5337                daySegSetTops(segs, getRowTops(getRowDivs()));
5338                elements = [];
5339                for (i=0; i<segCnt; i++) {
5340                        element = segs[i].element;
5341                        if (element) {
5342                                if (segs[i].row === adjustRow) {
5343                                        element.css('top', adjustTop);
5344                                }
5345                                elements.push(element[0]);
5346                        }
5347                }
5348                return $(elements);
5349        }
5350       
5351       
5352        function daySegHTML(segs) { // also sets seg.left and seg.outerWidth
5353                var rtl = opt('isRTL');
5354                var i;
5355                var segCnt=segs.length;
5356                var seg;
5357                var event;
5358                var url;
5359                var classes;
5360                var bounds = allDayBounds();
5361                var minLeft = bounds.left;
5362                var maxLeft = bounds.right;
5363                var leftCol;
5364                var rightCol;
5365                var left;
5366                var right;
5367                var skinCss;
5368                var html = '';
5369                // calculate desired position/dimensions, create html
5370                for (i=0; i<segCnt; i++) {
5371                        seg = segs[i];
5372                        event = seg.event;
5373                        classes = ['fc-event', 'fc-event-skin', 'fc-event-hori'];
5374                        if (isEventDraggable(event)) {
5375                                classes.push('fc-event-draggable');
5376                        }
5377                        if (rtl) {
5378                                if (seg.isStart) {
5379                                        classes.push('fc-corner-right');
5380                                }
5381                                if (seg.isEnd) {
5382                                        classes.push('fc-corner-left');
5383                                }
5384                                leftCol = dayOfWeekCol(seg.end.getDay()-1);
5385                                rightCol = dayOfWeekCol(seg.start.getDay());
5386                                left = seg.isEnd ? colContentLeft(leftCol) : minLeft;
5387                                right = seg.isStart ? colContentRight(rightCol) : maxLeft;
5388                        }else{
5389                                if (seg.isStart) {
5390                                        classes.push('fc-corner-left');
5391                                }
5392                                if (seg.isEnd) {
5393                                        classes.push('fc-corner-right');
5394                                }
5395                                leftCol = dayOfWeekCol(seg.start.getDay());
5396                                rightCol = dayOfWeekCol(seg.end.getDay()-1);
5397                                left = seg.isStart ? colContentLeft(leftCol) : minLeft;
5398                                right = seg.isEnd ? colContentRight(rightCol) : maxLeft;
5399                        }
5400                        classes = classes.concat(event.className);
5401                        if (event.source) {
5402                                classes = classes.concat(event.source.className || []);
5403                        }
5404                        url = event.url;
5405                        skinCss = getSkinCss(event, opt);
5406                        if (url) {
5407                                html += "<a href='" + htmlEscape(url) + "'";
5408                        }else{
5409                                html += "<div";
5410                        }
5411                        html +=
5412                                " class='" + classes.join(' ') + "'" +
5413                                " style='position:absolute;z-index:8;left:"+left+"px;" + skinCss + "'" +
5414                                ">" +
5415                                "<div" +
5416                                " class='fc-event-inner fc-event-skin'" +
5417                                (skinCss ? " style='" + skinCss + "'" : '') +
5418                                ">";
5419                        if (!event.allDay && seg.isStart) {
5420                                html +=
5421                                        "<span class='fc-event-time'>" +
5422                                        htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) +
5423                                        "</span>";
5424                        }
5425                        html +=
5426                                "<span class='fc-event-title'>" + htmlEscape(event.title) + "</span>" +
5427                                "</div>";
5428                        if (seg.isEnd && isEventResizable(event)) {
5429                                html +=
5430                                        "<div class='ui-resizable-handle ui-resizable-" + (rtl ? 'w' : 'e') + "'>" +
5431                                        "&nbsp;&nbsp;&nbsp;" + // makes hit area a lot better for IE6/7
5432                                        "</div>";
5433                        }
5434                        html +=
5435                                "</" + (url ? "a" : "div" ) + ">";
5436                        seg.left = left;
5437                        seg.outerWidth = right - left;
5438                        seg.startCol = leftCol;
5439                        seg.endCol = rightCol + 1; // needs to be exclusive
5440                }
5441                return html;
5442        }
5443       
5444       
5445        function daySegElementResolve(segs, elements) { // sets seg.element
5446                var i;
5447                var segCnt = segs.length;
5448                var seg;
5449                var event;
5450                var element;
5451                var triggerRes;
5452                for (i=0; i<segCnt; i++) {
5453                        seg = segs[i];
5454                        event = seg.event;
5455                        element = $(elements[i]); // faster than .eq()
5456                        triggerRes = trigger('eventRender', event, event, element);
5457                        if (triggerRes === false) {
5458                                element.remove();
5459                        }else{
5460                                if (triggerRes && triggerRes !== true) {
5461                                        triggerRes = $(triggerRes)
5462                                                .css({
5463                                                        position: 'absolute',
5464                                                        left: seg.left
5465                                                });
5466                                        element.replaceWith(triggerRes);
5467                                        element = triggerRes;
5468                                }
5469                                seg.element = element;
5470                        }
5471                }
5472        }
5473       
5474       
5475        function daySegElementReport(segs) {
5476                var i;
5477                var segCnt = segs.length;
5478                var seg;
5479                var element;
5480                for (i=0; i<segCnt; i++) {
5481                        seg = segs[i];
5482                        element = seg.element;
5483                        if (element) {
5484                                reportEventElement(seg.event, element);
5485                        }
5486                }
5487        }
5488       
5489       
5490        function daySegHandlers(segs, segmentContainer, modifiedEventId) {
5491                var i;
5492                var segCnt = segs.length;
5493                var seg;
5494                var element;
5495                var event;
5496                // retrieve elements, run through eventRender callback, bind handlers
5497                for (i=0; i<segCnt; i++) {
5498                        seg = segs[i];
5499                        element = seg.element;
5500                        if (element) {
5501                                event = seg.event;
5502                                if (event._id === modifiedEventId) {
5503                                        bindDaySeg(event, element, seg);
5504                                }else{
5505                                        element[0]._fci = i; // for lazySegBind
5506                                }
5507                        }
5508                }
5509                lazySegBind(segmentContainer, segs, bindDaySeg);
5510        }
5511       
5512       
5513        function daySegCalcHSides(segs) { // also sets seg.key
5514                var i;
5515                var segCnt = segs.length;
5516                var seg;
5517                var element;
5518                var key, val;
5519                var hsideCache = {};
5520                // record event horizontal sides
5521                for (i=0; i<segCnt; i++) {
5522                        seg = segs[i];
5523                        element = seg.element;
5524                        if (element) {
5525                                key = seg.key = cssKey(element[0]);
5526                                val = hsideCache[key];
5527                                if (val === undefined) {
5528                                        val = hsideCache[key] = hsides(element, true);
5529                                }
5530                                seg.hsides = val;
5531                        }
5532                }
5533        }
5534       
5535       
5536        function daySegSetWidths(segs) {
5537                var i;
5538                var segCnt = segs.length;
5539                var seg;
5540                var element;
5541                for (i=0; i<segCnt; i++) {
5542                        seg = segs[i];
5543                        element = seg.element;
5544                        if (element) {
5545                                element[0].style.width = Math.max(0, seg.outerWidth - seg.hsides) + 'px';
5546                        }
5547                }
5548        }
5549       
5550       
5551        function daySegCalcHeights(segs) {
5552                var i;
5553                var segCnt = segs.length;
5554                var seg;
5555                var element;
5556                var key, val;
5557                var vmarginCache = {};
5558                // record event heights
5559                for (i=0; i<segCnt; i++) {
5560                        seg = segs[i];
5561                        element = seg.element;
5562                        if (element) {
5563                                key = seg.key; // created in daySegCalcHSides
5564                                val = vmarginCache[key];
5565                                if (val === undefined) {
5566                                        val = vmarginCache[key] = vmargins(element);
5567                                }
5568                                seg.outerHeight = element[0].offsetHeight + val;
5569                        }
5570                }
5571        }
5572       
5573       
5574        function getRowDivs() {
5575                var i;
5576                var rowCnt = getRowCnt();
5577                var rowDivs = [];
5578                for (i=0; i<rowCnt; i++) {
5579                        rowDivs[i] = allDayRow(i)
5580                                .find('td:first div.fc-day-content > div'); // optimal selector?
5581                }
5582                return rowDivs;
5583        }
5584       
5585       
5586        function getRowTops(rowDivs) {
5587                var i;
5588                var rowCnt = rowDivs.length;
5589                var tops = [];
5590                for (i=0; i<rowCnt; i++) {
5591                        tops[i] = rowDivs[i][0].offsetTop; // !!?? but this means the element needs position:relative if in a table cell!!!!
5592                }
5593                return tops;
5594        }
5595       
5596       
5597        function daySegSetTops(segs, rowTops) { // also triggers eventAfterRender
5598                var i;
5599                var segCnt = segs.length;
5600                var seg;
5601                var element;
5602                var event;
5603                for (i=0; i<segCnt; i++) {
5604                        seg = segs[i];
5605                        element = seg.element;
5606                        if (element) {
5607                                element[0].style.top = rowTops[seg.row] + (seg.top||0) + 'px';
5608                                event = seg.event;
5609                                trigger('eventAfterRender', event, event, element);
5610                        }
5611                }
5612        }
5613       
5614       
5615       
5616        /* Resizing
5617        -----------------------------------------------------------------------------------*/
5618       
5619       
5620        function resizableDayEvent(event, element, seg) {
5621                var rtl = opt('isRTL');
5622                var direction = rtl ? 'w' : 'e';
5623                var handle = element.find('div.ui-resizable-' + direction);
5624                var isResizing = false;
5625               
5626                // TODO: look into using jquery-ui mouse widget for this stuff
5627                disableTextSelection(element); // prevent native <a> selection for IE
5628                element
5629                        .mousedown(function(ev) { // prevent native <a> selection for others
5630                                ev.preventDefault();
5631                        })
5632                        .click(function(ev) {
5633                                if (isResizing) {
5634                                        ev.preventDefault(); // prevent link from being visited (only method that worked in IE6)
5635                                        ev.stopImmediatePropagation(); // prevent fullcalendar eventClick handler from being called
5636                                                                       // (eventElementHandlers needs to be bound after resizableDayEvent)
5637                                }
5638                        });
5639               
5640                handle.mousedown(function(ev) {
5641                        if (ev.which != 1) {
5642                                return; // needs to be left mouse button
5643                        }
5644                        isResizing = true;
5645                        var hoverListener = t.getHoverListener();
5646                        var rowCnt = getRowCnt();
5647                        var colCnt = getColCnt();
5648                        var dis = rtl ? -1 : 1;
5649                        var dit = rtl ? colCnt-1 : 0;
5650                        var elementTop = element.css('top');
5651                        var dayDelta;
5652                        var helpers;
5653                        var eventCopy = $.extend({}, event);
5654                        var minCell = dateCell(event.start);
5655                        clearSelection();
5656                        $('body')
5657                                .css('cursor', direction + '-resize')
5658                                .one('mouseup', mouseup);
5659                        trigger('eventResizeStart', this, event, ev);
5660                        hoverListener.start(function(cell, origCell) {
5661                                if (cell) {
5662                                        var r = Math.max(minCell.row, cell.row);
5663                                        var c = cell.col;
5664                                        if (rowCnt == 1) {
5665                                                r = 0; // hack for all-day area in agenda views
5666                                        }
5667                                        if (r == minCell.row) {
5668                                                if (rtl) {
5669                                                        c = Math.min(minCell.col, c);
5670                                                }else{
5671                                                        c = Math.max(minCell.col, c);
5672                                                }
5673                                        }
5674                                        dayDelta = (r*7 + c*dis+dit) - (origCell.row*7 + origCell.col*dis+dit);
5675                                        var newEnd = addDays(eventEnd(event), dayDelta, true);
5676                                        if (dayDelta) {
5677                                                eventCopy.end = newEnd;
5678                                                var oldHelpers = helpers;
5679                                                helpers = renderTempDaySegs(compileDaySegs([eventCopy]), seg.row, elementTop);
5680                                                helpers.find('*').css('cursor', direction + '-resize');
5681                                                if (oldHelpers) {
5682                                                        oldHelpers.remove();
5683                                                }
5684                                                hideEvents(event);
5685                                        }else{
5686                                                if (helpers) {
5687                                                        showEvents(event);
5688                                                        helpers.remove();
5689                                                        helpers = null;
5690                                                }
5691                                        }
5692                                        clearOverlays();
5693                                        renderDayOverlay(event.start, addDays(cloneDate(newEnd), 1)); // coordinate grid already rebuild at hoverListener.start
5694                                }
5695                        }, ev);
5696                       
5697                        function mouseup(ev) {
5698                                trigger('eventResizeStop', this, event, ev);
5699                                $('body').css('cursor', '');
5700                                hoverListener.stop();
5701                                clearOverlays();
5702                                if (dayDelta) {
5703                                        eventResize(this, event, dayDelta, 0, ev);
5704                                        // event redraw will clear helpers
5705                                }
5706                                // otherwise, the drag handler already restored the old events
5707                               
5708                                setTimeout(function() { // make this happen after the element's click event
5709                                        isResizing = false;
5710                                },0);
5711                        }
5712                       
5713                });
5714        }
5715       
5716
5717}
5718
5719//BUG: unselect needs to be triggered when events are dragged+dropped
5720
5721function SelectionManager() {
5722        var t = this;
5723       
5724       
5725        // exports
5726        t.select = select;
5727        t.unselect = unselect;
5728        t.reportSelection = reportSelection;
5729        t.daySelectionMousedown = daySelectionMousedown;
5730       
5731       
5732        // imports
5733        var opt = t.opt;
5734        var trigger = t.trigger;
5735        var defaultSelectionEnd = t.defaultSelectionEnd;
5736        var renderSelection = t.renderSelection;
5737        var clearSelection = t.clearSelection;
5738       
5739       
5740        // locals
5741        var selected = false;
5742
5743
5744
5745        // unselectAuto
5746        if (opt('selectable') && opt('unselectAuto')) {
5747                $(document).mousedown(function(ev) {
5748                        var ignore = opt('unselectCancel');
5749                        if (ignore) {
5750                                if ($(ev.target).parents(ignore).length) { // could be optimized to stop after first match
5751                                        return;
5752                                }
5753                        }
5754                        unselect(ev);
5755                });
5756        }
5757       
5758
5759        function select(startDate, endDate, allDay) {
5760                unselect();
5761                if (!endDate) {
5762                        endDate = defaultSelectionEnd(startDate, allDay);
5763                }
5764                renderSelection(startDate, endDate, allDay);
5765                reportSelection(startDate, endDate, allDay);
5766        }
5767       
5768       
5769        function unselect(ev) {
5770                if (selected) {
5771                        selected = false;
5772                        clearSelection();
5773                        trigger('unselect', null, ev);
5774                }
5775        }
5776       
5777       
5778        function reportSelection(startDate, endDate, allDay, ev) {
5779                selected = true;
5780                trigger('select', null, startDate, endDate, allDay, ev);
5781        }
5782       
5783       
5784        function daySelectionMousedown(ev) { // not really a generic manager method, oh well
5785                var cellDate = t.cellDate;
5786                var cellIsAllDay = t.cellIsAllDay;
5787                var hoverListener = t.getHoverListener();
5788                var reportDayClick = t.reportDayClick; // this is hacky and sort of weird
5789                if (ev.which == 1 && opt('selectable')) { // which==1 means left mouse button
5790                        unselect(ev);
5791                        var _mousedownElement = this;
5792                        var dates;
5793                        hoverListener.start(function(cell, origCell) { // TODO: maybe put cellDate/cellIsAllDay info in cell
5794                                clearSelection();
5795                                if (cell && cellIsAllDay(cell)) {
5796                                        dates = [ cellDate(origCell), cellDate(cell) ].sort(cmp);
5797                                        renderSelection(dates[0], dates[1], true);
5798                                }else{
5799                                        dates = null;
5800                                }
5801                        }, ev);
5802                        $(document).one('mouseup', function(ev) {
5803                                hoverListener.stop();
5804                                if (dates) {
5805                                        if (+dates[0] == +dates[1]) {
5806                                                reportDayClick(dates[0], true, ev);
5807                                        }
5808                                        reportSelection(dates[0], dates[1], true, ev);
5809                                }
5810                        });
5811                }
5812        }
5813
5814
5815}
5816 
5817function OverlayManager() {
5818        var t = this;
5819       
5820       
5821        // exports
5822        t.renderOverlay = renderOverlay;
5823        t.clearOverlays = clearOverlays;
5824       
5825       
5826        // locals
5827        var usedOverlays = [];
5828        var unusedOverlays = [];
5829       
5830       
5831        function renderOverlay(rect, parent) {
5832                var e = unusedOverlays.shift();
5833                if (!e) {
5834                        e = $("<div class='fc-cell-overlay' style='position:absolute;z-index:3'/>");
5835                }
5836                if (e[0].parentNode != parent[0]) {
5837                        e.appendTo(parent);
5838                }
5839                usedOverlays.push(e.css(rect).show());
5840                return e;
5841        }
5842       
5843
5844        function clearOverlays() {
5845                var e;
5846                while (e = usedOverlays.shift()) {
5847                        unusedOverlays.push(e.hide().unbind());
5848                }
5849        }
5850
5851
5852}
5853
5854function CoordinateGrid(buildFunc) {
5855
5856        var t = this;
5857        var rows;
5858        var cols;
5859       
5860       
5861        t.build = function() {
5862                rows = [];
5863                cols = [];
5864                buildFunc(rows, cols);
5865        };
5866       
5867       
5868        t.cell = function(x, y) {
5869                var rowCnt = rows.length;
5870                var colCnt = cols.length;
5871                var i, r=-1, c=-1;
5872                for (i=0; i<rowCnt; i++) {
5873                        if (y >= rows[i][0] && y < rows[i][1]) {
5874                                r = i;
5875                                break;
5876                        }
5877                }
5878                for (i=0; i<colCnt; i++) {
5879                        if (x >= cols[i][0] && x < cols[i][1]) {
5880                                c = i;
5881                                break;
5882                        }
5883                }
5884                return (r>=0 && c>=0) ? { row:r, col:c } : null;
5885        };
5886       
5887       
5888        t.rect = function(row0, col0, row1, col1, originElement) { // row1,col1 is inclusive
5889                var origin = originElement.offset();
5890                return {
5891                        top: rows[row0][0] - origin.top,
5892                        left: cols[col0][0] - origin.left,
5893                        width: cols[col1][1] - cols[col0][0],
5894                        height: rows[row1][1] - rows[row0][0]
5895                };
5896        };
5897
5898}
5899
5900function HoverListener(coordinateGrid) {
5901
5902
5903        var t = this;
5904        var bindType;
5905        var change;
5906        var firstCell;
5907        var cell;
5908       
5909       
5910        t.start = function(_change, ev, _bindType) {
5911                change = _change;
5912                firstCell = cell = null;
5913                coordinateGrid.build();
5914                mouse(ev);
5915                bindType = _bindType || 'mousemove';
5916                $(document).bind(bindType, mouse);
5917        };
5918       
5919       
5920        function mouse(ev) {
5921                var newCell = coordinateGrid.cell(ev.pageX, ev.pageY);
5922                if (!newCell != !cell || newCell && (newCell.row != cell.row || newCell.col != cell.col)) {
5923                        if (newCell) {
5924                                if (!firstCell) {
5925                                        firstCell = newCell;
5926                                }
5927                                change(newCell, firstCell, newCell.row-firstCell.row, newCell.col-firstCell.col);
5928                        }else{
5929                                change(newCell, firstCell);
5930                        }
5931                        cell = newCell;
5932                }
5933        }
5934       
5935       
5936        t.stop = function() {
5937                $(document).unbind(bindType, mouse);
5938                return cell;
5939        };
5940       
5941       
5942}
5943
5944function HorizontalPositionCache(getElement) {
5945
5946        var t = this,
5947                elements = {},
5948                lefts = {},
5949                rights = {};
5950               
5951        function e(i) {
5952                return elements[i] = elements[i] || getElement(i);
5953        }
5954       
5955        t.left = function(i) {
5956                return lefts[i] = lefts[i] === undefined ? e(i).position().left : lefts[i];
5957        };
5958       
5959        t.right = function(i) {
5960                return rights[i] = rights[i] === undefined ? t.left(i) + e(i).width() : rights[i];
5961        };
5962       
5963        t.clear = function() {
5964                elements = {};
5965                lefts = {};
5966                rights = {};
5967        };
5968       
5969}
5970
5971})(jQuery);
Note: See TracBrowser for help on using the repository browser.