Index: fullcalendar.css =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/fullcalendar/fullcalendar.css,v retrieving revision 1.3 diff -u -p -r1.3 fullcalendar.css --- fullcalendar.css 11 Nov 2010 16:58:56 -0000 1.3 +++ fullcalendar.css 24 Nov 2010 16:34:58 -0000 @@ -1,14 +1,16 @@ -/* $Id: fullcalendar.css,v 1.3 2010/11/11 16:58:56 ablondeau Exp $ */ /* - * FullCalendar v1.4.7 Stylesheet + * FullCalendar v1.4.9 Stylesheet * * Feel free to edit this file to customize the look of FullCalendar. * When upgrading to newer versions, please upgrade this file as well, * porting over any customizations afterwards. * - * Date: Mon Jul 5 16:07:40 2010 -0700 + * Date: Fri Nov 19 22:45:44 2010 -0800 * */ + + +/* TODO: make font sizes look the same in all doctypes */ .fc, @@ -33,12 +35,6 @@ } -/* Custom for Drupal module - undo Drupal default theme link colors */ -.fc-content a:link, .fc-content a:visited { -color:#FFFFFF; -text-decoration:none; -} - /* Header ------------------------------------------------------------------------*/ @@ -222,7 +218,7 @@ table.fc-header { background: #ffc; } -.fc-content .fc-not-today { +.fc-content .fc-not-today { /* override jq-ui highlight (TODO: ui-widget-content) */ background: none; } @@ -239,12 +235,14 @@ table.fc-header { + + /* Global Event Styles ------------------------------------------------------------------------*/ -.fc-event-default, -.fc-agenda .fc-event-default .fc-event-time, -.fc-event-default a { +.fc-event, +.fc-agenda .fc-event-time, +.fc-event a { border-style: solid; border-color: #36c; /* default BORDER color (probably the same as background-color) */ background-color: #36c; /* default BACKGROUND color */ @@ -300,7 +298,7 @@ table.fc-header { /* resizable */ -.fc .ui-resizable-handle { +.fc .ui-resizable-handle { /*** TODO: don't use ui-resizable anoymore, change class ***/ display: block; position: absolute; z-index: 99999; @@ -365,6 +363,7 @@ table.fc-header { } + /* Month View, Basic Week View, Basic Day View ------------------------------------------------------------------------*/ @@ -500,6 +499,12 @@ table.fc-header { padding: 2px 2px 0; /* distance between events and day edges */ } +/* vertical background columns */ + +.fc .fc-agenda-bg .ui-state-highlight { + background-image: none; /* tall column, don't want repeating background image */ + } + /* Vertical Events Index: fullcalendar.js =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/fullcalendar/fullcalendar.js,v retrieving revision 1.2 diff -u -p -r1.2 fullcalendar.js --- fullcalendar.js 11 Nov 2010 17:09:21 -0000 1.2 +++ fullcalendar.js 24 Nov 2010 16:34:59 -0000 @@ -1,32 +1,23 @@ -/* $Id: fullcalendar.js,v 1.2 2010/11/11 17:09:21 ablondeau Exp $ */ /** * @preserve - * FullCalendar v1.4.7 + * FullCalendar v1.4.9 * http://arshaw.com/fullcalendar/ * * Use fullcalendar.css for basic styling. - * For event drag & drop, required jQuery UI draggable. + * For event drag & drop, requires jQuery UI draggable. * For event resizing, requires jQuery UI resizable. * - * Copyright (c) 2009 Adam Shaw - * Dual licensed under the MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html + * Copyright (c) 2010 Adam Shaw + * Dual licensed under the MIT and GPL licenses, located in + * MIT-LICENSE.txt and GPL-LICENSE.txt respectively. * - * Date: Mon Jul 5 16:07:40 2010 -0700 + * Date: Fri Nov 19 22:45:44 2010 -0800 * */ (function($, undefined) { -var fc = $.fullCalendar = {}; -var views = fc.views = {}; - - -/* Defaults ------------------------------------------------------------------------------*/ - var defaults = { // display @@ -45,6 +36,7 @@ var defaults = { //disableResizing: false, allDayDefault: true, + ignoreTimezone: true, // event ajax lazyFetching: true, @@ -117,31 +109,28 @@ var rtlDefaults = { } }; -// function for adding/overriding defaults -var setDefaults = fc.setDefaults = function(d) { - $.extend(true, defaults, d); -}; +var fc = $.fullCalendar = { version: "1.4.9" }; +var fcViews = fc.views = {}; -/* .fullCalendar jQuery function ------------------------------------------------------------------------------*/ $.fn.fullCalendar = function(options) { + // method calling if (typeof options == 'string') { - var args = Array.prototype.slice.call(arguments, 1), - res; + var args = Array.prototype.slice.call(arguments, 1); + var res; this.each(function() { - var data = $.data(this, 'fullCalendar'); - if (data) { - var meth = data[options]; - if (meth) { - var r = meth.apply(this, args); - if (res === undefined) { - res = r; - } + var calendar = $.data(this, 'fullCalendar'); + if (calendar && $.isFunction(calendar[options])) { + var r = calendar[options].apply(calendar, args); + if (res === undefined) { + res = r; + } + if (options == 'destroy') { + $.removeData(this, 'fullCalendar'); } } }); @@ -150,8 +139,9 @@ $.fn.fullCalendar = function(options) { } return this; } - - // pluck the 'events' and 'eventSources' options + + + // would like to have this logic in EventManager, but needs to happen before options are recursively extended var eventSources = options.eventSources || []; delete options.eventSources; if (options.events) { @@ -159,950 +149,1202 @@ $.fn.fullCalendar = function(options) { delete options.events; } - // first event source reserved for 'sticky' events - eventSources.unshift([]); - - // initialize options + options = $.extend(true, {}, defaults, (options.isRTL || options.isRTL===undefined && defaults.isRTL) ? rtlDefaults : {}, options ); - var tm = options.theme ? 'ui' : 'fc'; // for making theme classes - this.each(function() { + this.each(function(i, _element) { + var element = $(_element); + var calendar = new Calendar(element, options, eventSources); + element.data('fullCalendar', calendar); // TODO: look into memory leak implications + calendar.render(); + }); - /* Instance Initialization - -----------------------------------------------------------------------------*/ - - // element - var _element = this, - element = $(_element).addClass('fc'), - elementOuterWidth, - content = $("
").prependTo(_element), - suggestedViewHeight, - resizeUID = 0, - ignoreWindowResize = 0, - date = new Date(), - viewName, // the current view name (TODO: look into getting rid of) - view, // the current view - viewInstances = {}, - absoluteViewElement; - - - + return this; + +}; + + +// function for adding/overriding defaults +function setDefaults(d) { + $.extend(true, defaults, d); +} + + + + +function Calendar(element, options, eventSources) { + var t = this; + + + // exports + t.options = options; + t.render = render; + t.destroy = destroy; + t.refetchEvents = refetchEvents; + t.reportEvents = reportEvents; + t.reportEventChange = reportEventChange; + t.changeView = changeView; + t.select = select; + t.unselect = unselect; + t.prev = prev; + t.next = next; + t.prevYear = prevYear; + t.nextYear = nextYear; + t.today = today; + t.gotoDate = gotoDate; + t.incrementDate = incrementDate; + t.formatDate = function(format, date) { return formatDate(format, date, options) }; + t.formatDates = function(format, date1, date2) { return formatDates(format, date1, date2, options) }; + t.getDate = getDate; + t.getView = getView; + t.option = option; + t.trigger = trigger; + + + // imports + EventManager.call(t, options, eventSources); + var isFetchNeeded = t.isFetchNeeded; + var fetchEvents = t.fetchEvents; + + + // locals + var _element = element[0]; + var header; + var headerElement; + var content; + var tm; // for making theme classes + var currentView; + var viewInstances = {}; + var elementOuterWidth; + var suggestedViewHeight; + var absoluteViewElement; + var resizeUID = 0; + var ignoreWindowResize = 0; + var date = new Date(); + var events = []; + var _dragElement; + + + + /* Main Rendering + -----------------------------------------------------------------------------*/ + + + setYMD(date, options.year, options.month, options.date); + + + function render(inc) { + if (!content) { + initialRender(); + }else{ + calcSize(); + markSizesDirty(); + markEventsDirty(); + renderView(inc); + } + } + + + function initialRender() { + tm = options.theme ? 'ui' : 'fc'; + element.addClass('fc'); if (options.isRTL) { element.addClass('fc-rtl'); } if (options.theme) { element.addClass('ui-widget'); } - - setYMD(date, options.year, options.month, options.day); - - - - /* View Rendering - -----------------------------------------------------------------------------*/ - - function changeView(v) { - if (v != viewName) { - ignoreWindowResize++; // because setMinHeight might change the height before render (and subsequently setSize) is reached - - viewUnselect(); - - var oldView = view, - newViewElement; - - if (oldView) { - if (oldView.eventsChanged) { - eventsDirty(); - oldView.eventDirty = oldView.eventsChanged = false; - } - if (oldView.beforeHide) { - oldView.beforeHide(); // called before changing min-height. if called after, scroll state is reset (in Opera) - } - setMinHeight(content, content.height()); - oldView.element.hide(); - }else{ - setMinHeight(content, 1); // needs to be 1 (not 0) for IE7, or else view dimensions miscalculated - } - content.css('overflow', 'hidden'); - - if (viewInstances[v]) { - (view = viewInstances[v]).element.show(); - }else{ - view = viewInstances[v] = fc.views[v]( - newViewElement = absoluteViewElement = - $("
") - .appendTo(content), - options, - v // the view's name - ); - } - - if (header) { - // update 'active' view button - header.find('div.fc-button-' + viewName).removeClass(tm + '-state-active'); - header.find('div.fc-button-' + v).addClass(tm + '-state-active'); - } - - viewName = v; - render(); // after height has been set, will make absoluteViewElement's position=relative, then set to null - content.css('overflow', ''); - if (oldView) { - setMinHeight(content, 1); - } - if (!newViewElement && view.afterShow) { - view.afterShow(); // called after setting min-height/overflow, so in final scroll state (for Opera) - } - - ignoreWindowResize--; - } + content = $("
") + .prependTo(element); + header = new Header(t, options); + headerElement = header.render(); + if (headerElement) { + element.prepend(headerElement); } - - - function render(inc) { - if (elementVisible()) { - ignoreWindowResize++; // because view.renderEvents might temporarily change the height before setSize is reached + changeView(options.defaultView); + $(window).resize(windowResize); + // needed for IE in a 0x0 iframe, b/c when it is resized, never triggers a windowResize + if (!bodyVisible()) { + lateRender(); + } + } + + + // called when we know the calendar couldn't be rendered when it was initialized, + // but we think it's ready now + function lateRender() { + setTimeout(function() { // IE7 needs this so dimensions are calculated correctly + if (!currentView.start && bodyVisible()) { // !currentView.start makes sure this never happens more than once + renderView(); + } + },0); + } + + + function destroy() { + $(window).unbind('resize', windowResize); + header.destroy(); + content.remove(); + element.removeClass('fc fc-rtl fc-ui-widget'); + } + + + + function elementVisible() { + return _element.offsetWidth !== 0; + } + + + function bodyVisible() { + return $('body')[0].offsetWidth !== 0; + } + + + + /* View Rendering + -----------------------------------------------------------------------------*/ + + + function changeView(newViewName) { + if (!currentView || newViewName != currentView.name) { + ignoreWindowResize++; // because setMinHeight might change the height before render (and subsequently setSize) is reached - viewUnselect(); - - if (suggestedViewHeight === undefined) { - calcSize(); - } - - if (!view.start || inc || date < view.start || date >= view.end) { - view.render(date, inc || 0); // responsible for clearing events - setSize(true); - if (!eventStart || !options.lazyFetching || view.visStart < eventStart || view.visEnd > eventEnd) { - fetchAndRenderEvents(); - }else{ - view.renderEvents(events); // don't refetch - } - } - else if (view.sizeDirty || view.eventsDirty || !options.lazyFetching) { - view.clearEvents(); - if (view.sizeDirty) { - setSize(); - } - if (options.lazyFetching) { - view.renderEvents(events); // don't refetch - }else{ - fetchAndRenderEvents(); - } - } - elementOuterWidth = element.outerWidth(); - view.sizeDirty = false; - view.eventsDirty = false; - - if (header) { - // update title text - header.find('h2.fc-header-title').html(view.title); - // enable/disable 'today' button - var today = new Date(); - if (today >= view.start && today < view.end) { - header.find('div.fc-button-today').addClass(tm + '-state-disabled'); - }else{ - header.find('div.fc-button-today').removeClass(tm + '-state-disabled'); - } - } + unselect(); + + var oldView = currentView; + var newViewElement; - ignoreWindowResize--; - view.trigger('viewDisplay', _element); + if (oldView) { + (oldView.beforeHide || noop)(); // called before changing min-height. if called after, scroll state is reset (in Opera) + setMinHeight(content, content.height()); + oldView.element.hide(); + }else{ + setMinHeight(content, 1); // needs to be 1 (not 0) for IE7, or else view dimensions miscalculated } - } - - - function elementVisible() { - return _element.offsetWidth !== 0; - } - - function bodyVisible() { - return $('body')[0].offsetWidth !== 0; - } - - function viewUnselect() { - if (view) { - view.unselect(); + content.css('overflow', 'hidden'); + + currentView = viewInstances[newViewName]; + if (currentView) { + currentView.element.show(); + }else{ + currentView = viewInstances[newViewName] = new fcViews[newViewName]( + newViewElement = absoluteViewElement = + $("
") + .appendTo(content), + t // the calendar object + ); } - } - - - // called when any event objects have been added/removed/changed, rerenders - function eventsChanged() { - eventsDirty(); - if (elementVisible()) { - view.clearEvents(); - view.renderEvents(events); - view.eventsDirty = false; + + if (oldView) { + header.deactivateButton(oldView.name); } + header.activateButton(newViewName); + + renderView(); // after height has been set, will make absoluteViewElement's position=relative, then set to null + + content.css('overflow', ''); + if (oldView) { + setMinHeight(content, 1); + } + + if (!newViewElement) { + (currentView.afterShow || noop)(); // called after setting min-height/overflow, so in final scroll state (for Opera) + } + + ignoreWindowResize--; } - - // marks other views' events as dirty - function eventsDirty() { - $.each(viewInstances, function() { - this.eventsDirty = true; - }); - } - - // called when we know the element size has changed - function sizeChanged() { - sizesDirty(); - if (elementVisible()) { + } + + + + function renderView(inc) { + if (elementVisible()) { + ignoreWindowResize++; // because renderEvents might temporarily change the height before setSize is reached + + unselect(); + + if (suggestedViewHeight === undefined) { calcSize(); + } + + var forceEventRender = false; + if (!currentView.start || inc || date < currentView.start || date >= currentView.end) { + // view must render an entire new date range (and refetch/render events) + currentView.render(date, inc || 0); // responsible for clearing events + setSize(true); + forceEventRender = true; + } + else if (currentView.sizeDirty) { + // view must resize (and rerender events) + currentView.clearEvents(); setSize(); - viewUnselect(); - view.rerenderEvents(); - view.sizeDirty = false; + forceEventRender = true; + } + else if (currentView.eventsDirty) { + currentView.clearEvents(); + forceEventRender = true; + } + currentView.sizeDirty = false; + currentView.eventsDirty = false; + updateEvents(forceEventRender); + + elementOuterWidth = element.outerWidth(); + + header.updateTitle(currentView.title); + var today = new Date(); + if (today >= currentView.start && today < currentView.end) { + header.disableButton('today'); + }else{ + header.enableButton('today'); } + + ignoreWindowResize--; + currentView.trigger('viewDisplay', _element); } - - // marks other views' sizes as dirty - function sizesDirty() { - $.each(viewInstances, function() { - this.sizeDirty = true; - }); + } + + + + /* Resizing + -----------------------------------------------------------------------------*/ + + + function updateSize() { + markSizesDirty(); + if (elementVisible()) { + calcSize(); + setSize(); + unselect(); + currentView.clearEvents(); + currentView.renderEvents(events); + currentView.sizeDirty = false; } - - - - - /* Event Sources and Fetching - -----------------------------------------------------------------------------*/ - - var events = [], - eventStart, eventEnd; - - // Fetch from ALL sources. Clear 'events' array and populate - function fetchEvents(callback) { - events = []; - eventStart = cloneDate(view.visStart); - eventEnd = cloneDate(view.visEnd); - var queued = eventSources.length, - sourceDone = function() { - if (!--queued) { - if (callback) { - callback(events); + } + + + function markSizesDirty() { + $.each(viewInstances, function(i, inst) { + inst.sizeDirty = true; + }); + } + + + function calcSize() { + if (options.contentHeight) { + suggestedViewHeight = options.contentHeight; + } + else if (options.height) { + suggestedViewHeight = options.height - (headerElement ? headerElement.height() : 0) - vsides(content[0]); + } + else { + suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5)); + } + } + + + function setSize(dateChanged) { // todo: dateChanged? + ignoreWindowResize++; + currentView.setHeight(suggestedViewHeight, dateChanged); + if (absoluteViewElement) { + absoluteViewElement.css('position', 'relative'); + absoluteViewElement = null; + } + currentView.setWidth(content.width(), dateChanged); + ignoreWindowResize--; + } + + + function windowResize() { + if (!ignoreWindowResize) { + if (currentView.start) { // view has already been rendered + var uid = ++resizeUID; + setTimeout(function() { // add a delay + if (uid == resizeUID && !ignoreWindowResize && elementVisible()) { + if (elementOuterWidth != (elementOuterWidth = element.outerWidth())) { + ignoreWindowResize++; // in case the windowResize callback changes the height + updateSize(); + currentView.trigger('windowResize', _element); + ignoreWindowResize--; } } - }, i=0; - for (; i") + .append($("") + .append($("") + .append(renderSection(sections.left))) + .append($("") + .append(renderSection(sections.center))) + .append($("") + .append(renderSection(sections.right)))); + return element; + } + } + + + function destroy() { + element.remove(); + } + + + function renderSection(buttonStr) { + if (buttonStr) { + var tr = $(""); + $.each(buttonStr.split(' '), function(i) { + if (i > 0) { + tr.append(""); } - render(); - }, - - // - // Event Manipulation - // - - updateEvent: function(event) { // update an existing event - var i, len = events.length, e, - startDelta = event.start - event._start, - endDelta = event.end ? - (event.end - (event._end || view.defaultEventEnd(event))) // event._end would be null if event.end - : 0; // was null and event was just resized - for (i=0; i

 

"); + if (prevButton) { + prevButton.addClass(tm + '-corner-right'); } - e.title = event.title; - e.url = event.url; - e.allDay = event.allDay; - e.className = event.className; - e.editable = event.editable; - normalizeEvent(e, options); - } - } - normalizeEvent(event, options); - eventsChanged(); - }, - - renderEvent: function(event, stick) { // render a new event - normalizeEvent(event, options); - if (!event.source) { - if (stick) { - (event.source = eventSources[0]).push(event); - } - events.push(event); - } - eventsChanged(); - }, - - removeEvents: function(filter) { - if (!filter) { // remove all - events = []; - // clear all array sources - for (var i=0; i") - .append($("") - .append($("").append(buildSection(sections.left))) - .append($("").append(buildSection(sections.center))) - .append($("").append(buildSection(sections.right)))) - .prependTo(element); - } - function buildSection(buttonStr) { - if (buttonStr) { - var tr = $(""); - $.each(buttonStr.split(' '), function(i) { - if (i > 0) { - tr.append(""); - } - var prevButton; - $.each(this.split(','), function(j, buttonName) { - if (buttonName == 'title') { - tr.append("

 

"); + if (buttonClick) { if (prevButton) { - prevButton.addClass(tm + '-corner-right'); + prevButton.addClass(tm + '-no-right'); } - prevButton = null; - }else{ - var buttonClick; - if (publicMethods[buttonName]) { - buttonClick = publicMethods[buttonName]; + var button; + var icon = options.theme ? smartProperty(options.buttonIcons, buttonName) : null; + var text = smartProperty(options.buttonText, buttonName); + if (icon) { + button = $("
" + + "
"); } - else if (views[buttonName]) { - buttonClick = function() { - button.removeClass(tm + '-state-hover'); - changeView(buttonName); - }; + else if (text) { + button = $(""); } - if (buttonClick) { - if (prevButton) { - prevButton.addClass(tm + '-no-right'); - } - var button, - icon = options.theme ? smartProperty(options.buttonIcons, buttonName) : null, - text = smartProperty(options.buttonText, buttonName); - if (icon) { - button = $("
" + - "
"); - } - else if (text) { - button = $(""); - } - if (button) { - button - .click(function() { - if (!button.hasClass(tm + '-state-disabled')) { - buttonClick(); - } - }) - .mousedown(function() { + if (button) { + button + .click(function() { + if (!button.hasClass(tm + '-state-disabled')) { + buttonClick(); + } + }) + .mousedown(function() { + button + .not('.' + tm + '-state-active') + .not('.' + tm + '-state-disabled') + .addClass(tm + '-state-down'); + }) + .mouseup(function() { + button.removeClass(tm + '-state-down'); + }) + .hover( + function() { button .not('.' + tm + '-state-active') .not('.' + tm + '-state-disabled') - .addClass(tm + '-state-down'); - }) - .mouseup(function() { - button.removeClass(tm + '-state-down'); - }) - .hover( - function() { - button - .not('.' + tm + '-state-active') - .not('.' + tm + '-state-disabled') - .addClass(tm + '-state-hover'); - }, - function() { - button - .removeClass(tm + '-state-hover') - .removeClass(tm + '-state-down'); - } - ) - .appendTo($("").appendTo(tr)); - if (prevButton) { - prevButton.addClass(tm + '-no-right'); - }else{ - button.addClass(tm + '-corner-left'); - } - prevButton = button; + .addClass(tm + '-state-hover'); + }, + function() { + button + .removeClass(tm + '-state-hover') + .removeClass(tm + '-state-down'); + } + ) + .appendTo($("").appendTo(tr)); + if (prevButton) { + prevButton.addClass(tm + '-no-right'); + }else{ + button.addClass(tm + '-corner-left'); } + prevButton = button; } } - }); - if (prevButton) { - prevButton.addClass(tm + '-corner-right'); - } - }); - return $("").append(tr); - } - } - - - - /* Resizing - -----------------------------------------------------------------------------*/ - - - function calcSize() { - if (options.contentHeight) { - suggestedViewHeight = options.contentHeight; - } - else if (options.height) { - suggestedViewHeight = options.height - (header ? header.height() : 0) - vsides(content[0]); - } - else { - suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5)); - } - } - - - function setSize(dateChanged) { - ignoreWindowResize++; - view.setHeight(suggestedViewHeight, dateChanged); - if (absoluteViewElement) { - absoluteViewElement.css('position', 'relative'); - absoluteViewElement = null; - } - view.setWidth(content.width(), dateChanged); - ignoreWindowResize--; - } - - - function windowResize() { - if (!ignoreWindowResize) { - if (view.start) { // view has already been rendered - var uid = ++resizeUID; - setTimeout(function() { // add a delay - if (uid == resizeUID && !ignoreWindowResize && elementVisible()) { - if (elementOuterWidth != (elementOuterWidth = element.outerWidth())) { - ignoreWindowResize++; // in case the windowResize callback changes the height - sizeChanged(); - view.trigger('windowResize', _element); - ignoreWindowResize--; - } - } - }, 200); - }else{ - // calendar must have been initialized in a 0x0 iframe that has just been resized - lateRender(); - } - } - } - $(window).resize(windowResize); - - - - /* External event dropping - --------------------------------------------------------*/ - - if (options.droppable) { - var _dragElement; - $(document) - .bind('dragstart', function(ev, ui) { - var _e = ev.target; - var e = $(_e); - if (!e.parents('.fc').length) { // not already inside a calendar - var accept = options.dropAccept; - if ($.isFunction(accept) ? accept.call(_e, e) : e.is(accept)) { - _dragElement = _e; - view.dragStart(_dragElement, ev, ui); - } - } - }) - .bind('dragstop', function(ev, ui) { - if (_dragElement) { - view.dragStop(_dragElement, ev, ui); - _dragElement = null; } }); - } - - - - // let's begin... - changeView(options.defaultView); - - - // needed for IE in a 0x0 iframe, b/c when it is resized, never triggers a windowResize - if (!bodyVisible()) { - lateRender(); - } - - - // called when we know the calendar couldn't be rendered when it was initialized, - // but we think it's ready now - function lateRender() { - setTimeout(function() { // IE7 needs this so dimensions are calculated correctly - if (!view.start && bodyVisible()) { // !view.start makes sure this never happens more than once - render(); + if (prevButton) { + prevButton.addClass(tm + '-corner-right'); } - },0); + }); + return $("
").append(tr); } - - - }); + } - return this; -}; - - - -/* Important Event Utilities ------------------------------------------------------------------------------*/ - -var fakeID = 0; - -function normalizeEvent(event, options) { - event._id = event._id || (event.id === undefined ? '_fc' + fakeID++ : event.id + ''); - if (event.date) { - if (!event.start) { - event.start = event.date; - } - delete event.date; - } - event._start = cloneDate(event.start = parseDate(event.start)); - event.end = parseDate(event.end); - if (event.end && event.end <= event.start) { - event.end = null; - } - event._end = event.end ? cloneDate(event.end) : null; - if (event.allDay === undefined) { - event.allDay = options.allDayDefault; - } - if (event.className) { - if (typeof event.className == 'string') { - event.className = event.className.split(/\s+/); - } - }else{ - event.className = []; + function updateTitle(html) { + element.find('h2.fc-header-title') + .html(html); } -} -// TODO: if there is no start date, return false to indicate an invalid event - - -/* Grid-based Views: month, basicWeek, basicDay ------------------------------------------------------------------------------*/ - -setDefaults({ - weekMode: 'fixed' -}); - -views.month = function(element, options, viewName) { - return new Grid(element, options, { - render: function(date, delta) { - if (delta) { - addMonths(date, delta); - date.setDate(1); - } - // start/end - var start = this.start = cloneDate(date, true); - start.setDate(1); - this.end = addMonths(cloneDate(start), 1); - // visStart/visEnd - var visStart = this.visStart = cloneDate(start), - visEnd = this.visEnd = cloneDate(this.end), - nwe = options.weekends ? 0 : 1; - if (nwe) { - skipWeekend(visStart); - skipWeekend(visEnd, -1, true); - } - addDays(visStart, -((visStart.getDay() - Math.max(options.firstDay, nwe) + 7) % 7)); - addDays(visEnd, (7 - visEnd.getDay() + Math.max(options.firstDay, nwe)) % 7); - // row count - var rowCnt = Math.round((visEnd - visStart) / (DAY_MS * 7)); - if (options.weekMode == 'fixed') { - addDays(visEnd, (6 - rowCnt) * 7); - rowCnt = 6; - } - // title - this.title = formatDate( - start, - this.option('titleFormat'), - options - ); - // render - this.renderGrid( - rowCnt, options.weekends ? 7 : 5, - this.option('columnFormat'), - true - ); - } - }, viewName); -}; - -views.basicWeek = function(element, options, viewName) { - return new Grid(element, options, { - render: function(date, delta) { - if (delta) { - addDays(date, delta * 7); - } - var visStart = this.visStart = cloneDate( - this.start = addDays(cloneDate(date), -((date.getDay() - options.firstDay + 7) % 7)) - ), - visEnd = this.visEnd = cloneDate( - this.end = addDays(cloneDate(visStart), 7) - ); - if (!options.weekends) { - skipWeekend(visStart); - skipWeekend(visEnd, -1, true); - } - this.title = formatDates( - visStart, - addDays(cloneDate(visEnd), -1), - this.option('titleFormat'), - options - ); - this.renderGrid( - 1, options.weekends ? 7 : 5, - this.option('columnFormat'), - false - ); - } - }, viewName); -}; - -views.basicDay = function(element, options, viewName) { - return new Grid(element, options, { - render: function(date, delta) { - if (delta) { - addDays(date, delta); - if (!options.weekends) { - skipWeekend(date, delta < 0 ? -1 : 1); - } - } - this.title = formatDate(date, this.option('titleFormat'), options); - this.start = this.visStart = cloneDate(date, true); - this.end = this.visEnd = addDays(cloneDate(this.start), 1); - this.renderGrid( - 1, 1, - this.option('columnFormat'), - false - ); - } - }, viewName); -}; - - -// rendering bugs - -var tdHeightBug; - - -function Grid(element, options, methods, viewName) { - var tm, firstDay, - nwe, // no weekends (int) - rtl, dis, dit, // day index sign / translate - viewWidth, viewHeight, - rowCnt, colCnt, - colWidth, - thead, tbody, - cachedEvents=[], - segmentContainer, - dayContentPositions = new HorizontalPositionCache(function(dayOfWeek) { - return tbody.find('td:eq(' + ((dayOfWeek - Math.max(firstDay,nwe)+colCnt) % colCnt) + ') div div'); - }), - // ... - - // initialize superclass - view = $.extend(this, viewMethods, methods, { - renderGrid: renderGrid, - renderEvents: renderEvents, - rerenderEvents: rerenderEvents, - clearEvents: clearEvents, - setHeight: setHeight, - setWidth: setWidth, - defaultEventEnd: function(event) { // calculates an end if event doesnt have one, mostly for resizing - return cloneDate(event.start); - } - }); - view.name = viewName; - view.init(element, options); + function activateButton(buttonName) { + element.find('div.fc-button-' + buttonName) + .addClass(tm + '-state-active'); + } - /* Grid Rendering - -----------------------------------------------------------------------------*/ + function deactivateButton(buttonName) { + element.find('div.fc-button-' + buttonName) + .removeClass(tm + '-state-active'); + } - disableTextSelection(element.addClass('fc-grid')); + function disableButton(buttonName) { + element.find('div.fc-button-' + buttonName) + .addClass(tm + '-state-disabled'); + } - - function renderGrid(r, c, colFormat, showNumbers) { - rowCnt = r; + function enableButton(buttonName) { + element.find('div.fc-button-' + buttonName) + .removeClass(tm + '-state-disabled'); + } + + +} + +var eventGUID = 1; + +function EventManager(options, sources) { + var t = this; + + + // exports + t.isFetchNeeded = isFetchNeeded; + t.fetchEvents = fetchEvents; + t.addEventSource = addEventSource; + t.removeEventSource = removeEventSource; + t.updateEvent = updateEvent; + t.renderEvent = renderEvent; + t.removeEvents = removeEvents; + t.clientEvents = clientEvents; + t.normalizeEvent = normalizeEvent; + + + // imports + var trigger = t.trigger; + var getView = t.getView; + var reportEvents = t.reportEvents; + + + // locals + var rangeStart, rangeEnd; + var currentFetchID = 0; + var pendingSourceCnt = 0; + var loadingLevel = 0; + var dynamicEventSource = []; + var cache = []; + + + + /* Fetching + -----------------------------------------------------------------------------*/ + + + function isFetchNeeded(start, end) { + return !rangeStart || start < rangeStart || end > rangeEnd; + } + + + function fetchEvents(start, end) { + rangeStart = start; + rangeEnd = end; + cache = []; + var fetchID = ++currentFetchID; + var len = sources.length; + pendingSourceCnt = len; + for (var i=0; i" + formatDate(d, colFormat, options) + ""; + "'>" + formatDate(d, colFormat) + ""; addDays(d, 1); if (nwe) { skipWeekend(d); @@ -1123,7 +1365,7 @@ function Grid(element, options, methods, thead = $(s + "").appendTo(table); s = ""; - d = cloneDate(view.visStart); + d = cloneDate(t.visStart); for (i=0; i"; for (j=0; j").appendTo(table); dayBind(tbody.find('td')); - segmentContainer = $("
").appendTo(element); + daySegmentContainer = $("
").appendTo(element); }else{ // NOT first time, reuse as many cells as possible @@ -1181,7 +1423,7 @@ function Grid(element, options, methods, dayBind(tbody.find('td.fc-new').removeClass('fc-new')); // re-label and re-class existing cells - d = cloneDate(view.visStart); + d = cloneDate(t.visStart); tbody.find('td').each(function() { var td = $(this); if (rowCnt > 1) { @@ -1210,10 +1452,10 @@ function Grid(element, options, methods, if (rowCnt == 1) { // more changes likely (week or day view) // redo column header text and class - d = cloneDate(view.visStart); - thead.find('th').each(function() { - $(this).text(formatDate(d, colFormat, options)); - this.className = this.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); + d = cloneDate(t.visStart); + thead.find('th').each(function(i, th) { + $(th).text(formatDate(d, colFormat)); + th.className = th.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); addDays(d, 1); if (nwe) { skipWeekend(d); @@ -1221,9 +1463,9 @@ function Grid(element, options, methods, }); // redo cell day-of-weeks - d = cloneDate(view.visStart); - tbody.find('td').each(function() { - this.className = this.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); + d = cloneDate(t.visStart); + tbody.find('td').each(function(i, td) { + td.className = td.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); addDays(d, 1); if (nwe) { skipWeekend(d); @@ -1237,13 +1479,12 @@ function Grid(element, options, methods, } - function setHeight(height) { viewHeight = height; var leftTDs = tbody.find('tr td:first-child'), tbodyHeight = viewHeight - thead.height(), rowHeight1, rowHeight2; - if (options.weekMode == 'variable') { + if (opt('weekMode') == 'variable') { rowHeight1 = rowHeight2 = Math.floor(tbodyHeight / (rowCnt==1 ? 2 : 6)); }else{ rowHeight1 = Math.floor(tbodyHeight / rowCnt); @@ -1268,167 +1509,131 @@ function Grid(element, options, methods, function setWidth(width) { viewWidth = width; - dayContentPositions.clear(); - setOuterWidth( - thead.find('th').slice(0, -1), - colWidth = Math.floor(viewWidth / colCnt) - ); + colContentPositions.clear(); + colWidth = Math.floor(viewWidth / colCnt); + setOuterWidth(thead.find('th').slice(0, -1), colWidth); } - - /* Event Rendering - -----------------------------------------------------------------------------*/ + + /* Day clicking and binding + -----------------------------------------------------------*/ - function renderEvents(events) { - view.reportEvents(cachedEvents = events); - renderSegs(compileSegs(events)); + function dayBind(days) { + days.click(dayClick) + .mousedown(daySelectionMousedown); } - function rerenderEvents(modifiedEventId) { - clearEvents(); - renderSegs(compileSegs(cachedEvents), modifiedEventId); + function dayClick(ev) { + if (!opt('selectable')) { // SelectionManager will worry about dayClick + var n = parseInt(this.className.match(/fc\-day(\d+)/)[1]), + date = addDays( + cloneDate(t.visStart), + Math.floor(n/colCnt) * 7 + n % colCnt + ); + // TODO: what about weekends in middle of week? + trigger('dayClick', this, date, true, ev); + } } - function clearEvents() { - view._clearEvents(); // only clears the hashes - segmentContainer.empty(); - } + /* Semi-transparent Overlay Helpers + ------------------------------------------------------*/ - function compileSegs(events) { - var d1 = cloneDate(view.visStart), - d2 = addDays(cloneDate(d1), colCnt), - visEventsEnds = $.map(events, exclEndDay), - i, row, - j, level, - k, seg, - segs=[]; - for (i=0; i" + - "" + - (!event.allDay && seg.isStart ? - "" + - htmlEscape(formatDates(event.start, event.end, view.option('timeFormat'), options)) + - "" - :'') + - "" + htmlEscape(event.title) + "" + - "" + - ((event.editable || event.editable === undefined && options.editable) && !options.disableResizing && $.fn.resizable ? - "
" - : '') + - "
"; - seg.left = left; - seg.outerWidth = right - left; + + function clearEvents() { + reportEventClear(); + getDaySegmentContainer().empty(); } - segmentContainer[0].innerHTML = html; // faster than html() - eventElements = segmentContainer.children(); - // retrieve elements, run through eventRender callback, bind handlers - for (i=0; i div') // optimal selector? - .height(top + levelHeight); } + + +} + +fcViews.agendaWeek = AgendaWeekView; + +function AgendaWeekView(element, calendar) { + var t = this; - // calculate row tops - for (rowI=0; rowI" + - "
" + + "" + ""; for (i=0; i" + formatDate(d, colFormat, options) + ""; + "'>" + formatDate(d, colFormat) + ""; addDays(d, dis); if (nwe) { skipWeekend(d, dis); } } s += ""; - if (options.allDaySlot) { + if (opt('allDaySlot')) { s += "" + - "" + + "" + "" + "" + @@ -1922,10 +2108,10 @@ function Agenda(element, options, method s += ""; - addMinutes(d, options.slotMinutes); + addMinutes(d, opt('slotMinutes')); slotCnt++; } s += "
  
" + options.allDayText + "" + opt('allDayText') + "" + "
 
 
" + - ((!slotNormal || !minutes) ? formatDate(d, options.axisFormat) : ' ') + + ((!slotNormal || !minutes) ? formatDate(d, opt('axisFormat')) : ' ') + "
 
"; @@ -1962,9 +2148,9 @@ function Agenda(element, options, method clearEvents(); // redo column header text and class - head.find('tr:first th').slice(1, -1).each(function() { - $(this).text(formatDate(d, colFormat, options)); - this.className = this.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); + head.find('tr:first th').slice(1, -1).each(function(i, th) { + $(th).text(formatDate(d, colFormat)); + th.className = th.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); addDays(d, dis); if (nwe) { skipWeekend(d, dis); @@ -1973,15 +2159,15 @@ function Agenda(element, options, method // change classes of background stripes d = cloneDate(d0); - bg.find('td').each(function() { - this.className = this.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); + bg.find('td').each(function(i, td) { + td.className = td.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); if (+d == +today) { - $(this) + $(td) .removeClass('fc-not-today') .addClass('fc-today') .addClass(tm + '-state-highlight'); }else{ - $(this) + $(td) .addClass('fc-not-today') .removeClass('fc-today') .removeClass(tm + '-state-highlight'); @@ -1997,75 +2183,105 @@ function Agenda(element, options, method } - function resetScroll() { - var d0 = zeroDate(), - scrollDate = cloneDate(d0); - scrollDate.setHours(options.firstHour); - var top = timePosition(d0, scrollDate) + 1, // +1 for the border - scroll = function() { - body.scrollTop(top); - }; - scroll(); - setTimeout(scroll, 0); // overrides any previous scroll state made by the browser - } - function setHeight(height, dateChanged) { + + if (height === undefined) { + height = viewHeight; + } + viewHeight = height; slotTopCache = {}; - body.height(height - head.height()); + var bodyHeight = height - head.height(); + bodyHeight = Math.min(bodyHeight, bodyTable.height()); // shrink to fit table + body.height(bodyHeight); slotHeight = body.find('tr:first div').height() + 1; - bg.css({ - top: head.find('tr').height(), - height: height - }); - if (dateChanged) { resetScroll(); } } + function setWidth(width) { viewWidth = width; colContentPositions.clear(); - body.width(width); + body.width(width).css('overflow', 'auto'); bodyTable.width(''); var topTDs = head.find('tr:first th'), + allDayLastTH = head.find('tr.fc-all-day th:last'), stripeTDs = bg.find('td'), clientWidth = body[0].clientWidth; bodyTable.width(clientWidth); + clientWidth = body[0].clientWidth; // in ie6, sometimes previous clientWidth was wrongly reported + bodyTable.width(clientWidth); // time-axis width axisWidth = 0; setOuterWidth( head.find('tr:lt(2) th:first').add(body.find('tr:first th')) - .width('') + .width(1) .each(function() { axisWidth = Math.max(axisWidth, $(this).outerWidth()); }), axisWidth ); - // column width + // column width, except for last column colWidth = Math.floor((clientWidth - axisWidth) / colCnt); setOuterWidth(stripeTDs.slice(0, -1), colWidth); setOuterWidth(topTDs.slice(1, -2), colWidth); - setOuterWidth(topTDs.slice(-2, -1), clientWidth - axisWidth - colWidth*(colCnt-1)); + + // column width for last column + if (width != clientWidth) { // has scrollbar + setOuterWidth(topTDs.slice(-2, -1), clientWidth - axisWidth - colWidth*(colCnt-1)); + topTDs.slice(-1).show(); + allDayLastTH.show(); + }else{ + body.css('overflow', 'hidden'); + topTDs.slice(-2, -1).width(''); + topTDs.slice(-1).hide(); + allDayLastTH.hide(); + } bg.css({ + top: head.find('tr').height(), left: axisWidth, - width: clientWidth - axisWidth + width: clientWidth - axisWidth, + height: viewHeight }); } + function resetScroll() { + var d0 = zeroDate(), + scrollDate = cloneDate(d0); + scrollDate.setHours(opt('firstHour')); + var top = timePosition(d0, scrollDate) + 1, // +1 for the border + scroll = function() { + body.scrollTop(top); + }; + scroll(); + setTimeout(scroll, 0); // overrides any previous scroll state made by the browser + } + + + function beforeHide() { + savedScrollTop = body.scrollTop(); + } + + + function afterShow() { + body.scrollTop(savedScrollTop); + } + + /* Slot/Day clicking and binding -----------------------------------------------------------------------*/ @@ -2084,175 +2300,550 @@ function Agenda(element, options, method function slotClick(ev) { - if (!view.option('selectable')) { // SelectionManager will worry about dayClick + if (!opt('selectable')) { // SelectionManager will worry about dayClick var col = Math.min(colCnt-1, Math.floor((ev.pageX - bg.offset().left) / colWidth)), - date = addDays(cloneDate(view.visStart), col*dis+dit), + date = addDays(cloneDate(t.visStart), col*dis+dit), rowMatch = this.className.match(/fc-slot(\d+)/); if (rowMatch) { - var mins = parseInt(rowMatch[1]) * options.slotMinutes, + var mins = parseInt(rowMatch[1]) * opt('slotMinutes'), hours = Math.floor(mins/60); date.setHours(hours); date.setMinutes(mins%60 + minMinute); - view.trigger('dayClick', this, date, false, ev); + trigger('dayClick', this, date, false, ev); }else{ - view.trigger('dayClick', this, date, true, ev); + trigger('dayClick', this, date, true, ev); } } } - /* Event Rendering - -----------------------------------------------------------------------------*/ + /* Semi-transparent Overlay Helpers + -----------------------------------------------------*/ - function renderEvents(events, modifiedEventId) { - view.reportEvents(cachedEvents = events); - var i, len=events.length, - dayEvents=[], - slotEvents=[]; - for (i=0; i= addMinutes(cloneDate(day), maxMinute)) { + return bodyContent.height(); + } + var slotMinutes = opt('slotMinutes'), + minutes = time.getHours()*60 + time.getMinutes() - minMinute, + slotI = Math.floor(minutes / slotMinutes), + slotTop = slotTopCache[slotI]; + if (slotTop === undefined) { + slotTop = slotTopCache[slotI] = body.find('tr:eq(' + slotI + ') td div')[0].offsetTop; + } + return Math.max(0, Math.round( + slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes) + )); } - function compileSlotSegs(events) { - var d = addMinutes(cloneDate(view.visStart), minMinute), - visEventEnds = $.map(events, slotEventEnd), - i, col, - j, level, - k, seg, - segs=[]; - for (i=0; i= 0) { + addMinutes(d, minMinute + slotIndex*opt('slotMinutes')); + } + return d; + } + + + function cellIsAllDay(cell) { + return opt('allDaySlot') && !cell.row; } + function allDayBounds() { + return { + left: axisWidth, + right: viewWidth + } + } + + function allDayTR(index) { + return head.find('tr.fc-all-day'); + } - // renders 'all-day' events at the top - function renderDaySegs(segs, modifiedEventId) { - if (options.allDaySlot) { - _renderDaySegs( - segs, - 1, - view, - axisWidth, - viewWidth, - function() { - return head.find('tr.fc-all-day'); - }, - function(dayOfWeek) { - return axisWidth + colContentPositions.left(dayOfWeekCol(dayOfWeek)); - }, - function(dayOfWeek) { - return axisWidth + colContentPositions.right(dayOfWeekCol(dayOfWeek)); - }, - daySegmentContainer, - daySegBind, - modifiedEventId - ); - setHeight(viewHeight); // might have pushed the body down, so resize + function defaultEventEnd(event) { + var start = cloneDate(event.start); + if (event.allDay) { + return start; } + return addMinutes(start, opt('defaultEventMinutes')); } - // renders events in the 'time slots' at the bottom + /* Selection + ---------------------------------------------------------------------------------*/ - function renderSlotSegs(segs, modifiedEventId) { - var i, segCnt=segs.length, seg, - event, - className, - top, bottom, - colI, levelI, forward, - leftmost, - availWidth, - outerWidth, - left, - html='', - eventElements, - eventElement, - triggerRes, - vsideCache={}, - hsideCache={}, - key, val, - titleSpan, - height; - - // calculate position/dimensions, create html - for (i=0; i= 0 && col < colCnt) { // only works when times are on same day + var rect = coordinateGrid.rect(0, col, 0, col, bodyContent); // only for horizontal coords + var top = timePosition(startDate, startDate); + var bottom = timePosition(startDate, endDate); + if (bottom > top) { // protect against selections that are entirely before or after visible range + rect.top = top; + rect.height = bottom - top; + rect.left += 2; + rect.width -= 5; + if ($.isFunction(helperOption)) { + var helperRes = helperOption(startDate, endDate); + if (helperRes) { + rect.position = 'absolute'; + rect.zIndex = 8; + selectionHelper = $(helperRes) + .css(rect) + .appendTo(bodyContent); + } + }else{ + selectionHelper = $(slotSegHtml( + { + title: '', + start: startDate, + end: endDate, + className: [], + editable: false + }, + rect, + 'fc-event fc-event-vert fc-corner-top fc-corner-bottom ' + )); + if ($.browser.msie) { + selectionHelper.find('span.fc-event-bg').hide(); // nested opacities mess up in IE, just hide + } + selectionHelper.css('opacity', opt('dragOpacity')); + } + if (selectionHelper) { + slotBind(selectionHelper); + bodyContent.append(selectionHelper); + setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended + setOuterHeight(selectionHelper, rect.height, true); + } + } + } + }else{ + renderSlotOverlay(startDate, endDate); + } + } + + + function clearSelection() { + clearOverlays(); + if (selectionHelper) { + selectionHelper.remove(); + selectionHelper = null; + } + } + + + function slotSelectionMousedown(ev) { + if (ev.which == 1 && opt('selectable')) { // ev.which==1 means left mouse button + unselect(ev); + var _mousedownElement = this; + var dates; + hoverListener.start(function(cell, origCell) { + clearSelection(); + if (cell && cell.col == origCell.col && !cellIsAllDay(cell)) { + var d1 = cellDate(origCell); + var d2 = cellDate(cell); + dates = [ + d1, + addMinutes(cloneDate(d1), opt('slotMinutes')), + d2, + addMinutes(cloneDate(d2), opt('slotMinutes')) + ].sort(cmp); + renderSlotSelection(dates[0], dates[3]); + }else{ + dates = null; + } + }, ev); + $(document).one('mouseup', function(ev) { + hoverListener.stop(); + if (dates) { + if (+dates[0] == +dates[1]) { + trigger('dayClick', _mousedownElement, dates[0], false, ev); + // BUG: _mousedownElement will sometimes be the overlay + } + reportSelection(dates[0], dates[3], false, ev); + } + }); + } + } + + + + /* External Dragging + --------------------------------------------------------------------------------*/ + + + function dragStart(_dragElement, ev, ui) { + hoverListener.start(function(cell) { + clearOverlays(); + if (cell) { + if (cellIsAllDay(cell)) { + renderCellOverlay(cell.row, cell.col, cell.row, cell.col); + }else{ + var d1 = cellDate(cell); + var d2 = addMinutes(cloneDate(d1), opt('defaultEventMinutes')); + renderSlotOverlay(d1, d2); + } + } + }, ev); + } + + + function dragStop(_dragElement, ev, ui) { + var cell = hoverListener.stop(); + clearOverlays(); + if (cell) { + trigger('drop', _dragElement, cellDate(cell), cellIsAllDay(cell), ev, ui); + } + } + + +} + +function AgendaEventRenderer() { + var t = this; + + + // exports + t.renderEvents = renderEvents; + t.compileDaySegs = compileDaySegs; // for DayEventRenderer + t.clearEvents = clearEvents; + t.slotSegHtml = slotSegHtml; + t.bindDaySeg = bindDaySeg; + + + // imports + DayEventRenderer.call(t); + var opt = t.opt; + var trigger = t.trigger; + var eventEnd = t.eventEnd; + var reportEvents = t.reportEvents; + var reportEventClear = t.reportEventClear; + var eventElementHandlers = t.eventElementHandlers; + var setHeight = t.setHeight; + var getDaySegmentContainer = t.getDaySegmentContainer; + var getSlotSegmentContainer = t.getSlotSegmentContainer; + var getHoverListener = t.getHoverListener; + var getMaxMinute = t.getMaxMinute; + var getMinMinute = t.getMinMinute; + var timePosition = t.timePosition; + var colContentLeft = t.colContentLeft; + var colContentRight = t.colContentRight; + var renderDaySegs = t.renderDaySegs; + var resizableDayEvent = t.resizableDayEvent; // TODO: streamline binding architecture + var getColCnt = t.getColCnt; + var getColWidth = t.getColWidth; + var getSlotHeight = t.getSlotHeight; + var getBodyContent = t.getBodyContent; + var reportEventElement = t.reportEventElement; + var showEvents = t.showEvents; + var hideEvents = t.hideEvents; + var eventDrop = t.eventDrop; + var eventResize = t.eventResize; + var renderDayOverlay = t.renderDayOverlay; + var clearOverlays = t.clearOverlays; + var calendar = t.calendar; + var formatDate = calendar.formatDate; + var formatDates = calendar.formatDates; + + + + /* Rendering + ----------------------------------------------------------------------------*/ + + + function renderEvents(events, modifiedEventId) { + reportEvents(events); + var i, len=events.length, + dayEvents=[], + slotEvents=[]; + for (i=0; i" + "" + "" + - "" + htmlEscape(formatDates(event.start, event.end, view.option('timeFormat'))) + "" + + "" + htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) + "" + "" + htmlEscape(event.title) + "" + "" + - ((event.editable || event.editable === undefined && options.editable) && !options.disableResizing && $.fn.resizable ? + ((event.editable || event.editable === undefined && opt('editable')) && !opt('disableResizing') && $.fn.resizable ? "
=
" : '') + "
"; } - - function daySegBind(event, eventElement, seg) { - view.eventElementHandlers(event, eventElement); - if (event.editable || event.editable === undefined && options.editable) { + function bindDaySeg(event, eventElement, seg) { + eventElementHandlers(event, eventElement); + if (event.editable || event.editable === undefined && opt('editable')) { draggableDayEvent(event, eventElement, seg.isStart); if (seg.isEnd) { - view.resizableDayEvent(event, eventElement, colWidth); + resizableDayEvent(event, eventElement, seg); } } } - - function slotSegBind(event, eventElement, seg) { - view.eventElementHandlers(event, eventElement); - if (event.editable || event.editable === undefined && options.editable) { + function bindSlotSeg(event, eventElement, seg) { + eventElementHandlers(event, eventElement); + if (event.editable || event.editable === undefined && opt('editable')) { var timeElement = eventElement.find('span.fc-event-time'); draggableSlotEvent(event, eventElement, timeElement); if (seg.isEnd) { @@ -2381,33 +2972,36 @@ function Agenda(element, options, method } } } - - - /* Event Dragging - -----------------------------------------------------------------------------*/ + /* Dragging + -----------------------------------------------------------------------------------*/ // when event starts out FULL-DAY function draggableDayEvent(event, eventElement, isStart) { - if (!options.disableDragging && eventElement.draggable) { + if (!opt('disableDragging') && eventElement.draggable) { var origWidth; var allDay=true; var dayDelta; + var dis = opt('isRTL') ? -1 : 1; + var hoverListener = getHoverListener(); + var colWidth = getColWidth(); + var slotHeight = getSlotHeight(); + var minMinute = getMinMinute(); eventElement.draggable({ zIndex: 9, - opacity: view.option('dragOpacity', 'month'), // use whatever the month view was using - revertDuration: options.dragRevertDuration, + opacity: opt('dragOpacity', 'month'), // use whatever the month view was using + revertDuration: opt('dragRevertDuration'), start: function(ev, ui) { - view.trigger('eventDragStart', eventElement, event, ev, ui); - view.hideEvents(event, eventElement); + trigger('eventDragStart', eventElement, event, ev, ui); + hideEvents(event, eventElement); origWidth = eventElement.width(); hoverListener.start(function(cell, origCell, rowDelta, colDelta) { eventElement.draggable('option', 'revert', !cell || !rowDelta && !colDelta); - clearOverlay(); + clearOverlays(); if (cell) { dayDelta = colDelta * dis; if (!cell.row) { @@ -2424,8 +3018,8 @@ function Agenda(element, options, method setOuterHeight( eventElement.width(colWidth - 10), // don't use entire width slotHeight * Math.round( - (event.end ? ((event.end - event.start) / MINUTE_MS) : options.defaultEventMinutes) - / options.slotMinutes + (event.end ? ((event.end - event.start) / MINUTE_MS) : opt('defaultEventMinutes')) + / opt('slotMinutes') ) ); eventElement.draggable('option', 'grid', [colWidth, 1]); @@ -2437,26 +3031,26 @@ function Agenda(element, options, method }, stop: function(ev, ui) { var cell = hoverListener.stop(); - clearOverlay(); - view.trigger('eventDragStop', eventElement, event, ev, ui); + clearOverlays(); + trigger('eventDragStop', eventElement, event, ev, ui); if (cell && (!allDay || dayDelta)) { // changed! eventElement.find('a').removeAttr('href'); // prevents safari from visiting the link var minuteDelta = 0; if (!allDay) { - minuteDelta = Math.round((eventElement.offset().top - bodyContent.offset().top) / slotHeight) - * options.slotMinutes + minuteDelta = Math.round((eventElement.offset().top - getBodyContent().offset().top) / slotHeight) + * opt('slotMinutes') + minMinute - (event.start.getHours() * 60 + event.start.getMinutes()); } - view.eventDrop(this, event, dayDelta, minuteDelta, allDay, ev, ui); + eventDrop(this, event, dayDelta, minuteDelta, allDay, ev, ui); }else{ // hasn't moved or is out of bounds (draggable has already reverted) resetElement(); if ($.browser.msie) { eventElement.css('filter', ''); // clear IE opacity side-effects } - view.showEvents(event, eventElement); + showEvents(event, eventElement); } } }); @@ -2473,26 +3067,30 @@ function Agenda(element, options, method } - // when event starts out IN TIMESLOTS function draggableSlotEvent(event, eventElement, timeElement) { - if (!options.disableDragging && eventElement.draggable) { + if (!opt('disableDragging') && eventElement.draggable) { var origPosition; var allDay=false; var dayDelta; var minuteDelta; var prevMinuteDelta; + var dis = opt('isRTL') ? -1 : 1; + var hoverListener = getHoverListener(); + var colCnt = getColCnt(); + var colWidth = getColWidth(); + var slotHeight = getSlotHeight(); eventElement.draggable({ zIndex: 9, scroll: false, grid: [colWidth, slotHeight], axis: colCnt==1 ? 'y' : false, - opacity: view.option('dragOpacity'), - revertDuration: options.dragRevertDuration, + opacity: opt('dragOpacity'), + revertDuration: opt('dragRevertDuration'), start: function(ev, ui) { - view.trigger('eventDragStart', eventElement, event, ev, ui); - view.hideEvents(event, eventElement); + trigger('eventDragStart', eventElement, event, ev, ui); + hideEvents(event, eventElement); if ($.browser.msie) { eventElement.find('span.fc-event-bg').hide(); // nested opacities mess up in IE, just hide } @@ -2500,10 +3098,10 @@ function Agenda(element, options, method minuteDelta = prevMinuteDelta = 0; hoverListener.start(function(cell, origCell, rowDelta, colDelta) { eventElement.draggable('option', 'revert', !cell); - clearOverlay(); + clearOverlays(); if (cell) { dayDelta = colDelta * dis; - if (options.allDaySlot && !cell.row) { + if (opt('allDaySlot') && !cell.row) { // over full days if (!allDay) { // convert to temporary all-day event @@ -2523,7 +3121,7 @@ function Agenda(element, options, method }, ev, 'drag'); }, drag: function(ev, ui) { - minuteDelta = Math.round((ui.position.top - origPosition.top) / slotHeight) * options.slotMinutes; + minuteDelta = Math.round((ui.position.top - origPosition.top) / slotHeight) * opt('slotMinutes'); if (minuteDelta != prevMinuteDelta) { if (!allDay) { updateTimeText(minuteDelta); @@ -2533,11 +3131,11 @@ function Agenda(element, options, method }, stop: function(ev, ui) { var cell = hoverListener.stop(); - clearOverlay(); - view.trigger('eventDragStop', eventElement, event, ev, ui); + clearOverlays(); + trigger('eventDragStop', eventElement, event, ev, ui); if (cell && (dayDelta || minuteDelta || allDay)) { // changed! - view.eventDrop(this, event, dayDelta, allDay ? 0 : minuteDelta, allDay, ev, ui); + eventDrop(this, event, dayDelta, allDay ? 0 : minuteDelta, allDay, ev, ui); }else{ // either no change or out-of-bounds (draggable has already reverted) resetElement(); @@ -2549,7 +3147,7 @@ function Agenda(element, options, method .find('span.fc-event-bg') .css('display', ''); // .show() made display=inline } - view.showEvents(event, eventElement); + showEvents(event, eventElement); } } }); @@ -2559,7 +3157,7 @@ function Agenda(element, options, method if (event.end) { newEnd = addMinutes(cloneDate(event.end), minuteDelta); } - timeElement.text(formatDates(newStart, newEnd, view.option('timeFormat'))); + timeElement.text(formatDates(newStart, newEnd, opt('timeFormat'))); } function resetElement() { // convert back to original slot-event @@ -2574,15 +3172,14 @@ function Agenda(element, options, method + /* Resizing + --------------------------------------------------------------------------------------*/ - /* Event Resizing - -----------------------------------------------------------------------------*/ - // for TIMESLOT events - function resizableSlotEvent(event, eventElement, timeElement) { - if (!options.disableResizing && eventElement.resizable) { + if (!opt('disableResizing') && eventElement.resizable) { var slotDelta, prevSlotDelta; + var slotHeight = getSlotHeight(); eventElement.resizable({ handles: { s: 'div.ui-resizable-s' @@ -2590,375 +3187,46 @@ function Agenda(element, options, method grid: slotHeight, start: function(ev, ui) { slotDelta = prevSlotDelta = 0; - view.hideEvents(event, eventElement); + hideEvents(event, eventElement); if ($.browser.msie && $.browser.version == '6.0') { eventElement.css('overflow', 'hidden'); } eventElement.css('z-index', 9); - view.trigger('eventResizeStart', this, event, ev, ui); + trigger('eventResizeStart', this, event, ev, ui); }, - resize: function(ev, ui) { - // don't rely on ui.size.height, doesn't take grid into account - slotDelta = Math.round((Math.max(slotHeight, eventElement.height()) - ui.originalSize.height) / slotHeight); - if (slotDelta != prevSlotDelta) { - timeElement.text( - formatDates( - event.start, - (!slotDelta && !event.end) ? null : // no change, so don't display time range - addMinutes(view.eventEnd(event), options.slotMinutes*slotDelta), - view.option('timeFormat') - ) - ); - prevSlotDelta = slotDelta; - } - }, - stop: function(ev, ui) { - view.trigger('eventResizeStop', this, event, ev, ui); - if (slotDelta) { - view.eventResize(this, event, 0, options.slotMinutes*slotDelta, ev, ui); - }else{ - eventElement.css('z-index', 8); - view.showEvents(event, eventElement); - // BUG: if event was really short, need to put title back in span - } - } - }); - } - } - - - - - /* Coordinate Utilities - -----------------------------------------------------------------------------*/ - - var coordinateGrid = new CoordinateGrid(function(rows, cols) { - var e, n, p; - bg.find('td').each(function(i, _e) { - e = $(_e); - n = e.offset().left; - if (i) { - p[1] = n; - } - p = [n]; - cols[i] = p; - }); - p[1] = n + e.outerWidth(); - if (options.allDaySlot) { - e = head.find('td'); - n = e.offset().top; - rows[0] = [n, n+e.outerHeight()]; - } - var bodyContentTop = bodyContent.offset().top; - var bodyTop = body.offset().top; - var bodyBottom = bodyTop + body.outerHeight(); - function constrain(n) { - return Math.max(bodyTop, Math.min(bodyBottom, n)); - } - for (var i=0; i= addMinutes(cloneDate(day), maxMinute)) { - return bodyContent.height(); - } - var slotMinutes = options.slotMinutes, - minutes = time.getHours()*60 + time.getMinutes() - minMinute, - slotI = Math.floor(minutes / slotMinutes), - slotTop = slotTopCache[slotI]; - if (slotTop === undefined) { - slotTop = slotTopCache[slotI] = body.find('tr:eq(' + slotI + ') td div')[0].offsetTop; - } - return Math.max(0, Math.round( - slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes) - )); - } - - - - - /* Selecting - -----------------------------------------------------------------------------*/ - - var selected = false; - var daySelectionMousedown = selection_dayMousedown( - view, hoverListener, cellDate, cellIsAllDay, renderDayOverlay, clearOverlay, reportSelection, unselect - ); - - function slotSelectionMousedown(ev) { - if (view.option('selectable')) { - unselect(ev); - var _mousedownElement = this; - var dates; - hoverListener.start(function(cell, origCell) { - clearSelection(); - if (cell && cell.col == origCell.col && !cellIsAllDay(cell)) { - var d1 = cellDate(origCell); - var d2 = cellDate(cell); - dates = [ - d1, - addMinutes(cloneDate(d1), options.slotMinutes), - d2, - addMinutes(cloneDate(d2), options.slotMinutes) - ].sort(cmp); - renderSlotSelection(dates[0], dates[3]); - }else{ - dates = null; - } - }, ev); - $(document).one('mouseup', function(ev) { - hoverListener.stop(); - if (dates) { - if (+dates[0] == +dates[1]) { - view.trigger('dayClick', _mousedownElement, dates[0], false, ev); - // BUG: _mousedownElement will sometimes be the overlay - } - reportSelection(dates[0], dates[3], false, ev); - } - }); - } - } - - view.select = function(startDate, endDate, allDay) { - coordinateGrid.build(); - unselect(); - if (allDay) { - if (options.allDaySlot) { - if (!endDate) { - endDate = cloneDate(startDate); - } - renderDayOverlay(startDate, addDays(cloneDate(endDate), 1)); - } - }else{ - if (!endDate) { - endDate = addMinutes(cloneDate(startDate), options.slotMinutes); - } - renderSlotSelection(startDate, endDate); - } - reportSelection(startDate, endDate, allDay); - }; - - function reportSelection(startDate, endDate, allDay, ev) { - selected = true; - view.trigger('select', view, startDate, endDate, allDay, ev); - } - - function unselect(ev) { - if (selected) { - clearSelection(); - selected = false; - view.trigger('unselect', view, ev); - } - } - view.unselect = unselect; - - selection_unselectAuto(view, unselect); - - - - - /* Selecting drawing utils - -----------------------------------------------------------------------------*/ - - var selectionHelper; - - function renderSlotSelection(startDate, endDate) { - var helperOption = view.option('selectHelper'); - if (helperOption) { - var col = dayDiff(startDate, view.visStart) * dis + dit; - if (col >= 0 && col < colCnt) { // only works when times are on same day - var rect = coordinateGrid.rect(0, col, 0, col, bodyContent); // only for horizontal coords - var top = timePosition(startDate, startDate); - var bottom = timePosition(startDate, endDate); - if (bottom > top) { // protect against selections that are entirely before or after visible range - rect.top = top; - rect.height = bottom - top; - rect.left += 2; - rect.width -= 5; - if ($.isFunction(helperOption)) { - var helperRes = helperOption(startDate, endDate); - if (helperRes) { - rect.position = 'absolute'; - rect.zIndex = 8; - selectionHelper = $(helperRes) - .css(rect) - .appendTo(bodyContent); - } - }else{ - selectionHelper = $(slotSegHtml( - { - title: '', - start: startDate, - end: endDate, - className: [], - editable: false - }, - rect, - 'fc-event fc-event-vert fc-corner-top fc-corner-bottom ' - )); - if ($.browser.msie) { - selectionHelper.find('span.fc-event-bg').hide(); // nested opacities mess up in IE, just hide - } - selectionHelper.css('opacity', view.option('dragOpacity')); - } - if (selectionHelper) { - slotBind(selectionHelper); - bodyContent.append(selectionHelper); - setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended - setOuterHeight(selectionHelper, rect.height, true); - } - } - } - }else{ - renderSlotOverlay(startDate, endDate); - } - } - - function clearSelection() { - clearOverlay(); - if (selectionHelper) { - selectionHelper.remove(); - selectionHelper = null; - } - } - - - - - /* Semi-transparent Overlay Helpers - -----------------------------------------------------*/ - - function renderDayOverlay(startDate, endDate) { - var startCol, endCol; - if (rtl) { - startCol = dayDiff(endDate, view.visStart)*dis+dit+1; - endCol = dayDiff(startDate, view.visStart)*dis+dit+1; - }else{ - startCol = dayDiff(startDate, view.visStart); - endCol = dayDiff(endDate, view.visStart); - } - startCol = Math.max(0, startCol); - endCol = Math.min(colCnt, endCol); - if (startCol < endCol) { - dayBind( - _renderDayOverlay(0, startCol, 0, endCol-1) - ); - } - } - - function _renderDayOverlay(col0, row0, col1, row1) { - var rect = coordinateGrid.rect(col0, row0, col1, row1, head); - return view.renderOverlay(rect, head); - } - - function renderSlotOverlay(overlayStart, overlayEnd) { - var dayStart = cloneDate(view.visStart); - var dayEnd = addDays(cloneDate(dayStart), 1); - for (var i=0; i= 0) { - addMinutes(d, minMinute + slotIndex*options.slotMinutes); + resize: function(ev, ui) { + // don't rely on ui.size.height, doesn't take grid into account + slotDelta = Math.round((Math.max(slotHeight, eventElement.height()) - ui.originalSize.height) / slotHeight); + if (slotDelta != prevSlotDelta) { + timeElement.text( + formatDates( + event.start, + (!slotDelta && !event.end) ? null : // no change, so don't display time range + addMinutes(eventEnd(event), opt('slotMinutes')*slotDelta), + opt('timeFormat') + ) + ); + prevSlotDelta = slotDelta; + } + }, + stop: function(ev, ui) { + trigger('eventResizeStop', this, event, ev, ui); + if (slotDelta) { + eventResize(this, event, 0, opt('slotMinutes')*slotDelta, ev, ui); + }else{ + eventElement.css('z-index', 8); + showEvents(event, eventElement); + // BUG: if event was really short, need to put title back in span + } + } + }); } - return d; - } - - function cellIsAllDay(cell) { - return options.allDaySlot && !cell.row; } - } -// count the number of colliding, higher-level segments (for event squishing) - function countForwardSegs(levels) { var i, j, k, level, segForward, segBack; for (i=levels.length-1; i>0; i--) { @@ -2976,67 +3244,72 @@ function countForwardSegs(levels) { } -/* Methods & Utilities for All Views ------------------------------------------------------------------------------*/ -var viewMethods = { - /* - * Objects inheriting these methods must implement the following properties/methods: - * - title - * - start - * - end - * - visStart - * - visEnd - * - defaultEventEnd(event) - * - render(events) - * - rerenderEvents() - * - * - * z-index reservations: - * 3 - day-overlay - * 8 - events - * 9 - dragging/resizing events - * - */ - +function View(element, calendar, viewName) { + var t = this; - - init: function(element, options) { - this.element = element; - this.options = options; - this.eventsByID = {}; - this.eventElements = []; - this.eventElementsByID = {}; - this.usedOverlays = []; - this.unusedOverlays = []; - }, + // exports + t.element = element; + t.calendar = calendar; + t.name = viewName; + t.opt = opt; + t.trigger = trigger; + t.reportEvents = reportEvents; + t.eventEnd = eventEnd; + t.reportEventElement = reportEventElement; + t.reportEventClear = reportEventClear; + t.eventElementHandlers = eventElementHandlers; + t.showEvents = showEvents; + t.hideEvents = hideEvents; + t.eventDrop = eventDrop; + t.eventResize = eventResize; + // t.title + // t.start, t.end + // t.visStart, t.visEnd + + + // imports + var defaultEventEnd = t.defaultEventEnd; + var normalizeEvent = calendar.normalizeEvent; // in EventManager + var reportEventChange = calendar.reportEventChange; + + + // locals + var eventsByID = {}; + var eventElements = []; + var eventElementsByID = {}; + var options = calendar.options; - // triggers an event handler, always append view as last arg - trigger: function(name, thisObj) { - if (this.options[name]) { - return this.options[name].apply(thisObj || this, Array.prototype.slice.call(arguments, 2).concat([this])); + function opt(name, viewNameOverride) { + var v = options[name]; + if (typeof v == 'object') { + return smartProperty(v, viewNameOverride || viewName); } - }, - + return v; + } + + function trigger(name, thisObj) { + return calendar.trigger.apply( + calendar, + [name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t]) + ); + } - // returns a Date object for an event's end - eventEnd: function(event) { - return event.end ? cloneDate(event.end) : this.defaultEventEnd(event); // TODO: make sure always using copies - }, + /* Event Data + ------------------------------------------------------------------------------*/ // report when view receives new events - - reportEvents: function(events) { // events are already normalized at this point - var i, len=events.length, event, - eventsByID = this.eventsByID = {}; + function reportEvents(events) { // events are already normalized at this point + eventsByID = {}; + var i, len=events.length, event; for (i=0; i"); - } - if (e[0].parentNode != parent[0]) { - e.appendTo(parent); - } - this.usedOverlays.push(e.css(rect).show()); - return e; - }, - clearOverlays: function() { - var e; - while (e = this.usedOverlays.shift()) { - this.unusedOverlays.push(e.hide().unbind()); - } - }, +} + +function DayEventRenderer() { + var t = this; + + // exports + t.renderDaySegs = renderDaySegs; + t.resizableDayEvent = resizableDayEvent; + + + // imports + var opt = t.opt; + var trigger = t.trigger; + var eventEnd = t.eventEnd; + var reportEventElement = t.reportEventElement; + var showEvents = t.showEvents; + var hideEvents = t.hideEvents; + var eventResize = t.eventResize; + var getRowCnt = t.getRowCnt; + var getColCnt = t.getColCnt; + var getColWidth = t.getColWidth; + var allDayTR = t.allDayTR; + var allDayBounds = t.allDayBounds; + var colContentLeft = t.colContentLeft; + var colContentRight = t.colContentRight; + var dayOfWeekCol = t.dayOfWeekCol; + var dateCell = t.dateCell; + var compileDaySegs = t.compileDaySegs; + var getDaySegmentContainer = t.getDaySegmentContainer; + var bindDaySeg = t.bindDaySeg; //TODO: streamline this + var formatDates = t.calendar.formatDates; + var renderDayOverlay = t.renderDayOverlay; + var clearOverlays = t.clearOverlays; + var clearSelection = t.clearSelection; - // common horizontal event resizing - - resizableDayEvent: function(event, eventElement, colWidth) { - var view = this; - if (!view.options.disableResizing && eventElement.resizable) { - eventElement.resizable({ - handles: view.options.isRTL ? {w:'div.ui-resizable-w'} : {e:'div.ui-resizable-e'}, - grid: colWidth, - minWidth: colWidth/2, // need this or else IE throws errors when too small - containment: view.element.parent().parent(), // the main element... - // ... a fix. wouldn't allow extending to last column in agenda views (jq ui bug?) - start: function(ev, ui) { - eventElement.css('z-index', 9); - view.hideEvents(event, eventElement); - view.trigger('eventResizeStart', this, event, ev, ui); - }, - stop: function(ev, ui) { - view.trigger('eventResizeStop', this, event, ev, ui); - // ui.size.width wasn't working with grid correctly, use .width() - var dayDelta = Math.round((eventElement.width() - ui.originalSize.width) / colWidth); - if (dayDelta) { - view.eventResize(this, event, dayDelta, 0, ev, ui); - }else{ - eventElement.css('z-index', 8); - view.showEvents(event, eventElement); - } - } - }); - } - }, + /* Rendering + -----------------------------------------------------------------------------*/ + function renderDaySegs(segs, modifiedEventId) { + var segmentContainer = getDaySegmentContainer(); + var rowDivs; + var rowCnt = getRowCnt(); + var colCnt = getColCnt(); + var i = 0; + var rowI; + var levelI; + var colHeights; + var j; + var segCnt = segs.length; + var seg; + var top; + var k; + segmentContainer[0].innerHTML = daySegHTML(segs); // faster than .html() + daySegElementResolve(segs, segmentContainer.children()); + daySegElementReport(segs); + daySegHandlers(segs, segmentContainer, modifiedEventId); + daySegCalcHSides(segs); + daySegSetWidths(segs); + daySegCalcHeights(segs); + rowDivs = getRowDivs(); + // set row heights, calculate event tops (in relation to row top) + for (rowI=0; rowI"); + var elements; + var segmentContainer = getDaySegmentContainer(); + var i; + var segCnt = segs.length; + var element; + tempContainer[0].innerHTML = daySegHTML(segs); // faster than .html() + elements = tempContainer.children(); + segmentContainer.append(elements); + daySegElementResolve(segs, elements); + daySegCalcHSides(segs); + daySegSetWidths(segs); + daySegCalcHeights(segs); + daySegSetTops(segs, getRowTops(getRowDivs())); + elements = []; + for (i=0; i" + + "" + + (!event.allDay && seg.isStart ? + "" + + htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) + + "" + :'') + + "" + htmlEscape(event.title) + "" + + "" + + (seg.isEnd && (event.editable || event.editable === undefined && opt('editable')) && !opt('disableResizing') ? + "
" + : '') + + "
"; + seg.left = left; + seg.outerWidth = right - left; + cols.sort(cmp); + seg.startCol = cols[0]; + seg.endCol = cols[1] + 1; + } + return html; + } - // get a property from the 'options' object, using smart view naming - option: function(name, viewName) { - var v = this.options[name]; - if (typeof v == 'object') { - return smartProperty(v, viewName || this.name); + function daySegElementResolve(segs, elements) { // sets seg.element + var i; + var segCnt = segs.length; + var seg; + var event; + var element; + var triggerRes; + for (i=0; i start && eventStart < end) { - if (eventStart < start) { - segStart = cloneDate(start); - isStart = false; - }else{ - segStart = eventStart; - isStart = true; - } - if (eventEnd > end) { - segEnd = cloneDate(end); - isEnd = false; + function daySegHandlers(segs, segmentContainer, modifiedEventId) { + var i; + var segCnt = segs.length; + var seg; + var element; + var event; + // retrieve elements, run through eventRender callback, bind handlers + for (i=0; i div'); // optimal selector? + } + return rowDivs; + } + + + function getRowTops(rowDivs) { + var i; + var rowCnt = rowDivs.length; + var tops = []; + for (i=0; i seg2.start && seg1.start < seg2.end; -} +//BUG: unselect needs to be triggered when events are dragged+dropped +function SelectionManager() { + var t = this; + + + // exports + t.select = select; + t.unselect = unselect; + t.reportSelection = reportSelection; + t.daySelectionMousedown = daySelectionMousedown; + + + // imports + var opt = t.opt; + var trigger = t.trigger; + var defaultSelectionEnd = t.defaultSelectionEnd; + var renderSelection = t.renderSelection; + var clearSelection = t.clearSelection; + + + // locals + var selected = false; + // unselectAuto + if (opt('selectable') && opt('unselectAuto')) { + $(document).mousedown(function(ev) { + var ignore = opt('unselectCancel'); + if (ignore) { + if ($(ev.target).parents(ignore).length) { // could be optimized to stop after first match + return; + } + } + unselect(ev); + }); + } + -function selection_dayMousedown(view, hoverListener, cellDate, cellIsAllDay, renderSelection, clearSelection, reportSelection, unselect) { - return function(ev) { - if (view.option('selectable')) { + function select(startDate, endDate, allDay) { + unselect(); + if (!endDate) { + endDate = defaultSelectionEnd(startDate, allDay); + } + renderSelection(startDate, endDate, allDay); + reportSelection(startDate, endDate, allDay); + } + + + function unselect(ev) { + if (selected) { + selected = false; + clearSelection(); + trigger('unselect', null, ev); + } + } + + + function reportSelection(startDate, endDate, allDay, ev) { + selected = true; + trigger('select', null, startDate, endDate, allDay, ev); + } + + + function daySelectionMousedown(ev) { // not really a generic manager method, oh well + var cellDate = t.cellDate; + var cellIsAllDay = t.cellIsAllDay; + var hoverListener = t.getHoverListener(); + if (ev.which == 1 && opt('selectable')) { // which==1 means left mouse button unselect(ev); var _mousedownElement = this; var dates; - hoverListener.start(function(cell, origCell) { + hoverListener.start(function(cell, origCell) { // TODO: maybe put cellDate/cellIsAllDay info in cell clearSelection(); if (cell && cellIsAllDay(cell)) { dates = [ cellDate(origCell), cellDate(cell) ].sort(cmp); - renderSelection(dates[0], addDays(cloneDate(dates[1]), 1), true); + renderSelection(dates[0], dates[1], true); }else{ dates = null; } @@ -3371,7 +3993,7 @@ function selection_dayMousedown(view, ho hoverListener.stop(); if (dates) { if (+dates[0] == +dates[1]) { - view.trigger('dayClick', _mousedownElement, dates[0], true, ev); + trigger('dayClick', _mousedownElement, dates[0], true, ev); // BUG: _mousedownElement will sometimes be the overlay } reportSelection(dates[0], dates[1], true, ev); @@ -3379,29 +4001,180 @@ function selection_dayMousedown(view, ho }); } } + + +} + +function OverlayManager() { + var t = this; + + + // exports + t.renderOverlay = renderOverlay; + t.clearOverlays = clearOverlays; + + + // locals + var usedOverlays = []; + var unusedOverlays = []; + + + function renderOverlay(rect, parent) { + var e = unusedOverlays.shift(); + if (!e) { + e = $("
"); + } + if (e[0].parentNode != parent[0]) { + e.appendTo(parent); + } + usedOverlays.push(e.css(rect).show()); + return e; + } + + + function clearOverlays() { + var e; + while (e = usedOverlays.shift()) { + unusedOverlays.push(e.hide().unbind()); + } + } + + } +function CoordinateGrid(buildFunc) { -function selection_unselectAuto(view, unselect) { - if (view.option('selectable') && view.option('unselectAuto')) { - $(document).mousedown(function(ev) { - var ignore = view.option('unselectCancel'); - if (ignore) { - if ($(ev.target).parents(ignore).length) { // could be optimized to stop after first match - return; + var t = this; + var rows; + var cols; + + t.build = function() { + rows = []; + cols = []; + buildFunc(rows, cols); + }; + + t.cell = function(x, y) { + var rowCnt = rows.length; + var colCnt = cols.length; + var i, r=-1, c=-1; + for (i=0; i= rows[i][0] && y < rows[i][1]) { + r = i; + break; + } + } + for (i=0; i= cols[i][0] && x < cols[i][1]) { + c = i; + break; + } + } + return (r>=0 && c>=0) ? { row:r, col:c } : null; + }; + + t.rect = function(row0, col0, row1, col1, originElement) { // row1,col1 is inclusive + var origin = originElement.offset(); + return { + top: rows[row0][0] - origin.top, + left: cols[col0][0] - origin.left, + width: cols[col1][1] - cols[col0][0], + height: rows[row1][1] - rows[row0][0] + }; + }; + +} + +function HoverListener(coordinateGrid) { + + + var t = this; + var bindType; + var change; + var firstCell; + var cell; + + + t.start = function(_change, ev, _bindType) { + change = _change; + firstCell = cell = null; + coordinateGrid.build(); + mouse(ev); + bindType = _bindType || 'mousemove'; + $(document).bind(bindType, mouse); + }; + + + function mouse(ev) { + var newCell = coordinateGrid.cell(ev.pageX, ev.pageY); + if (!newCell != !cell || newCell && (newCell.row != cell.row || newCell.col != cell.col)) { + if (newCell) { + if (!firstCell) { + firstCell = newCell; } + change(newCell, firstCell, newCell.row-firstCell.row, newCell.col-firstCell.col); + }else{ + change(newCell, firstCell); } - unselect(ev); - }); + cell = newCell; + } + } + + + t.stop = function() { + $(document).unbind(bindType, mouse); + return cell; + }; + + +} + +function HorizontalPositionCache(getElement) { + + var t = this, + elements = {}, + lefts = {}, + rights = {}; + + function e(i) { + return elements[i] = elements[i] || getElement(i); } + + t.left = function(i) { + return lefts[i] = lefts[i] === undefined ? e(i).position().left : lefts[i]; + }; + + t.right = function(i) { + return rights[i] = rights[i] === undefined ? t.left(i) + e(i).width() : rights[i]; + }; + + t.clear = function() { + elements = {}; + lefts = {}; + rights = {}; + }; + } + +fc.addDays = addDays; +fc.cloneDate = cloneDate; +fc.parseDate = parseDate; +fc.parseISO8601 = parseISO8601; +fc.parseTime = parseTime; +fc.formatDate = formatDate; +fc.formatDates = formatDates; + + + /* Date Math -----------------------------------------------------------------------------*/ -var DAY_MS = 86400000, +var dayIDs = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'], + DAY_MS = 86400000, HOUR_MS = 3600000, MINUTE_MS = 60000; + function addYears(d, n, keepTime) { d.setFullYear(d.getFullYear() + n); @@ -3411,6 +4184,7 @@ function addYears(d, n, keepTime) { return d; } + function addMonths(d, n, keepTime) { // prevents day overflow/underflow if (+d) { // prevent infinite looping on invalid dates var m = d.getMonth() + n, @@ -3428,6 +4202,7 @@ function addMonths(d, n, keepTime) { // return d; } + function addDays(d, n, keepTime) { // deals with daylight savings if (+d) { var dd = d.getDate() + n, @@ -3442,7 +4217,7 @@ function addDays(d, n, keepTime) { // de } return d; } -fc.addDays = addDays; + function fixDate(d, check) { // force d to be on check's YMD, for daylight savings purposes if (+d) { // prevent infinite looping on invalid dates @@ -3452,11 +4227,13 @@ function fixDate(d, check) { // force d } } + function addMinutes(d, n) { d.setMinutes(d.getMinutes() + n); return d; } + function clearTime(d) { d.setHours(0); d.setMinutes(0); @@ -3465,13 +4242,14 @@ function clearTime(d) { return d; } + function cloneDate(d, dontKeepTime) { if (dontKeepTime) { return clearTime(new Date(+d)); } return new Date(+d); } -fc.cloneDate = cloneDate; + function zeroDate() { // returns a Date with time 00:00:00 and dateOfMonth=1 var i=0, d; @@ -3481,6 +4259,7 @@ function zeroDate() { // returns a Date return d; } + function skipWeekend(date, inc, excl) { inc = inc || 1; while (!date.getDay() || (excl && date.getDay()==1 || !excl && date.getDay()==6)) { @@ -3489,10 +4268,12 @@ function skipWeekend(date, inc, excl) { return date; } + function dayDiff(d1, d2) { // d1 - d2 return Math.round((cloneDate(d1, true) - cloneDate(d2, true)) / DAY_MS); } + function setYMD(date, y, m, d) { if (y !== undefined && y != date.getFullYear()) { date.setDate(1); @@ -3513,7 +4294,8 @@ function setYMD(date, y, m, d) { /* Date Parsing -----------------------------------------------------------------------------*/ -var parseDate = fc.parseDate = function(s) { + +function parseDate(s, ignoreTimezone) { // ignoreTimezone defaults to true if (typeof s == 'object') { // already a Date object return s; } @@ -3524,55 +4306,69 @@ var parseDate = fc.parseDate = function( if (s.match(/^\d+$/)) { // a UNIX timestamp return new Date(parseInt(s) * 1000); } - return parseISO8601(s, true) || (s ? new Date(s) : null); + if (ignoreTimezone === undefined) { + ignoreTimezone = true; + } + return parseISO8601(s, ignoreTimezone) || (s ? new Date(s) : null); } // TODO: never return invalid dates (like from new Date()), return null instead return null; -}; +} + -var parseISO8601 = fc.parseISO8601 = function(s, ignoreTimezone) { +function parseISO8601(s, ignoreTimezone) { // ignoreTimezone defaults to false // derived from http://delete.me.uk/2005/03/iso8601.html // TODO: for a know glitch/feature, read tests/issue_206_parseDate_dst.html 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})))?)?)?)?$/); if (!m) { return null; } - var date = new Date(m[1], 0, 1), - check = new Date(m[1], 0, 1, 9, 0), - offset = 0; - if (m[3]) { - date.setMonth(m[3] - 1); - check.setMonth(m[3] - 1); - } - if (m[5]) { - date.setDate(m[5]); - check.setDate(m[5]); - } - fixDate(date, check); - if (m[7]) { - date.setHours(m[7]); - } - if (m[8]) { - date.setMinutes(m[8]); - } - if (m[10]) { - date.setSeconds(m[10]); - } - if (m[12]) { - date.setMilliseconds(Number("0." + m[12]) * 1000); - } - fixDate(date, check); - if (!ignoreTimezone) { - if (m[14]) { - offset = Number(m[16]) * 60 + Number(m[17]); - offset *= m[15] == '-' ? 1 : -1; + var date = new Date(m[1], 0, 1); + if (ignoreTimezone || !m[14]) { + var check = new Date(m[1], 0, 1, 9, 0); + if (m[3]) { + date.setMonth(m[3] - 1); + check.setMonth(m[3] - 1); + } + if (m[5]) { + date.setDate(m[5]); + check.setDate(m[5]); + } + fixDate(date, check); + if (m[7]) { + date.setHours(m[7]); + } + if (m[8]) { + date.setMinutes(m[8]); } - offset -= date.getTimezoneOffset(); + if (m[10]) { + date.setSeconds(m[10]); + } + if (m[12]) { + date.setMilliseconds(Number("0." + m[12]) * 1000); + } + fixDate(date, check); + }else{ + date.setUTCFullYear( + m[1], + m[3] ? m[3] - 1 : 0, + m[5] || 1 + ); + date.setUTCHours( + m[7] || 0, + m[8] || 0, + m[10] || 0, + m[12] ? Number("0." + m[12]) * 1000 : 0 + ); + var offset = Number(m[16]) * 60 + Number(m[17]); + offset *= m[15] == '-' ? 1 : -1; + date = new Date(+date + (offset * 60 * 1000)); } - return new Date(+date + (offset * 60 * 1000)); -}; + return date; +} -var parseTime = fc.parseTime = function(s) { // returns minutes since start of day + +function parseTime(s) { // returns minutes since start of day if (typeof s == 'number') { // an hour return s * 60; } @@ -3590,18 +4386,21 @@ var parseTime = fc.parseTime = function( } return h * 60 + (m[2] ? parseInt(m[2]) : 0); } -}; +} /* Date Formatting -----------------------------------------------------------------------------*/ +// TODO: use same function formatDate(date, [date2], format, [options]) + -var formatDate = fc.formatDate = function(date, format, options) { +function formatDate(date, format, options) { return formatDates(date, null, format, options); -}; +} -var formatDates = fc.formatDates = function(date1, date2, format, options) { + +function formatDates(date1, date2, format, options) { options = options || defaults; var date = date1, otherDate = date2, @@ -3678,6 +4477,7 @@ var formatDates = fc.formatDates = funct return res; }; + var dateFormatters = { s : function(d) { return d.getSeconds() }, ss : function(d) { return zeroPad(d.getSeconds()) }, @@ -3713,51 +4513,191 @@ var dateFormatters = { + +/* Event Date Math +-----------------------------------------------------------------------------*/ + + +function exclEndDay(event) { + if (event.end) { + return _exclEndDay(event.end, event.allDay); + }else{ + return addDays(cloneDate(event.start), 1); + } +} + + +function _exclEndDay(end, allDay) { + end = cloneDate(end); + return allDay || end.getHours() || end.getMinutes() ? addDays(end, 1) : clearTime(end); +} + + +function segCmp(a, b) { + return (b.msLength - a.msLength) * 100 + (a.event.start - b.event.start); +} + + +function segsCollide(seg1, seg2) { + return seg1.end > seg2.start && seg1.start < seg2.end; +} + + + +/* Event Sorting +-----------------------------------------------------------------------------*/ + + +// event rendering utilities +function sliceSegs(events, visEventEnds, start, end) { + var segs = [], + i, len=events.length, event, + eventStart, eventEnd, + segStart, segEnd, + isStart, isEnd; + for (i=0; i start && eventStart < end) { + if (eventStart < start) { + segStart = cloneDate(start); + isStart = false; + }else{ + segStart = eventStart; + isStart = true; + } + if (eventEnd > end) { + segEnd = cloneDate(end); + isEnd = false; + }else{ + segEnd = eventEnd; + isEnd = true; + } + segs.push({ + event: event, + start: segStart, + end: segEnd, + isStart: isStart, + isEnd: isEnd, + msLength: segEnd - segStart + }); + } + } + return segs.sort(segCmp); +} + + +// event rendering calculation utilities +function stackSegs(segs) { + var levels = [], + i, len = segs.length, seg, + j, collide, k; + for (i=0; i= rows[i][0] && y < rows[i][1]) { - r = i; - break; - } - } - for (i=0; i= cols[i][0] && x < cols[i][1]) { - c = i; - break; - } - } - return (r>=0 && c>=0) ? { row:r, col:c } : null; - }; - - t.rect = function(row0, col0, row1, col1, originElement) { // row1,col1 is inclusive - var origin = originElement.offset(); - return { - top: rows[row0][0] - origin.top, - left: cols[col0][0] - origin.left, - width: cols[col1][1] - cols[col0][0], - height: rows[row1][1] - rows[row0][0] - }; - }; - -} +//TODO: arraySlice +//TODO: isFunction, grep ? -/* Hover Listener ------------------------------------------------------------------------------*/ +function noop() { } -function HoverListener(coordinateGrid) { - var t = this; - var bindType; - var change; - var firstCell; - var cell; - - t.start = function(_change, ev, _bindType) { - change = _change; - firstCell = cell = null; - coordinateGrid.build(); - mouse(ev); - bindType = _bindType || 'mousemove'; - $(document).bind(bindType, mouse); - }; - - function mouse(ev) { - var newCell = coordinateGrid.cell(ev.pageX, ev.pageY); - if (!newCell != !cell || newCell && (newCell.row != cell.row || newCell.col != cell.col)) { - if (newCell) { - if (!firstCell) { - firstCell = newCell; - } - change(newCell, firstCell, newCell.row-firstCell.row, newCell.col-firstCell.col); - }else{ - change(newCell, firstCell); - } - cell = newCell; - } - } - - t.stop = function() { - $(document).unbind(bindType, mouse); - return cell; - }; - +function cmp(a, b) { + return a - b; } +function arrayMax(a) { + return Math.max.apply(Math, a); +} -/* Misc Utils ------------------------------------------------------------------------------*/ - -var dayIDs = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; function zeroPad(n) { return (n < 10 ? '0' : '') + n; } + function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object if (obj[name] !== undefined) { return obj[name]; @@ -3906,6 +4771,7 @@ function smartProperty(obj, name) { // g return obj['']; } + function htmlEscape(s) { return s.replace(/&/g, '&') .replace(/