1$(document).ready(function () { 2 /** 3 * @preserve 4 * FullCalendar v1.5.1 5 * http://arshaw.com/fullcalendar/ 6 * 7 * Use fullcalendar.css for basic styling. 8 * For event drag & drop, requires jQuery UI draggable. 9 * For event resizing, requires jQuery UI resizable. 10 * 11 * Copyright (c) 2011 Adam Shaw 12 * Dual licensed under the MIT and GPL licenses, located in 13 * MIT-LICENSE.txt and GPL-LICENSE.txt respectively. 14 * 15 * Date: Sat Apr 9 14:09:51 2011 -0700 16 * 17 */ 18 19 (function($, undefined) { 20 21 22 var defaults = { 23 24 // display 25 defaultView: 'month', 26 aspectRatio: 1.35, 27 header: { 28 left: 'title', 29 center: '', 30 right: 'today prev,next' 31 }, 32 weekends: true, 33 34 // editing 35 //editable: false, 36 //disableDragging: false, 37 //disableResizing: false, 38 39 allDayDefault: true, 40 ignoreTimezone: true, 41 42 // event ajax 43 lazyFetching: true, 44 startParam: 'start', 45 endParam: 'end', 46 47 // time formats 48 titleFormat: { 49 month: 'MMMM yyyy', 50 week: "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", 51 day: 'dddd, MMM d, yyyy' 52 }, 53 columnFormat: { 54 month: 'ddd', 55 week: 'ddd M/d', 56 day: 'dddd M/d' 57 }, 58 timeFormat: { // for event elements 59 '': 'h(:mm)t' // default 60 }, 61 62 // locale 63 isRTL: false, 64 firstDay: 0, 65 monthNames: ['January','February','March','April','May','June','July','August','September','October','November','December'], 66 monthNamesShort: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'], 67 dayNames: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'], 68 dayNamesShort: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'], 69 buttonText: { 70 prev: ' ◄ ', 71 next: ' ► ', 72 prevYear: ' << ', 73 nextYear: ' >> ', 74 today: 'today', 75 month: 'month', 76 week: 'week', 77 day: 'day' 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.1" }; 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> </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 1387 // TODO: never return invalid dates (like from new Date(<string>)), return null instead 1388 return null; 1389 } 1390 1391 1392 function parseISO8601(s, ignoreTimezone) { // ignoreTimezone defaults to false 1393 // derived from http://delete.me.uk/2005/03/iso8601.html 1394 // TODO: for a know glitch/feature, read tests/issue_206_parseDate_dst.html 1395 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}))?))?)?)?)?$/); 1396 if (!m) { 1397 return null; 1398 } 1399 var date = new Date(m[1], 0, 1); 1400 if (ignoreTimezone || !m[14]) { 1401 var check = new Date(m[1], 0, 1, 9, 0); 1402 if (m[3]) { 1403 date.setMonth(m[3] - 1); 1404 check.setMonth(m[3] - 1); 1405 } 1406 if (m[5]) { 1407 date.setDate(m[5]); 1408 check.setDate(m[5]); 1409 } 1410 fixDate(date, check); 1411 if (m[7]) { 1412 date.setHours(m[7]); 1413 } 1414 if (m[8]) { 1415 date.setMinutes(m[8]); 1416 } 1417 if (m[10]) { 1418 date.setSeconds(m[10]); 1419 } 1420 if (m[12]) { 1421 date.setMilliseconds(Number("0." + m[12]) * 1000); 1422 } 1423 fixDate(date, check); 1424 }else{ 1425 date.setUTCFullYear( 1426 m[1], 1427 m[3] ? m[3] - 1 : 0, 1428 m[5] || 1 1429 ); 1430 date.setUTCHours( 1431 m[7] || 0, 1432 m[8] || 0, 1433 m[10] || 0, 1434 m[12] ? Number("0." + m[12]) * 1000 : 0 1435 ); 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 return date; 1441 } 1442 1443 1444 function parseTime(s) { // returns minutes since start of day 1445 if (typeof s == 'number') { // an hour 1446 return s * 60; 1447 } 1448 if (typeof s == 'object') { // a Date object 1449 return s.getHours() * 60 + s.getMinutes(); 1450 } 1451 var m = s.match(/(\d+)(?::(\d+))?\s*(\w+)?/); 1452 if (m) { 1453 var h = parseInt(m[1], 10); 1454 if (m[3]) { 1455 h %= 12; 1456 if (m[3].toLowerCase().charAt(0) == 'p') { 1457 h += 12; 1458 } 1459 } 1460 return h * 60 + (m[2] ? parseInt(m[2], 10) : 0); 1461 } 1462 } 1463 1464 1465 1466 /* Date Formatting 1467 -----------------------------------------------------------------------------*/ 1468 // TODO: use same function formatDate(date, [date2], format, [options]) 1469 1470 1471 function formatDate(date, format, options) { 1472 return formatDates(date, null, format, options); 1473 } 1474 1475 1476 function formatDates(date1, date2, format, options) { 1477 options = options || defaults; 1478 var date = date1, 1479 otherDate = date2, 1480 i, len = format.length, c, 1481 i2, formatter, 1482 res = ''; 1483 for (i=0; i<len; i++) { 1484 c = format.charAt(i); 1485 if (c == "'") { 1486 for (i2=i+1; i2<len; i2++) { 1487 if (format.charAt(i2) == "'") { 1488 if (date) { 1489 if (i2 == i+1) { 1490 res += "'"; 1491 }else{ 1492 res += format.substring(i+1, i2); 1493 } 1494 i = i2; 1495 } 1496 break; 1497 } 1498 } 1499 } 1500 else if (c == '(') { 1501 for (i2=i+1; i2<len; i2++) { 1502 if (format.charAt(i2) == ')') { 1503 var subres = formatDate(date, format.substring(i+1, i2), options); 1504 if (parseInt(subres.replace(/\D/, ''), 10)) { 1505 res += subres; 1506 } 1507 i = i2; 1508 break; 1509 } 1510 } 1511 } 1512 else if (c == '[') { 1513 for (i2=i+1; i2<len; i2++) { 1514 if (format.charAt(i2) == ']') { 1515 var subformat = format.substring(i+1, i2); 1516 var subres = formatDate(date, subformat, options); 1517 if (subres != formatDate(otherDate, subformat, options)) { 1518 res += subres; 1519 } 1520 i = i2; 1521 break; 1522 } 1523 } 1524 } 1525 else if (c == '{') { 1526 date = date2; 1527 otherDate = date1; 1528 } 1529 else if (c == '}') { 1530 date = date1; 1531 otherDate = date2; 1532 } 1533 else { 1534 for (i2=len; i2>i; i2--) { 1535 if (formatter = dateFormatters[format.substring(i, i2)]) { 1536 if (date) { 1537 res += formatter(date, options); 1538 } 1539 i = i2 - 1; 1540 break; 1541 } 1542 } 1543 if (i2 == i) { 1544 if (date) { 1545 res += c; 1546 } 1547 } 1548 } 1549 } 1550 return res; 1551 }; 1552 1553 1554 var dateFormatters = { 1555 s : function(d) { return d.getSeconds() }, 1556 ss : function(d) { return zeroPad(d.getSeconds()) }, 1557 m : function(d) { return d.getMinutes() }, 1558 mm : function(d) { return zeroPad(d.getMinutes()) }, 1559 h : function(d) { return d.getHours() % 12 || 12 }, 1560 hh : function(d) { return zeroPad(d.getHours() % 12 || 12) }, 1561 H : function(d) { return d.getHours() }, 1562 HH : function(d) { return zeroPad(d.getHours()) }, 1563 d : function(d) { return d.getDate() }, 1564 dd : function(d) { return zeroPad(d.getDate()) }, 1565 ddd : function(d,o) { return o.dayNamesShort[d.getDay()] }, 1566 dddd: function(d,o) { return o.dayNames[d.getDay()] }, 1567 M : function(d) { return d.getMonth() + 1 }, 1568 MM : function(d) { return zeroPad(d.getMonth() + 1) }, 1569 MMM : function(d,o) { return o.monthNamesShort[d.getMonth()] }, 1570 MMMM: function(d,o) { return o.monthNames[d.getMonth()] }, 1571 yy : function(d) { return (d.getFullYear()+'').substring(2) }, 1572 yyyy: function(d) { return d.getFullYear() }, 1573 t : function(d) { return d.getHours() < 12 ? 'a' : 'p' }, 1574 tt : function(d) { return d.getHours() < 12 ? 'am' : 'pm' }, 1575 T : function(d) { return d.getHours() < 12 ? 'A' : 'P' }, 1576 TT : function(d) { return d.getHours() < 12 ? 'AM' : 'PM' }, 1577 u : function(d) { return formatDate(d, "yyyy-MM-dd'T'HH:mm:ss'Z'") }, 1578 S : function(d) { 1579 var date = d.getDate(); 1580 if (date > 10 && date < 20) { 1581 return 'th'; 1582 } 1583 return ['st', 'nd', 'rd'][date%10-1] || 'th'; 1584 } 1585 }; 1586 1587 1588 1589 fc.applyAll = applyAll; 1590 1591 1592 /* Event Date Math 1593 -----------------------------------------------------------------------------*/ 1594 1595 1596 function exclEndDay(event) {
1597 if (event.end) { 1598 return _exclEndDay(event.end, event.allDay); 1599 }else{ 1600 return addDays(cloneDate(event.start), 1); 1601 } 1602 } 1603 1604 1605 function _exclEndDay(end, allDay) { 1606 end = cloneDate(end); 1607 return allDay || end.getHours() || end.getMinutes() ? addDays(end, 1) : clearTime(end); 1608 } 1609 1610 1611 function segCmp(a, b) { 1612 return (b.msLength - a.msLength) * 100 + (a.event.start - b.event.start); 1613 } 1614 1615 1616 function segsCollide(seg1, seg2) { 1617 return seg1.end > seg2.start && seg1.start < seg2.end; 1618 } 1619 1620 1621 1622 /* Event Sorting 1623 -----------------------------------------------------------------------------*/ 1624 1625 1626 // event rendering utilities 1627 function sliceSegs(events, visEventEnds, start, end) { 1628 var segs = [], 1629 i, len=events.length, event, 1630 eventStart, eventEnd, 1631 segStart, segEnd, 1632 isStart, isEnd; 1633 for (i=0; i<len; i++) { 1634 event = events[i]; 1635 eventStart = event.start; 1636 eventEnd = visEventEnds[i]; 1637 if (eventEnd > start && eventStart < end) { 1638 if (eventStart < start) { 1639 segStart = cloneDate(start); 1640 isStart = false;
1641 }else{ 1642 segStart = eventStart; 1643 isStart = true; 1644 } 1645 if (eventEnd > end) { 1646 segEnd = cloneDate(end); 1647 isEnd = false; 1648 }else{ 1649 segEnd = eventEnd; 1650 isEnd = true; 1651 } 1652 segs.push({ 1653 event: event, 1654 start: segStart, 1655 end: segEnd, 1656 isStart: isStart, 1657 isEnd: isEnd, 1658 msLength: segEnd - segStart 1659 }); 1660 } 1661 } 1662 return segs.sort(segCmp); 1663 } 1664 1665 1666 // event rendering calculation utilities 1667 function stackSegs(segs) { 1668 var levels = [], 1669 i, len = segs.length, seg, 1670 j, collide, k; 1671 for (i=0; i<len; i++) { 1672 seg = segs[i]; 1673 j = 0; // the level index where seg should belong 1674 while (true) { 1675 collide = false; 1676 if (levels[j]) { 1677 for (k=0; k<levels[j].length; k++) { 1678 if (segsCollide(levels[j][k], seg)) { 1679 collide = true; 1680 break; 1681 } 1682 } 1683 } 1684 if (collide) { 1685 j++; 1686 }else{ 1687 break; 1688 } 1689 } 1690 if (levels[j]) { 1691 levels[j].push(seg); 1692 }else{ 1693 levels[j] = [seg]; 1694 } 1695 } 1696 return levels; 1697 } 1698 1699 1700 1701 /* Event Element Binding 1702 -----------------------------------------------------------------------------*/ 1703 1704 1705 function lazySegBind(container, segs, bindHandlers) { 1706 container.unbind('mouseover').mouseover(function(ev) { 1707 var parent=ev.target, e, 1708 i, seg; 1709 while (parent != this) { 1710 e = parent; 1711 parent = parent.parentNode; 1712 } 1713 if ((i = e._fci) !== undefined) { 1714 e._fci = undefined; 1715 seg = segs[i]; 1716 bindHandlers(seg.event, seg.element, seg); 1717 $(ev.target).trigger(ev); 1718 } 1719 ev.stopPropagation(); 1720 }); 1721 } 1722 1723 1724 1725 /* Element Dimensions 1726 -----------------------------------------------------------------------------*/ 1727 1728 1729 function setOuterWidth(element, width, includeMargins) { 1730 for (var i=0, e; i<element.length; i++) { 1731 e = $(element[i]); 1732 e.width(Math.max(0, width - hsides(e, includeMargins))); 1733 } 1734 } 1735 1736 1737 function setOuterHeight(element, height, includeMargins) { 1738 for (var i=0, e; i<element.length; i++) { 1739 e = $(element[i]); 1740 e.height(Math.max(0, height - vsides(e, includeMargins))); 1741 } 1742 } 1743 1744 1745 // TODO: curCSS has been deprecated (jQuery 1.4.3 - 10/16/2010) 1746 1747 1748 function hsides(element, includeMargins) { 1749 return hpadding(element) + hborders(element) + (includeMargins ? hmargins(element) : 0); 1750 } 1751 1752 1753 function hpadding(element) { 1754 return (parseFloat($.curCSS(element[0], 'paddingLeft', true)) || 0) + 1755 (parseFloat($.curCSS(element[0], 'paddingRight', true)) || 0); 1756 } 1757 1758 1759 function hmargins(element) { 1760 return (parseFloat($.curCSS(element[0], 'marginLeft', true)) || 0) + 1761 (parseFloat($.curCSS(element[0], 'marginRight', true)) || 0); 1762 } 1763 1764 1765 function hborders(element) { 1766 return (parseFloat($.curCSS(element[0], 'borderLeftWidth', true)) || 0) + 1767 (parseFloat($.curCSS(element[0], 'borderRightWidth', true)) || 0); 1768 } 1769 1770 1771 function vsides(element, includeMargins) { 1772 return vpadding(element) + vborders(element) + (includeMargins ? vmargins(element) : 0); 1773 } 1774 1775 1776 function vpadding(element) { 1777 return (parseFloat($.curCSS(element[0], 'paddingTop', true)) || 0) + 1778 (parseFloat($.curCSS(element[0], 'paddingBottom', true)) || 0); 1779 } 1780 1781 1782 function vmargins(element) { 1783 return (parseFloat($.curCSS(element[0], 'marginTop', true)) || 0) + 1784 (parseFloat($.curCSS(element[0], 'marginBottom', true)) || 0); 1785 } 1786 1787 1788 function vborders(element) { 1789 return (parseFloat($.curCSS(element[0], 'borderTopWidth', true)) || 0) + 1790 (parseFloat($.curCSS(element[0], 'borderBottomWidth', true)) || 0); 1791 } 1792 1793 1794 function setMinHeight(element, height) { 1795 height = (typeof height == 'number' ? height + 'px' : height); 1796 element.each(function(i, _element) { 1797 _element.style.cssText += ';min-height:' + height + ';_height:' + height; 1798 // why can't we just use .css() ? i forget 1799 }); 1800 } 1801 1802 1803 1804 /* Misc Utils 1805 -----------------------------------------------------------------------------*/ 1806 1807 1808 //TODO: arraySlice 1809 //TODO: isFunction, grep ? 1810 1811 1812 function noop() { } 1813 1814 1815 function cmp(a, b) { 1816 return a - b; 1817 } 1818 1819 1820 function arrayMax(a) { 1821 return Math.max.apply(Math, a); 1822 } 1823 1824 1825 function zeroPad(n) { 1826 return (n < 10 ? '0' : '') + n; 1827 } 1828 1829 1830 function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object 1831 if (obj[name] !== undefined) { 1832 return obj[name]; 1833 } 1834 var parts = name.split(/(?=[A-Z])/), 1835 i=parts.length-1, res; 1836 for (; i>=0; i--) { 1837 res = obj[parts[i].toLowerCase()]; 1838 if (res !== undefined) { 1839 return res; 1840 } 1841 } 1842 return obj['']; 1843 } 1844 1845 1846 function htmlEscape(s) { 1847 return s.replace(/&/g, '&') 1848 .replace(/</g, '<') 1849 .replace(/>/g, '>') 1850 .replace(/'/g, ''') 1851 .replace(/"/g, '"') 1852 .replace(/\n/g, '<br />'); 1853 } 1854 1855 1856 function cssKey(_element) {
1857 return _element.id + '/' + _element.className + '/' + _element.style.cssText.replace(/(^|;)\s*(top|left|width|height)\s*:[^;]*/ig, ''); 1858 } 1859 1860 1861 function disableTextSelection(element) { 1862 element 1863 .attr('unselectable', 'on') 1864 .css('MozUserSelect', 'none') 1865 .bind('selectstart.ui', function() { return false; }); 1866 } 1867 1868 1869 /* 1870 function enableTextSelection(element) { 1871 element 1872 .attr('unselectable', 'off') 1873 .css('MozUserSelect', '') 1874 .unbind('selectstart.ui'); 1875 } 1876 */ 1877 1878 1879 function markFirstLast(e) { 1880 e.children() 1881 .removeClass('fc-first fc-last') 1882 .filter(':first-child') 1883 .addClass('fc-first') 1884 .end() 1885 .filter(':last-child') 1886 .addClass('fc-last'); 1887 } 1888 1889 1890 function setDayID(cell, date) { 1891 cell.each(function(i, _cell) { 1892 _cell.className = _cell.className.replace(/^fc-\w*/, 'fc-' + dayIDs[date.getDay()]); 1893 // TODO: make a way that doesn't rely on order of classes 1894 }); 1895 } 1896 1897 1898 function getSkinCss(event, opt) { 1899 var source = event.source || {}; 1900 var eventColor = event.color; 1901 var sourceColor = source.color; 1902 var optionColor = opt('eventColor'); 1903 var backgroundColor = 1904 event.backgroundColor || 1905 eventColor || 1906 source.backgroundColor || 1907 sourceColor || 1908 opt('eventBackgroundColor') || 1909 optionColor; 1910 var borderColor = 1911 event.borderColor || 1912 eventColor || 1913 source.borderColor || 1914 sourceColor || 1915 opt('eventBorderColor') || 1916 optionColor; 1917 var textColor = 1918 event.textColor || 1919 source.textColor || 1920 opt('eventTextColor'); 1921 var statements = []; 1922 if (backgroundColor) { 1923 statements.push('background-color:' + backgroundColor); 1924 } 1925 if (borderColor) { 1926 statements.push('border-color:' + borderColor); 1927 } 1928 if (textColor) { 1929 statements.push('color:' + textColor); 1930 } 1931 return statements.join(';'); 1932 } 1933 1934 1935 function applyAll(functions, thisObj, args) { 1936 if ($.isFunction(functions)) { 1937 functions = [ functions ]; 1938 } 1939 if (functions) { 1940 var i; 1941 var ret; 1942 for (i=0; i<functions.length; i++) { 1943 ret = functions[i].apply(thisObj, args) || ret; 1944 } 1945 return ret; 1946 } 1947 } 1948 1949 1950 function firstDefined() { 1951 for (var i=0; i<arguments.length; i++) { 1952 if (arguments[i] !== undefined) { 1953 return arguments[i]; 1954 } 1955 } 1956 } 1957 1958 1959 1960 fcViews.month = MonthView; 1961 1962 function MonthView(element, calendar) { 1963 var t = this; 1964 1965 1966 // exports 1967 t.render = render; 1968 1969 1970 // imports 1971 BasicView.call(t, element, calendar, 'month'); 1972 var opt = t.opt; 1973 var renderBasic = t.renderBasic; 1974 var formatDate = calendar.formatDate; 1975 1976 1977 1978 function render(date, delta) { 1979 if (delta) { 1980 addMonths(date, delta); 1981 date.setDate(1); 1982 } 1983 var start = cloneDate(date, true); 1984 start.setDate(1); 1985 var end = addMonths(cloneDate(start), 1); 1986 var visStart = cloneDate(start); 1987 var visEnd = cloneDate(end); 1988 var firstDay = opt('firstDay'); 1989 var nwe = opt('weekends') ? 0 : 1; 1990 if (nwe) { 1991 skipWeekend(visStart); 1992 skipWeekend(visEnd, -1, true); 1993 } 1994 addDays(visStart, -((visStart.getDay() - Math.max(firstDay, nwe) + 7) % 7)); 1995 addDays(visEnd, (7 - visEnd.getDay() + Math.max(firstDay, nwe)) % 7); 1996 var rowCnt = Math.round((visEnd - visStart) / (DAY_MS * 7)); 1997 if (opt('weekMode') == 'fixed') { 1998 addDays(visEnd, (6 - rowCnt) * 7); 1999 rowCnt = 6; 2000 } 2001 t.title = formatDate(start, opt('titleFormat')); 2002 t.start = start; 2003 t.end = end; 2004 t.visStart = visStart; 2005 t.visEnd = visEnd; 2006 renderBasic(6, rowCnt, nwe ? 5 : 7, true); 2007 } 2008 2009 2010 } 2011 2012 fcViews.basicWeek = BasicWeekView; 2013 2014 function BasicWeekView(element, calendar) { 2015 var t = this; 2016 2017 2018 // exports 2019 t.render = render; 2020 2021 2022 // imports 2023 BasicView.call(t, element, calendar, 'basicWeek'); 2024 var opt = t.opt; 2025 var renderBasic = t.renderBasic; 2026 var formatDates = calendar.formatDates; 2027 2028 2029 2030 function render(date, delta) { 2031 if (delta) { 2032 addDays(date, delta * 7); 2033 } 2034 var start = addDays(cloneDate(date), -((date.getDay() - opt('firstDay') + 7) % 7)); 2035 var end = addDays(cloneDate(start), 7); 2036 var visStart = cloneDate(start); 2037 var visEnd = cloneDate(end); 2038 var weekends = opt('weekends'); 2039 if (!weekends) { 2040 skipWeekend(visStart); 2041 skipWeekend(visEnd, -1, true); 2042 } 2043 t.title = formatDates( 2044 visStart, 2045 addDays(cloneDate(visEnd), -1), 2046 opt('titleFormat') 2047 ); 2048 t.start = start; 2049 t.end = end; 2050 t.visStart = visStart; 2051 t.visEnd = visEnd; 2052 renderBasic(1, 1, weekends ? 7 : 5, false); 2053 } 2054 2055 2056 } 2057 2058 fcViews.basicDay = BasicDayView; 2059 2060 //TODO: when calendar's date starts out on a weekend, shouldn't happen 2061 2062 2063 function BasicDayView(element, calendar) { 2064 var t = this; 2065
2066 2067 // exports 2068 t.render = render; 2069 2070 2071 // imports 2072 BasicView.call(t, element, calendar, 'basicDay'); 2073 var opt = t.opt; 2074 var renderBasic = t.renderBasic; 2075 var formatDate = calendar.formatDate; 2076 2077 2078 2079 function render(date, delta) { 2080 if (delta) { 2081 addDays(date, delta); 2082 if (!opt('weekends')) { 2083 skipWeekend(date, delta < 0 ? -1 : 1); 2084 } 2085 } 2086 t.title = formatDate(date, opt('titleFormat')); 2087 t.start = t.visStart = cloneDate(date, true); 2088 t.end = t.visEnd = addDays(cloneDate(t.start), 1); 2089 renderBasic(1, 1, 1, false); 2090 } 2091 2092 2093 } 2094 2095 setDefaults({ 2096 weekMode: 'fixed' 2097 }); 2098 2099 2100 function BasicView(element, calendar, viewName) { 2101 var t = this; 2102 2103 2104 // exports 2105 t.renderBasic = renderBasic; 2106 t.setHeight = setHeight; 2107 t.setWidth = setWidth; 2108 t.renderDayOverlay = renderDayOverlay; 2109 t.defaultSelectionEnd = defaultSelectionEnd; 2110 t.renderSelection = renderSelection; 2111 t.clearSelection = clearSelection; 2112 t.reportDayClick = reportDayClick; // for selection (kinda hacky) 2113 t.dragStart = dragStart; 2114 t.dragStop = dragStop; 2115 t.defaultEventEnd = defaultEventEnd; 2116 t.getHoverListener = function() { return hoverListener }; 2117 t.colContentLeft = colContentLeft; 2118 t.colContentRight = colContentRight; 2119 t.dayOfWeekCol = dayOfWeekCol; 2120 t.dateCell = dateCell; 2121 t.cellDate = cellDate; 2122 t.cellIsAllDay = function() { return true }; 2123 t.allDayRow = allDayRow; 2124 t.allDayBounds = allDayBounds; 2125 t.getRowCnt = function() { return rowCnt }; 2126 t.getColCnt = function() { return colCnt }; 2127 t.getColWidth = function() { return colWidth }; 2128 t.getDaySegmentContainer = function() { return daySegmentContainer }; 2129 2130 2131 // imports 2132 View.call(t, element, calendar, viewName); 2133 OverlayManager.call(t); 2134 SelectionManager.call(t); 2135 BasicEventRenderer.call(t); 2136 var opt = t.opt; 2137 var trigger = t.trigger; 2138 var clearEvents = t.clearEvents; 2139 var renderOverlay = t.renderOverlay; 2140 var clearOverlays = t.clearOverlays; 2141 var daySelectionMousedown = t.daySelectionMousedown; 2142 var formatDate = calendar.formatDate; 2143 2144 2145 // locals 2146 2147 var head; 2148 var headCells; 2149 var body; 2150 var bodyRows; 2151 var bodyCells; 2152 var bodyFirstCells; 2153 var bodyCellTopInners; 2154 var daySegmentContainer; 2155 2156 var viewWidth; 2157 var viewHeight; 2158 var colWidth; 2159 2160 var rowCnt, colCnt; 2161 var coordinateGrid; 2162 var hoverListener; 2163 var colContentPositions; 2164 2165 var rtl, dis, dit; 2166 var firstDay; 2167 var nwe; 2168 var tm; 2169 var colFormat; 2170 2171 2172 2173 /* Rendering 2174 ------------------------------------------------------------*/ 2175 2176 2177 disableTextSelection(element.addClass('fc-grid')); 2178 2179 2180 function renderBasic(maxr, r, c, showNumbers) { 2181 rowCnt = r; 2182 colCnt = c; 2183 updateOptions(); 2184 var firstTime = !body; 2185 if (firstTime) { 2186 buildSkeleton(maxr, showNumbers); 2187 }else{ 2188 clearEvents(); 2189 } 2190 updateCells(firstTime); 2191 } 2192 2193 2194 2195 function updateOptions() { 2196 rtl = opt('isRTL'); 2197 if (rtl) { 2198 dis = -1; 2199 dit = colCnt - 1; 2200 }else{ 2201 dis = 1; 2202 dit = 0; 2203 } 2204 firstDay = opt('firstDay'); 2205 nwe = opt('weekends') ? 0 : 1; 2206 tm = opt('theme') ? 'ui' : 'fc'; 2207 colFormat = opt('columnFormat'); 2208 } 2209 2210 2211 2212 function buildSkeleton(maxRowCnt, showNumbers) { 2213 var s; 2214 var headerClass = tm + "-widget-header"; 2215 var contentClass = tm + "-widget-content"; 2216 var i, j; 2217 var table; 2218 2219 s = 2220 "<table class='fc-border-separate' style='width:100%' cellspacing='0'>" + 2221 "<thead>" + 2222 "<tr>"; 2223 for (i=0; i<colCnt; i++) { 2224 s += 2225 "<th class='fc- " + headerClass + "'/>"; // need fc- for setDayID 2226 } 2227 s += 2228 "</tr>" + 2229 "</thead>" + 2230 "<tbody>"; 2231 for (i=0; i<maxRowCnt; i++) { 2232 s += 2233 "<tr class='fc-week" + i + "'>"; 2234 for (j=0; j<colCnt; j++) { 2235 s += 2236 "<td class='fc- " + contentClass + " fc-day" + (i*colCnt+j) + "'>" + // need fc- for setDayID 2237 "<div>" + 2238 (showNumbers ? 2239 "<div class='fc-day-number'/>" : 2240 '' 2241 ) + 2242 "<div class='fc-day-content'>" + 2243 "<div style='position:relative'> </div>" + 2244 "</div>" + 2245 "</div>" + 2246 "</td>"; 2247 } 2248 s += 2249 "</tr>"; 2250 } 2251 s += 2252 "</tbody>" + 2253 "</table>"; 2254 table = $(s).appendTo(element); 2255 2256 head = table.find('thead'); 2257 headCells = head.find('th'); 2258 body = table.find('tbody'); 2259 bodyRows = body.find('tr'); 2260 bodyCells = body.find('td'); 2261 bodyFirstCells = bodyCells.filter(':first-child'); 2262 bodyCellTopInners = bodyRows.eq(0).find('div.fc-day-content div'); 2263 2264 markFirstLast(head.add(head.find('tr'))); // marks first+last tr/th's 2265 markFirstLast(bodyRows); // marks first+last td's 2266 bodyRows.eq(0).addClass('fc-first'); // fc-last is done in updateCells 2267 2268 dayBind(bodyCells); 2269 2270 daySegmentContainer = 2271 $("<div style='position:absolute;z-index:8;top:0;left:0'/>") 2272 .appendTo(element); 2273 } 2274 2275 2276 2277 function updateCells(firstTime) { 2278 var dowDirty = firstTime || rowCnt == 1; // could the cells' day-of-weeks need updating?
2279 var month = t.start.getMonth(); 2280 var today = clearTime(new Date()); 2281 var cell; 2282 var date; 2283 var row; 2284 2285 if (dowDirty) { 2286 headCells.each(function(i, _cell) { 2287 cell = $(_cell); 2288 date = indexDate(i); 2289 cell.html(formatDate(date, colFormat)); 2290 setDayID(cell, date); 2291 }); 2292 } 2293 2294 bodyCells.each(function(i, _cell) { 2295 cell = $(_cell); 2296 date = indexDate(i); 2297 if (date.getMonth() == month) { 2298 cell.removeClass('fc-other-month'); 2299 }else{ 2300 cell.addClass('fc-other-month'); 2301 } 2302 if (+date == +today) { 2303 cell.addClass(tm + '-state-highlight fc-today'); 2304 }else{ 2305 cell.removeClass(tm + '-state-highlight fc-today'); 2306 } 2307 cell.find('div.fc-day-number').text(date.getDate()); 2308 if (dowDirty) { 2309 setDayID(cell, date); 2310 } 2311 }); 2312 2313 bodyRows.each(function(i, _row) { 2314 row = $(_row); 2315 if (i < rowCnt) { 2316 row.show(); 2317 if (i == rowCnt-1) { 2318 row.addClass('fc-last'); 2319 }else{ 2320 row.removeClass('fc-last'); 2321 } 2322 }else{ 2323 row.hide(); 2324 } 2325 }); 2326 } 2327 2328 2329 2330 function setHeight(height) { 2331 viewHeight = height; 2332 2333 var bodyHeight = viewHeight - head.height(); 2334 var rowHeight; 2335 var rowHeightLast; 2336 var cell; 2337 2338 if (opt('weekMode') == 'variable') { 2339 rowHeight = rowHeightLast = Math.floor(bodyHeight / (rowCnt==1 ? 2 : 6)); 2340 }else{ 2341 rowHeight = Math.floor(bodyHeight / rowCnt); 2342 rowHeightLast = bodyHeight - rowHeight * (rowCnt-1); 2343 } 2344 2345 bodyFirstCells.each(function(i, _cell) { 2346 if (i < rowCnt) { 2347 cell = $(_cell); 2348 setMinHeight( 2349 cell.find('> div'), 2350 (i==rowCnt-1 ? rowHeightLast : rowHeight) - vsides(cell) 2351 ); 2352 } 2353 }); 2354 2355 } 2356 2357 2358 function setWidth(width) { 2359 viewWidth = width; 2360 colContentPositions.clear(); 2361 colWidth = Math.floor(viewWidth / colCnt); 2362 setOuterWidth(headCells.slice(0, -1), colWidth); 2363 } 2364 2365 2366 2367 /* Day clicking and binding 2368 -----------------------------------------------------------*/ 2369 2370 2371 function dayBind(days) { 2372 days.click(dayClick) 2373 .mousedown(daySelectionMousedown); 2374 } 2375 2376 2377 function dayClick(ev) { 2378 if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick 2379 var index = parseInt(this.className.match(/fc\-day(\d+)/)[1]); // TODO: maybe use .data 2380 var date = indexDate(index); 2381 trigger('dayClick', this, date, true, ev); 2382 } 2383 } 2384 2385 2386 2387 /* Semi-transparent Overlay Helpers 2388 ------------------------------------------------------*/ 2389 2390 2391 function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive 2392 if (refreshCoordinateGrid) { 2393 coordinateGrid.build(); 2394 } 2395 var rowStart = cloneDate(t.visStart); 2396 var rowEnd = addDays(cloneDate(rowStart), colCnt); 2397 for (var i=0; i<rowCnt; i++) { 2398 var stretchStart = new Date(Math.max(rowStart, overlayStart)); 2399 var stretchEnd = new Date(Math.min(rowEnd, overlayEnd)); 2400 if (stretchStart < stretchEnd) { 2401 var colStart, colEnd; 2402 if (rtl) { 2403 colStart = dayDiff(stretchEnd, rowStart)*dis+dit+1; 2404 colEnd = dayDiff(stretchStart, rowStart)*dis+dit+1; 2405 }else{ 2406 colStart = dayDiff(stretchStart, rowStart); 2407 colEnd = dayDiff(stretchEnd, rowStart); 2408 } 2409 dayBind( 2410 renderCellOverlay(i, colStart, i, colEnd-1) 2411 ); 2412 } 2413 addDays(rowStart, 7); 2414 addDays(rowEnd, 7); 2415 } 2416 } 2417 2418 2419 function renderCellOverlay(row0, col0, row1, col1) { // row1,col1 is inclusive 2420 var rect = coordinateGrid.rect(row0, col0, row1, col1, element); 2421 return renderOverlay(rect, element); 2422 } 2423 2424 2425 2426 /* Selection 2427 -----------------------------------------------------------------------*/ 2428 2429 2430 function defaultSelectionEnd(startDate, allDay) { 2431 return cloneDate(startDate); 2432 } 2433 2434
2435 function renderSelection(startDate, endDate, allDay) { 2436 renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true); // rebuild every time??? 2437 } 2438 2439 2440 function clearSelection() { 2441 clearOverlays(); 2442 } 2443 2444 2445 function reportDayClick(date, allDay, ev) { 2446 var cell = dateCell(date); 2447 var _element = bodyCells[cell.row*colCnt + cell.col]; 2448 trigger('dayClick', _element, date, allDay, ev); 2449 } 2450 2451 2452 2453 /* External Dragging 2454 -----------------------------------------------------------------------*/ 2455 2456 2457 function dragStart(_dragElement, ev, ui) { 2458 hoverListener.start(function(cell) { 2459 clearOverlays(); 2460 if (cell) { 2461 renderCellOverlay(cell.row, cell.col, cell.row, cell.col); 2462 } 2463 }, ev); 2464 } 2465 2466 2467 function dragStop(_dragElement, ev, ui) { 2468 var cell = hoverListener.stop(); 2469 clearOverlays(); 2470 if (cell) { 2471 var d = cellDate(cell); 2472 trigger('drop', _dragElement, d, true, ev, ui); 2473 } 2474 } 2475 2476 2477 2478 /* Utilities 2479 --------------------------------------------------------*/ 2480 2481 2482 function defaultEventEnd(event) { 2483 return cloneDate(event.start); 2484 } 2485 2486 2487 coordinateGrid = new CoordinateGrid(function(rows, cols) { 2488 var e, n, p; 2489 headCells.each(function(i, _e) { 2490 e = $(_e); 2491 n = e.offset().left; 2492 if (i) { 2493 p[1] = n; 2494 } 2495 p = [n]; 2496 cols[i] = p; 2497 }); 2498 p[1] = n + e.outerWidth(); 2499 bodyRows.each(function(i, _e) { 2500 if (i < rowCnt) { 2501 e = $(_e); 2502 n = e.offset().top; 2503 if (i) { 2504 p[1] = n; 2505 } 2506 p = [n]; 2507 rows[i] = p; 2508 } 2509 }); 2510 p[1] = n + e.outerHeight(); 2511 }); 2512 2513 2514 hoverListener = new HoverListener(coordinateGrid); 2515 2516 2517 colContentPositions = new HorizontalPositionCache(function(col) { 2518 return bodyCellTopInners.eq(col); 2519 }); 2520 2521 2522 function colContentLeft(col) { 2523 return colContentPositions.left(col); 2524 } 2525 2526 2527 function colContentRight(col) { 2528 return colContentPositions.right(col); 2529 } 2530 2531 2532 2533 2534 function dateCell(date) { 2535 return { 2536 row: Math.floor(dayDiff(date, t.visStart) / 7), 2537 col: dayOfWeekCol(date.getDay()) 2538 }; 2539 } 2540 2541 2542 function cellDate(cell) { 2543 return _cellDate(cell.row, cell.col); 2544 } 2545 2546 2547 function _cellDate(row, col) { 2548 return addDays(cloneDate(t.visStart), row*7 + col*dis+dit); 2549 // what about weekends in middle of week? 2550 } 2551 2552 2553 function indexDate(index) { 2554 return _cellDate(Math.floor(index/colCnt), index%colCnt); 2555 } 2556 2557 2558 function dayOfWeekCol(dayOfWeek) { 2559 return ((dayOfWeek - Math.max(firstDay, nwe) + colCnt) % colCnt) * dis + dit; 2560 } 2561 2562 2563 2564 2565 function allDayRow(i) { 2566 return bodyRows.eq(i); 2567 } 2568 2569 2570 function allDayBounds(i) { 2571 return { 2572 left: 0, 2573 right: viewWidth 2574 }; 2575 } 2576 2577 2578 } 2579 2580 function BasicEventRenderer() { 2581 var t = this; 2582 2583 2584 // exports 2585 t.renderEvents = renderEvents; 2586 t.compileDaySegs = compileSegs; // for DayEventRenderer 2587 t.clearEvents = clearEvents; 2588 t.bindDaySeg = bindDaySeg; 2589 2590 2591 // imports 2592 DayEventRenderer.call(t); 2593 var opt = t.opt; 2594 var trigger = t.trigger; 2595 //var setOverflowHidden = t.setOverflowHidden; 2596 var isEventDraggable = t.isEventDraggable; 2597 var isEventResizable = t.isEventResizable; 2598 var reportEvents = t.reportEvents; 2599 var reportEventClear = t.reportEventClear; 2600 var eventElementHandlers = t.eventElementHandlers; 2601 var showEvents = t.showEvents; 2602 var hideEvents = t.hideEvents; 2603 var eventDrop = t.eventDrop; 2604 var getDaySegmentContainer = t.getDaySegmentContainer; 2605 var getHoverListener = t.getHoverListener; 2606 var renderDayOverlay = t.renderDayOverlay; 2607 var clearOverlays = t.clearOverlays; 2608 var getRowCnt = t.getRowCnt; 2609 var getColCnt = t.getColCnt; 2610 var renderDaySegs = t.renderDaySegs; 2611 var resizableDayEvent = t.resizableDayEvent; 2612 2613 2614 2615 /* Rendering 2616 --------------------------------------------------------------------*/ 2617 2618 2619 function renderEvents(events, modifiedEventId) { 2620 reportEvents(events); 2621 renderDaySegs(compileSegs(events), modifiedEventId); 2622 } 2623 2624 2625 function clearEvents() { 2626 reportEventClear(); 2627 getDaySegmentContainer().empty(); 2628 } 2629 2630 2631 function compileSegs(events) {
2632 var rowCnt = getRowCnt(), 2633 colCnt = getColCnt(), 2634 d1 = cloneDate(t.visStart), 2635 d2 = addDays(cloneDate(d1), colCnt), 2636 visEventsEnds = $.map(events, exclEndDay), 2637 i, row, 2638 j, level, 2639 k, seg, 2640 segs=[]; 2641 for (i=0; i<rowCnt; i++) { 2642 row = stackSegs(sliceSegs(events, visEventsEnds, d1, d2)); 2643 for (j=0; j<row.length; j++) { 2644 level = row[j]; 2645 for (k=0; k<level.length; k++) { 2646 seg = level[k]; 2647 seg.row = i; 2648 seg.level = j; // not needed anymore 2649 segs.push(seg); 2650 } 2651 } 2652 addDays(d1, 7); 2653 addDays(d2, 7); 2654 } 2655 return segs; 2656 } 2657 2658 2659 function bindDaySeg(event, eventElement, seg) { 2660 if (isEventDraggable(event)) { 2661 draggableDayEvent(event, eventElement); 2662 } 2663 if (seg.isEnd && isEventResizable(event)) { 2664 resizableDayEvent(event, eventElement, seg); 2665 } 2666 eventElementHandlers(event, eventElement); 2667 // needs to be after, because resizableDayEvent might stopImmediatePropagation on click 2668 } 2669 2670 2671 2672 /* Dragging 2673 ----------------------------------------------------------------------------*/ 2674 2675 2676 function draggableDayEvent(event, eventElement) { 2677 var hoverListener = getHoverListener(); 2678 var dayDelta; 2679 eventElement.draggable({ 2680 zIndex: 9, 2681 delay: 50, 2682 opacity: opt('dragOpacity'), 2683 revertDuration: opt('dragRevertDuration'), 2684 start: function(ev, ui) { 2685 trigger('eventDragStart', eventElement, event, ev, ui); 2686 hideEvents(event, eventElement); 2687 hoverListener.start(function(cell, origCell, rowDelta, colDelta) { 2688 eventElement.draggable('option', 'revert', !cell || !rowDelta && !colDelta); 2689 clearOverlays(); 2690 if (cell) { 2691 //setOverflowHidden(true); 2692 dayDelta = rowDelta*7 + colDelta * (opt('isRTL') ? -1 : 1); 2693 renderDayOverlay( 2694 addDays(cloneDate(event.start), dayDelta), 2695 addDays(exclEndDay(event), dayDelta) 2696 ); 2697 }else{ 2698 //setOverflowHidden(false); 2699 dayDelta = 0; 2700 } 2701 }, ev, 'drag'); 2702 }, 2703 stop: function(ev, ui) { 2704 hoverListener.stop(); 2705 clearOverlays(); 2706 trigger('eventDragStop', eventElement, event, ev, ui); 2707 if (dayDelta) { 2708 eventDrop(this, event, dayDelta, 0, event.allDay, ev, ui); 2709 }else{ 2710 eventElement.css('filter', ''); // clear IE opacity side-effects 2711 showEvents(event, eventElement); 2712 } 2713 //setOverflowHidden(false); 2714 } 2715 }); 2716 } 2717 2718 2719 } 2720 2721 fcViews.agendaWeek = AgendaWeekView; 2722 2723 function AgendaWeekView(element, calendar) { 2724 var t = this; 2725 2726 2727 // exports 2728 t.render = render; 2729 2730 2731 // imports 2732 AgendaView.call(t, element, calendar, 'agendaWeek'); 2733 var opt = t.opt; 2734 var renderAgenda = t.renderAgenda; 2735 var formatDates = calendar.formatDates; 2736 2737 2738 2739 function render(date, delta) { 2740 if (delta) { 2741 addDays(date, delta * 7); 2742 } 2743 var start = addDays(cloneDate(date), -((date.getDay() - opt('firstDay') + 7) % 7)); 2744 var end = addDays(cloneDate(start), 7); 2745 var visStart = cloneDate(start); 2746 var visEnd = cloneDate(end); 2747 var weekends = opt('weekends'); 2748 if (!weekends) { 2749 skipWeekend(visStart); 2750 skipWeekend(visEnd, -1, true); 2751 } 2752 t.title = formatDates( 2753 visStart, 2754 addDays(cloneDate(visEnd), -1), 2755 opt('titleFormat') 2756 ); 2757 t.start = start; 2758 t.end = end; 2759 t.visStart = visStart; 2760 t.visEnd = visEnd; 2761 renderAgenda(weekends ? 7 : 5); 2762 } 2763 2764 2765 } 2766 2767 fcViews.agendaDay = AgendaDayView; 2768 2769 function AgendaDayView(element, calendar) { 2770 var t = this; 2771 2772 2773 // exports 2774 t.render = render; 2775 2776 2777 // imports 2778 AgendaView.call(t, element, calendar, 'agendaDay'); 2779 var opt = t.opt; 2780 var renderAgenda = t.renderAgenda; 2781 var formatDate = calendar.formatDate; 2782 2783 2784 2785 function render(date, delta) { 2786 if (delta) { 2787 addDays(date, delta); 2788 if (!opt('weekends')) { 2789 skipWeekend(date, delta < 0 ? -1 : 1); 2790 } 2791 } 2792 var start = cloneDate(date, true); 2793 var end = addDays(cloneDate(start), 1); 2794 t.title = formatDate(date, opt('titleFormat')); 2795 t.start = t.visStart = start; 2796 t.end = t.visEnd = end; 2797 renderAgenda(1); 2798 } 2799 2800 2801 } 2802 2803 setDefaults({ 2804 allDaySlot: true, 2805 allDayText: 'all-day', 2806 firstHour: 6, 2807 slotMinutes: 30, 2808 defaultEventMinutes: 120, 2809 axisFormat: 'h(:mm)tt', 2810 timeFormat: { 2811 agenda: 'h:mm{ - h:mm}' 2812 }, 2813 dragOpacity: { 2814 agenda: .5 2815 }, 2816 minTime: 0, 2817 maxTime: 24 2818 }); 2819 2820 2821 // TODO: make it work in quirks mode (event corners, all-day height) 2822 // TODO: test liquid width, especially in IE6 2823 2824 2825 function AgendaView(element, calendar, viewName) { 2826 var t = this; 2827
2828 2829 // exports 2830 t.renderAgenda = renderAgenda; 2831 t.setWidth = setWidth; 2832 t.setHeight = setHeight; 2833 t.beforeHide = beforeHide; 2834 t.afterShow = afterShow; 2835 t.defaultEventEnd = defaultEventEnd; 2836 t.timePosition = timePosition; 2837 t.dayOfWeekCol = dayOfWeekCol; 2838 t.dateCell = dateCell; 2839 t.cellDate = cellDate; 2840 t.cellIsAllDay = cellIsAllDay; 2841 t.allDayRow = getAllDayRow; 2842 t.allDayBounds = allDayBounds; 2843 t.getHoverListener = function() { return hoverListener }; 2844 t.colContentLeft = colContentLeft; 2845 t.colContentRight = colContentRight; 2846 t.getDaySegmentContainer = function() { return daySegmentContainer }; 2847 t.getSlotSegmentContainer = function() { return slotSegmentContainer }; 2848 t.getMinMinute = function() { return minMinute }; 2849 t.getMaxMinute = function() { return maxMinute }; 2850 t.getBodyContent = function() { return slotContent }; // !!?? 2851 t.getRowCnt = function() { return 1 }; 2852 t.getColCnt = function() { return colCnt }; 2853 t.getColWidth = function() { return colWidth }; 2854 t.getSlotHeight = function() { return slotHeight }; 2855 t.defaultSelectionEnd = defaultSelectionEnd; 2856 t.renderDayOverlay = renderDayOverlay; 2857 t.renderSelection = renderSelection; 2858 t.clearSelection = clearSelection; 2859 t.reportDayClick = reportDayClick; // selection mousedown hack 2860 t.dragStart = dragStart; 2861 t.dragStop = dragStop; 2862 2863 2864 // imports 2865 View.call(t, element, calendar, viewName); 2866 OverlayManager.call(t); 2867 SelectionManager.call(t); 2868 AgendaEventRenderer.call(t); 2869 var opt = t.opt; 2870 var trigger = t.trigger; 2871 var clearEvents = t.clearEvents; 2872 var renderOverlay = t.renderOverlay; 2873 var clearOverlays = t.clearOverlays; 2874 var reportSelection = t.reportSelection; 2875 var unselect = t.unselect; 2876 var daySelectionMousedown = t.daySelectionMousedown; 2877 var slotSegHtml = t.slotSegHtml; 2878 var formatDate = calendar.formatDate; 2879 2880 2881 // locals 2882 2883 var dayTable; 2884 var dayHead; 2885 var dayHeadCells; 2886 var dayBody; 2887 var dayBodyCells; 2888 var dayBodyCellInners; 2889 var dayBodyFirstCell; 2890 var dayBodyFirstCellStretcher; 2891 var slotLayer; 2892 var daySegmentContainer; 2893 var allDayTable; 2894 var allDayRow; 2895 var slotScroller; 2896 var slotContent; 2897 var slotSegmentContainer; 2898 var slotTable; 2899 var slotTableFirstInner; 2900 var axisFirstCells; 2901 var gutterCells; 2902 var selectionHelper; 2903 2904 var viewWidth; 2905 var viewHeight; 2906 var axisWidth; 2907 var colWidth; 2908 var gutterWidth; 2909 var slotHeight; // TODO: what if slotHeight changes? (see issue 650) 2910 var savedScrollTop; 2911 2912 var colCnt; 2913 var slotCnt; 2914 var coordinateGrid; 2915 var hoverListener; 2916 var colContentPositions; 2917 var slotTopCache = {}; 2918 2919 var tm; 2920 var firstDay; 2921 var nwe; // no weekends (int) 2922 var rtl, dis, dit; // day index sign / translate 2923 var minMinute, maxMinute; 2924 var colFormat; 2925 2926 2927 2928 /* Rendering 2929 -----------------------------------------------------------------------------*/ 2930 2931 2932 disableTextSelection(element.addClass('fc-agenda')); 2933 2934 2935 function renderAgenda(c) { 2936 colCnt = c; 2937 updateOptions(); 2938 if (!dayTable) { 2939 buildSkeleton(); 2940 }else{ 2941 clearEvents(); 2942 } 2943 updateCells(); 2944 } 2945 2946 2947 2948 function updateOptions() { 2949 tm = opt('theme') ? 'ui' : 'fc'; 2950 nwe = opt('weekends') ? 0 : 1; 2951 firstDay = opt('firstDay'); 2952 if (rtl = opt('isRTL')) { 2953 dis = -1; 2954 dit = colCnt - 1; 2955 }else{ 2956 dis = 1; 2957 dit = 0; 2958 } 2959 minMinute = parseTime(opt('minTime')); 2960 maxMinute = parseTime(opt('maxTime')); 2961 colFormat = opt('columnFormat'); 2962 } 2963 2964 2965 2966 function buildSkeleton() { 2967 var headerClass = tm + "-widget-header"; 2968 var contentClass = tm + "-widget-content"; 2969 var s; 2970 var i; 2971 var d; 2972 var maxd; 2973 var minutes; 2974 var slotNormal = opt('slotMinutes') % 15 == 0; 2975 2976 s = 2977 "<table style='width:100%' class='fc-agenda-days fc-border-separate' cellspacing='0'>" + 2978 "<thead>" + 2979 "<tr>" + 2980 "<th class='fc-agenda-axis " + headerClass + "'> </th>"; 2981 for (i=0; i<colCnt; i++) { 2982 s += 2983 "<th class='fc- fc-col" + i + ' ' + headerClass + "'/>"; // fc- needed for setDayID 2984 } 2985 s += 2986 "<th class='fc-agenda-gutter " + headerClass + "'> </th>" + 2987 "</tr>" + 2988 "</thead>" + 2989 "<tbody>" + 2990 "<tr>" + 2991 "<th class='fc-agenda-axis " + headerClass + "'> </th>"; 2992 for (i=0; i<colCnt; i++) { 2993 s += 2994 "<td class='fc- fc-col" + i + ' ' + contentClass + "'>" + // fc- needed for setDayID 2995 "<div>" + 2996 "<div class='fc-day-content'>" + 2997 "<div style='position:relative'>
2997 </div>" + 2998 "</div>" + 2999 "</div>" + 3000 "</td>"; 3001 } 3002 s += 3003 "<td class='fc-agenda-gutter " + contentClass + "'> </td>" + 3004 "</tr>" + 3005 "</tbody>" + 3006 "</table>"; 3007 dayTable = $(s).appendTo(element); 3008 dayHead = dayTable.find('thead'); 3009 dayHeadCells = dayHead.find('th').slice(1, -1); 3010 dayBody = dayTable.find('tbody'); 3011 dayBodyCells = dayBody.find('td').slice(0, -1); 3012 dayBodyCellInners = dayBodyCells.find('div.fc-day-content div'); 3013 dayBodyFirstCell = dayBodyCells.eq(0); 3014 dayBodyFirstCellStretcher = dayBodyFirstCell.find('> div'); 3015 3016 markFirstLast(dayHead.add(dayHead.find('tr'))); 3017 markFirstLast(dayBody.add(dayBody.find('tr'))); 3018 3019 axisFirstCells = dayHead.find('th:first'); 3020 gutterCells = dayTable.find('.fc-agenda-gutter'); 3021 3022 slotLayer = 3023 $("<div style='position:absolute;z-index:2;left:0;width:100%'/>") 3024 .appendTo(element); 3025 3026 if (opt('allDaySlot')) { 3027 3028 daySegmentContainer = 3029 $("<div style='position:absolute;z-index:8;top:0;left:0'/>") 3030 .appendTo(slotLayer); 3031 3032 s = 3033 "<table style='width:100%' class='fc-agenda-allday' cellspacing='0'>" + 3034 "<tr>" + 3035 "<th class='" + headerClass + " fc-agenda-axis'>" + opt('allDayText') + "</th>" + 3036 "<td>" + 3037 "<div class='fc-day-content'><div style='position:relative'/></div>" + 3038 "</td>" + 3039 "<th class='" + headerClass + " fc-agenda-gutter'> </th>" + 3040 "</tr>" + 3041 "</table>"; 3042 allDayTable = $(s).appendTo(slotLayer); 3043 allDayRow = allDayTable.find('tr'); 3044 3045 dayBind(allDayRow.find('td')); 3046 3047 axisFirstCells = axisFirstCells.add(allDayTable.find('th:first')); 3048 gutterCells = gutterCells.add(allDayTable.find('th.fc-agenda-gutter')); 3049 3050 slotLayer.append( 3051 "<div class='fc-agenda-divider " + headerClass + "'>" + 3052 "<div class='fc-agenda-divider-inner'/>" + 3053 "</div>" 3054 ); 3055 3056 }else{ 3057 3058 daySegmentContainer = $([]); // in jQuery 1.4, we can just do $() 3059 3060 } 3061 3062 slotScroller = 3063 $("<div style='position:absolute;width:100%;overflow-x:hidden;overflow-y:auto'/>") 3064 .appendTo(slotLayer); 3065 3066 slotContent = 3067 $("<div style='position:relative;width:100%;overflow:hidden'/>") 3068 .appendTo(slotScroller); 3069 3070 slotSegmentContainer = 3071 $("<div style='position:absolute;z-index:8;top:0;left:0'/>") 3072 .appendTo(slotContent); 3073 3074 s = 3075 "<table class='fc-agenda-slots' style='width:100%' cellspacing='0'>" + 3076 "<tbody>"; 3077 d = zeroDate(); 3078 maxd = addMinutes(cloneDate(d), maxMinute); 3079 addMinutes(d, minMinute); 3080 slotCnt = 0; 3081 for (i=0; d < maxd; i++) { 3082 minutes = d.getMinutes(); 3083 s += 3084 "<tr class='fc-slot" + i + ' ' + (!minutes ? '' : 'fc-minor') + "'>" + 3085 "<th class='fc-agenda-axis " + headerClass + "'>" + 3086 ((!slotNormal || !minutes) ? formatDate(d, opt('axisFormat')) : ' ') + 3087 "</th>" + 3088 "<td class='" + contentClass + "'>" + 3089 "<div style='position:relative'> </div>" + 3090 "</td>" + 3091 "</tr>"; 3092 addMinutes(d, opt('slotMinutes')); 3093 slotCnt++; 3094 } 3095 s += 3096 "</tbody>" + 3097 "</table>"; 3098 slotTable = $(s).appendTo(slotContent); 3099 slotTableFirstInner = slotTable.find('div:first'); 3100 3101 slotBind(slotTable.find('td')); 3102 3103 axisFirstCells = axisFirstCells.add(slotTable.find('th:first')); 3104 } 3105 3106 3107 3108 function updateCells() { 3109 var i; 3110 var headCell; 3111 var bodyCell; 3112 var date; 3113 var today = clearTime(new Date()); 3114 for (i=0; i<colCnt; i++) { 3115 date = colDate(i); 3116 headCell = dayHeadCells.eq(i); 3117 headCell.html(formatDate(date, colFormat)); 3118 bodyCell = dayBodyCells.eq(i); 3119 if (+date == +today) { 3120 bodyCell.addClass(tm + '-state-highlight fc-today'); 3121 }else{ 3122 bodyCell.removeClass(tm + '-state-highlight fc-today'); 3123 } 3124 setDayID(headCell.add(bodyCell), date); 3125 } 3126 } 3127 3128 3129 3130 function setHeight(height, dateChanged) { 3131 if (height === undefined) { 3132 height = viewHeight; 3133 } 3134 viewHeight = height; 3135 slotTopCache = {}; 3136 3137 var headHeight = dayBody.position().top; 3138 var allDayHeight = slotScroller.position().top; // including divider 3139 var bodyHeight = Math.min( // total body height, including borders 3140 height - headHeight, // when scrollbars 3141 slotTable.height() + allDayHeight + 1 // when no scrollbars. +1 for bottom border 3142 ); 3143 3144 dayBodyFirstCellStretcher 3145 .height(bodyHeight - vsides(dayBodyFirstCell)); 3146 3147 slotLayer.css('top', headHeight); 3148 3149 slotScroller.height(bodyHeight - allDayHeight - 1); 3150 3151 slotHeight = slotTableFirstInner.height() + 1; // +1 for border 3152 3153 if (dateChanged) { 3154 resetScroll(); 3155 } 3156 } 3157 3158 3159 3160 function setWidth(width) { 3161 viewWidth = width; 3162 colContentPositions.clear(); 3163 3164 axisWidth = 0; 3165 setOuterWidth( 3166 axisFirstCells 3167 .width('') 3168 .each(function(i, _cell) { 3169 axisWidth = Math.max(axisWidth, $(_cell).outerWidth()); 3170 }), 3171 axisWidth 3172 ); 3173 3174 var slotTableWidth = slotScroller[0].clientWidth; // needs to be done after axisWidth (for IE7) 3175 //slotTable.width(slotTableWidth); 3176 3177 gutterWidth = slotScroller.width() - slotTableWidth; 3178 if (gutterWidth) { 3179 setOuterWidth(gutterCells, gutterWidth); 3180 gutterCells 3181 .show() 3182 .prev() 3183 .removeClass('fc-last'); 3184 }else{ 3185 gutterCells 3186 .hide() 3187 .prev() 3188 .addClass('fc-last'); 3189 } 3190 3191 colWidth = Math.floor((slotTableWidth - axisWidth) / colCnt); 3192 setOuterWidth(dayHeadCells.slice(0, -1), colWidth); 3193 } 3194 3195 3196 3197 function resetScroll() { 3198 var d0 = zeroDate(); 3199 var scrollDate = cloneDate(d0); 3200 scrollDate.setHours(opt('firstHour')); 3201 var top = timePosition(d0, scrollDate) + 1; // +1 for the border 3202 function scroll() { 3203 slotScroller.scrollTop(top); 3204 } 3205 scroll(); 3206 setTimeout(scroll, 0); // overrides any previous scroll state made by the browser 3207 } 3208 3209 3210 function beforeHide() { 3211 savedScrollTop = slotScroller.scrollTop(); 3212 } 3213 3214 3215 function afterShow() { 3216 slotScroller.scrollTop(savedScrollTop); 3217 } 3218 3219 3220 3221 /* Slot/Day clicking and binding 3222 -----------------------------------------------------------------------*/ 3223 3224 3225 function dayBind(cells) { 3226 cells.click(slotClick) 3227 .mousedown(daySelectionMousedown); 3228 } 3229 3230 3231 function slotBind(cells) { 3232 cells.click(slotClick) 3233 .mousedown(slotSelectionMousedown); 3234 } 3235 3236 3237 function slotClick(ev) { 3238 if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick 3239 var col = Math.min(colCnt-1, Math.floor((ev.pageX - dayTable.offset().left - axisWidth) / colWidth)); 3240 var date = colDate(col); 3241 var rowMatch = this.parentNode.className.match(/fc-slot(\d+)/); // TODO: maybe use data 3242 if (rowMatch) { 3243 var mins = parseInt(rowMatch[1]) * opt('slotMinutes'); 3244 var hours = Math.floor(mins/60); 3245 date.setHours(hours); 3246 date.setMinutes(mins%60 + minMinute); 3247 trigger('dayClick', dayBodyCells[col], date, false, ev); 3248 }else{ 3249 trigger('dayClick', dayBodyCells[col], date, true, ev); 3250 } 3251 } 3252 } 3253 3254 3255 3256 /* Semi-transparent Overlay Helpers 3257 -----------------------------------------------------*/ 3258 3259 3260 function renderDayOverlay(startDate, endDate, refreshCoordinateGrid) { // endDate is exclusive 3261 if (refreshCoordinateGrid) { 3262 coordinateGrid.build(); 3263 } 3264 var visStart = cloneDate(t.visStart); 3265 var startCol, endCol; 3266 if (rtl) { 3267 startCol = dayDiff(endDate, visStart)*dis+dit+1; 3268 endCol = dayDiff(startDate, visStart)*dis+dit+1; 3269 }else{ 3270 startCol = dayDiff(startDate, visStart); 3271 endCol = dayDiff(endDate, visStart); 3272 } 3273 startCol = Math.max(0, startCol); 3274 endCol = Math.min(colCnt, endCol); 3275 if (startCol < endCol) { 3276 dayBind( 3277 renderCellOverlay(0, startCol, 0, endCol-1) 3278 ); 3279 } 3280 } 3281 3282 3283 function renderCellOverlay(row0, col0, row1, col1) { // only for all-day? 3284 var rect = coordinateGrid.rect(row0, col0, row1, col1, slotLayer); 3285 return renderOverlay(rect, slotLayer); 3286 } 3287 3288
3289 function renderSlotOverlay(overlayStart, overlayEnd) { 3290 var dayStart = cloneDate(t.visStart); 3291 var dayEnd = addDays(cloneDate(dayStart), 1); 3292 for (var i=0; i<colCnt; i++) { 3293 var stretchStart = new Date(Math.max(dayStart, overlayStart)); 3294 var stretchEnd = new Date(Math.min(dayEnd, overlayEnd)); 3295 if (stretchStart < stretchEnd) { 3296 var col = i*dis+dit; 3297 var rect = coordinateGrid.rect(0, col, 0, col, slotContent); // only use it for horizontal coords 3298 var top = timePosition(dayStart, stretchStart); 3299 var bottom = timePosition(dayStart, stretchEnd); 3300 rect.top = top; 3301 rect.height = bottom - top; 3302 slotBind( 3303 renderOverlay(rect, slotContent) 3304 ); 3305 } 3306 addDays(dayStart, 1); 3307 addDays(dayEnd, 1); 3308 } 3309 } 3310 3311 3312 3313 /* Coordinate Utilities 3314 -----------------------------------------------------------------------------*/ 3315 3316 3317 coordinateGrid = new CoordinateGrid(function(rows, cols) { 3318 var e, n, p; 3319 dayHeadCells.each(function(i, _e) { 3320 e = $(_e); 3321 n = e.offset().left; 3322 if (i) { 3323 p[1] = n; 3324 } 3325 p = [n]; 3326 cols[i] = p; 3327 }); 3328 p[1] = n + e.outerWidth(); 3329 if (opt('allDaySlot')) { 3330 e = allDayRow; 3331 n = e.offset().top; 3332 rows[0] = [n, n+e.outerHeight()]; 3333 } 3334 var slotTableTop = slotContent.offset().top; 3335 var slotScrollerTop = slotScroller.offset().top; 3336 var slotScrollerBottom = slotScrollerTop + slotScroller.outerHeight(); 3337 function constrain(n) { 3338 return Math.max(slotScrollerTop, Math.min(slotScrollerBottom, n)); 3339 } 3340 for (var i=0; i<slotCnt; i++) { 3341 rows.push([ 3342 constrain(slotTableTop + slotHeight*i), 3343 constrain(slotTableTop + slotHeight*(i+1)) 3344 ]); 3345 } 3346 }); 3347 3348 3349 hoverListener = new HoverListener(coordinateGrid); 3350 3351 3352 colContentPositions = new HorizontalPositionCache(function(col) { 3353 return dayBodyCellInners.eq(col); 3354 }); 3355 3356 3357 function colContentLeft(col) { 3358 return colContentPositions.left(col); 3359 } 3360 3361 3362 function colContentRight(col) { 3363 return colContentPositions.right(col); 3364 } 3365 3366 3367 3368 3369 function dateCell(date) { // "cell" terminology is now confusing 3370 return { 3371 row: Math.floor(dayDiff(date, t.visStart) / 7), 3372 col: dayOfWeekCol(date.getDay()) 3373 }; 3374 } 3375 3376 3377 function cellDate(cell) { 3378 var d = colDate(cell.col); 3379 var slotIndex = cell.row; 3380 if (opt('allDaySlot')) { 3381 slotIndex--; 3382 } 3383 if (slotIndex >= 0) { 3384 addMinutes(d, minMinute + slotIndex * opt('slotMinutes')); 3385 } 3386 return d; 3387 } 3388 3389 3390 function colDate(col) { // returns dates with 00:00:00 3391 return addDays(cloneDate(t.visStart), col*dis+dit); 3392 } 3393 3394 3395 function cellIsAllDay(cell) { 3396 return opt('allDaySlot') && !cell.row; 3397 } 3398 3399 3400 function dayOfWeekCol(dayOfWeek) { 3401 return ((dayOfWeek - Math.max(firstDay, nwe) + colCnt) % colCnt)*dis+dit; 3402 } 3403 3404 3405 3406 3407 // get the Y coordinate of the given time on the given day (both Date objects) 3408 function timePosition(day, time) { // both date objects. day holds 00:00 of current day 3409 day = cloneDate(day, true); 3410 if (time < addMinutes(cloneDate(day), minMinute)) { 3411 return 0; 3412 } 3413 if (time >= addMinutes(cloneDate(day), maxMinute)) { 3414 return slotTable.height(); 3415 } 3416 var slotMinutes = opt('slotMinutes'), 3417 minutes = time.getHours()*60 + time.getMinutes() - minMinute, 3418 slotI = Math.floor(minutes / slotMinutes), 3419 slotTop = slotTopCache[slotI]; 3420 if (slotTop === undefined) { 3421 slotTop = slotTopCache[slotI] = slotTable.find('tr:eq(' + slotI + ') td div')[0].offsetTop; //.position().top; // need this optimization??? 3422 } 3423 return Math.max(0, Math.round( 3424 slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes) 3425 )); 3426 } 3427 3428 3429 function allDayBounds() { 3430 return { 3431 left: axisWidth, 3432 right: viewWidth - gutterWidth 3433 } 3434 } 3435 3436 3437 function getAllDayRow(index) { 3438 return allDayRow; 3439 } 3440 3441 3442 function defaultEventEnd(event) { 3443 var start = cloneDate(event.start); 3444 if (event.allDay) { 3445 return start; 3446 } 3447 return addMinutes(start, opt('defaultEventMinutes')); 3448 } 3449 3450 3451 3452 /* Selection 3453 ---------------------------------------------------------------------------------*/ 3454 3455 3456 function defaultSelectionEnd(startDate, allDay) { 3457 if (allDay) { 3458 return cloneDate(startDate); 3459 } 3460 return addMinutes(cloneDate(startDate), opt('slotMinutes')); 3461 } 3462 3463
3464 function renderSelection(startDate, endDate, allDay) { // only for all-day 3465 if (allDay) { 3466 if (opt('allDaySlot')) { 3467 renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true); 3468 } 3469 }else{ 3470 renderSlotSelection(startDate, endDate); 3471 } 3472 } 3473 3474 3475 function renderSlotSelection(startDate, endDate) { 3476 var helperOption = opt('selectHelper'); 3477 coordinateGrid.build(); 3478 if (helperOption) { 3479 var col = dayDiff(startDate, t.visStart) * dis + dit; 3480 if (col >= 0 && col < colCnt) { // only works when times are on same day 3481 var rect = coordinateGrid.rect(0, col, 0, col, slotContent); // only for horizontal coords 3482 var top = timePosition(startDate, startDate); 3483 var bottom = timePosition(startDate, endDate); 3484 if (bottom > top) { // protect against selections that are entirely before or after visible range 3485 rect.top = top; 3486 rect.height = bottom - top; 3487 rect.left += 2; 3488 rect.width -= 5; 3489 if ($.isFunction(helperOption)) { 3490 var helperRes = helperOption(startDate, endDate); 3491 if (helperRes) { 3492 rect.position = 'absolute'; 3493 rect.zIndex = 8; 3494 selectionHelper = $(helperRes) 3495 .css(rect) 3496 .appendTo(slotContent); 3497 } 3498 }else{ 3499 rect.isStart = true; // conside rect a "seg" now 3500 rect.isEnd = true; // 3501 selectionHelper = $(slotSegHtml( 3502 { 3503 title: '', 3504 start: startDate, 3505 end: endDate, 3506 className: ['fc-select-helper'], 3507 editable: false 3508 }, 3509 rect 3510 )); 3511 selectionHelper.css('opacity', opt('dragOpacity')); 3512 } 3513 if (selectionHelper) { 3514 slotBind(selectionHelper); 3515 slotContent.append(selectionHelper); 3516 setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended 3517 setOuterHeight(selectionHelper, rect.height, true); 3518 } 3519 } 3520 } 3521 }else{ 3522 renderSlotOverlay(startDate, endDate); 3523 } 3524 } 3525 3526 3527 function clearSelection() { 3528 clearOverlays(); 3529 if (selectionHelper) { 3530 selectionHelper.remove(); 3531 selectionHelper = null; 3532 } 3533 } 3534 3535 3536 function slotSelectionMousedown(ev) { 3537 if (ev.which == 1 && opt('selectable')) { // ev.which==1 means left mouse button 3538 unselect(ev); 3539 var dates; 3540 hoverListener.start(function(cell, origCell) { 3541 clearSelection(); 3542 if (cell && cell.col == origCell.col && !cellIsAllDay(cell)) { 3543 var d1 = cellDate(origCell); 3544 var d2 = cellDate(cell); 3545 dates = [ 3546 d1, 3547 addMinutes(cloneDate(d1), opt('slotMinutes')), 3548 d2, 3549 addMinutes(cloneDate(d2), opt('slotMinutes')) 3550 ].sort(cmp); 3551 renderSlotSelection(dates[0], dates[3]); 3552 }else{ 3553 dates = null; 3554 } 3555 }, ev); 3556 $(document).one('mouseup', function(ev) { 3557 hoverListener.stop(); 3558 if (dates) { 3559 if (+dates[0] == +dates[1]) { 3560 reportDayClick(dates[0], false, ev); 3561 } 3562 reportSelection(dates[0], dates[3], false, ev); 3563 } 3564 }); 3565 } 3566 } 3567 3568 3569 function reportDayClick(date, allDay, ev) { 3570 trigger('dayClick', dayBodyCells[dayOfWeekCol(date.getDay())], date, allDay, ev); 3571 } 3572 3573 3574 3575 /* External Dragging 3576 --------------------------------------------------------------------------------*/ 3577 3578 3579 function dragStart(_dragElement, ev, ui) { 3580 hoverListener.start(function(cell) { 3581 clearOverlays(); 3582 if (cell) { 3583 if (cellIsAllDay(cell)) { 3584 renderCellOverlay(cell.row, cell.col, cell.row, cell.col); 3585 }else{ 3586 var d1 = cellDate(cell); 3587 var d2 = addMinutes(cloneDate(d1), opt('defaultEventMinutes')); 3588 renderSlotOverlay(d1, d2); 3589 } 3590 } 3591 }, ev); 3592 } 3593 3594 3595 function dragStop(_dragElement, ev, ui) { 3596 var cell = hoverListener.stop(); 3597 clearOverlays(); 3598 if (cell) { 3599 trigger('drop', _dragElement, cellDate(cell), cellIsAllDay(cell), ev, ui); 3600 } 3601 } 3602 3603 3604 } 3605 3606 function AgendaEventRenderer() { 3607 var t = this; 3608 3609 3610 // exports 3611 t.renderEvents = renderEvents; 3612 t.compileDaySegs = compileDaySegs; // for DayEventRenderer 3613 t.clearEvents = clearEvents; 3614 t.slotSegHtml = slotSegHtml; 3615 t.bindDaySeg = bindDaySeg; 3616 3617 3618 // imports 3619 DayEventRenderer.call(t); 3620 var opt = t.opt; 3621 var trigger = t.trigger; 3622 //var setOverflowHidden = t.setOverflowHidden; 3623 var isEventDraggable = t.isEventDraggable; 3624 var isEventResizable = t.isEventResizable; 3625 var eventEnd = t.eventEnd; 3626 var reportEvents = t.reportEvents; 3627 var reportEventClear = t.reportEventClear; 3628 var eventElementHandlers = t.eventElementHandlers; 3629 var setHeight = t.setHeight; 3630 var getDaySegmentContainer = t.getDaySegmentContainer; 3631 var getSlotSegmentContainer = t.getSlotSegmentContainer; 3632 var getHoverListener = t.getHoverListener; 3633 var getMaxMinute = t.getMaxMinute; 3634 var getMinMinute = t.getMinMinute; 3635 var timePosition = t.timePosition; 3636 var colContentLeft = t.colContentLeft; 3637 var colContentRight = t.colContentRight; 3638 var renderDaySegs = t.renderDaySegs; 3639 var resizableDayEvent = t.resizableDayEvent; // TODO: streamline binding architecture 3640 var getColCnt = t.getColCnt; 3641 var getColWidth = t.getColWidth; 3642 var getSlotHeight = t.getSlotHeight; 3643 var getBodyContent = t.getBodyContent; 3644 var reportEventElement = t.reportEventElement; 3645 var showEvents = t.showEvents; 3646 var hideEvents = t.hideEvents; 3647 var eventDrop = t.eventDrop; 3648 var eventResize = t.eventResize; 3649 var renderDayOverlay = t.renderDayOverlay; 3650 var clearOverlays = t.clearOverlays; 3651 var calendar = t.calendar; 3652 var formatDate = calendar.formatDate; 3653 var formatDates = calendar.formatDates; 3654 3655 3656 3657 /* Rendering 3658 ----------------------------------------------------------------------------*/ 3659 3660 3661 function renderEvents(events, modifiedEventId) { 3662 reportEvents(events); 3663 var i, len=events.length, 3664 dayEvents=[], 3665 slotEvents=[]; 3666 for (i=0; i<len; i++) { 3667 if (events[i].allDay) { 3668 dayEvents.push(events[i]); 3669 }else{ 3670 slotEvents.push(events[i]); 3671 } 3672 } 3673 if (opt('allDaySlot')) { 3674 renderDaySegs(compileDaySegs(dayEvents), modifiedEventId); 3675 setHeight(); // no params means set to viewHeight 3676 } 3677 renderSlotSegs(compileSlotSegs(slotEvents), modifiedEventId); 3678 } 3679 3680 3681 function clearEvents() { 3682 reportEventClear(); 3683 getDaySegmentContainer().empty(); 3684 getSlotSegmentContainer().empty(); 3685 } 3686 3687 3688 function compileDaySegs(events) { 3689 var levels = stackSegs(sliceSegs(events, $.map(events, exclEndDay), t.visStart, t.visEnd)), 3690 i, levelCnt=levels.length, level, 3691 j, seg, 3692 segs=[]; 3693 for (i=0; i<levelCnt; i++) { 3694 level = levels[i]; 3695 for (j=0; j<level.length; j++) { 3696 seg = level[j]; 3697 seg.row = 0; 3698 seg.level = i; // not needed anymore 3699 segs.push(seg); 3700 } 3701 } 3702 return segs; 3703 } 3704 3705 3706 function compileSlotSegs(events) { 3707 var colCnt = getColCnt(), 3708 minMinute = getMinMinute(), 3709 maxMinute = getMaxMinute(), 3710 d = addMinutes(cloneDate(t.visStart), minMinute), 3711 visEventEnds = $.map(events, slotEventEnd), 3712 i, col, 3713 j, level, 3714 k, seg, 3715 segs=[]; 3716 for (i=0; i<colCnt; i++) { 3717 col = stackSegs(sliceSegs(events, visEventEnds, d, addMinutes(cloneDate(d), maxMinute-minMinute))); 3718 countForwardSegs(col); 3719 for (j=0; j<col.length; j++) { 3720 level = col[j]; 3721 for (k=0; k<level.length; k++) { 3722 seg = level[k]; 3723 seg.col = i; 3724 seg.level = j; 3725 segs.push(seg); 3726 } 3727 } 3728 addDays(d, 1, true); 3729 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 4294 function isEventResizable(event) { // but also need to make sure the seg.isEnd == true 4295 return isEventEditable(event) && !opt('disableResizing'); 4296 } 4297 4298 4299 function isEventEditable(event) { 4300 return firstDefined(event.editable, (event.source || {}).editable, opt('editable')); 4301 } 4302 4303 4304 4305 /* Event Data 4306 ------------------------------------------------------------------------------*/ 4307 4308 4309 // report when view receives new events 4310 function reportEvents(events) { // events are already normalized at this point 4311 eventsByID = {}; 4312 var i, len=events.length, event; 4313 for (i=0; i<len; i++) { 4314 event = events[i]; 4315 if (eventsByID[event._id]) { 4316 eventsByID[event._id].push(event); 4317 }else{ 4318 eventsByID[event._id] = [event]; 4319 } 4320 } 4321 } 4322 4323 4324 // returns a Date object for an event's end 4325 function eventEnd(event) {
4326 return event.end ? cloneDate(event.end) : defaultEventEnd(event); 4327 } 4328 4329 4330 4331 /* Event Elements 4332 ------------------------------------------------------------------------------*/ 4333 4334 4335 // report when view creates an element for an event 4336 function reportEventElement(event, element) { 4337 eventElements.push(element); 4338 if (eventElementsByID[event._id]) { 4339 eventElementsByID[event._id].push(element); 4340 }else{ 4341 eventElementsByID[event._id] = [element]; 4342 } 4343 } 4344 4345 4346 function reportEventClear() { 4347 eventElements = []; 4348 eventElementsByID = {}; 4349 } 4350 4351 4352 // attaches eventClick, eventMouseover, eventMouseout 4353 function eventElementHandlers(event, eventElement) { 4354 eventElement 4355 .click(function(ev) { 4356 if (!eventElement.hasClass('ui-draggable-dragging') && 4357 !eventElement.hasClass('ui-resizable-resizing')) { 4358 return trigger('eventClick', this, event, ev); 4359 } 4360 }) 4361 .hover( 4362 function(ev) { 4363 trigger('eventMouseover', this, event, ev); 4364 }, 4365 function(ev) { 4366 trigger('eventMouseout', this, event, ev); 4367 } 4368 ); 4369 // TODO: don't fire eventMouseover/eventMouseout *while* dragging is occuring (on subject element) 4370 // TODO: same for resizing 4371 } 4372 4373 4374 function showEvents(event, exceptElement) { 4375 eachEventElement(event, exceptElement, 'show'); 4376 } 4377 4378 4379 function hideEvents(event, exceptElement) { 4380 eachEventElement(event, exceptElement, 'hide'); 4381 } 4382 4383 4384 function eachEventElement(event, exceptElement, funcName) { 4385 var elements = eventElementsByID[event._id], 4386 i, len = elements.length; 4387 for (i=0; i<len; i++) { 4388 if (!exceptElement || elements[i][0] != exceptElement[0]) { 4389 elements[i][funcName](); 4390 } 4391 } 4392 } 4393 4394 4395 4396 /* Event Modification Reporting 4397 ---------------------------------------------------------------------------------*/ 4398 4399 4400 function eventDrop(e, event, dayDelta, minuteDelta, allDay, ev, ui) { 4401 var oldAllDay = event.allDay; 4402 var eventId = event._id; 4403 moveEvents(eventsByID[eventId], dayDelta, minuteDelta, allDay); 4404 trigger( 4405 'eventDrop', 4406 e, 4407 event, 4408 dayDelta, 4409 minuteDelta, 4410 allDay, 4411 function() { 4412 // TODO: investigate cases where this inverse technique might not work 4413 moveEvents(eventsByID[eventId], -dayDelta, -minuteDelta, oldAllDay); 4414 reportEventChange(eventId); 4415 }, 4416 ev, 4417 ui 4418 ); 4419 reportEventChange(eventId); 4420 } 4421 4422 4423 function eventResize(e, event, dayDelta, minuteDelta, ev, ui) { 4424 var eventId = event._id; 4425 elongateEvents(eventsByID[eventId], dayDelta, minuteDelta); 4426 trigger( 4427 'eventResize', 4428 e, 4429 event, 4430 dayDelta, 4431 minuteDelta, 4432 function() { 4433 // TODO: investigate cases where this inverse technique might not work 4434 elongateEvents(eventsByID[eventId], -dayDelta, -minuteDelta); 4435 reportEventChange(eventId); 4436 }, 4437 ev, 4438 ui 4439 ); 4440 reportEventChange(eventId); 4441 } 4442 4443 4444 4445 /* Event Modification Math 4446 ---------------------------------------------------------------------------------*/ 4447 4448 4449 function moveEvents(events, dayDelta, minuteDelta, allDay) { 4450 minuteDelta = minuteDelta || 0; 4451 for (var e, len=events.length, i=0; i<len; i++) { 4452 e = events[i]; 4453 if (allDay !== undefined) { 4454 e.allDay = allDay; 4455 } 4456 addMinutes(addDays(e.start, dayDelta, true), minuteDelta); 4457 if (e.end) { 4458 e.end = addMinutes(addDays(e.end, dayDelta, true), minuteDelta); 4459 } 4460 normalizeEvent(e, options); 4461 } 4462 } 4463 4464 4465 function elongateEvents(events, dayDelta, minuteDelta) { 4466 minuteDelta = minuteDelta || 0; 4467 for (var e, len=events.length, i=0; i<len; i++) { 4468 e = events[i]; 4469 e.end = addMinutes(addDays(eventEnd(e), dayDelta, true), minuteDelta); 4470 normalizeEvent(e, options); 4471 } 4472 } 4473 4474 4475 } 4476 4477 function DayEventRenderer() { 4478 var t = this; 4479 4480 4481 // exports 4482 t.renderDaySegs = renderDaySegs; 4483 t.resizableDayEvent = resizableDayEvent; 4484 4485 4486 // imports 4487 var opt = t.opt; 4488 var trigger = t.trigger; 4489 var isEventDraggable = t.isEventDraggable; 4490 var isEventResizable = t.isEventResizable; 4491 var eventEnd = t.eventEnd; 4492 var reportEventElement = t.reportEventElement; 4493 var showEvents = t.showEvents; 4494 var hideEvents = t.hideEvents; 4495 var eventResize = t.eventResize; 4496 var getRowCnt = t.getRowCnt; 4497 var getColCnt = t.getColCnt; 4498 var getColWidth = t.getColWidth; 4499 var allDayRow = t.allDayRow; 4500 var allDayBounds = t.allDayBounds; 4501 var colContentLeft = t.colContentLeft; 4502 var colContentRight = t.colContentRight; 4503 var dayOfWeekCol = t.dayOfWeekCol; 4504 var dateCell = t.dateCell; 4505 var compileDaySegs = t.compileDaySegs; 4506 var getDaySegmentContainer = t.getDaySegmentContainer; 4507 var bindDaySeg = t.bindDaySeg; //TODO: streamline this 4508 var formatDates = t.calendar.formatDates; 4509 var renderDayOverlay = t.renderDayOverlay; 4510 var clearOverlays = t.clearOverlays; 4511 var clearSelection = t.clearSelection; 4512 4513 4514 4515 /* Rendering 4516 -----------------------------------------------------------------------------*/ 4517 4518 4519 function renderDaySegs(segs, modifiedEventId) { 4520 var segmentContainer = getDaySegmentContainer(); 4521 var rowDivs;
4522 var rowCnt = getRowCnt(); 4523 var colCnt = getColCnt(); 4524 var i = 0; 4525 var rowI; 4526 var levelI; 4527 var colHeights; 4528 var j; 4529 var segCnt = segs.length; 4530 var seg; 4531 var top; 4532 var k; 4533 segmentContainer[0].innerHTML = daySegHTML(segs); // faster than .html() 4534 daySegElementResolve(segs, segmentContainer.children()); 4535 daySegElementReport(segs); 4536 daySegHandlers(segs, segmentContainer, modifiedEventId); 4537 daySegCalcHSides(segs); 4538 daySegSetWidths(segs); 4539 daySegCalcHeights(segs); 4540 rowDivs = getRowDivs(); 4541 // set row heights, calculate event tops (in relation to row top) 4542 for (rowI=0; rowI<rowCnt; rowI++) { 4543 levelI = 0; 4544 colHeights = []; 4545 for (j=0; j<colCnt; j++) { 4546 colHeights[j] = 0; 4547 } 4548 while (i<segCnt && (seg = segs[i]).row == rowI) { 4549 // loop through segs in a row 4550 top = arrayMax(colHeights.slice(seg.startCol, seg.endCol)); 4551 seg.top = top; 4552 top += seg.outerHeight; 4553 for (k=seg.startCol; k<seg.endCol; k++) { 4554 colHeights[k] = top; 4555 } 4556 i++; 4557 } 4558 rowDivs[rowI].height(arrayMax(colHeights)); 4559 } 4560 daySegSetTops(segs, getRowTops(rowDivs)); 4561 } 4562 4563 4564 function renderTempDaySegs(segs, adjustRow, adjustTop) { 4565 var tempContainer = $("<div/>"); 4566 var elements; 4567 var segmentContainer = getDaySegmentContainer(); 4568 var i; 4569 var segCnt = segs.length; 4570 var element; 4571 tempContainer[0].innerHTML = daySegHTML(segs); // faster than .html() 4572 elements = tempContainer.children(); 4573 segmentContainer.append(elements); 4574 daySegElementResolve(segs, elements); 4575 daySegCalcHSides(segs); 4576 daySegSetWidths(segs); 4577 daySegCalcHeights(segs); 4578 daySegSetTops(segs, getRowTops(getRowDivs())); 4579 elements = []; 4580 for (i=0; i<segCnt; i++) { 4581 element = segs[i].element; 4582 if (element) { 4583 if (segs[i].row === adjustRow) { 4584 element.css('top', adjustTop); 4585 } 4586 elements.push(element[0]); 4587 } 4588 } 4589 return $(elements); 4590 } 4591 4592 4593 function daySegHTML(segs) { // also sets seg.left and seg.outerWidth 4594 var rtl = opt('isRTL'); 4595 var i; 4596 var segCnt=segs.length; 4597 var seg; 4598 var event; 4599 var url; 4600 var classes; 4601 var bounds = allDayBounds(); 4602 var minLeft = bounds.left; 4603 var maxLeft = bounds.right; 4604 var leftCol; 4605 var rightCol; 4606 var left; 4607 var right; 4608 var skinCss; 4609 var html = ''; 4610 // calculate desired position/dimensions, create html 4611 for (i=0; i<segCnt; i++) { 4612 seg = segs[i]; 4613 event = seg.event; 4614 classes = ['fc-event', 'fc-event-skin', 'fc-event-hori']; 4615 if (isEventDraggable(event)) { 4616 classes.push('fc-event-draggable'); 4617 } 4618 if (rtl) { 4619 if (seg.isStart) { 4620 classes.push('fc-corner-right'); 4621 } 4622 if (seg.isEnd) { 4623 classes.push('fc-corner-left'); 4624 } 4625 leftCol = dayOfWeekCol(seg.end.getDay()-1); 4626 rightCol = dayOfWeekCol(seg.start.getDay()); 4627 left = seg.isEnd ? colContentLeft(leftCol) : minLeft; 4628 right = seg.isStart ? colContentRight(rightCol) : maxLeft; 4629 }else{ 4630 if (seg.isStart) { 4631 classes.push('fc-corner-left'); 4632 } 4633 if (seg.isEnd) { 4634 classes.push('fc-corner-right'); 4635 } 4636 leftCol = dayOfWeekCol(seg.start.getDay()); 4637 rightCol = dayOfWeekCol(seg.end.getDay()-1); 4638 left = seg.isStart ? colContentLeft(leftCol) : minLeft; 4639 right = seg.isEnd ? colContentRight(rightCol) : maxLeft; 4640 } 4641 classes = classes.concat(event.className); 4642 if (event.source) { 4643 classes = classes.concat(event.source.className || []); 4644 } 4645 url = event.url; 4646 skinCss = getSkinCss(event, opt); 4647 if (url) { 4648 html += "<a href='" + htmlEscape(url) + "'"; 4649 }else{ 4650 html += "<div"; 4651 } 4652 html += 4653 " class='" + classes.join(' ') + "'" + 4654 " style='position:absolute;z-index:8;left:"+left+"px;" + skinCss + "'" + 4655 ">" + 4656 "<div" + 4657 " class='fc-event-inner fc-event-skin'" + 4658 (skinCss ? " style='" + skinCss + "'" : '') + 4659 ">"; 4660 if (!event.allDay && seg.isStart) { 4661 html += 4662 "<span class='fc-event-time'>" + 4663 htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) + 4664 "</span>"; 4665 } 4666 html += 4667 "<span class='fc-event-title'>" + htmlEscape(event.title) + "</span>" + 4668 "</div>"; 4669 if (seg.isEnd && isEventResizable(event)) { 4670 html += 4671 "<div class='ui-resizable-handle ui-resizable-" + (rtl ? 'w' : 'e') + "'>" + 4672 " " + // makes hit area a lot better for IE6/7 4673 "</div>"; 4674 } 4675 html += 4676 "</" + (url ? "a" : "div" ) + ">"; 4677 seg.left = left; 4678 seg.outerWidth = right - left; 4679 seg.startCol = leftCol; 4680 seg.endCol = rightCol + 1; // needs to be exclusive 4681 } 4682 return html; 4683 } 4684 4685 4686 function daySegElementResolve(segs, elements) { // sets seg.element 4687 var i; 4688 var segCnt = segs.length; 4689 var seg; 4690 var event; 4691 var element; 4692 var triggerRes; 4693 for (i=0; i<segCnt; i++) { 4694 seg = segs[i]; 4695 event = seg.event; 4696 element = $(elements[i]); // faster than .eq() 4697 triggerRes = trigger('eventRender', event, event, element); 4698 if (triggerRes === false) { 4699 element.remove(); 4700 }else{ 4701 if (triggerRes && triggerRes !== true) { 4702 triggerRes = $(triggerRes) 4703 .css({ 4704 position: 'absolute', 4705 left: seg.left 4706 }); 4707 element.replaceWith(triggerRes); 4708 element = triggerRes; 4709 } 4710 seg.element = element; 4711 } 4712 } 4713 } 4714 4715 4716 function daySegElementReport(segs) { 4717 var i; 4718 var segCnt = segs.length; 4719 var seg; 4720 var element; 4721 for (i=0; i<segCnt; i++) { 4722 seg = segs[i]; 4723 element = seg.element; 4724 if (element) { 4725 reportEventElement(seg.event, element); 4726 } 4727 } 4728 } 4729 4730 4731 function daySegHandlers(segs, segmentContainer, modifiedEventId) { 4732 var i; 4733 var segCnt = segs.length; 4734 var seg; 4735 var element; 4736 var event; 4737 // retrieve elements, run through eventRender callback, bind handlers 4738 for (i=0; i<segCnt; i++) { 4739 seg = segs[i]; 4740 element = seg.element; 4741 if (element) { 4742 event = seg.event; 4743 if (event._id === modifiedEventId) { 4744 bindDaySeg(event, element, seg); 4745 }else{ 4746 element[0]._fci = i; // for lazySegBind 4747 } 4748 } 4749 } 4750 lazySegBind(segmentContainer, segs, bindDaySeg); 4751 } 4752 4753 4754 function daySegCalcHSides(segs) { // also sets seg.key 4755 var i; 4756 var segCnt = segs.length; 4757 var seg; 4758 var element; 4759 var key, val; 4760 var hsideCache = {}; 4761 // record event horizontal sides 4762 for (i=0; i<segCnt; i++) { 4763 seg = segs[i]; 4764 element = seg.element; 4765 if (element) { 4766 key = seg.key = cssKey(element[0]); 4767 val = hsideCache[key]; 4768 if (val === undefined) { 4769 val = hsideCache[key] = hsides(element, true); 4770 } 4771 seg.hsides = val; 4772 } 4773 } 4774 } 4775 4776 4777 function daySegSetWidths(segs) { 4778 var i; 4779 var segCnt = segs.length; 4780 var seg; 4781 var element; 4782 for (i=0; i<segCnt; i++) { 4783 seg = segs[i]; 4784 element = seg.element; 4785 if (element) { 4786 element[0].style.width = Math.max(0, seg.outerWidth - seg.hsides) + 'px'; 4787 } 4788 } 4789 } 4790 4791 4792 function daySegCalcHeights(segs) { 4793 var i; 4794 var segCnt = segs.length; 4795 var seg; 4796 var element; 4797 var key, val; 4798 var vmarginCache = {}; 4799 // record event heights 4800 for (i=0; i<segCnt; i++) { 4801 seg = segs[i]; 4802 element = seg.element; 4803 if (element) { 4804 key = seg.key; // created in daySegCalcHSides 4805 val = vmarginCache[key]; 4806 if (val === undefined) { 4807 val = vmarginCache[key] = vmargins(element); 4808 } 4809 seg.outerHeight = element[0].offsetHeight + val; 4810 } 4811 } 4812 } 4813 4814 4815 function getRowDivs() { 4816 var i;
4817 var rowCnt = getRowCnt(); 4818 var rowDivs = []; 4819 for (i=0; i<rowCnt; i++) { 4820 rowDivs[i] = allDayRow(i) 4821 .find('td:first div.fc-day-content > div'); // optimal selector? 4822 } 4823 return rowDivs; 4824 } 4825 4826 4827 function getRowTops(rowDivs) { 4828 var i; 4829 var rowCnt = rowDivs.length; 4830 var tops = []; 4831 for (i=0; i<rowCnt; i++) { 4832 tops[i] = rowDivs[i][0].offsetTop; // !!?? but this means the element needs position:relative if in a table cell!!!! 4833 } 4834 return tops; 4835 } 4836 4837 4838 function daySegSetTops(segs, rowTops) { // also triggers eventAfterRender 4839 var i; 4840 var segCnt = segs.length; 4841 var seg; 4842 var element; 4843 var event; 4844 for (i=0; i<segCnt; i++) { 4845 seg = segs[i]; 4846 element = seg.element; 4847 if (element) { 4848 element[0].style.top = rowTops[seg.row] + (seg.top||0) + 'px'; 4849 event = seg.event; 4850 trigger('eventAfterRender', event, event, element); 4851 } 4852 } 4853 } 4854 4855 4856 4857 /* Resizing 4858 -----------------------------------------------------------------------------------*/ 4859 4860 4861 function resizableDayEvent(event, element, seg) { 4862 var rtl = opt('isRTL'); 4863 var direction = rtl ? 'w' : 'e'; 4864 var handle = element.find('div.ui-resizable-' + direction); 4865 var isResizing = false; 4866 4867 // TODO: look into using jquery-ui mouse widget for this stuff 4868 disableTextSelection(element); // prevent native <a> selection for IE 4869 element 4870 .mousedown(function(ev) { // prevent native <a> selection for others 4871 ev.preventDefault(); 4872 }) 4873 .click(function(ev) { 4874 if (isResizing) { 4875 ev.preventDefault(); // prevent link from being visited (only method that worked in IE6) 4876 ev.stopImmediatePropagation(); // prevent fullcalendar eventClick handler from being called 4877 // (eventElementHandlers needs to be bound after resizableDayEvent) 4878 } 4879 }); 4880 4881 handle.mousedown(function(ev) { 4882 if (ev.which != 1) { 4883 return; // needs to be left mouse button 4884 } 4885 isResizing = true; 4886 var hoverListener = t.getHoverListener();
4887 var rowCnt = getRowCnt(); 4888 var colCnt = getColCnt(); 4889 var dis = rtl ? -1 : 1; 4890 var dit = rtl ? colCnt-1 : 0; 4891 var elementTop = element.css('top'); 4892 var dayDelta; 4893 var helpers; 4894 var eventCopy = $.extend({}, event); 4895 var minCell = dateCell(event.start); 4896 clearSelection(); 4897 $('body') 4898 .css('cursor', direction + '-resize') 4899 .one('mouseup', mouseup); 4900 trigger('eventResizeStart', this, event, ev); 4901 hoverListener.start(function(cell, origCell) { 4902 if (cell) { 4903 var r = Math.max(minCell.row, cell.row); 4904 var c = cell.col; 4905 if (rowCnt == 1) { 4906 r = 0; // hack for all-day area in agenda views 4907 } 4908 if (r == minCell.row) { 4909 if (rtl) { 4910 c = Math.min(minCell.col, c); 4911 }else{ 4912 c = Math.max(minCell.col, c); 4913 } 4914 } 4915 dayDelta = (r*7 + c*dis+dit) - (origCell.row*7 + origCell.col*dis+dit); 4916 var newEnd = addDays(eventEnd(event), dayDelta, true); 4917 if (dayDelta) { 4918 eventCopy.end = newEnd; 4919 var oldHelpers = helpers; 4920 helpers = renderTempDaySegs(compileDaySegs([eventCopy]), seg.row, elementTop); 4921 helpers.find('*').css('cursor', direction + '-resize'); 4922 if (oldHelpers) { 4923 oldHelpers.remove(); 4924 } 4925 hideEvents(event); 4926 }else{ 4927 if (helpers) { 4928 showEvents(event); 4929 helpers.remove(); 4930 helpers = null; 4931 } 4932 } 4933 clearOverlays(); 4934 renderDayOverlay(event.start, addDays(cloneDate(newEnd), 1)); // coordinate grid already rebuild at hoverListener.start 4935 } 4936 }, ev); 4937 4938 function mouseup(ev) { 4939 trigger('eventResizeStop', this, event, ev); 4940 $('body').css('cursor', ''); 4941 hoverListener.stop(); 4942 clearOverlays(); 4943 if (dayDelta) { 4944 eventResize(this, event, dayDelta, 0, ev); 4945 // event redraw will clear helpers 4946 } 4947 // otherwise, the drag handler already restored the old events 4948 4949 setTimeout(function() { // make this happen after the element's click event 4950 isResizing = false; 4951 },0); 4952 } 4953 4954 }); 4955 } 4956 4957 4958 } 4959 4960 //BUG: unselect needs to be triggered when events are dragged+dropped 4961 4962 function SelectionManager() { 4963 var t = this; 4964 4965 4966 // exports 4967 t.select = select; 4968 t.unselect = unselect; 4969 t.reportSelection = reportSelection; 4970 t.daySelectionMousedown = daySelectionMousedown; 4971 4972 4973 // imports 4974 var opt = t.opt; 4975 var trigger = t.trigger; 4976 var defaultSelectionEnd = t.defaultSelectionEnd; 4977 var renderSelection = t.renderSelection; 4978 var clearSelection = t.clearSelection; 4979 4980 4981 // locals 4982 var selected = false; 4983 4984 4985 4986 // unselectAuto 4987 if (opt('selectable') && opt('unselectAuto')) { 4988 $(document).mousedown(function(ev) { 4989 var ignore = opt('unselectCancel'); 4990 if (ignore) { 4991 if ($(ev.target).parents(ignore).length) { // could be optimized to stop after first match 4992 return; 4993 } 4994 } 4995 unselect(ev); 4996 }); 4997 } 4998 4999 5000 function select(startDate, endDate, allDay) { 5001 unselect(); 5002 if (!endDate) { 5003 endDate = defaultSelectionEnd(startDate, allDay); 5004 } 5005 renderSelection(startDate, endDate, allDay); 5006 reportSelection(startDate, endDate, allDay); 5007 } 5008 5009 5010 function unselect(ev) { 5011 if (selected) { 5012 selected = false; 5013 clearSelection(); 5014 trigger('unselect', null, ev); 5015 } 5016 } 5017 5018 5019 function reportSelection(startDate, endDate, allDay, ev) { 5020 selected = true; 5021 trigger('select', null, startDate, endDate, allDay, ev); 5022 } 5023 5024 5025 function daySelectionMousedown(ev) { // not really a generic manager method, oh well 5026 var cellDate = t.cellDate; 5027 var cellIsAllDay = t.cellIsAllDay; 5028 var hoverListener = t.getHoverListener(); 5029 var reportDayClick = t.reportDayClick; // this is hacky and sort of weird 5030 if (ev.which == 1 && opt('selectable')) { // which==1 means left mouse button 5031 unselect(ev); 5032 var _mousedownElement = this; 5033 var dates; 5034 hoverListener.start(function(cell, origCell) { // TODO: maybe put cellDate/cellIsAllDay info in cell 5035 clearSelection(); 5036 if (cell && cellIsAllDay(cell)) { 5037 dates = [ cellDate(origCell), cellDate(cell) ].sort(cmp); 5038 renderSelection(dates[0], dates[1], true); 5039 }else{ 5040 dates = null; 5041 } 5042 }, ev); 5043 $(document).one('mouseup', function(ev) { 5044 hoverListener.stop(); 5045 if (dates) { 5046 if (+dates[0] == +dates[1]) { 5047 reportDayClick(dates[0], true, ev); 5048 } 5049 reportSelection(dates[0], dates[1], true, ev); 5050 } 5051 }); 5052 } 5053 } 5054 5055 5056 } 5057 5058 function OverlayManager() { 5059 var t = this; 5060
5061 5062 // exports 5063 t.renderOverlay = renderOverlay; 5064 t.clearOverlays = clearOverlays; 5065 5066 5067 // locals 5068 var usedOverlays = []; 5069 var unusedOverlays = []; 5070 5071 5072 function renderOverlay(rect, parent) { 5073 var e = unusedOverlays.shift(); 5074 if (!e) { 5075 e = $("<div class='fc-cell-overlay' style='position:absolute;z-index:3'/>"); 5076 } 5077 if (e[0].parentNode != parent[0]) { 5078 e.appendTo(parent); 5079 } 5080 usedOverlays.push(e.css(rect).show()); 5081 return e; 5082 } 5083 5084 5085 function clearOverlays() { 5086 var e; 5087 while (e = usedOverlays.shift()) { 5088 unusedOverlays.push(e.hide().unbind()); 5089 } 5090 } 5091 5092 5093 } 5094 5095 function CoordinateGrid(buildFunc) { 5096 5097 var t = this; 5098 var rows; 5099 var cols; 5100 5101 5102 t.build = function() { 5103 rows = []; 5104 cols = []; 5105 buildFunc(rows, cols); 5106 }; 5107 5108 5109 t.cell = function(x, y) { 5110 var rowCnt = rows.length; 5111 var colCnt = cols.length; 5112 var i, r=-1, c=-1; 5113 for (i=0; i<rowCnt; i++) { 5114 if (y >= rows[i][0] && y < rows[i][1]) { 5115 r = i; 5116 break; 5117 } 5118 } 5119 for (i=0; i<colCnt; i++) { 5120 if (x >= cols[i][0] && x < cols[i][1]) { 5121 c = i; 5122 break; 5123 } 5124 } 5125 return (r>=0 && c>=0) ? { row:r, col:c } : null; 5126 }; 5127 5128 5129 t.rect = function(row0, col0, row1, col1, originElement) { // row1,col1 is inclusive 5130 var origin = originElement.offset(); 5131 return { 5132 top: rows[row0][0] - origin.top, 5133 left: cols[col0][0] - origin.left, 5134 width: cols[col1][1] - cols[col0][0], 5135 height: rows[row1][1] - rows[row0][0] 5136 }; 5137 }; 5138 5139 } 5140 5141 function HoverListener(coordinateGrid) { 5142 5143 5144 var t = this; 5145 var bindType; 5146 var change; 5147 var firstCell; 5148 var cell; 5149 5150 5151 t.start = function(_change, ev, _bindType) { 5152 change = _change; 5153 firstCell = cell = null; 5154 coordinateGrid.build(); 5155 mouse(ev); 5156 bindType = _bindType || 'mousemove'; 5157 $(document).bind(bindType, mouse); 5158 }; 5159 5160 5161 function mouse(ev) { 5162 var newCell = coordinateGrid.cell(ev.pageX, ev.pageY); 5163 if (!newCell != !cell || newCell && (newCell.row != cell.row || newCell.col != cell.col)) { 5164 if (newCell) { 5165 if (!firstCell) { 5166 firstCell = newCell; 5167 } 5168 change(newCell, firstCell, newCell.row-firstCell.row, newCell.col-firstCell.col); 5169 }else{ 5170 change(newCell, firstCell); 5171 } 5172 cell = newCell; 5173 } 5174 } 5175 5176 5177 t.stop = function() { 5178 $(document).unbind(bindType, mouse); 5179 return cell; 5180 }; 5181 5182 5183 } 5184 5185 function HorizontalPositionCache(getElement) { 5186 5187 var t = this, 5188 elements = {}, 5189 lefts = {}, 5190 rights = {}; 5191 5192 function e(i) { 5193 return elements[i] = elements[i] || getElement(i); 5194 } 5195 5196 t.left = function(i) { 5197 return lefts[i] = lefts[i] === undefined ? e(i).position().left : lefts[i]; 5198 }; 5199 5200 t.right = function(i) { 5201 return rights[i] = rights[i] === undefined ? t.left(i) + e(i).width() : rights[i]; 5202 }; 5203 5204 t.clear = function() { 5205 elements = {}; 5206 lefts = {}; 5207 rights = {}; 5208 }; 5209 5210 } 5211 5212 })(jQuery); 5213 5214 5215/* 5216 * FullCalendar v1.5.1 Google Calendar Plugin 5217 * 5218 * Copyright (c) 2011 Adam Shaw 5219 * Dual licensed under the MIT and GPL licenses, located in 5220 * MIT-LICENSE.txt and GPL-LICENSE.txt respectively. 5221 * 5222 * Date: Sat Apr 9 14:09:51 2011 -0700 5223 * 5224 */ 5225 5226(function($) { 5227 5228 5229var fc = $.fullCalendar; 5230var formatDate = fc.formatDate; 5231var parseISO8601 = fc.parseISO8601; 5232var addDays = fc.addDays; 5233var applyAll = fc.applyAll; 5234 5235 5236fc.sourceNormalizers.push(function(sourceOptions) { 5237 if (sourceOptions.dataType == 'gcal' || 5238 sourceOptions.dataType === undefined && 5239 (sourceOptions.url || '').match(/^(http|https):\/\/www.google.com\/calendar\/feeds\//)) { 5240 sourceOptions.dataType = 'gcal'; 5241 if (sourceOptions.editable === undefined) { 5242 sourceOptions.editable = false; 5243 } 5244 } 5245}); 5246 5247 5248fc.sourceFetchers.push(function(sourceOptions, start, end) { 5249 if (sourceOptions.dataType == 'gcal') { 5250 return transformOptions(sourceOptions, start, end); 5251 } 5252}); 5253 5254 5255function transformOptions(sourceOptions, start, end) { 5256 5257 var success = sourceOptions.success; 5258 var data = $.extend({}, sourceOptions.data || {}, { 5259 'start-min': formatDate(start, 'u'), 5260 'start-max': formatDate(end, 'u'), 5261 'singleevents': true, 5262 'max-results': 9999 5263 }); 5264 5265 var ctz = sourceOptions.currentTimezone; 5266 if (ctz) { 5267 data.ctz = ctz = ctz.replace(' ', '_'); 5268 } 5269 5270 return $.extend({}, sourceOptions, { 5271 url: sourceOptions.url.replace(/\/basic$/, '/full') + '?alt=json-in-script&callback=?', 5272 dataType: 'jsonp', 5273 data: data, 5274 startParam: false, 5275 endParam: false, 5276 success: function(data) { 5277 var events = []; 5278 if (data.feed.entry) { 5279 $.each(data.feed.entry, function(i, entry) { 5280 var startStr = entry['gd$when'][0]['startTime'];
5281 var start = parseISO8601(startStr, true); 5282 var end = parseISO8601(entry['gd$when'][0]['endTime'], true); 5283 var allDay = startStr.indexOf('T') == -1; 5284 var url; 5285 $.each(entry.link, function(i, link) { 5286 if (link.type == 'text/html') { 5287 url = link.href; 5288 if (ctz) { 5289 url += (url.indexOf('?') == -1 ? '?' : '&') + 'ctz=' + ctz; 5290 } 5291 } 5292 }); 5293 if (allDay) { 5294 addDays(end, -1); // make inclusive 5295 } 5296 events.push({ 5297 id: entry['gCal$uid']['value'], 5298 title: entry['title']['$t'], 5299 url: url, 5300 start: start, 5301 end: end, 5302 allDay: allDay, 5303 location: entry['gd$where'][0]['valueString'], 5304 description: entry['content']['$t'] 5305 }); 5306 }); 5307 } 5308 var args = [events].concat(Array.prototype.slice.call(arguments, 1)); 5309 var res = applyAll(success, this, args); 5310 if ($.isArray(res)) { 5311 return res; 5312 } 5313 return events; 5314 } 5315 }); 5316 5317} 5318 5319 5320// legacy 5321fc.gcalFeed = function(url, sourceOptions) { 5322 return $.extend({}, sourceOptions, { url: url, dataType: 'gcal' }); 5323}; 5324 5325 5326})(jQuery); 5327 5328 5329 5330});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.