[ Index ] |
PHP Cross Reference of vtigercrm-6.1.0 |
[Summary view] [Print] [Text view]
1 /** 2 * @preserve 3 * FullCalendar v1.5.3 4 * http://arshaw.com/fullcalendar/ 5 * 6 * Use fullcalendar.css for basic styling. 7 * For event drag & drop, requires jQuery UI draggable. 8 * For event resizing, requires jQuery UI resizable. 9 * 10 * Copyright (c) 2011 Adam Shaw 11 * Dual licensed under the MIT and GPL licenses, located in 12 * MIT-LICENSE.txt and GPL-LICENSE.txt respectively. 13 * 14 * Date: Mon Feb 6 22:40:40 2012 -0800 15 * 16 */ 17 18 (function($, undefined) { 19 20 21 var defaults = { 22 23 // display 24 defaultView: 'month', 25 aspectRatio: 1.35, 26 header: { 27 left: 'title', 28 center: '', 29 right: 'today prev,next' 30 }, 31 weekends: true, 32 33 // editing 34 //editable: false, 35 //disableDragging: false, 36 //disableResizing: false, 37 38 allDayDefault: true, 39 ignoreTimezone: true, 40 41 // event ajax 42 lazyFetching: true, 43 startParam: 'start', 44 endParam: 'end', 45 46 // time formats 47 titleFormat: { 48 month: 'MMMM yyyy', 49 week: "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", 50 day: 'dddd, MMM d, yyyy' 51 }, 52 columnFormat: { 53 month: 'ddd', 54 week: 'ddd M/d', 55 day: 'dddd M/d' 56 }, 57 timeFormat: { // for event elements 58 '': 'h(:mm)t' // default 59 }, 60 61 // locale 62 isRTL: false, 63 firstDay: 0, 64 monthNames: ['January','February','March','April','May','June','July','August','September','October','November','December'], 65 monthNamesShort: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'], 66 dayNames: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'], 67 dayNamesShort: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'], 68 buttonText: { 69 prev: ' ◄ ', 70 next: ' ► ', 71 prevYear: ' << ', 72 nextYear: ' >> ', 73 today: 'today', 74 month: 'month', 75 week: 'week', 76 day: 'day' 77 }, 78 79 // jquery-ui theming 80 theme: false, 81 buttonIcons: { 82 prev: 'circle-triangle-w', 83 next: 'circle-triangle-e' 84 }, 85 86 //selectable: false, 87 unselectAuto: true, 88 89 dropAccept: '*' 90 91 }; 92 93 // right-to-left defaults 94 var rtlDefaults = { 95 header: { 96 left: 'next,prev today', 97 center: '', 98 right: 'title' 99 }, 100 buttonText: { 101 prev: ' ► ', 102 next: ' ◄ ', 103 prevYear: ' >> ', 104 nextYear: ' << ' 105 }, 106 buttonIcons: { 107 prev: 'circle-triangle-e', 108 next: 'circle-triangle-w' 109 } 110 }; 111 112 113 114 var fc = $.fullCalendar = { version: "1.5.3" }; 115 var fcViews = fc.views = {}; 116 117 118 $.fn.fullCalendar = function(options) { 119 120 121 // method calling 122 if (typeof options == 'string') { 123 var args = Array.prototype.slice.call(arguments, 1); 124 var res; 125 this.each(function() { 126 var calendar = $.data(this, 'fullCalendar'); 127 if (calendar && $.isFunction(calendar[options])) { 128 var r = calendar[options].apply(calendar, args); 129 if (res === undefined) { 130 res = r; 131 } 132 if (options == 'destroy') { 133 $.removeData(this, 'fullCalendar'); 134 } 135 } 136 }); 137 if (res !== undefined) { 138 return res; 139 } 140 return this; 141 } 142 143 144 // would like to have this logic in EventManager, but needs to happen before options are recursively extended 145 var eventSources = options.eventSources || []; 146 delete options.eventSources; 147 if (options.events) { 148 eventSources.push(options.events); 149 delete options.events; 150 } 151 152 153 options = $.extend(true, {}, 154 defaults, 155 (options.isRTL || options.isRTL===undefined && defaults.isRTL) ? rtlDefaults : {}, 156 options 157 ); 158 159 160 this.each(function(i, _element) { 161 var element = $(_element); 162 var calendar = new Calendar(element, options, eventSources); 163 element.data('fullCalendar', calendar); // TODO: look into memory leak implications 164 calendar.render(); 165 }); 166 167 168 return this; 169 170 }; 171 172 173 // function for adding/overriding defaults 174 function setDefaults(d) { 175 $.extend(true, defaults, d); 176 } 177 178 179 180 181 function Calendar(element, options, eventSources) { 182 var t = this; 183 184 185 // exports 186 t.options = options; 187 t.render = render; 188 t.destroy = destroy; 189 t.refetchEvents = refetchEvents; 190 t.reportEvents = reportEvents; 191 t.reportEventChange = reportEventChange; 192 t.rerenderEvents = rerenderEvents; 193 t.changeView = changeView; 194 t.select = select; 195 t.unselect = unselect; 196 t.prev = prev; 197 t.next = next; 198 t.prevYear = prevYear; 199 t.nextYear = nextYear; 200 t.today = today; 201 t.gotoDate = gotoDate; 202 t.incrementDate = incrementDate; 203 t.formatDate = function(format, date) { return formatDate(format, date, options) }; 204 t.formatDates = function(format, date1, date2) { return formatDates(format, date1, date2, options) }; 205 t.getDate = getDate; 206 t.getView = getView; 207 t.option = option; 208 t.trigger = trigger; 209 210 211 // imports 212 EventManager.call(t, options, eventSources); 213 var isFetchNeeded = t.isFetchNeeded; 214 var fetchEvents = t.fetchEvents; 215 216 217 // locals 218 var _element = element[0]; 219 var header; 220 var headerElement; 221 var content; 222 var tm; // for making theme classes 223 var currentView; 224 var viewInstances = {}; 225 var elementOuterWidth; 226 var suggestedViewHeight; 227 var absoluteViewElement; 228 var resizeUID = 0; 229 var ignoreWindowResize = 0; 230 var date = new Date(); 231 var events = []; 232 var _dragElement; 233 234 235 236 /* Main Rendering 237 -----------------------------------------------------------------------------*/ 238 239 240 setYMD(date, options.year, options.month, options.date); 241 242 243 function render(inc) { 244 if (!content) { 245 initialRender(); 246 }else{ 247 calcSize(); 248 markSizesDirty(); 249 markEventsDirty(); 250 renderView(inc); 251 } 252 } 253 254 255 function initialRender() { 256 tm = options.theme ? 'ui' : 'fc'; 257 element.addClass('fc'); 258 if (options.isRTL) { 259 element.addClass('fc-rtl'); 260 } 261 if (options.theme) { 262 element.addClass('ui-widget'); 263 } 264 content = $("<div class='fc-content' style='position:relative'/>") 265 .prependTo(element); 266 header = new Header(t, options); 267 headerElement = header.render(); 268 if (headerElement) { 269 element.prepend(headerElement); 270 } 271 changeView(options.defaultView); 272 $(window).resize(windowResize); 273 // needed for IE in a 0x0 iframe, b/c when it is resized, never triggers a windowResize 274 if (!bodyVisible()) { 275 lateRender(); 276 } 277 } 278 279 280 // called when we know the calendar couldn't be rendered when it was initialized, 281 // but we think it's ready now 282 function lateRender() { 283 setTimeout(function() { // IE7 needs this so dimensions are calculated correctly 284 if (!currentView.start && bodyVisible()) { // !currentView.start makes sure this never happens more than once 285 renderView(); 286 } 287 },0); 288 } 289 290 291 function destroy() { 292 $(window).unbind('resize', windowResize); 293 header.destroy(); 294 content.remove(); 295 element.removeClass('fc fc-rtl ui-widget'); 296 } 297 298 299 300 function elementVisible() { 301 return _element.offsetWidth !== 0; 302 } 303 304 305 function bodyVisible() { 306 return $('body')[0].offsetWidth !== 0; 307 } 308 309 310 311 /* View Rendering 312 -----------------------------------------------------------------------------*/ 313 314 // TODO: improve view switching (still weird transition in IE, and FF has whiteout problem) 315 316 function changeView(newViewName) { 317 if (!currentView || newViewName != currentView.name) { 318 ignoreWindowResize++; // because setMinHeight might change the height before render (and subsequently setSize) is reached 319 320 unselect(); 321 322 var oldView = currentView; 323 var newViewElement; 324 325 if (oldView) { 326 (oldView.beforeHide || noop)(); // called before changing min-height. if called after, scroll state is reset (in Opera) 327 setMinHeight(content, content.height()); 328 oldView.element.hide(); 329 }else{ 330 setMinHeight(content, 1); // needs to be 1 (not 0) for IE7, or else view dimensions miscalculated 331 } 332 content.css('overflow', 'hidden'); 333 334 currentView = viewInstances[newViewName]; 335 if (currentView) { 336 currentView.element.show(); 337 }else{ 338 currentView = viewInstances[newViewName] = new fcViews[newViewName]( 339 newViewElement = absoluteViewElement = 340 $("<div class='fc-view fc-view-" + newViewName + "' style='position:absolute'/>") 341 .appendTo(content), 342 t // the calendar object 343 ); 344 } 345 346 if (oldView) { 347 header.deactivateButton(oldView.name); 348 } 349 header.activateButton(newViewName); 350 351 renderView(); // after height has been set, will make absoluteViewElement's position=relative, then set to null 352 353 content.css('overflow', ''); 354 if (oldView) { 355 setMinHeight(content, 1); 356 } 357 358 if (!newViewElement) { 359 (currentView.afterShow || noop)(); // called after setting min-height/overflow, so in final scroll state (for Opera) 360 } 361 362 ignoreWindowResize--; 363 } 364 } 365 366 367 368 function renderView(inc) { 369 if (elementVisible()) { 370 ignoreWindowResize++; // because renderEvents might temporarily change the height before setSize is reached 371 372 unselect(); 373 374 if (suggestedViewHeight === undefined) { 375 calcSize(); 376 } 377 378 var forceEventRender = false; 379 if (!currentView.start || inc || date < currentView.start || date >= currentView.end) { 380 // view must render an entire new date range (and refetch/render events) 381 currentView.render(date, inc || 0); // responsible for clearing events 382 setSize(true); 383 forceEventRender = true; 384 } 385 else if (currentView.sizeDirty) { 386 // view must resize (and rerender events) 387 currentView.clearEvents(); 388 setSize(); 389 forceEventRender = true; 390 } 391 else if (currentView.eventsDirty) { 392 currentView.clearEvents(); 393 forceEventRender = true; 394 } 395 currentView.sizeDirty = false; 396 currentView.eventsDirty = false; 397 updateEvents(forceEventRender); 398 399 elementOuterWidth = element.outerWidth(); 400 401 header.updateTitle(currentView.title); 402 var today = new Date(); 403 if (today >= currentView.start && today < currentView.end) { 404 header.disableButton('today'); 405 }else{ 406 header.enableButton('today'); 407 } 408 409 ignoreWindowResize--; 410 currentView.trigger('viewDisplay', _element); 411 } 412 } 413 414 415 416 /* Resizing 417 -----------------------------------------------------------------------------*/ 418 419 420 function updateSize() { 421 markSizesDirty(); 422 if (elementVisible()) { 423 calcSize(); 424 setSize(); 425 unselect(); 426 currentView.clearEvents(); 427 currentView.renderEvents(events); 428 currentView.sizeDirty = false; 429 } 430 } 431 432 433 function markSizesDirty() { 434 $.each(viewInstances, function(i, inst) { 435 inst.sizeDirty = true; 436 }); 437 } 438 439 440 function calcSize() { 441 if (options.contentHeight) { 442 suggestedViewHeight = options.contentHeight; 443 } 444 else if (options.height) { 445 suggestedViewHeight = options.height - (headerElement ? headerElement.height() : 0) - vsides(content); 446 } 447 else { 448 suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5)); 449 } 450 } 451 452 453 function setSize(dateChanged) { // todo: dateChanged? 454 ignoreWindowResize++; 455 currentView.setHeight(suggestedViewHeight, dateChanged); 456 if (absoluteViewElement) { 457 absoluteViewElement.css('position', 'relative'); 458 absoluteViewElement = null; 459 } 460 currentView.setWidth(content.width(), dateChanged); 461 ignoreWindowResize--; 462 } 463 464 465 function windowResize() { 466 if (!ignoreWindowResize) { 467 if (currentView.start) { // view has already been rendered 468 var uid = ++resizeUID; 469 setTimeout(function() { // add a delay 470 if (uid == resizeUID && !ignoreWindowResize && elementVisible()) { 471 if (elementOuterWidth != (elementOuterWidth = element.outerWidth())) { 472 ignoreWindowResize++; // in case the windowResize callback changes the height 473 updateSize(); 474 currentView.trigger('windowResize', _element); 475 ignoreWindowResize--; 476 } 477 } 478 }, 200); 479 }else{ 480 // calendar must have been initialized in a 0x0 iframe that has just been resized 481 lateRender(); 482 } 483 } 484 } 485 486 487 488 /* Event Fetching/Rendering 489 -----------------------------------------------------------------------------*/ 490 491 492 // fetches events if necessary, rerenders events if necessary (or if forced) 493 function updateEvents(forceRender) { 494 if (!options.lazyFetching || isFetchNeeded(currentView.visStart, currentView.visEnd)) { 495 refetchEvents(); 496 } 497 else if (forceRender) { 498 rerenderEvents(); 499 } 500 } 501 502 503 function refetchEvents() { 504 fetchEvents(currentView.visStart, currentView.visEnd); // will call reportEvents 505 } 506 507 508 // called when event data arrives 509 function reportEvents(_events) { 510 events = _events; 511 rerenderEvents(); 512 } 513 514 515 // called when a single event's data has been changed 516 function reportEventChange(eventID) { 517 rerenderEvents(eventID); 518 } 519 520 521 // attempts to rerenderEvents 522 function rerenderEvents(modifiedEventID) { 523 markEventsDirty(); 524 if (elementVisible()) { 525 currentView.clearEvents(); 526 currentView.renderEvents(events, modifiedEventID); 527 currentView.eventsDirty = false; 528 } 529 } 530 531 532 function markEventsDirty() { 533 $.each(viewInstances, function(i, inst) { 534 inst.eventsDirty = true; 535 }); 536 } 537 538 539 540 /* Selection 541 -----------------------------------------------------------------------------*/ 542 543 544 function select(start, end, allDay) { 545 currentView.select(start, end, allDay===undefined ? true : allDay); 546 } 547 548 549 function unselect() { // safe to be called before renderView 550 if (currentView) { 551 currentView.unselect(); 552 } 553 } 554 555 556 557 /* Date 558 -----------------------------------------------------------------------------*/ 559 560 561 function prev() { 562 renderView(-1); 563 } 564 565 566 function next() { 567 renderView(1); 568 } 569 570 571 function prevYear() { 572 addYears(date, -1); 573 renderView(); 574 } 575 576 577 function nextYear() { 578 addYears(date, 1); 579 renderView(); 580 } 581 582 583 function today() { 584 date = new Date(); 585 renderView(); 586 } 587 588 589 function gotoDate(year, month, dateOfMonth) { 590 if (year instanceof Date) { 591 date = cloneDate(year); // provided 1 argument, a Date 592 }else{ 593 setYMD(date, year, month, dateOfMonth); 594 } 595 renderView(); 596 } 597 598 599 function incrementDate(years, months, days) { 600 if (years !== undefined) { 601 addYears(date, years); 602 } 603 if (months !== undefined) { 604 addMonths(date, months); 605 } 606 if (days !== undefined) { 607 addDays(date, days); 608 } 609 renderView(); 610 } 611 612 613 function getDate() { 614 return cloneDate(date); 615 } 616 617 618 619 /* Misc 620 -----------------------------------------------------------------------------*/ 621 622 623 function getView() { 624 return currentView; 625 } 626 627 628 function option(name, value) { 629 if (value === undefined) { 630 return options[name]; 631 } 632 if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') { 633 options[name] = value; 634 updateSize(); 635 } 636 } 637 638 639 function trigger(name, thisObj) { 640 if (options[name]) { 641 return options[name].apply( 642 thisObj || _element, 643 Array.prototype.slice.call(arguments, 2) 644 ); 645 } 646 } 647 648 649 650 /* External Dragging 651 ------------------------------------------------------------------------*/ 652 653 if (options.droppable) { 654 $(document) 655 .bind('dragstart', function(ev, ui) { 656 var _e = ev.target; 657 var e = $(_e); 658 if (!e.parents('.fc').length) { // not already inside a calendar 659 var accept = options.dropAccept; 660 if ($.isFunction(accept) ? accept.call(_e, e) : e.is(accept)) { 661 _dragElement = _e; 662 currentView.dragStart(_dragElement, ev, ui); 663 } 664 } 665 }) 666 .bind('dragstop', function(ev, ui) { 667 if (_dragElement) { 668 currentView.dragStop(_dragElement, ev, ui); 669 _dragElement = null; 670 } 671 }); 672 } 673 674 675 } 676 677 function Header(calendar, options) { 678 var t = this; 679 680 681 // exports 682 t.render = render; 683 t.destroy = destroy; 684 t.updateTitle = updateTitle; 685 t.activateButton = activateButton; 686 t.deactivateButton = deactivateButton; 687 t.disableButton = disableButton; 688 t.enableButton = enableButton; 689 690 691 // locals 692 var element = $([]); 693 var tm; 694 695 696 697 function render() { 698 tm = options.theme ? 'ui' : 'fc'; 699 var sections = options.header; 700 if (sections) { 701 element = $("<table class='fc-header' style='width:100%'/>") 702 .append( 703 $("<tr/>") 704 .append(renderSection('left')) 705 .append(renderSection('center')) 706 .append(renderSection('right')) 707 ); 708 return element; 709 } 710 } 711 712 713 function destroy() { 714 element.remove(); 715 } 716 717 718 function renderSection(position) { 719 var e = $("<td class='fc-header-" + position + "'/>"); 720 var buttonStr = options.header[position]; 721 if (buttonStr) { 722 $.each(buttonStr.split(' '), function(i) { 723 if (i > 0) { 724 e.append("<span class='fc-header-space'/>"); 725 } 726 var prevButton; 727 $.each(this.split(','), function(j, buttonName) { 728 if (buttonName == 'title') { 729 e.append("<span class='fc-header-title'><h2> </h2></span>"); 730 if (prevButton) { 731 prevButton.addClass(tm + '-corner-right'); 732 } 733 prevButton = null; 734 }else{ 735 var buttonClick; 736 if (calendar[buttonName]) { 737 buttonClick = calendar[buttonName]; // calendar method 738 } 739 else if (fcViews[buttonName]) { 740 buttonClick = function() { 741 button.removeClass(tm + '-state-hover'); // forget why 742 calendar.changeView(buttonName); 743 }; 744 } 745 if (buttonClick) { 746 var icon = options.theme ? smartProperty(options.buttonIcons, buttonName) : null; // why are we using smartProperty here? 747 var text = smartProperty(options.buttonText, buttonName); // why are we using smartProperty here? 748 var button = $( 749 "<span class='fc-button fc-button-" + buttonName + " " + tm + "-state-default'>" + 750 "<span class='fc-button-inner'>" + 751 "<span class='fc-button-content'>" + 752 (icon ? 753 "<span class='fc-icon-wrap'>" + 754 "<span class='ui-icon ui-icon-" + icon + "'/>" + 755 "</span>" : 756 text 757 ) + 758 "</span>" + 759 "<span class='fc-button-effect'><span></span></span>" + 760 "</span>" + 761 "</span>" 762 ); 763 if (button) { 764 button 765 .click(function() { 766 if (!button.hasClass(tm + '-state-disabled')) { 767 buttonClick(); 768 } 769 }) 770 .mousedown(function() { 771 button 772 .not('.' + tm + '-state-active') 773 .not('.' + tm + '-state-disabled') 774 .addClass(tm + '-state-down'); 775 }) 776 .mouseup(function() { 777 button.removeClass(tm + '-state-down'); 778 }) 779 .hover( 780 function() { 781 button 782 .not('.' + tm + '-state-active') 783 .not('.' + tm + '-state-disabled') 784 .addClass(tm + '-state-hover'); 785 }, 786 function() { 787 button 788 .removeClass(tm + '-state-hover') 789 .removeClass(tm + '-state-down'); 790 } 791 ) 792 .appendTo(e); 793 if (!prevButton) { 794 button.addClass(tm + '-corner-left'); 795 } 796 prevButton = button; 797 } 798 } 799 } 800 }); 801 if (prevButton) { 802 prevButton.addClass(tm + '-corner-right'); 803 } 804 }); 805 } 806 return e; 807 } 808 809 810 function updateTitle(html) { 811 element.find('h2') 812 .html(html); 813 } 814 815 816 function activateButton(buttonName) { 817 element.find('span.fc-button-' + buttonName) 818 .addClass(tm + '-state-active'); 819 } 820 821 822 function deactivateButton(buttonName) { 823 element.find('span.fc-button-' + buttonName) 824 .removeClass(tm + '-state-active'); 825 } 826 827 828 function disableButton(buttonName) { 829 element.find('span.fc-button-' + buttonName) 830 .addClass(tm + '-state-disabled'); 831 } 832 833 834 function enableButton(buttonName) { 835 element.find('span.fc-button-' + buttonName) 836 .removeClass(tm + '-state-disabled'); 837 } 838 839 840 } 841 842 fc.sourceNormalizers = []; 843 fc.sourceFetchers = []; 844 845 var ajaxDefaults = { 846 dataType: 'json', 847 cache: false 848 }; 849 850 var eventGUID = 1; 851 852 853 function EventManager(options, _sources) { 854 var t = this; 855 856 857 // exports 858 t.isFetchNeeded = isFetchNeeded; 859 t.fetchEvents = fetchEvents; 860 t.addEventSource = addEventSource; 861 t.removeEventSource = removeEventSource; 862 t.updateEvent = updateEvent; 863 t.renderEvent = renderEvent; 864 t.removeEvents = removeEvents; 865 t.clientEvents = clientEvents; 866 t.normalizeEvent = normalizeEvent; 867 868 869 // imports 870 var trigger = t.trigger; 871 var getView = t.getView; 872 var reportEvents = t.reportEvents; 873 874 875 // locals 876 var stickySource = { events: [] }; 877 var sources = [ stickySource ]; 878 var rangeStart, rangeEnd; 879 var currentFetchID = 0; 880 var pendingSourceCnt = 0; 881 var loadingLevel = 0; 882 var cache = []; 883 884 885 for (var i=0; i<_sources.length; i++) { 886 _addEventSource(_sources[i]); 887 } 888 889 890 891 /* Fetching 892 -----------------------------------------------------------------------------*/ 893 894 895 function isFetchNeeded(start, end) { 896 return !rangeStart || start < rangeStart || end > rangeEnd; 897 } 898 899 900 function fetchEvents(start, end) { 901 rangeStart = start; 902 rangeEnd = end; 903 cache = []; 904 var fetchID = ++currentFetchID; 905 var len = sources.length; 906 pendingSourceCnt = len; 907 for (var i=0; i<len; i++) { 908 fetchEventSource(sources[i], fetchID); 909 } 910 } 911 912 913 function fetchEventSource(source, fetchID) { 914 _fetchEventSource(source, function(events) { 915 if (fetchID == currentFetchID) { 916 if (events) { 917 for (var i=0; i<events.length; i++) { 918 events[i].source = source; 919 normalizeEvent(events[i]); 920 } 921 cache = cache.concat(events); 922 } 923 pendingSourceCnt--; 924 if (!pendingSourceCnt) { 925 reportEvents(cache); 926 } 927 } 928 }); 929 } 930 931 932 function _fetchEventSource(source, callback) { 933 var i; 934 var fetchers = fc.sourceFetchers; 935 var res; 936 for (i=0; i<fetchers.length; i++) { 937 res = fetchers[i](source, rangeStart, rangeEnd, callback); 938 if (res === true) { 939 // the fetcher is in charge. made its own async request 940 return; 941 } 942 else if (typeof res == 'object') { 943 // the fetcher returned a new source. process it 944 _fetchEventSource(res, callback); 945 return; 946 } 947 } 948 var events = source.events; 949 if (events) { 950 if ($.isFunction(events)) { 951 pushLoading(); 952 events(cloneDate(rangeStart), cloneDate(rangeEnd), function(events) { 953 callback(events); 954 popLoading(); 955 }); 956 } 957 else if ($.isArray(events)) { 958 callback(events); 959 } 960 else { 961 callback(); 962 } 963 }else{ 964 var url = source.url; 965 if (url) { 966 var success = source.success; 967 var error = source.error; 968 var complete = source.complete; 969 var data = $.extend({}, source.data || {}); 970 var startParam = firstDefined(source.startParam, options.startParam); 971 var endParam = firstDefined(source.endParam, options.endParam); 972 if (startParam) { 973 data[startParam] = Math.round(+rangeStart / 1000); 974 } 975 if (endParam) { 976 data[endParam] = Math.round(+rangeEnd / 1000); 977 } 978 pushLoading(); 979 $.ajax($.extend({}, ajaxDefaults, source, { 980 data: data, 981 success: function(events) { 982 events = events || []; 983 var res = applyAll(success, this, arguments); 984 if ($.isArray(res)) { 985 events = res; 986 } 987 callback(events); 988 }, 989 error: function() { 990 applyAll(error, this, arguments); 991 callback(); 992 }, 993 complete: function() { 994 applyAll(complete, this, arguments); 995 popLoading(); 996 } 997 })); 998 }else{ 999 callback(); 1000 } 1001 } 1002 } 1003 1004 1005 1006 /* Sources 1007 -----------------------------------------------------------------------------*/ 1008 1009 1010 function addEventSource(source) { 1011 source = _addEventSource(source); 1012 if (source) { 1013 pendingSourceCnt++; 1014 fetchEventSource(source, currentFetchID); // will eventually call reportEvents 1015 } 1016 } 1017 1018 1019 function _addEventSource(source) { 1020 if ($.isFunction(source) || $.isArray(source)) { 1021 source = { events: source }; 1022 } 1023 else if (typeof source == 'string') { 1024 source = { url: source }; 1025 } 1026 if (typeof source == 'object') { 1027 normalizeSource(source); 1028 sources.push(source); 1029 return source; 1030 } 1031 } 1032 1033 1034 function removeEventSource(source) { 1035 sources = $.grep(sources, function(src) { 1036 return !isSourcesEqual(src, source); 1037 }); 1038 // remove all client events from that source 1039 cache = $.grep(cache, function(e) { 1040 return !isSourcesEqual(e.source, source); 1041 }); 1042 reportEvents(cache); 1043 } 1044 1045 1046 1047 /* Manipulation 1048 -----------------------------------------------------------------------------*/ 1049 1050 1051 function updateEvent(event) { // update an existing event 1052 var i, len = cache.length, e, 1053 defaultEventEnd = getView().defaultEventEnd, // getView??? 1054 startDelta = event.start - event._start, 1055 endDelta = event.end ? 1056 (event.end - (event._end || defaultEventEnd(event))) // event._end would be null if event.end 1057 : 0; // was null and event was just resized 1058 for (i=0; i<len; i++) { 1059 e = cache[i]; 1060 if (e._id == event._id && e != event) { 1061 e.start = new Date(+e.start + startDelta); 1062 if (event.end) { 1063 if (e.end) { 1064 e.end = new Date(+e.end + endDelta); 1065 }else{ 1066 e.end = new Date(+defaultEventEnd(e) + endDelta); 1067 } 1068 }else{ 1069 e.end = null; 1070 } 1071 e.title = event.title; 1072 e.url = event.url; 1073 e.allDay = event.allDay; 1074 e.className = event.className; 1075 e.editable = event.editable; 1076 e.color = event.color; 1077 e.backgroudColor = event.backgroudColor; 1078 e.borderColor = event.borderColor; 1079 e.textColor = event.textColor; 1080 normalizeEvent(e); 1081 } 1082 } 1083 normalizeEvent(event); 1084 reportEvents(cache); 1085 } 1086 1087 1088 function renderEvent(event, stick) { 1089 normalizeEvent(event); 1090 if (!event.source) { 1091 if (stick) { 1092 stickySource.events.push(event); 1093 event.source = stickySource; 1094 } 1095 cache.push(event); 1096 } 1097 reportEvents(cache); 1098 } 1099 1100 1101 function removeEvents(filter) { 1102 if (!filter) { // remove all 1103 cache = []; 1104 // clear all array sources 1105 for (var i=0; i<sources.length; i++) { 1106 if ($.isArray(sources[i].events)) { 1107 sources[i].events = []; 1108 } 1109 } 1110 }else{ 1111 if (!$.isFunction(filter)) { // an event ID 1112 var id = filter + ''; 1113 filter = function(e) { 1114 return e._id == id; 1115 }; 1116 } 1117 cache = $.grep(cache, filter, true); 1118 // remove events from array sources 1119 for (var i=0; i<sources.length; i++) { 1120 if ($.isArray(sources[i].events)) { 1121 sources[i].events = $.grep(sources[i].events, filter, true); 1122 } 1123 } 1124 } 1125 reportEvents(cache); 1126 } 1127 1128 1129 function clientEvents(filter) { 1130 if ($.isFunction(filter)) { 1131 return $.grep(cache, filter); 1132 } 1133 else if (filter) { // an event ID 1134 filter += ''; 1135 return $.grep(cache, function(e) { 1136 return e._id == filter; 1137 }); 1138 } 1139 return cache; // else, return all 1140 } 1141 1142 1143 1144 /* Loading State 1145 -----------------------------------------------------------------------------*/ 1146 1147 1148 function pushLoading() { 1149 if (!loadingLevel++) { 1150 trigger('loading', null, true); 1151 } 1152 } 1153 1154 1155 function popLoading() { 1156 if (!--loadingLevel) { 1157 trigger('loading', null, false); 1158 } 1159 } 1160 1161 1162 1163 /* Event Normalization 1164 -----------------------------------------------------------------------------*/ 1165 1166 1167 function normalizeEvent(event) { 1168 var source = event.source || {}; 1169 var ignoreTimezone = firstDefined(source.ignoreTimezone, options.ignoreTimezone); 1170 event._id = event._id || (event.id === undefined ? '_fc' + eventGUID++ : event.id + ''); 1171 if (event.date) { 1172 if (!event.start) { 1173 event.start = event.date; 1174 } 1175 delete event.date; 1176 } 1177 event._start = cloneDate(event.start = parseDate(event.start, ignoreTimezone)); 1178 event.end = parseDate(event.end, ignoreTimezone); 1179 if (event.end && event.end <= event.start) { 1180 event.end = null; 1181 } 1182 event._end = event.end ? cloneDate(event.end) : null; 1183 if (event.allDay === undefined) { 1184 event.allDay = firstDefined(source.allDayDefault, options.allDayDefault); 1185 } 1186 if (event.className) { 1187 if (typeof event.className == 'string') { 1188 event.className = event.className.split(/\s+/); 1189 } 1190 }else{ 1191 event.className = []; 1192 } 1193 // TODO: if there is no start date, return false to indicate an invalid event 1194 } 1195 1196 1197 1198 /* Utils 1199 ------------------------------------------------------------------------------*/ 1200 1201 1202 function normalizeSource(source) { 1203 if (source.className) { 1204 // TODO: repeat code, same code for event classNames 1205 if (typeof source.className == 'string') { 1206 source.className = source.className.split(/\s+/); 1207 } 1208 }else{ 1209 source.className = []; 1210 } 1211 var normalizers = fc.sourceNormalizers; 1212 for (var i=0; i<normalizers.length; i++) { 1213 normalizers[i](source); 1214 } 1215 } 1216 1217 1218 function isSourcesEqual(source1, source2) { 1219 return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2); 1220 } 1221 1222 1223 function getSourcePrimitive(source) { 1224 return ((typeof source == 'object') ? (source.events || source.url) : '') || source; 1225 } 1226 1227 1228 } 1229 1230 1231 fc.addDays = addDays; 1232 fc.cloneDate = cloneDate; 1233 fc.parseDate = parseDate; 1234 fc.parseISO8601 = parseISO8601; 1235 fc.parseTime = parseTime; 1236 fc.formatDate = formatDate; 1237 fc.formatDates = formatDates; 1238 1239 1240 1241 /* Date Math 1242 -----------------------------------------------------------------------------*/ 1243 1244 var dayIDs = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'], 1245 DAY_MS = 86400000, 1246 HOUR_MS = 3600000, 1247 MINUTE_MS = 60000; 1248 1249 1250 function addYears(d, n, keepTime) { 1251 d.setFullYear(d.getFullYear() + n); 1252 if (!keepTime) { 1253 clearTime(d); 1254 } 1255 return d; 1256 } 1257 1258 1259 function addMonths(d, n, keepTime) { // prevents day overflow/underflow 1260 if (+d) { // prevent infinite looping on invalid dates 1261 var m = d.getMonth() + n, 1262 check = cloneDate(d); 1263 check.setDate(1); 1264 check.setMonth(m); 1265 d.setMonth(m); 1266 if (!keepTime) { 1267 clearTime(d); 1268 } 1269 while (d.getMonth() != check.getMonth()) { 1270 d.setDate(d.getDate() + (d < check ? 1 : -1)); 1271 } 1272 } 1273 return d; 1274 } 1275 1276 1277 function addDays(d, n, keepTime) { // deals with daylight savings 1278 if (+d) { 1279 var dd = d.getDate() + n, 1280 check = cloneDate(d); 1281 check.setHours(9); // set to middle of day 1282 check.setDate(dd); 1283 d.setDate(dd); 1284 if (!keepTime) { 1285 clearTime(d); 1286 } 1287 fixDate(d, check); 1288 } 1289 return d; 1290 } 1291 1292 1293 function fixDate(d, check) { // force d to be on check's YMD, for daylight savings purposes 1294 if (+d) { // prevent infinite looping on invalid dates 1295 while (d.getDate() != check.getDate()) { 1296 d.setTime(+d + (d < check ? 1 : -1) * HOUR_MS); 1297 } 1298 } 1299 } 1300 1301 1302 function addMinutes(d, n) { 1303 d.setMinutes(d.getMinutes() + n); 1304 return d; 1305 } 1306 1307 1308 function clearTime(d) { 1309 d.setHours(0); 1310 d.setMinutes(0); 1311 d.setSeconds(0); 1312 d.setMilliseconds(0); 1313 return d; 1314 } 1315 1316 1317 function cloneDate(d, dontKeepTime) { 1318 if (dontKeepTime) { 1319 return clearTime(new Date(+d)); 1320 } 1321 return new Date(+d); 1322 } 1323 1324 1325 function zeroDate() { // returns a Date with time 00:00:00 and dateOfMonth=1 1326 var i=0, d; 1327 do { 1328 d = new Date(1970, i++, 1); 1329 } while (d.getHours()); // != 0 1330 return d; 1331 } 1332 1333 1334 function skipWeekend(date, inc, excl) { 1335 inc = inc || 1; 1336 while (!date.getDay() || (excl && date.getDay()==1 || !excl && date.getDay()==6)) { 1337 addDays(date, inc); 1338 } 1339 return date; 1340 } 1341 1342 1343 function dayDiff(d1, d2) { // d1 - d2 1344 return Math.round((cloneDate(d1, true) - cloneDate(d2, true)) / DAY_MS); 1345 } 1346 1347 1348 function setYMD(date, y, m, d) { 1349 if (y !== undefined && y != date.getFullYear()) { 1350 date.setDate(1); 1351 date.setMonth(0); 1352 date.setFullYear(y); 1353 } 1354 if (m !== undefined && m != date.getMonth()) { 1355 date.setDate(1); 1356 date.setMonth(m); 1357 } 1358 if (d !== undefined) { 1359 date.setDate(d); 1360 } 1361 } 1362 1363 1364 1365 /* Date Parsing 1366 -----------------------------------------------------------------------------*/ 1367 1368 1369 function parseDate(s, ignoreTimezone) { // ignoreTimezone defaults to true 1370 if (typeof s == 'object') { // already a Date object 1371 return s; 1372 } 1373 if (typeof s == 'number') { // a UNIX timestamp 1374 return new Date(s * 1000); 1375 } 1376 if (typeof s == 'string') { 1377 if (s.match(/^\d+(\.\d+)?$/)) { // a UNIX timestamp 1378 return new Date(parseFloat(s) * 1000); 1379 } 1380 if (ignoreTimezone === undefined) { 1381 ignoreTimezone = true; 1382 } 1383 return parseISO8601(s, ignoreTimezone) || (s ? new Date(s) : null); 1384 } 1385 // TODO: never return invalid dates (like from new Date(<string>)), return null instead 1386 return null; 1387 } 1388 1389 1390 function parseISO8601(s, ignoreTimezone) { // ignoreTimezone defaults to false 1391 // derived from http://delete.me.uk/2005/03/iso8601.html 1392 // TODO: for a know glitch/feature, read tests/issue_206_parseDate_dst.html 1393 var m = s.match(/^([0-9]{4})(-([0-9]{2})(-([0-9]{2})([T ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2})(:?([0-9]{2}))?))?)?)?)?$/); 1394 if (!m) { 1395 return null; 1396 } 1397 var date = new Date(m[1], 0, 1); 1398 if (ignoreTimezone || !m[13]) { 1399 var check = new Date(m[1], 0, 1, 9, 0); 1400 if (m[3]) { 1401 date.setMonth(m[3] - 1); 1402 check.setMonth(m[3] - 1); 1403 } 1404 if (m[5]) { 1405 date.setDate(m[5]); 1406 check.setDate(m[5]); 1407 } 1408 fixDate(date, check); 1409 if (m[7]) { 1410 date.setHours(m[7]); 1411 } 1412 if (m[8]) { 1413 date.setMinutes(m[8]); 1414 } 1415 if (m[10]) { 1416 date.setSeconds(m[10]); 1417 } 1418 if (m[12]) { 1419 date.setMilliseconds(Number("0." + m[12]) * 1000); 1420 } 1421 fixDate(date, check); 1422 }else{ 1423 date.setUTCFullYear( 1424 m[1], 1425 m[3] ? m[3] - 1 : 0, 1426 m[5] || 1 1427 ); 1428 date.setUTCHours( 1429 m[7] || 0, 1430 m[8] || 0, 1431 m[10] || 0, 1432 m[12] ? Number("0." + m[12]) * 1000 : 0 1433 ); 1434 if (m[14]) { 1435 var offset = Number(m[16]) * 60 + (m[18] ? Number(m[18]) : 0); 1436 offset *= m[15] == '-' ? 1 : -1; 1437 date = new Date(+date + (offset * 60 * 1000)); 1438 } 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'> </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 return segs; 3731 } 3732 3733 3734 function slotEventEnd(event) { 3735 if (event.end) { 3736 return cloneDate(event.end); 3737 }else{ 3738 return addMinutes(cloneDate(event.start), opt('defaultEventMinutes')); 3739 } 3740 } 3741 3742 3743 // renders events in the 'time slots' at the bottom 3744 3745 function renderSlotSegs(segs, modifiedEventId) { 3746 3747 var i, segCnt=segs.length, seg, 3748 event, 3749 classes, 3750 top, bottom, 3751 colI, levelI, forward, 3752 leftmost, 3753 availWidth, 3754 outerWidth, 3755 left, 3756 html='', 3757 eventElements, 3758 eventElement, 3759 triggerRes, 3760 vsideCache={}, 3761 hsideCache={}, 3762 key, val, 3763 contentElement, 3764 height, 3765 slotSegmentContainer = getSlotSegmentContainer(), 3766 rtl, dis, dit, 3767 colCnt = getColCnt(); 3768 3769 if (rtl = opt('isRTL')) { 3770 dis = -1; 3771 dit = colCnt - 1; 3772 }else{ 3773 dis = 1; 3774 dit = 0; 3775 } 3776 3777 // calculate position/dimensions, create html 3778 for (i=0; i<segCnt; i++) { 3779 seg = segs[i]; 3780 event = seg.event; 3781 top = timePosition(seg.start, seg.start); 3782 bottom = timePosition(seg.start, seg.end); 3783 colI = seg.col; 3784 levelI = seg.level; 3785 forward = seg.forward || 0; 3786 leftmost = colContentLeft(colI*dis + dit); 3787 availWidth = colContentRight(colI*dis + dit) - leftmost; 3788 availWidth = Math.min(availWidth-6, availWidth*.95); // TODO: move this to CSS 3789 if (levelI) { 3790 // indented and thin 3791 outerWidth = availWidth / (levelI + forward + 1); 3792 }else{ 3793 if (forward) { 3794 // moderately wide, aligned left still 3795 outerWidth = ((availWidth / (forward + 1)) - (12/2)) * 2; // 12 is the predicted width of resizer = 3796 }else{ 3797 // can be entire width, aligned left 3798 outerWidth = availWidth; 3799 } 3800 } 3801 left = leftmost + // leftmost possible 3802 (availWidth / (levelI + forward + 1) * levelI) // indentation 3803 * dis + (rtl ? availWidth - outerWidth : 0); // rtl 3804 seg.top = top; 3805 seg.left = left; 3806 seg.outerWidth = outerWidth; 3807 seg.outerHeight = bottom - top; 3808 html += slotSegHtml(event, seg); 3809 } 3810 slotSegmentContainer[0].innerHTML = html; // faster than html() 3811 eventElements = slotSegmentContainer.children(); 3812 3813 // retrieve elements, run through eventRender callback, bind event handlers 3814 for (i=0; i<segCnt; i++) { 3815 seg = segs[i]; 3816 event = seg.event; 3817 eventElement = $(eventElements[i]); // faster than eq() 3818 triggerRes = trigger('eventRender', event, event, eventElement); 3819 if (triggerRes === false) { 3820 eventElement.remove(); 3821 }else{ 3822 if (triggerRes && triggerRes !== true) { 3823 eventElement.remove(); 3824 eventElement = $(triggerRes) 3825 .css({ 3826 position: 'absolute', 3827 top: seg.top, 3828 left: seg.left 3829 }) 3830 .appendTo(slotSegmentContainer); 3831 } 3832 seg.element = eventElement; 3833 if (event._id === modifiedEventId) { 3834 bindSlotSeg(event, eventElement, seg); 3835 }else{ 3836 eventElement[0]._fci = i; // for lazySegBind 3837 } 3838 reportEventElement(event, eventElement); 3839 } 3840 } 3841 3842 lazySegBind(slotSegmentContainer, segs, bindSlotSeg); 3843 3844 // record event sides and title positions 3845 for (i=0; i<segCnt; i++) { 3846 seg = segs[i]; 3847 if (eventElement = seg.element) { 3848 val = vsideCache[key = seg.key = cssKey(eventElement[0])]; 3849 seg.vsides = val === undefined ? (vsideCache[key] = vsides(eventElement, true)) : val; 3850 val = hsideCache[key]; 3851 seg.hsides = val === undefined ? (hsideCache[key] = hsides(eventElement, true)) : val; 3852 contentElement = eventElement.find('div.fc-event-content'); 3853 if (contentElement.length) { 3854 seg.contentTop = contentElement[0].offsetTop; 3855 } 3856 } 3857 } 3858 3859 // set all positions/dimensions at once 3860 for (i=0; i<segCnt; i++) { 3861 seg = segs[i]; 3862 if (eventElement = seg.element) { 3863 eventElement[0].style.width = Math.max(0, seg.outerWidth - seg.hsides) + 'px'; 3864 height = Math.max(0, seg.outerHeight - seg.vsides); 3865 eventElement[0].style.height = height + 'px'; 3866 event = seg.event; 3867 if (seg.contentTop !== undefined && height - seg.contentTop < 10) { 3868 // not enough room for title, put it in the time header 3869 eventElement.find('div.fc-event-time') 3870 .text(formatDate(event.start, opt('timeFormat')) + ' - ' + event.title); 3871 eventElement.find('div.fc-event-title') 3872 .remove(); 3873 } 3874 trigger('eventAfterRender', event, event, eventElement); 3875 } 3876 } 3877 3878 } 3879 3880 3881 function slotSegHtml(event, seg) { 3882 var html = "<"; 3883 var url = event.url; 3884 var skinCss = getSkinCss(event, opt); 3885 var skinCssAttr = (skinCss ? " style='" + skinCss + "'" : ''); 3886 var classes = ['fc-event', 'fc-event-skin', 'fc-event-vert']; 3887 if (isEventDraggable(event)) { 3888 classes.push('fc-event-draggable'); 3889 } 3890 if (seg.isStart) { 3891 classes.push('fc-corner-top'); 3892 } 3893 if (seg.isEnd) { 3894 classes.push('fc-corner-bottom'); 3895 } 3896 classes = classes.concat(event.className); 3897 if (event.source) { 3898 classes = classes.concat(event.source.className || []); 3899 } 3900 if (url) { 3901 html += "a href='" + htmlEscape(event.url) + "'"; 3902 }else{ 3903 html += "div"; 3904 } 3905 html += 3906 " class='" + classes.join(' ') + "'" + 3907 " style='position:absolute;z-index:8;top:" + seg.top + "px;left:" + seg.left + "px;" + skinCss + "'" + 3908 ">" + 3909 "<div class='fc-event-inner fc-event-skin'" + skinCssAttr + ">" + 3910 "<div class='fc-event-head fc-event-skin'" + skinCssAttr + ">" + 3911 "<div class='fc-event-time'>" + 3912 htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) + 3913 "</div>" + 3914 "</div>" + 3915 "<div class='fc-event-content'>" + 3916 "<div class='fc-event-title'>" + 3917 htmlEscape(event.title) + 3918 "</div>" + 3919 "</div>" + 3920 "<div class='fc-event-bg'></div>" + 3921 "</div>"; // close inner 3922 if (seg.isEnd && isEventResizable(event)) { 3923 html += 3924 "<div class='ui-resizable-handle ui-resizable-s'>=</div>"; 3925 } 3926 html += 3927 "</" + (url ? "a" : "div") + ">"; 3928 return html; 3929 } 3930 3931 3932 function bindDaySeg(event, eventElement, seg) { 3933 if (isEventDraggable(event)) { 3934 draggableDayEvent(event, eventElement, seg.isStart); 3935 } 3936 if (seg.isEnd && isEventResizable(event)) { 3937 resizableDayEvent(event, eventElement, seg); 3938 } 3939 eventElementHandlers(event, eventElement); 3940 // needs to be after, because resizableDayEvent might stopImmediatePropagation on click 3941 } 3942 3943 3944 function bindSlotSeg(event, eventElement, seg) { 3945 var timeElement = eventElement.find('div.fc-event-time'); 3946 if (isEventDraggable(event)) { 3947 draggableSlotEvent(event, eventElement, timeElement); 3948 } 3949 if (seg.isEnd && isEventResizable(event)) { 3950 resizableSlotEvent(event, eventElement, timeElement); 3951 } 3952 eventElementHandlers(event, eventElement); 3953 } 3954 3955 3956 3957 /* Dragging 3958 -----------------------------------------------------------------------------------*/ 3959 3960 3961 // when event starts out FULL-DAY 3962 3963 function draggableDayEvent(event, eventElement, isStart) { 3964 var origWidth; 3965 var revert; 3966 var allDay=true; 3967 var dayDelta; 3968 var dis = opt('isRTL') ? -1 : 1; 3969 var hoverListener = getHoverListener(); 3970 var colWidth = getColWidth(); 3971 var slotHeight = getSlotHeight(); 3972 var minMinute = getMinMinute(); 3973 eventElement.draggable({ 3974 zIndex: 9, 3975 opacity: opt('dragOpacity', 'month'), // use whatever the month view was using 3976 revertDuration: opt('dragRevertDuration'), 3977 start: function(ev, ui) { 3978 trigger('eventDragStart', eventElement, event, ev, ui); 3979 hideEvents(event, eventElement); 3980 origWidth = eventElement.width(); 3981 hoverListener.start(function(cell, origCell, rowDelta, colDelta) { 3982 clearOverlays(); 3983 if (cell) { 3984 //setOverflowHidden(true); 3985 revert = false; 3986 dayDelta = colDelta * dis; 3987 if (!cell.row) { 3988 // on full-days 3989 renderDayOverlay( 3990 addDays(cloneDate(event.start), dayDelta), 3991 addDays(exclEndDay(event), dayDelta) 3992 ); 3993 resetElement(); 3994 }else{ 3995 // mouse is over bottom slots 3996 if (isStart) { 3997 if (allDay) { 3998 // convert event to temporary slot-event 3999 eventElement.width(colWidth - 10); // don't use entire width 4000 setOuterHeight( 4001 eventElement, 4002 slotHeight * Math.round( 4003 (event.end ? ((event.end - event.start) / MINUTE_MS) : opt('defaultEventMinutes')) 4004 / opt('slotMinutes') 4005 ) 4006 ); 4007 eventElement.draggable('option', 'grid', [colWidth, 1]); 4008 allDay = false; 4009 } 4010 }else{ 4011 revert = true; 4012 } 4013 } 4014 revert = revert || (allDay && !dayDelta); 4015 }else{ 4016 resetElement(); 4017 //setOverflowHidden(false); 4018 revert = true; 4019 } 4020 eventElement.draggable('option', 'revert', revert); 4021 }, ev, 'drag'); 4022 }, 4023 stop: function(ev, ui) { 4024 hoverListener.stop(); 4025 clearOverlays(); 4026 trigger('eventDragStop', eventElement, event, ev, ui); 4027 if (revert) { 4028 // hasn't moved or is out of bounds (draggable has already reverted) 4029 resetElement(); 4030 eventElement.css('filter', ''); // clear IE opacity side-effects 4031 showEvents(event, eventElement); 4032 }else{ 4033 // changed! 4034 var minuteDelta = 0; 4035 if (!allDay) { 4036 minuteDelta = Math.round((eventElement.offset().top - getBodyContent().offset().top) / slotHeight) 4037 * opt('slotMinutes') 4038 + minMinute 4039 - (event.start.getHours() * 60 + event.start.getMinutes()); 4040 } 4041 eventDrop(this, event, dayDelta, minuteDelta, allDay, ev, ui); 4042 } 4043 //setOverflowHidden(false); 4044 } 4045 }); 4046 function resetElement() { 4047 if (!allDay) { 4048 eventElement 4049 .width(origWidth) 4050 .height('') 4051 .draggable('option', 'grid', null); 4052 allDay = true; 4053 } 4054 } 4055 } 4056 4057 4058 // when event starts out IN TIMESLOTS 4059 4060 function draggableSlotEvent(event, eventElement, timeElement) { 4061 var origPosition; 4062 var allDay=false; 4063 var dayDelta; 4064 var minuteDelta; 4065 var prevMinuteDelta; 4066 var dis = opt('isRTL') ? -1 : 1; 4067 var hoverListener = getHoverListener(); 4068 var colCnt = getColCnt(); 4069 var colWidth = getColWidth(); 4070 var slotHeight = getSlotHeight(); 4071 eventElement.draggable({ 4072 zIndex: 9, 4073 scroll: false, 4074 grid: [colWidth, slotHeight], 4075 axis: colCnt==1 ? 'y' : false, 4076 opacity: opt('dragOpacity'), 4077 revertDuration: opt('dragRevertDuration'), 4078 start: function(ev, ui) { 4079 trigger('eventDragStart', eventElement, event, ev, ui); 4080 hideEvents(event, eventElement); 4081 origPosition = eventElement.position(); 4082 minuteDelta = prevMinuteDelta = 0; 4083 hoverListener.start(function(cell, origCell, rowDelta, colDelta) { 4084 eventElement.draggable('option', 'revert', !cell); 4085 clearOverlays(); 4086 if (cell) { 4087 dayDelta = colDelta * dis; 4088 if (opt('allDaySlot') && !cell.row) { 4089 // over full days 4090 if (!allDay) { 4091 // convert to temporary all-day event 4092 allDay = true; 4093 timeElement.hide(); 4094 eventElement.draggable('option', 'grid', null); 4095 } 4096 renderDayOverlay( 4097 addDays(cloneDate(event.start), dayDelta), 4098 addDays(exclEndDay(event), dayDelta) 4099 ); 4100 }else{ 4101 // on slots 4102 resetElement(); 4103 } 4104 } 4105 }, ev, 'drag'); 4106 }, 4107 drag: function(ev, ui) { 4108 minuteDelta = Math.round((ui.position.top - origPosition.top) / slotHeight) * opt('slotMinutes'); 4109 if (minuteDelta != prevMinuteDelta) { 4110 if (!allDay) { 4111 updateTimeText(minuteDelta); 4112 } 4113 prevMinuteDelta = minuteDelta; 4114 } 4115 }, 4116 stop: function(ev, ui) { 4117 var cell = hoverListener.stop(); 4118 clearOverlays(); 4119 trigger('eventDragStop', eventElement, event, ev, ui); 4120 if (cell && (dayDelta || minuteDelta || allDay)) { 4121 // changed! 4122 eventDrop(this, event, dayDelta, allDay ? 0 : minuteDelta, allDay, ev, ui); 4123 }else{ 4124 // either no change or out-of-bounds (draggable has already reverted) 4125 resetElement(); 4126 eventElement.css('filter', ''); // clear IE opacity side-effects 4127 eventElement.css(origPosition); // sometimes fast drags make event revert to wrong position 4128 updateTimeText(0); 4129 showEvents(event, eventElement); 4130 } 4131 } 4132 }); 4133 function updateTimeText(minuteDelta) { 4134 var newStart = addMinutes(cloneDate(event.start), minuteDelta); 4135 var newEnd; 4136 if (event.end) { 4137 newEnd = addMinutes(cloneDate(event.end), minuteDelta); 4138 } 4139 timeElement.text(formatDates(newStart, newEnd, opt('timeFormat'))); 4140 } 4141 function resetElement() { 4142 // convert back to original slot-event 4143 if (allDay) { 4144 timeElement.css('display', ''); // show() was causing display=inline 4145 eventElement.draggable('option', 'grid', [colWidth, slotHeight]); 4146 allDay = false; 4147 } 4148 } 4149 } 4150 4151 4152 4153 /* Resizing 4154 --------------------------------------------------------------------------------------*/ 4155 4156 4157 function resizableSlotEvent(event, eventElement, timeElement) { 4158 var slotDelta, prevSlotDelta; 4159 var slotHeight = getSlotHeight(); 4160 eventElement.resizable({ 4161 handles: { 4162 s: 'div.ui-resizable-s' 4163 }, 4164 grid: slotHeight, 4165 start: function(ev, ui) { 4166 slotDelta = prevSlotDelta = 0; 4167 hideEvents(event, eventElement); 4168 eventElement.css('z-index', 9); 4169 trigger('eventResizeStart', this, event, ev, ui); 4170 }, 4171 resize: function(ev, ui) { 4172 // don't rely on ui.size.height, doesn't take grid into account 4173 slotDelta = Math.round((Math.max(slotHeight, eventElement.height()) - ui.originalSize.height) / slotHeight); 4174 if (slotDelta != prevSlotDelta) { 4175 timeElement.text( 4176 formatDates( 4177 event.start, 4178 (!slotDelta && !event.end) ? null : // no change, so don't display time range 4179 addMinutes(eventEnd(event), opt('slotMinutes')*slotDelta), 4180 opt('timeFormat') 4181 ) 4182 ); 4183 prevSlotDelta = slotDelta; 4184 } 4185 }, 4186 stop: function(ev, ui) { 4187 trigger('eventResizeStop', this, event, ev, ui); 4188 if (slotDelta) { 4189 eventResize(this, event, 0, opt('slotMinutes')*slotDelta, ev, ui); 4190 }else{ 4191 eventElement.css('z-index', 8); 4192 showEvents(event, eventElement); 4193 // BUG: if event was really short, need to put title back in span 4194 } 4195 } 4196 }); 4197 } 4198 4199 4200 } 4201 4202 4203 function countForwardSegs(levels) { 4204 var i, j, k, level, segForward, segBack; 4205 for (i=levels.length-1; i>0; i--) { 4206 level = levels[i]; 4207 for (j=0; j<level.length; j++) { 4208 segForward = level[j]; 4209 for (k=0; k<levels[i-1].length; k++) { 4210 segBack = levels[i-1][k]; 4211 if (segsCollide(segForward, segBack)) { 4212 segBack.forward = Math.max(segBack.forward||0, (segForward.forward||0)+1); 4213 } 4214 } 4215 } 4216 } 4217 } 4218 4219 4220 4221 4222 function View(element, calendar, viewName) { 4223 var t = this; 4224 4225 4226 // exports 4227 t.element = element; 4228 t.calendar = calendar; 4229 t.name = viewName; 4230 t.opt = opt; 4231 t.trigger = trigger; 4232 //t.setOverflowHidden = setOverflowHidden; 4233 t.isEventDraggable = isEventDraggable; 4234 t.isEventResizable = isEventResizable; 4235 t.reportEvents = reportEvents; 4236 t.eventEnd = eventEnd; 4237 t.reportEventElement = reportEventElement; 4238 t.reportEventClear = reportEventClear; 4239 t.eventElementHandlers = eventElementHandlers; 4240 t.showEvents = showEvents; 4241 t.hideEvents = hideEvents; 4242 t.eventDrop = eventDrop; 4243 t.eventResize = eventResize; 4244 // t.title 4245 // t.start, t.end 4246 // t.visStart, t.visEnd 4247 4248 4249 // imports 4250 var defaultEventEnd = t.defaultEventEnd; 4251 var normalizeEvent = calendar.normalizeEvent; // in EventManager 4252 var reportEventChange = calendar.reportEventChange; 4253 4254 4255 // locals 4256 var eventsByID = {}; 4257 var eventElements = []; 4258 var eventElementsByID = {}; 4259 var options = calendar.options; 4260 4261 4262 4263 function opt(name, viewNameOverride) { 4264 var v = options[name]; 4265 if (typeof v == 'object') { 4266 return smartProperty(v, viewNameOverride || viewName); 4267 } 4268 return v; 4269 } 4270 4271 4272 function trigger(name, thisObj) { 4273 return calendar.trigger.apply( 4274 calendar, 4275 [name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t]) 4276 ); 4277 } 4278 4279 4280 /* 4281 function setOverflowHidden(bool) { 4282 element.css('overflow', bool ? 'hidden' : ''); 4283 } 4284 */ 4285 4286 4287 function isEventDraggable(event) { 4288 return isEventEditable(event) && !opt('disableDragging'); 4289 } 4290 4291 4292 function isEventResizable(event) { // but also need to make sure the seg.isEnd == true 4293 return isEventEditable(event) && !opt('disableResizing'); 4294 } 4295 4296 4297 function isEventEditable(event) { 4298 return firstDefined(event.editable, (event.source || {}).editable, opt('editable')); 4299 } 4300 4301 4302 4303 /* Event Data 4304 ------------------------------------------------------------------------------*/ 4305 4306 4307 // report when view receives new events 4308 function reportEvents(events) { // events are already normalized at this point 4309 eventsByID = {}; 4310 var i, len=events.length, event; 4311 for (i=0; i<len; i++) { 4312 event = events[i]; 4313 if (eventsByID[event._id]) { 4314 eventsByID[event._id].push(event); 4315 }else{ 4316 eventsByID[event._id] = [event]; 4317 } 4318 } 4319 } 4320 4321 4322 // returns a Date object for an event's end 4323 function eventEnd(event) { 4324 return event.end ? cloneDate(event.end) : defaultEventEnd(event); 4325 } 4326 4327 4328 4329 /* Event Elements 4330 ------------------------------------------------------------------------------*/ 4331 4332 4333 // report when view creates an element for an event 4334 function reportEventElement(event, element) { 4335 eventElements.push(element); 4336 if (eventElementsByID[event._id]) { 4337 eventElementsByID[event._id].push(element); 4338 }else{ 4339 eventElementsByID[event._id] = [element]; 4340 } 4341 } 4342 4343 4344 function reportEventClear() { 4345 eventElements = []; 4346 eventElementsByID = {}; 4347 } 4348 4349 4350 // attaches eventClick, eventMouseover, eventMouseout 4351 function eventElementHandlers(event, eventElement) { 4352 eventElement 4353 .click(function(ev) { 4354 if (!eventElement.hasClass('ui-draggable-dragging') && 4355 !eventElement.hasClass('ui-resizable-resizing')) { 4356 return trigger('eventClick', this, event, ev); 4357 } 4358 }) 4359 .hover( 4360 function(ev) { 4361 trigger('eventMouseover', this, event, ev); 4362 }, 4363 function(ev) { 4364 trigger('eventMouseout', this, event, ev); 4365 } 4366 ); 4367 // TODO: don't fire eventMouseover/eventMouseout *while* dragging is occuring (on subject element) 4368 // TODO: same for resizing 4369 } 4370 4371 4372 function showEvents(event, exceptElement) { 4373 eachEventElement(event, exceptElement, 'show'); 4374 } 4375 4376 4377 function hideEvents(event, exceptElement) { 4378 eachEventElement(event, exceptElement, 'hide'); 4379 } 4380 4381 4382 function eachEventElement(event, exceptElement, funcName) { 4383 var elements = eventElementsByID[event._id], 4384 i, len = elements.length; 4385 for (i=0; i<len; i++) { 4386 if (!exceptElement || elements[i][0] != exceptElement[0]) { 4387 elements[i][funcName](); 4388 } 4389 } 4390 } 4391 4392 4393 4394 /* Event Modification Reporting 4395 ---------------------------------------------------------------------------------*/ 4396 4397 4398 function eventDrop(e, event, dayDelta, minuteDelta, allDay, ev, ui) { 4399 var oldAllDay = event.allDay; 4400 var eventId = event._id; 4401 moveEvents(eventsByID[eventId], dayDelta, minuteDelta, allDay); 4402 trigger( 4403 'eventDrop', 4404 e, 4405 event, 4406 dayDelta, 4407 minuteDelta, 4408 allDay, 4409 function() { 4410 // TODO: investigate cases where this inverse technique might not work 4411 moveEvents(eventsByID[eventId], -dayDelta, -minuteDelta, oldAllDay); 4412 reportEventChange(eventId); 4413 }, 4414 ev, 4415 ui 4416 ); 4417 reportEventChange(eventId); 4418 } 4419 4420 4421 function eventResize(e, event, dayDelta, minuteDelta, ev, ui) { 4422 var eventId = event._id; 4423 elongateEvents(eventsByID[eventId], dayDelta, minuteDelta); 4424 trigger( 4425 'eventResize', 4426 e, 4427 event, 4428 dayDelta, 4429 minuteDelta, 4430 function() { 4431 // TODO: investigate cases where this inverse technique might not work 4432 elongateEvents(eventsByID[eventId], -dayDelta, -minuteDelta); 4433 reportEventChange(eventId); 4434 }, 4435 ev, 4436 ui 4437 ); 4438 reportEventChange(eventId); 4439 } 4440 4441 4442 4443 /* Event Modification Math 4444 ---------------------------------------------------------------------------------*/ 4445 4446 4447 function moveEvents(events, dayDelta, minuteDelta, allDay) { 4448 minuteDelta = minuteDelta || 0; 4449 for (var e, len=events.length, i=0; i<len; i++) { 4450 e = events[i]; 4451 if (allDay !== undefined) { 4452 e.allDay = allDay; 4453 } 4454 addMinutes(addDays(e.start, dayDelta, true), minuteDelta); 4455 if (e.end) { 4456 e.end = addMinutes(addDays(e.end, dayDelta, true), minuteDelta); 4457 } 4458 normalizeEvent(e, options); 4459 } 4460 } 4461 4462 4463 function elongateEvents(events, dayDelta, minuteDelta) { 4464 minuteDelta = minuteDelta || 0; 4465 for (var e, len=events.length, i=0; i<len; i++) { 4466 e = events[i]; 4467 e.end = addMinutes(addDays(eventEnd(e), dayDelta, true), minuteDelta); 4468 normalizeEvent(e, options); 4469 } 4470 } 4471 4472 4473 } 4474 4475 function DayEventRenderer() { 4476 var t = this; 4477 4478 4479 // exports 4480 t.renderDaySegs = renderDaySegs; 4481 t.resizableDayEvent = resizableDayEvent; 4482 4483 4484 // imports 4485 var opt = t.opt; 4486 var trigger = t.trigger; 4487 var isEventDraggable = t.isEventDraggable; 4488 var isEventResizable = t.isEventResizable; 4489 var eventEnd = t.eventEnd; 4490 var reportEventElement = t.reportEventElement; 4491 var showEvents = t.showEvents; 4492 var hideEvents = t.hideEvents; 4493 var eventResize = t.eventResize; 4494 var getRowCnt = t.getRowCnt; 4495 var getColCnt = t.getColCnt; 4496 var getColWidth = t.getColWidth; 4497 var allDayRow = t.allDayRow; 4498 var allDayBounds = t.allDayBounds; 4499 var colContentLeft = t.colContentLeft; 4500 var colContentRight = t.colContentRight; 4501 var dayOfWeekCol = t.dayOfWeekCol; 4502 var dateCell = t.dateCell; 4503 var compileDaySegs = t.compileDaySegs; 4504 var getDaySegmentContainer = t.getDaySegmentContainer; 4505 var bindDaySeg = t.bindDaySeg; //TODO: streamline this 4506 var formatDates = t.calendar.formatDates; 4507 var renderDayOverlay = t.renderDayOverlay; 4508 var clearOverlays = t.clearOverlays; 4509 var clearSelection = t.clearSelection; 4510 4511 4512 4513 /* Rendering 4514 -----------------------------------------------------------------------------*/ 4515 4516 4517 function renderDaySegs(segs, modifiedEventId) { 4518 var segmentContainer = getDaySegmentContainer(); 4519 var rowDivs; 4520 var rowCnt = getRowCnt(); 4521 var colCnt = getColCnt(); 4522 var i = 0; 4523 var rowI; 4524 var levelI; 4525 var colHeights; 4526 var j; 4527 var segCnt = segs.length; 4528 var seg; 4529 var top; 4530 var k; 4531 segmentContainer[0].innerHTML = daySegHTML(segs); // faster than .html() 4532 daySegElementResolve(segs, segmentContainer.children()); 4533 daySegElementReport(segs); 4534 daySegHandlers(segs, segmentContainer, modifiedEventId); 4535 daySegCalcHSides(segs); 4536 daySegSetWidths(segs); 4537 daySegCalcHeights(segs); 4538 rowDivs = getRowDivs(); 4539 // set row heights, calculate event tops (in relation to row top) 4540 for (rowI=0; rowI<rowCnt; rowI++) { 4541 levelI = 0; 4542 colHeights = []; 4543 for (j=0; j<colCnt; j++) { 4544 colHeights[j] = 0; 4545 } 4546 while (i<segCnt && (seg = segs[i]).row == rowI) { 4547 // loop through segs in a row 4548 top = arrayMax(colHeights.slice(seg.startCol, seg.endCol)); 4549 seg.top = top; 4550 top += seg.outerHeight; 4551 for (k=seg.startCol; k<seg.endCol; k++) { 4552 colHeights[k] = top; 4553 } 4554 i++; 4555 } 4556 rowDivs[rowI].height(arrayMax(colHeights)); 4557 } 4558 daySegSetTops(segs, getRowTops(rowDivs)); 4559 } 4560 4561 4562 function renderTempDaySegs(segs, adjustRow, adjustTop) { 4563 var tempContainer = $("<div/>"); 4564 var elements; 4565 var segmentContainer = getDaySegmentContainer(); 4566 var i; 4567 var segCnt = segs.length; 4568 var element; 4569 tempContainer[0].innerHTML = daySegHTML(segs); // faster than .html() 4570 elements = tempContainer.children(); 4571 segmentContainer.append(elements); 4572 daySegElementResolve(segs, elements); 4573 daySegCalcHSides(segs); 4574 daySegSetWidths(segs); 4575 daySegCalcHeights(segs); 4576 daySegSetTops(segs, getRowTops(getRowDivs())); 4577 elements = []; 4578 for (i=0; i<segCnt; i++) { 4579 element = segs[i].element; 4580 if (element) { 4581 if (segs[i].row === adjustRow) { 4582 element.css('top', adjustTop); 4583 } 4584 elements.push(element[0]); 4585 } 4586 } 4587 return $(elements); 4588 } 4589 4590 4591 function daySegHTML(segs) { // also sets seg.left and seg.outerWidth 4592 var rtl = opt('isRTL'); 4593 var i; 4594 var segCnt=segs.length; 4595 var seg; 4596 var event; 4597 var url; 4598 var classes; 4599 var bounds = allDayBounds(); 4600 var minLeft = bounds.left; 4601 var maxLeft = bounds.right; 4602 var leftCol; 4603 var rightCol; 4604 var left; 4605 var right; 4606 var skinCss; 4607 var html = ''; 4608 // calculate desired position/dimensions, create html 4609 for (i=0; i<segCnt; i++) { 4610 seg = segs[i]; 4611 event = seg.event; 4612 classes = ['fc-event', 'fc-event-skin', 'fc-event-hori']; 4613 if (isEventDraggable(event)) { 4614 classes.push('fc-event-draggable'); 4615 } 4616 if (rtl) { 4617 if (seg.isStart) { 4618 classes.push('fc-corner-right'); 4619 } 4620 if (seg.isEnd) { 4621 classes.push('fc-corner-left'); 4622 } 4623 leftCol = dayOfWeekCol(seg.end.getDay()-1); 4624 rightCol = dayOfWeekCol(seg.start.getDay()); 4625 left = seg.isEnd ? colContentLeft(leftCol) : minLeft; 4626 right = seg.isStart ? colContentRight(rightCol) : maxLeft; 4627 }else{ 4628 if (seg.isStart) { 4629 classes.push('fc-corner-left'); 4630 } 4631 if (seg.isEnd) { 4632 classes.push('fc-corner-right'); 4633 } 4634 leftCol = dayOfWeekCol(seg.start.getDay()); 4635 rightCol = dayOfWeekCol(seg.end.getDay()-1); 4636 left = seg.isStart ? colContentLeft(leftCol) : minLeft; 4637 right = seg.isEnd ? colContentRight(rightCol) : maxLeft; 4638 } 4639 classes = classes.concat(event.className); 4640 if (event.source) { 4641 classes = classes.concat(event.source.className || []); 4642 } 4643 url = event.url; 4644 skinCss = getSkinCss(event, opt); 4645 if (url) { 4646 html += "<a href='" + htmlEscape(url) + "'"; 4647 }else{ 4648 html += "<div"; 4649 } 4650 html += 4651 " class='" + classes.join(' ') + "'" + 4652 " style='position:absolute;z-index:8;left:"+left+"px;" + skinCss + "'" + 4653 ">" + 4654 "<div" + 4655 " class='fc-event-inner fc-event-skin'" + 4656 (skinCss ? " style='" + skinCss + "'" : '') + 4657 ">"; 4658 if (!event.allDay && seg.isStart) { 4659 html += 4660 "<span class='fc-event-time'>" + 4661 htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) + 4662 "</span>"; 4663 } 4664 html += 4665 "<span class='fc-event-title'>" + htmlEscape(event.title) + "</span>" + 4666 "</div>"; 4667 if (seg.isEnd && isEventResizable(event)) { 4668 html += 4669 "<div class='ui-resizable-handle ui-resizable-" + (rtl ? 'w' : 'e') + "'>" + 4670 " " + // makes hit area a lot better for IE6/7 4671 "</div>"; 4672 } 4673 html += 4674 "</" + (url ? "a" : "div" ) + ">"; 4675 seg.left = left; 4676 seg.outerWidth = right - left; 4677 seg.startCol = leftCol; 4678 seg.endCol = rightCol + 1; // needs to be exclusive 4679 } 4680 return html; 4681 } 4682 4683 4684 function daySegElementResolve(segs, elements) { // sets seg.element 4685 var i; 4686 var segCnt = segs.length; 4687 var seg; 4688 var event; 4689 var element; 4690 var triggerRes; 4691 for (i=0; i<segCnt; i++) { 4692 seg = segs[i]; 4693 event = seg.event; 4694 element = $(elements[i]); // faster than .eq() 4695 triggerRes = trigger('eventRender', event, event, element); 4696 if (triggerRes === false) { 4697 element.remove(); 4698 }else{ 4699 if (triggerRes && triggerRes !== true) { 4700 triggerRes = $(triggerRes) 4701 .css({ 4702 position: 'absolute', 4703 left: seg.left 4704 }); 4705 element.replaceWith(triggerRes); 4706 element = triggerRes; 4707 } 4708 seg.element = element; 4709 } 4710 } 4711 } 4712 4713 4714 function daySegElementReport(segs) { 4715 var i; 4716 var segCnt = segs.length; 4717 var seg; 4718 var element; 4719 for (i=0; i<segCnt; i++) { 4720 seg = segs[i]; 4721 element = seg.element; 4722 if (element) { 4723 reportEventElement(seg.event, element); 4724 } 4725 } 4726 } 4727 4728 4729 function daySegHandlers(segs, segmentContainer, modifiedEventId) { 4730 var i; 4731 var segCnt = segs.length; 4732 var seg; 4733 var element; 4734 var event; 4735 // retrieve elements, run through eventRender callback, bind handlers 4736 for (i=0; i<segCnt; i++) { 4737 seg = segs[i]; 4738 element = seg.element; 4739 if (element) { 4740 event = seg.event; 4741 if (event._id === modifiedEventId) { 4742 bindDaySeg(event, element, seg); 4743 }else{ 4744 element[0]._fci = i; // for lazySegBind 4745 } 4746 } 4747 } 4748 lazySegBind(segmentContainer, segs, bindDaySeg); 4749 } 4750 4751 4752 function daySegCalcHSides(segs) { // also sets seg.key 4753 var i; 4754 var segCnt = segs.length; 4755 var seg; 4756 var element; 4757 var key, val; 4758 var hsideCache = {}; 4759 // record event horizontal sides 4760 for (i=0; i<segCnt; i++) { 4761 seg = segs[i]; 4762 element = seg.element; 4763 if (element) { 4764 key = seg.key = cssKey(element[0]); 4765 val = hsideCache[key]; 4766 if (val === undefined) { 4767 val = hsideCache[key] = hsides(element, true); 4768 } 4769 seg.hsides = val; 4770 } 4771 } 4772 } 4773 4774 4775 function daySegSetWidths(segs) { 4776 var i; 4777 var segCnt = segs.length; 4778 var seg; 4779 var element; 4780 for (i=0; i<segCnt; i++) { 4781 seg = segs[i]; 4782 element = seg.element; 4783 if (element) { 4784 element[0].style.width = Math.max(0, seg.outerWidth - seg.hsides) + 'px'; 4785 } 4786 } 4787 } 4788 4789 4790 function daySegCalcHeights(segs) { 4791 var i; 4792 var segCnt = segs.length; 4793 var seg; 4794 var element; 4795 var key, val; 4796 var vmarginCache = {}; 4797 // record event heights 4798 for (i=0; i<segCnt; i++) { 4799 seg = segs[i]; 4800 element = seg.element; 4801 if (element) { 4802 key = seg.key; // created in daySegCalcHSides 4803 val = vmarginCache[key]; 4804 if (val === undefined) { 4805 val = vmarginCache[key] = vmargins(element); 4806 } 4807 seg.outerHeight = element[0].offsetHeight + val; 4808 } 4809 } 4810 } 4811 4812 4813 function getRowDivs() { 4814 var i; 4815 var rowCnt = getRowCnt(); 4816 var rowDivs = []; 4817 for (i=0; i<rowCnt; i++) { 4818 rowDivs[i] = allDayRow(i) 4819 .find('td:first div.fc-day-content > div'); // optimal selector? 4820 } 4821 return rowDivs; 4822 } 4823 4824 4825 function getRowTops(rowDivs) { 4826 var i; 4827 var rowCnt = rowDivs.length; 4828 var tops = []; 4829 for (i=0; i<rowCnt; i++) { 4830 tops[i] = rowDivs[i][0].offsetTop; // !!?? but this means the element needs position:relative if in a table cell!!!! 4831 } 4832 return tops; 4833 } 4834 4835 4836 function daySegSetTops(segs, rowTops) { // also triggers eventAfterRender 4837 var i; 4838 var segCnt = segs.length; 4839 var seg; 4840 var element; 4841 var event; 4842 for (i=0; i<segCnt; i++) { 4843 seg = segs[i]; 4844 element = seg.element; 4845 if (element) { 4846 element[0].style.top = rowTops[seg.row] + (seg.top||0) + 'px'; 4847 event = seg.event; 4848 trigger('eventAfterRender', event, event, element); 4849 } 4850 } 4851 } 4852 4853 4854 4855 /* Resizing 4856 -----------------------------------------------------------------------------------*/ 4857 4858 4859 function resizableDayEvent(event, element, seg) { 4860 var rtl = opt('isRTL'); 4861 var direction = rtl ? 'w' : 'e'; 4862 var handle = element.find('div.ui-resizable-' + direction); 4863 var isResizing = false; 4864 4865 // TODO: look into using jquery-ui mouse widget for this stuff 4866 disableTextSelection(element); // prevent native <a> selection for IE 4867 element 4868 .mousedown(function(ev) { // prevent native <a> selection for others 4869 ev.preventDefault(); 4870 }) 4871 .click(function(ev) { 4872 if (isResizing) { 4873 ev.preventDefault(); // prevent link from being visited (only method that worked in IE6) 4874 ev.stopImmediatePropagation(); // prevent fullcalendar eventClick handler from being called 4875 // (eventElementHandlers needs to be bound after resizableDayEvent) 4876 } 4877 }); 4878 4879 handle.mousedown(function(ev) { 4880 if (ev.which != 1) { 4881 return; // needs to be left mouse button 4882 } 4883 isResizing = true; 4884 var hoverListener = t.getHoverListener(); 4885 var rowCnt = getRowCnt(); 4886 var colCnt = getColCnt(); 4887 var dis = rtl ? -1 : 1; 4888 var dit = rtl ? colCnt-1 : 0; 4889 var elementTop = element.css('top'); 4890 var dayDelta; 4891 var helpers; 4892 var eventCopy = $.extend({}, event); 4893 var minCell = dateCell(event.start); 4894 clearSelection(); 4895 $('body') 4896 .css('cursor', direction + '-resize') 4897 .one('mouseup', mouseup); 4898 trigger('eventResizeStart', this, event, ev); 4899 hoverListener.start(function(cell, origCell) { 4900 if (cell) { 4901 var r = Math.max(minCell.row, cell.row); 4902 var c = cell.col; 4903 if (rowCnt == 1) { 4904 r = 0; // hack for all-day area in agenda views 4905 } 4906 if (r == minCell.row) { 4907 if (rtl) { 4908 c = Math.min(minCell.col, c); 4909 }else{ 4910 c = Math.max(minCell.col, c); 4911 } 4912 } 4913 dayDelta = (r*7 + c*dis+dit) - (origCell.row*7 + origCell.col*dis+dit); 4914 var newEnd = addDays(eventEnd(event), dayDelta, true); 4915 if (dayDelta) { 4916 eventCopy.end = newEnd; 4917 var oldHelpers = helpers; 4918 helpers = renderTempDaySegs(compileDaySegs([eventCopy]), seg.row, elementTop); 4919 helpers.find('*').css('cursor', direction + '-resize'); 4920 if (oldHelpers) { 4921 oldHelpers.remove(); 4922 } 4923 hideEvents(event); 4924 }else{ 4925 if (helpers) { 4926 showEvents(event); 4927 helpers.remove(); 4928 helpers = null; 4929 } 4930 } 4931 clearOverlays(); 4932 renderDayOverlay(event.start, addDays(cloneDate(newEnd), 1)); // coordinate grid already rebuild at hoverListener.start 4933 } 4934 }, ev); 4935 4936 function mouseup(ev) { 4937 trigger('eventResizeStop', this, event, ev); 4938 $('body').css('cursor', ''); 4939 hoverListener.stop(); 4940 clearOverlays(); 4941 if (dayDelta) { 4942 eventResize(this, event, dayDelta, 0, ev); 4943 // event redraw will clear helpers 4944 } 4945 // otherwise, the drag handler already restored the old events 4946 4947 setTimeout(function() { // make this happen after the element's click event 4948 isResizing = false; 4949 },0); 4950 } 4951 4952 }); 4953 } 4954 4955 4956 } 4957 4958 //BUG: unselect needs to be triggered when events are dragged+dropped 4959 4960 function SelectionManager() { 4961 var t = this; 4962 4963 4964 // exports 4965 t.select = select; 4966 t.unselect = unselect; 4967 t.reportSelection = reportSelection; 4968 t.daySelectionMousedown = daySelectionMousedown; 4969 4970 4971 // imports 4972 var opt = t.opt; 4973 var trigger = t.trigger; 4974 var defaultSelectionEnd = t.defaultSelectionEnd; 4975 var renderSelection = t.renderSelection; 4976 var clearSelection = t.clearSelection; 4977 4978 4979 // locals 4980 var selected = false; 4981 4982 4983 4984 // unselectAuto 4985 if (opt('selectable') && opt('unselectAuto')) { 4986 $(document).mousedown(function(ev) { 4987 var ignore = opt('unselectCancel'); 4988 if (ignore) { 4989 if ($(ev.target).parents(ignore).length) { // could be optimized to stop after first match 4990 return; 4991 } 4992 } 4993 unselect(ev); 4994 }); 4995 } 4996 4997 4998 function select(startDate, endDate, allDay) { 4999 unselect(); 5000 if (!endDate) { 5001 endDate = defaultSelectionEnd(startDate, allDay); 5002 } 5003 renderSelection(startDate, endDate, allDay); 5004 reportSelection(startDate, endDate, allDay); 5005 } 5006 5007 5008 function unselect(ev) { 5009 if (selected) { 5010 selected = false; 5011 clearSelection(); 5012 trigger('unselect', null, ev); 5013 } 5014 } 5015 5016 5017 function reportSelection(startDate, endDate, allDay, ev) { 5018 selected = true; 5019 trigger('select', null, startDate, endDate, allDay, ev); 5020 } 5021 5022 5023 function daySelectionMousedown(ev) { // not really a generic manager method, oh well 5024 var cellDate = t.cellDate; 5025 var cellIsAllDay = t.cellIsAllDay; 5026 var hoverListener = t.getHoverListener(); 5027 var reportDayClick = t.reportDayClick; // this is hacky and sort of weird 5028 if (ev.which == 1 && opt('selectable')) { // which==1 means left mouse button 5029 unselect(ev); 5030 var _mousedownElement = this; 5031 var dates; 5032 hoverListener.start(function(cell, origCell) { // TODO: maybe put cellDate/cellIsAllDay info in cell 5033 clearSelection(); 5034 if (cell && cellIsAllDay(cell)) { 5035 dates = [ cellDate(origCell), cellDate(cell) ].sort(cmp); 5036 renderSelection(dates[0], dates[1], true); 5037 }else{ 5038 dates = null; 5039 } 5040 }, ev); 5041 $(document).one('mouseup', function(ev) { 5042 hoverListener.stop(); 5043 if (dates) { 5044 if (+dates[0] == +dates[1]) { 5045 reportDayClick(dates[0], true, ev); 5046 } 5047 reportSelection(dates[0], dates[1], true, ev); 5048 } 5049 }); 5050 } 5051 } 5052 5053 5054 } 5055 5056 function OverlayManager() { 5057 var t = this; 5058 5059 5060 // exports 5061 t.renderOverlay = renderOverlay; 5062 t.clearOverlays = clearOverlays; 5063 5064 5065 // locals 5066 var usedOverlays = []; 5067 var unusedOverlays = []; 5068 5069 5070 function renderOverlay(rect, parent) { 5071 var e = unusedOverlays.shift(); 5072 if (!e) { 5073 e = $("<div class='fc-cell-overlay' style='position:absolute;z-index:3'/>"); 5074 } 5075 if (e[0].parentNode != parent[0]) { 5076 e.appendTo(parent); 5077 } 5078 usedOverlays.push(e.css(rect).show()); 5079 return e; 5080 } 5081 5082 5083 function clearOverlays() { 5084 var e; 5085 while (e = usedOverlays.shift()) { 5086 unusedOverlays.push(e.hide().unbind()); 5087 } 5088 } 5089 5090 5091 } 5092 5093 function CoordinateGrid(buildFunc) { 5094 5095 var t = this; 5096 var rows; 5097 var cols; 5098 5099 5100 t.build = function() { 5101 rows = []; 5102 cols = []; 5103 buildFunc(rows, cols); 5104 }; 5105 5106 5107 t.cell = function(x, y) { 5108 var rowCnt = rows.length; 5109 var colCnt = cols.length; 5110 var i, r=-1, c=-1; 5111 for (i=0; i<rowCnt; i++) { 5112 if (y >= rows[i][0] && y < rows[i][1]) { 5113 r = i; 5114 break; 5115 } 5116 } 5117 for (i=0; i<colCnt; i++) { 5118 if (x >= cols[i][0] && x < cols[i][1]) { 5119 c = i; 5120 break; 5121 } 5122 } 5123 return (r>=0 && c>=0) ? { row:r, col:c } : null; 5124 }; 5125 5126 5127 t.rect = function(row0, col0, row1, col1, originElement) { // row1,col1 is inclusive 5128 var origin = originElement.offset(); 5129 return { 5130 top: rows[row0][0] - origin.top, 5131 left: cols[col0][0] - origin.left, 5132 width: cols[col1][1] - cols[col0][0], 5133 height: rows[row1][1] - rows[row0][0] 5134 }; 5135 }; 5136 5137 } 5138 5139 function HoverListener(coordinateGrid) { 5140 5141 5142 var t = this; 5143 var bindType; 5144 var change; 5145 var firstCell; 5146 var cell; 5147 5148 5149 t.start = function(_change, ev, _bindType) { 5150 change = _change; 5151 firstCell = cell = null; 5152 coordinateGrid.build(); 5153 mouse(ev); 5154 bindType = _bindType || 'mousemove'; 5155 $(document).bind(bindType, mouse); 5156 }; 5157 5158 5159 function mouse(ev) { 5160 _fixUIEvent(ev); // see below 5161 var newCell = coordinateGrid.cell(ev.pageX, ev.pageY); 5162 if (!newCell != !cell || newCell && (newCell.row != cell.row || newCell.col != cell.col)) { 5163 if (newCell) { 5164 if (!firstCell) { 5165 firstCell = newCell; 5166 } 5167 change(newCell, firstCell, newCell.row-firstCell.row, newCell.col-firstCell.col); 5168 }else{ 5169 change(newCell, firstCell); 5170 } 5171 cell = newCell; 5172 } 5173 } 5174 5175 5176 t.stop = function() { 5177 $(document).unbind(bindType, mouse); 5178 return cell; 5179 }; 5180 5181 5182 } 5183 5184 5185 5186 // this fix was only necessary for jQuery UI 1.8.16 (and jQuery 1.7 or 1.7.1) 5187 // upgrading to jQuery UI 1.8.17 (and using either jQuery 1.7 or 1.7.1) fixed the problem 5188 // but keep this in here for 1.8.16 users 5189 // and maybe remove it down the line 5190 5191 function _fixUIEvent(event) { // for issue 1168 5192 if (event.pageX === undefined) { 5193 event.pageX = event.originalEvent.pageX; 5194 event.pageY = event.originalEvent.pageY; 5195 } 5196 } 5197 function HorizontalPositionCache(getElement) { 5198 5199 var t = this, 5200 elements = {}, 5201 lefts = {}, 5202 rights = {}; 5203 5204 function e(i) { 5205 return elements[i] = elements[i] || getElement(i); 5206 } 5207 5208 t.left = function(i) { 5209 return lefts[i] = lefts[i] === undefined ? e(i).position().left : lefts[i]; 5210 }; 5211 5212 t.right = function(i) { 5213 return rights[i] = rights[i] === undefined ? t.left(i) + e(i).width() : rights[i]; 5214 }; 5215 5216 t.clear = function() { 5217 elements = {}; 5218 lefts = {}; 5219 rights = {}; 5220 }; 5221 5222 } 5223 5224 })(jQuery);
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 20:08:37 2014 | Cross-referenced by PHPXref 0.7.1 |