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