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