/*! * FullCalendar v1.3.2 * http://arshaw.com/fullcalendar/ * * Use fullcalendar.css for basic styling. * For event drag & drop, required 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 * * Date: 2009-10-04 22:43:06 -0700 (Sun, 04 Oct 2009) * Revision: 48 */ (function($) { var fc = $.fullCalendar = {}; var views = fc.views = {}; /* Defaults -----------------------------------------------------------------------------*/ var defaults = { // display defaultView: 'month', aspectRatio: 1.35, header: { left: 'title', center: '', right: ''//tpday,prev,next }, // editing //editable: false, //disableDragging: false, //disableResizing: false, allDayDefault: true, // event ajax startParam: 'start', endParam: 'end', cacheParam: '_', // time formats timeFormat: 'h(:mm)t', // for events titleFormat: { month: 'MMMM yyyy', week: "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", day: 'dddd, MMM d, yyyy' }, columnFormat: { month: 'ddd', week: 'ddd M/d', day: 'dddd M/d' }, // locale isRTL: false, firstDay: 0, monthNames: ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez'], monthNamesShort: ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez'], dayNames: ['D','S','T','Q','Q','S','S'], dayNamesShort: ['D','S','T','Q','Q','S','S'], buttonText: { prev: ' ◄ ', next: ' ► ', today: 'today', month: 'month', week: 'week', day: 'day' }, // jquery-ui theming theme: false, buttonIcons: { prev: 'circle-triangle-w', next: 'circle-triangle-e' } }; // right-to-left defaults var rtlDefaults = { header: { left: 'next,prev today', center: '', right: 'title' }, buttonText: { prev: ' ► ', next: ' ◄ ' } }; // function for adding/overriding defaults var setDefaults = fc.setDefaults = function(d) { $.extend(true, defaults, d); } /* .fullCalendar jQuery function -----------------------------------------------------------------------------*/ $.fn.fullCalendar = function(options) { // method calling if (typeof options == 'string') { var args = Array.prototype.slice.call(arguments, 1), res; this.each(function() { var r = $.data(this, 'fullCalendar')[options].apply(this, args); if (res == undefined) { res = r; } }); if (res != undefined) { return res; } return this; } // pluck the 'events' and 'eventSources' options var eventSources = options.eventSources || []; delete options.eventSources; if (options.events) { eventSources.push(options.events); delete options.event; } // 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() { /* Instance Initialization -----------------------------------------------------------------------------*/ // element var _element = this, element = $(this).addClass('fc'), content = $("
").appendTo(this); if (options.isRTL) { element.addClass('fc-rtl'); } if (options.theme) { element.addClass('ui-widget'); } // view managing var date = new Date(), viewName, view, // the current view prevView, viewInstances = {}; if (options.year != undefined) { date.setYear(options.year); } if (options.month != undefined) { date.setMonth(options.month); } if (options.date != undefined) { date.setDate(options.date); } /* View Rendering -----------------------------------------------------------------------------*/ function changeView(v) { if (v != viewName) { prevView = view; if (viewInstances[v]) { (view = viewInstances[v]).element.show(); }else{ view = viewInstances[v] = $.fullCalendar.views[v]( $("
").appendTo(content), options); } if (prevView && prevView.eventsChanged) { // if previous view's events have been changed, mark future views' events as dirty eventsDirtyExcept(prevView); prevView.eventsChanged = false; } 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'); } view.name = viewName = v; render(); if (prevView) { // hide the old element AFTER the new has been rendered, preserves scrollbars prevView.element.hide(); } } } function render(inc) { if (_element.offsetWidth !== 0) { // visible on the screen if (inc || !view.date || +view.date != +date) { // !view.date means it hasn't been rendered yet ignoreWindowResizes = true; view.render(date, inc || 0, function(callback) { // dont refetch if new view contains the same events (or a subset) if (!eventStart || view.visStart < eventStart || view.visEnd > eventEnd) { fetchEvents(callback); }else{ callback(events); // no refetching } }); ignoreWindowResizes = false; view.date = cloneDate(date); } else if (view.sizeDirty) { view.updateSize(); view.rerenderEvents(); } else if (view.eventsDirty) { // ensure events are rerendered if another view messed with them // pass in 'events' b/c event might have been added/removed view.clearEvents(); view.renderEvents(events); } 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'); } } view.sizeDirty = false; view.eventsDirty = false; view.trigger('viewDisplay', _element); } } // marks other views' events as dirty function eventsDirtyExcept(exceptView) { $.each(viewInstances, function() { if (this != exceptView) { this.eventsDirty = true; } }); } // marks other views' sizes as dirty function sizesDirtyExcept(exceptView) { $.each(viewInstances, function() { if (this != exceptView) { this.sizeDirty = true; } }); } // called when any event objects have been added/removed/changed, rerenders function eventsChanged() { view.clearEvents(); view.renderEvents(events); eventsDirtyExcept(view); } /* 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 == 0) { if (callback) { callback(events); } } }, i=0; for (; 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) { var buttonName = this, buttonNameShort = this.replace(/^(basic|agenda)/, '').toLowerCase(); if (buttonName == 'title') { tr.append("

"); if (prevButton) { prevButton.addClass(tm + '-corner-right'); } prevButton = null; }else{ var buttonClick; if (publicMethods[buttonNameShort]) { buttonClick = publicMethods[buttonNameShort]; } else if (views[buttonName]) { buttonClick = function() { changeView(buttonName) }; } if (buttonClick) { if (prevButton) { prevButton.addClass(tm + '-no-right'); } var button, icon = options.theme ? options.buttonIcons[buttonNameShort] : null, text = options.buttonText[buttonNameShort]; if (icon) { button = $("
" + "
"); } else if (text) { button = $(""); } if (button) { button .mousedown(function() { button.addClass(tm + '-state-down'); }) .mouseup(function() { button.removeClass(tm + '-state-down'); }) .hover( function() { button.addClass(tm + '-state-hover'); }, function() { button.removeClass(tm + '-state-hover') .removeClass(tm + '-state-down'); } ) .appendTo($("").appendTo(tr)); if (publicMethods[buttonNameShort]) { button.click(publicMethods[buttonNameShort]); } else if (views[buttonName]) { button.click(function() { changeView(buttonName); }); } 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 -----------------------------------------------------------------------------*/ var elementWidth, ignoreWindowResizes = false, resizeCnt = 0; $(window).resize(function() { if (!ignoreWindowResizes && view.date) { // view.date means the view has been rendered var rcnt = ++resizeCnt; // add a delay setTimeout(function() { if (rcnt == resizeCnt) { var newWidth = element.width(); if (newWidth != elementWidth) { elementWidth = newWidth; view.updateSize(); view.rerenderEvents(true); sizesDirtyExcept(view); view.trigger('windowResize', _element); } } }, 200); } }); // let's begin... changeView(options.defaultView); elementWidth = element.width(); }); 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; } } /* Grid-based Views: month, basicWeek, basicDay -----------------------------------------------------------------------------*/ setDefaults({ weekMode: 'fixed' }); views.month = function(element, options) { return new Grid(element, options, { render: function(date, delta, fetchEvents) { if (delta) { addMonths(date, delta); date.setDate(1); } var start = this.start = cloneDate(date, true); start.setDate(1); this.title = formatDates( start, addDays(cloneDate(this.end = addMonths(cloneDate(start), 1)), -1), strProp(options.titleFormat, 'month'), options ); addDays(this.visStart = cloneDate(start), -((start.getDay() - options.firstDay + 7) % 7)); addDays(this.visEnd = cloneDate(this.end), (7 - this.visEnd.getDay() + options.firstDay) % 7); var rowCnt = Math.round((this.visEnd - this.visStart) / (DAY_MS * 7)); if (options.weekMode == 'fixed') { addDays(this.visEnd, (6 - rowCnt) * 7); rowCnt = 6; } this.renderGrid(rowCnt, 7, strProp(options.columnFormat, 'month'), true, fetchEvents); } }); } views.basicWeek = function(element, options) { return new Grid(element, options, { render: function(date, delta, fetchEvents) { if (delta) { addDays(date, delta * 7); } this.title = formatDates( this.start = this.visStart = addDays(cloneDate(date), -((date.getDay() - options.firstDay + 7) % 7)), addDays(cloneDate(this.end = this.visEnd = addDays(cloneDate(this.start), 7)), -1), strProp(options.titleFormat, 'week'), options ); this.renderGrid(1, 7, strProp(options.columnFormat, 'week'), false, fetchEvents); } }); }; views.basicDay = function(element, options) { return new Grid(element, options, { render: function(date, delta, fetchEvents) { if (delta) { addDays(date, delta); } this.title = formatDate(date, strProp(options.titleFormat, 'day'), options); this.start = this.visStart = cloneDate(date, true); this.end = this.visEnd = addDays(cloneDate(this.start), 1); this.renderGrid(1, 1, strProp(options.columnFormat, 'day'), false, fetchEvents); } }); } // rendering bugs var tdTopBug, trTopBug, tbodyTopBug, tdHeightBug, rtlLeftDiff; function Grid(element, options, methods) { var tm, firstDay, rtl, dis, dit, // day index sign / translate rowCnt, colCnt, colWidth, thead, tbody, cachedSegs, //... // initialize superclass view = $.extend(this, viewMethods, methods, { renderGrid: renderGrid, renderEvents: renderEvents, rerenderEvents: rerenderEvents, updateSize: updateSize, defaultEventEnd: function(event) { // calculates an end if event doesnt have one, mostly for resizing return cloneDate(event.start); }, visEventEnd: function(event) { // returns exclusive 'visible' end, for rendering if (event.end) { var end = cloneDate(event.end); return (event.allDay || end.getHours() || end.getMinutes()) ? addDays(end, 1) : end; }else{ return addDays(cloneDate(event.start), 1); } } }); view.init(element, options); /* Grid Rendering -----------------------------------------------------------------------------*/ element.addClass('fc-grid').css('position', 'relative'); if (element.disableSelection) { element.disableSelection(); } function renderGrid(r, c, colFormat, showNumbers, fetchEvents) { rowCnt = r; colCnt = c; var month = view.start.getMonth(), today = clearTime(new Date()), s, i, j, d = cloneDate(view.visStart); // update option-derived variables tm = options.theme ? 'ui' : 'fc'; firstDay = options.firstDay; if (rtl = options.isRTL) { dis = -1; dit = colCnt - 1; }else{ dis = 1; dit = 0; } if (!tbody) { // first time, build all cells from scratch var table = $("
").appendTo(element); s = ""; for (i=0; i" + formatDate(d, colFormat, options) + ""; addDays(d, 1); } thead = $(s + "").appendTo(table); s = ""; d = cloneDate(view.visStart); for (i=0; i"; for (j=0; j1 && d.getMonth() != month ? ' fc-other-month' : '') + (+d == +today ? ' fc-not-today '+tm+'-state-highlight' : ' fc-not-today') + "'>" + (showNumbers ? "
" + d.getDate() + "
" : '') + "
 
"; addDays(d, 1); } s += ""; } tbody = $(s + "
").appendTo(table); tbody.find('td').click(dayClick); }else{ // NOT first time, reuse as many cells as possible view.clearEvents(); var prevRowCnt = tbody.find('tr').length; if (rowCnt < prevRowCnt) { tbody.find('tr:gt(' + (rowCnt-1) + ')').remove(); // remove extra rows } else if (rowCnt > prevRowCnt) { // needs to create new rows... s = ''; for (i=prevRowCnt; i"; for (j=0; j" + (showNumbers ? "
" : '') + "
 
" + ""; addDays(d, 1); } s += ""; } tbody.append(s); } tbody.find('td.fc-new').removeClass('fc-new').click(dayClick); // re-label and re-class existing cells d = cloneDate(view.visStart); tbody.find('td').each(function() { var td = $(this); if (rowCnt > 1) { if (d.getMonth() == month) { td.removeClass('fc-other-month'); }else{ td.addClass('fc-other-month'); } } if (+d == +today) { td.removeClass('fc-not-today') .addClass('fc-not-today') .addClass(tm + '-state-highlight'); }else{ td.addClass('fc-not-today') .removeClass('fc-not-today') .removeClass(tm + '-state-highlight'); } td.find('div.fc-day-number').text(d.getDate()); addDays(d, 1); }); 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()]); addDays(d, 1); }); // 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()]); addDays(d, 1); }); } } updateSize(); fetchEvents(renderEvents); }; function dayClick() { var date = addDays( cloneDate(view.visStart), parseInt(this.className.match(/fc\-day(\d+)/)[1]) ); view.trigger('dayClick', this, date); } function updateSize() { var height = Math.round(element.width() / options.aspectRatio), leftTDs = tbody.find('tr td:first-child'), tbodyHeight = height - thead.height(), rowHeight1, rowHeight2; if (options.weekMode == 'variable') { rowHeight1 = rowHeight2 = Math.floor(tbodyHeight / (rowCnt==1 ? 2 : 6)); }else{ rowHeight1 = Math.floor(tbodyHeight / rowCnt); rowHeight2 = tbodyHeight - rowHeight1*(rowCnt-1); } if (tdTopBug == undefined) { // nasty bugs in opera 9.25 // position() returning relative to direct parent var tr = tbody.find('tr:first'), td = tr.find('td:first'), trTop = tr.position().top, tdTop = td.position().top; tdTopBug = tdTop < 0; trTopBug = trTop != tdTop; tbodyTopBug = tbody.position().top != trTop; } if (tdHeightBug == undefined) { // bug in firefox where cell height includes padding td.height(rowHeight1); tdHeightBug = rowHeight1 != td.height(); } if (tdHeightBug) { leftTDs.slice(0, -1).height(rowHeight1); leftTDs.slice(-1).height(rowHeight2); }else{ setOuterHeight(leftTDs.slice(0, -1), rowHeight1); setOuterHeight(leftTDs.slice(-1), rowHeight2); } setOuterWidth( thead.find('th').slice(0, -1), colWidth = Math.floor(element.width() / colCnt) ); } /* Event Rendering -----------------------------------------------------------------------------*/ function renderEvents(events) { view.reportEvents(events); renderSegs(cachedSegs = compileSegs(events)); } function rerenderEvents(skipCompile) { view.clearEvents(); if (skipCompile) { renderSegs(cachedSegs); }else{ renderEvents(view.cachedEvents); } } function compileSegs(events) { var d1 = cloneDate(view.visStart); var d2 = addDays(cloneDate(d1), colCnt); var rows = []; for (var i=0; i") .append(eventAnchor = $("") .append(event.allDay || !seg.isStart ? null : $("") .html(formatDates(event.start, event.end, options.timeFormat, options))) .append($("") .text(event.title))); if (event.url) { eventAnchor.attr('href', event.url); } triggerRes = view.trigger('eventRender', event, event, eventElement); if (triggerRes !== false) { if (triggerRes && typeof triggerRes != 'boolean') { eventElement = $(triggerRes); } eventElement .css({ position: 'absolute', top: top, left: left1 + (rtlLeftDiff||0), zIndex: 2 }) .appendTo(element); setOuterWidth(eventElement, left2-left1, true); if (rtl && rtlLeftDiff == undefined) { // bug in IE6 where offsets are miscalculated with direction:rtl rtlLeftDiff = left1 - eventElement.position().left; if (rtlLeftDiff) { eventElement.css('left', left1 + rtlLeftDiff); } } eventElementHandlers(event, eventElement); if (event.editable || event.editable == undefined && options.editable) { draggableEvent(event, eventElement); if (seg.isEnd) { resizableEvent(event, eventElement); } } view.reportEventElement(event, eventElement); levelHeight = Math.max(levelHeight, eventElement.outerHeight(true)); } } weekHeight += levelHeight; top += levelHeight; } innerDiv.height(weekHeight); } } function eventElementHandlers(event, eventElement) { eventElement .click(function(ev) { if (!eventElement.hasClass('ui-draggable-dragging')) { return view.trigger('eventClick', this, event, ev); } }) .hover( function(ev) { view.trigger('eventMouseover', this, event, ev); }, function(ev) { view.trigger('eventMouseout', this, event, ev); } ); } /* Draggable -----------------------------------------------------------------------------*/ function draggableEvent(event, eventElement) { if (!options.disableDragging && eventElement.draggable) { var matrix; eventElement.draggable({ zIndex: 3, delay: 50, opacity: options.dragOpacity, revertDuration: options.dragRevertDuration, start: function(ev, ui) { matrix = new HoverMatrix(function(cell) { eventElement.draggable('option', 'revert', !cell || !cell.rowDelta && !cell.colDelta); if (cell) { view.showOverlay(cell); }else{ view.hideOverlay(); } }); tbody.find('tr').each(function() { matrix.row(this, tbodyTopBug); }); var tds = tbody.find('tr:first td'); if (rtl) { tds = $(tds.get().reverse()); } tds.each(function() { matrix.col(this); }); view.hideEvents(event, eventElement); view.trigger('eventDragStart', eventElement, event, ev, ui); matrix.mouse(ev.pageX, ev.pageY); }, drag: function(ev) { matrix.mouse(ev.pageX, ev.pageY); }, stop: function(ev, ui) { view.hideOverlay(); view.trigger('eventDragStop', eventElement, event, ev, ui); var cell = matrix.cell; if (!cell || !cell.rowDelta && !cell.colDelta) { view.showEvents(event, eventElement); }else{ var dayDelta = cell.rowDelta*7 + cell.colDelta*dis; view.moveEvent(event, dayDelta); view.trigger('eventDrop', this, event, dayDelta, 0, function() { view.moveEvent(event, -dayDelta); rerenderEvents(); }, ev, ui); eventElement.find('a').removeAttr('href'); // prevents safari from visiting the link rerenderEvents(); } } }); } } /* Resizable -----------------------------------------------------------------------------*/ function resizableEvent(event, eventElement) { if (!options.disableResizing && eventElement.resizable) { eventElement.resizable({ handles: rtl ? 'w' : 'e', grid: colWidth, minWidth: colWidth/2, // need this or else IE throws errors when too small containment: element, start: function(ev, ui) { eventElement.css('z-index', 3); 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.resizeEvent(event, dayDelta); view.trigger('eventResize', this, event, dayDelta, 0, function() { view.resizeEvent(event, -dayDelta); rerenderEvents(); }, ev, ui); rerenderEvents(); }else{ view.showEvents(event, eventElement); } eventElement.css('z-index', 2); } }); } } }; /* Methods & Utilities for All Views -----------------------------------------------------------------------------*/ var viewMethods = { /* * Objects inheriting these methods must implement the following properties/methods: * - title * - start * - end * - visStart * - visEnd * - defaultEventEnd(event) * - visEventEnd(event) * - render(events) * - rerenderEvents() * * * z-index reservations: * 1. day-overlay * 2. events * 3. dragging/resizing events * */ init: function(element, options) { this.element = element; this.options = options; this.cachedEvents = []; this.eventsByID = {}; this.eventElements = []; this.eventElementsByID = {}; }, // 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])); } }, // returns a Date object for an event's end eventEnd: function(event) { return event.end || this.defaultEventEnd(event); }, // 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 = {}, cachedEvents = this.cachedEvents = []; for (i=0; i