/** * @preserve * FullCalendar v1.4.6 * 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: Mon May 31 10:18:29 2010 -0700 * */ (function($) { var fc = $.fullCalendar = {}; var views = fc.views = {}; /* Defaults -----------------------------------------------------------------------------*/ var defaults = { // display defaultView: 'month', aspectRatio: 1.35, header: { left: 'title', center: '', right: 'today prev,next' }, weekends: true, // editing //editable: false, //disableDragging: false, //disableResizing: false, allDayDefault: true, // event ajax lazyFetching: true, startParam: 'start', endParam: 'end', // time formats 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' }, timeFormat: { // for event elements '': 'h(:mm)t' // default }, // locale isRTL: false, firstDay: 0, monthNames: ['January','February','March','April','May','June','July','August','September','October','November','December'], monthNamesShort: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'], dayNames: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'], dayNamesShort: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'], buttonText: { prev: ' ◄ ', next: ' ► ', prevYear: ' << ', nextYear: ' >> ', today: 'today', month: 'month', week: 'week', day: 'day' }, // jquery-ui theming theme: false, buttonIcons: { prev: 'circle-triangle-w', next: 'circle-triangle-e' }, //selectable: false, unselectAuto: true }; // right-to-left defaults var rtlDefaults = { header: { left: 'next,prev today', center: '', right: 'title' }, buttonText: { prev: ' ► ', next: ' ◄ ', prevYear: ' >> ', nextYear: ' << ' }, buttonIcons: { prev: 'circle-triangle-e', next: 'circle-triangle-w' } }; // 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 data = $.data(this, 'fullCalendar'); if (data) { var meth = data[options]; if (meth) { var r = meth.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.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() { /* 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; if (options.isRTL) { element.addClass('fc-rtl'); } if (options.theme) { element.addClass('ui-widget'); } if (options.year !== undefined && options.year != date.getFullYear()) { date.setDate(1); date.setMonth(0); date.setFullYear(options.year); } if (options.month !== undefined && options.month != date.getMonth()) { date.setDate(1); date.setMonth(options.month); } if (options.date !== undefined) { date.setDate(options.date); } /* 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 ); } 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(); // 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--; } } function render(inc) { if (elementVisible()) { ignoreWindowResize++; // because view.renderEvents might temporarily change the height before 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'); } } ignoreWindowResize--; view.trigger('viewDisplay', _element); } } function elementVisible() { return _element.offsetWidth !== 0; } function bodyVisible() { return $('body')[0].offsetWidth !== 0; } function viewUnselect() { if (view) { view.unselect(); } } // called when any event objects have been added/removed/changed, rerenders function eventsChanged() { eventsDirty(); if (elementVisible()) { view.clearEvents(); view.renderEvents(events); view.eventsDirty = false; } } // 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()) { calcSize(); setSize(); viewUnselect(); view.rerenderEvents(); view.sizeDirty = false; } } // marks other views' sizes as dirty function sizesDirty() { $.each(viewInstances, function() { this.sizeDirty = true; }); } /* 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); } } }, i = 0; for (; i < eventSources.length; i++) { fetchEventSource(eventSources[i], sourceDone); } } // Fetch from a particular source. Append to the 'events' array function fetchEventSource(src, callback) { var prevViewName = view.name, prevDate = cloneDate(date), reportEvents = function(a) { if (prevViewName == view.name && +prevDate == +date && // protects from fast switching $.inArray(src, eventSources) != -1) { // makes sure source hasn't been removed for (var i = 0; i < a.length; i++) { normalizeEvent(a[i], options); a[i].source = src; } events = events.concat(a); if (callback) { callback(a); } } }, reportEventsAndPop = function(a) { reportEvents(a); popLoading(); }; if (typeof src == 'string') { var params = {}; params[options.startParam] = Math.round(eventStart.getTime() / 1000); params[options.endParam] = Math.round(eventEnd.getTime() / 1000); if (options.cacheParam) { params[options.cacheParam] = (new Date()).getTime(); // TODO: deprecate cacheParam } pushLoading(); $.ajax({ url: src, dataType: 'json', data: params, cache: options.cacheParam || false, // don't let jquery prevent caching if cacheParam is being used success: reportEventsAndPop }); } else if ($.isFunction(src)) { pushLoading(); src(cloneDate(eventStart), cloneDate(eventEnd), reportEventsAndPop); } else { reportEvents(src); // src is an array } } // for convenience function fetchAndRenderEvents() { fetchEvents(function(events) { view.renderEvents(events); // maintain `this` in view }); } /* Loading State -----------------------------------------------------------------------------*/ var loadingLevel = 0; function pushLoading() { if (!loadingLevel++) { view.trigger('loading', _element, true); } } function popLoading() { if (!--loadingLevel) { view.trigger('loading', _element, false); } } /* Public Methods -----------------------------------------------------------------------------*/ var publicMethods = { render: function() { calcSize(); sizesDirty(); eventsDirty(); render(); }, changeView: changeView, getView: function() { return view; }, getDate: function() { return date; }, option: function(name, value) { if (value === undefined) { return options[name]; } if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') { options[name] = value; sizeChanged(); } }, destroy: function() { $(window).unbind('resize', windowResize); if (header) { header.remove(); } content.remove(); $.removeData(_element, 'fullCalendar'); }, // // Navigation // prev: function() { render(-1); }, next: function() { render(1); }, prevYear: function() { addYears(date, -1); render(); }, nextYear: function() { addYears(date, 1); render(); }, today: function() { date = new Date(); render(); }, gotoDate: function(year, month, dateNum) { if (typeof year == 'object') { date = cloneDate(year); // provided 1 argument, a Date } else { if (year !== undefined) { date.setFullYear(year); } if (month !== undefined) { date.setMonth(month); } if (dateNum !== undefined) { date.setDate(dateNum); } } render(); }, incrementDate: function(years, months, days) { if (years !== undefined) { addYears(date, years); } if (months !== undefined) { addMonths(date, months); } if (days !== undefined) { addDays(date, days); } 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 < len; i++) { e = events[i]; if (e._id == event._id && e != event) { e.start = new Date(+e.start + startDelta); if (event.end) { if (e.end) { e.end = new Date(+e.end + endDelta); } else { e.end = new Date(+view.defaultEventEnd(e) + endDelta); } } else { e.end = null; } 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 < eventSources.length; i++) { if (typeof eventSources[i] == 'object') { eventSources[i] = []; } } } else { if (!$.isFunction(filter)) { // an event ID var id = filter + ''; filter = function(e) { return e._id == id; }; } events = $.grep(events, filter, true); // remove events from array sources for (var i = 0; i < eventSources.length; i++) { if (typeof eventSources[i] == 'object') { eventSources[i] = $.grep(eventSources[i], filter, true); } } } eventsChanged(); }, clientEvents: function(filter) { if ($.isFunction(filter)) { return $.grep(events, filter); } else if (filter) { // an event ID filter += ''; return $.grep(events, function(e) { return e._id == filter; }); } return events; // else, return all }, rerenderEvents: eventsChanged, // TODO: think of renaming eventsChanged // // Event Source // addEventSource: function(source) { eventSources.push(source); fetchEventSource(source, eventsChanged); }, removeEventSource: function(source) { eventSources = $.grep(eventSources, function(src) { return src != source; }); // remove all client events from that source events = $.grep(events, function(e) { return e.source != source; }); eventsChanged(); }, refetchEvents: function() { fetchEvents(eventsChanged); }, // // selection // select: function(start, end, allDay) { view.select(start, end, allDay === undefined ? true : allDay); }, unselect: function() { view.unselect(); } }; $.data(this, 'fullCalendar', publicMethods); // TODO: look into memory leak implications /* Header -----------------------------------------------------------------------------*/ var header, sections = options.header; if (sections) { header = $("") .append($("") .append($(""); $.each(buttonStr.split(' '), function(i) { if (i > 0) { tr.append(""); } var prevButton; $.each(this.split(','), function(j, buttonName) { if (buttonName == 'title') { tr.append(""); if (prevButton) { prevButton.addClass(tm + '-corner-right'); } prevButton = null; } else { var buttonClick; if (publicMethods[buttonName]) { buttonClick = publicMethods[buttonName]; } else if (views[buttonName]) { buttonClick = function() { button.removeClass(tm + '-state-hover'); changeView(buttonName); }; } 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() { 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($("
").append(buildSection(sections.left))) .append($("").append(buildSection(sections.center))) .append($("").append(buildSection(sections.right)))) .prependTo(element); } function buildSection(buttonStr) { if (buttonStr) { var tr = $("

 

").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); // 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(); } }, 0); } }); 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 = []; } } // 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) { 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 ); } }); }; views.basicWeek = function(element, options) { 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 ); } }); }; views.basicDay = function(element, options) { 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 ); } }); }; // rendering bugs var tdHeightBug; function Grid(element, options, methods) { 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'); }), selectionManager, selectionMatrix, // ... // 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.init(element, options); /* Grid Rendering -----------------------------------------------------------------------------*/ disableTextSelection(element.addClass('fc-grid')); function renderGrid(r, c, colFormat, showNumbers) { rowCnt = r; colCnt = c; // update option-derived variables tm = options.theme ? 'ui' : 'fc'; nwe = options.weekends ? 0 : 1; firstDay = options.firstDay; if (rtl = options.isRTL) { dis = -1; dit = colCnt - 1; } else { dis = 1; dit = 0; } var month = view.start.getMonth(), today = clearTime(new Date()), s, i, j, d = cloneDate(view.visStart); if (!tbody) { // first time, build all cells from scratch var table = $("
").appendTo(element); s = ""; for (i = 0; i < colCnt; i++) { s += ""; addDays(d, 1); if (nwe) { skipWeekend(d); } } thead = $(s + "").appendTo(table); s = ""; d = cloneDate(view.visStart); for (i = 0; i < rowCnt; i++) { s += ""; for (j = 0; j < colCnt; j++) { s += ""; addDays(d, 1); if (nwe) { skipWeekend(d); } } s += ""; } tbody = $(s + "").appendTo(table); dayBind(tbody.find('td')); segmentContainer = $("
").appendTo(element); } else { // NOT first time, reuse as many cells as possible 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 < rowCnt; i++) { s += "
"; for (j = 0; j < colCnt; j++) { s += ""; addDays(d, 1); if (nwe) { skipWeekend(d); } } s += ""; } tbody.append(s); } dayBind(tbody.find('td.fc-new').removeClass('fc-new')); // 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-today') .addClass(tm + '-state-highlight'); } else { td.addClass('fc-not-today') .removeClass('fc-today') .removeClass(tm + '-state-highlight'); } td.find('div.fc-day-number').text(d.getDate()); addDays(d, 1); if (nwe) { skipWeekend(d); } }); 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); if (nwe) { skipWeekend(d); } }); // 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); if (nwe) { skipWeekend(d); } }); } } } function setHeight(height) { viewHeight = height; var leftTDs = tbody.find('tr td:first-child'), tbodyHeight = viewHeight - 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 (tdHeightBug === undefined) { // bug in firefox where cell height includes padding var tr = tbody.find('tr:first'), td = tr.find('td:first'); 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); } } function setWidth(width) { viewWidth = width; dayContentPositions.clear(); setOuterWidth( thead.find('th').slice(0, -1), colWidth = Math.floor(viewWidth / colCnt) ); } /* Event Rendering -----------------------------------------------------------------------------*/ function renderEvents(events) { view.reportEvents(cachedEvents = events); renderSegs(compileSegs(events)); } function rerenderEvents(modifiedEventId) { clearEvents(); renderSegs(compileSegs(cachedEvents), modifiedEventId); } function clearEvents() { view._clearEvents(); // only clears the hashes segmentContainer.empty(); } 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 < rowCnt; i++) { row = stackSegs(view.sliceSegs(events, visEventsEnds, d1, d2)); for (j = 0; j < row.length; j++) { level = row[j]; for (k = 0; k < level.length; k++) { seg = level[k]; seg.row = i; seg.level = j; segs.push(seg); } } addDays(d1, 7); addDays(d2, 7); } return segs; } function renderSegs(segs, modifiedEventId) { _renderDaySegs( segs, rowCnt, view, 0, viewWidth, function(i) { return tbody.find('tr:eq(' + i + ')') }, dayContentPositions.left, dayContentPositions.right, segmentContainer, bindSegHandlers, modifiedEventId ); } function bindSegHandlers(event, eventElement, seg) { view.eventElementHandlers(event, eventElement); if (event.editable || event.editable === undefined && options.editable) { draggableEvent(event, eventElement); if (seg.isEnd) { view.resizableDayEvent(event, eventElement, colWidth); } } } /* Event Dragging -----------------------------------------------------------------------------*/ function draggableEvent(event, eventElement) { if (!options.disableDragging && eventElement.draggable) { var matrix, dayDelta = 0; eventElement.draggable({ zIndex: 9, delay: 50, opacity: view.option('dragOpacity'), revertDuration: options.dragRevertDuration, start: function(ev, ui) { view.hideEvents(event, eventElement); view.trigger('eventDragStart', eventElement, event, ev, ui); matrix = buildDayMatrix(function(cell) { eventElement.draggable('option', 'revert', !cell || !cell.rowDelta && !cell.colDelta); clearOverlays(); if (cell) { dayDelta = cell.rowDelta * 7 + cell.colDelta * dis; renderDayOverlays( matrix, addDays(cloneDate(event.start), dayDelta), addDays(exclEndDay(event), dayDelta) ); } else { dayDelta = 0; } }); matrix.mouse(ev); }, drag: function(ev) { matrix.mouse(ev); }, stop: function(ev, ui) { clearOverlays(); view.trigger('eventDragStop', eventElement, event, ev, ui); if (dayDelta) { eventElement.find('a').removeAttr('href'); // prevents safari from visiting the link view.eventDrop(this, event, dayDelta, 0, event.allDay, ev, ui); } else { if ($.browser.msie) { eventElement.css('filter', ''); // clear IE opacity side-effects } view.showEvents(event, eventElement); } } }); } } /* Day clicking and binding ---------------------------------------------------------*/ function dayBind(days) { days.click(dayClick) .mousedown(selectionMousedown); } function dayClick(ev) { if (!view.option('selectable')) { // SelectionManager will worry about dayClick var n = parseInt(this.className.match(/fc\-day(\d+)/)[1]), date = addDays( cloneDate(view.visStart), Math.floor(n / colCnt) * 7 + n % colCnt ); // TODO: what about weekends in middle of week? view.trigger('dayClick', this, date, true, ev); } } /* Selecting --------------------------------------------------------*/ selectionManager = new SelectionManager( view, unselect, function(startDate, endDate, allDay) { renderDayOverlays( selectionMatrix, startDate, addDays(cloneDate(endDate), 1) ); }, clearOverlays ); function selectionMousedown(ev) { if (view.option('selectable')) { selectionMatrix = buildDayMatrix(function(cell) { if (cell) { var d = cellDate(cell.row, cell.col); selectionManager.drag(d, d, true); } else { selectionManager.drag(); } }); documentDragHelp( function(ev) { selectionMatrix.mouse(ev); }, function(ev) { selectionManager.dragStop(ev); } ); selectionManager.dragStart(ev); selectionMatrix.mouse(ev); return false; // prevent auto-unselect and text selection } } documentUnselectAuto(view, unselect); view.select = function(start, end, allDay) { if (!end) { end = cloneDate(start); } selectionMatrix = buildDayMatrix(); selectionManager.select(start, end, allDay); }; function unselect() { selectionManager.unselect(); } view.unselect = unselect; /* Semi-transparent Overlay Helpers ------------------------------------------------------*/ function renderDayOverlays(matrix, overlayStart, overlayEnd) { // overlayEnd is exclusive var rowStart = cloneDate(view.visStart); var rowEnd = addDays(cloneDate(rowStart), colCnt); for (var i = 0; i < rowCnt; i++) { var stretchStart = new Date(Math.max(rowStart, overlayStart)); var stretchEnd = new Date(Math.min(rowEnd, overlayEnd)); if (stretchStart < stretchEnd) { var colStart, colEnd; if (rtl) { colStart = dayDiff(stretchEnd, rowStart) * dis + dit + 1; colEnd = dayDiff(stretchStart, rowStart) * dis + dit + 1; } else { colStart = dayDiff(stretchStart, rowStart); colEnd = dayDiff(stretchEnd, rowStart); } var rect = matrix.rect(i, colStart, i + 1, colEnd, element); dayBind( view.renderOverlay(rect, element) ); } addDays(rowStart, 7); addDays(rowEnd, 7); } } function clearOverlays() { view.clearOverlays(); } /* Utils ---------------------------------------------------*/ function buildDayMatrix(changeCallback) { var tds = tbody.find('tr:first td'); if (rtl) { tds = $(tds.get().reverse()); } return new HoverMatrix(tbody.find('tr'), tds, changeCallback); } function cellDate(r, c) { // convert r,c to date return addDays(cloneDate(view.visStart), r * 7 + c * dis + dit); // TODO: what about weekends in middle of week? } } function _renderDaySegs(segs, rowCnt, view, minLeft, maxLeft, getRow, dayContentLeft, dayContentRight, segmentContainer, bindSegHandlers, modifiedEventId) { var options = view.options, rtl = options.isRTL, i, segCnt = segs.length, seg, event, className, left, right, html = '', eventElements, eventElement, triggerRes, hsideCache = {}, vmarginCache = {}, key, val, rowI, top, levelI, levelHeight, rowDivs = [], rowDivTops = []; // calculate desired position/dimensions, create html for (i = 0; i < segCnt; i++) { seg = segs[i]; event = seg.event; className = 'fc-event fc-event-hori '; if (rtl) { if (seg.isStart) { className += 'fc-corner-right '; } if (seg.isEnd) { className += 'fc-corner-left '; } left = seg.isEnd ? dayContentLeft(seg.end.getDay() - 1) : minLeft; right = seg.isStart ? dayContentRight(seg.start.getDay()) : maxLeft; } else { if (seg.isStart) { className += 'fc-corner-left '; } if (seg.isEnd) { className += 'fc-corner-right '; } left = seg.isStart ? dayContentLeft(seg.start.getDay()) : minLeft; right = seg.isEnd ? dayContentRight(seg.end.getDay() - 1) : maxLeft; } html += "
" + "" + (!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; } segmentContainer[0].innerHTML = html; // faster than html() eventElements = segmentContainer.children(); // retrieve elements, run through eventRender callback, bind handlers for (i = 0; i < segCnt; i++) { seg = segs[i]; eventElement = $(eventElements[i]); // faster than eq() event = seg.event; triggerRes = view.trigger('eventRender', event, event, eventElement); if (triggerRes === false) { eventElement.remove(); } else { if (triggerRes && triggerRes !== true) { eventElement.remove(); eventElement = $(triggerRes) .css({ position: 'absolute', left: seg.left }) .appendTo(segmentContainer); } seg.element = eventElement; if (event._id === modifiedEventId) { bindSegHandlers(event, eventElement, seg); } else { eventElement[0]._fci = i; // for lazySegBind } view.reportEventElement(event, eventElement); } } lazySegBind(segmentContainer, segs, bindSegHandlers); // record event horizontal sides for (i = 0; i < segCnt; i++) { seg = segs[i]; if (eventElement = seg.element) { val = hsideCache[key = seg.key = cssKey(eventElement[0])]; seg.hsides = val === undefined ? (hsideCache[key] = hsides(eventElement[0], true)) : val; } } // set event widths for (i = 0; i < segCnt; i++) { seg = segs[i]; if (eventElement = seg.element) { eventElement[0].style.width = seg.outerWidth - seg.hsides + 'px'; } } // record event heights for (i = 0; i < segCnt; i++) { seg = segs[i]; if (eventElement = seg.element) { val = vmarginCache[key = seg.key]; seg.outerHeight = eventElement[0].offsetHeight + ( val === undefined ? (vmarginCache[key] = vmargins(eventElement[0])) : val ); } } // set row heights, calculate event tops (in relation to row top) for (i = 0,rowI = 0; rowI < rowCnt; rowI++) { top = levelI = levelHeight = 0; while (i < segCnt && (seg = segs[i]).row == rowI) { if (seg.level != levelI) { top += levelHeight; levelHeight = 0; levelI++; } levelHeight = Math.max(levelHeight, seg.outerHeight || 0); seg.top = top; i++; } rowDivs[rowI] = getRow(rowI).find('td:first div.fc-day-content > div')// optimal selector? .height(top + levelHeight); } // calculate row tops for (rowI = 0; rowI < rowCnt; rowI++) { rowDivTops[rowI] = rowDivs[rowI][0].offsetTop; } // set event tops for (i = 0; i < segCnt; i++) { seg = segs[i]; if (eventElement = seg.element) { eventElement[0].style.top = rowDivTops[seg.row] + seg.top + 'px'; event = seg.event; view.trigger('eventAfterRender', event, event, eventElement); } } } /* Agenda Views: agendaWeek/agendaDay -----------------------------------------------------------------------------*/ setDefaults({ allDaySlot: true, allDayText: 'all-day', firstHour: 6, slotMinutes: 30, defaultEventMinutes: 120, axisFormat: 'h(:mm)tt', timeFormat: { agenda: 'h:mm{ - h:mm}' }, dragOpacity: { agenda: .5 }, minTime: 0, maxTime: 24 }); views.agendaWeek = function(element, options) { return new Agenda(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.renderAgenda( options.weekends ? 7 : 5, this.option('columnFormat') ); } }); }; views.agendaDay = function(element, options) { return new Agenda(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.renderAgenda( 1, this.option('columnFormat') ); } }); }; function Agenda(element, options, methods) { var head, body, bodyContent, bodyTable, bg, colCnt, slotCnt = 0, // spanning all the way across axisWidth, colWidth, slotHeight, viewWidth, viewHeight, savedScrollTop, cachedEvents = [], daySegmentContainer, slotSegmentContainer, tm, firstDay, nwe, // no weekends (int) rtl, dis, dit, // day index sign / translate minMinute, maxMinute, colContentPositions = new HorizontalPositionCache(function(col) { return bg.find('td:eq(' + col + ') div div'); }), slotTopCache = {}, daySelectionManager, slotSelectionManager, selectionHelper, selectionMatrix, // ... view = $.extend(this, viewMethods, methods, { renderAgenda: renderAgenda, renderEvents: renderEvents, rerenderEvents: rerenderEvents, clearEvents: clearEvents, setHeight: setHeight, setWidth: setWidth, beforeHide: function() { savedScrollTop = body.scrollTop(); }, afterShow: function() { body.scrollTop(savedScrollTop); }, defaultEventEnd: function(event) { var start = cloneDate(event.start); if (event.allDay) { return start; } return addMinutes(start, options.defaultEventMinutes); } }); view.init(element, options); /* Time-slot rendering -----------------------------------------------------------------------------*/ disableTextSelection(element.addClass('fc-agenda')); function renderAgenda(c, colFormat) { colCnt = c; // update option-derived variables tm = options.theme ? 'ui' : 'fc'; nwe = options.weekends ? 0 : 1; firstDay = options.firstDay; if (rtl = options.isRTL) { dis = -1; dit = colCnt - 1; } else { dis = 1; dit = 0; } minMinute = parseTime(options.minTime); maxMinute = parseTime(options.maxTime); var d0 = rtl ? addDays(cloneDate(view.visEnd), -1) : cloneDate(view.visStart), d = cloneDate(d0), today = clearTime(new Date()); if (!head) { // first time rendering, build from scratch var i, minutes, slotNormal = options.slotMinutes % 15 == 0, //... // head s = "
" + "
" + formatDate(d, colFormat, options) + "
1 && d.getMonth() != month ? ' fc-other-month' : '') + (+d == +today ? ' fc-today ' + tm + '-state-highlight' : ' fc-not-today') + "'>" + (showNumbers ? "
" + d.getDate() + "
" : '') + "
 
" + (showNumbers ? "
" : '') + "
 
" + "
" + "" + ""; for (i = 0; i < colCnt; i++) { s += ""; addDays(d, dis); if (nwe) { skipWeekend(d, dis); } } s += ""; if (options.allDaySlot) { s += "" + "" + "" + "" + ""; } s += "
 " + formatDate(d, colFormat, options) + " 
" + options.allDayText + "" + "
 
 
"; head = $(s).appendTo(element); dayBind(head.find('td')); // all-day event container daySegmentContainer = $("
").appendTo(head); // body d = zeroDate(); var maxd = addMinutes(cloneDate(d), maxMinute); addMinutes(d, minMinute); s = ""; for (i = 0; d < maxd; i++) { minutes = d.getMinutes(); s += ""; addMinutes(d, options.slotMinutes); slotCnt++; } s += "
" + ((!slotNormal || !minutes) ? formatDate(d, options.axisFormat) : ' ') + "
 
"; body = $("
") .append(bodyContent = $("
") .append(bodyTable = $(s))) .appendTo(element); slotBind(body.find('td')); // slot event container slotSegmentContainer = $("
").appendTo(bodyContent); // background stripes d = cloneDate(d0); s = "
" + ""; for (i = 0; i < colCnt; i++) { s += ""; addDays(d, dis); if (nwe) { skipWeekend(d, dis); } } s += "
 
"; bg = $(s).appendTo(element); } else { // skeleton already built, just modify it 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()]); addDays(d, dis); if (nwe) { skipWeekend(d, dis); } }); // change classes of background stripes d = cloneDate(d0); bg.find('td').each(function() { this.className = this.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); if (+d == +today) { $(this) .removeClass('fc-not-today') .addClass('fc-today') .addClass(tm + '-state-highlight'); } else { $(this) .addClass('fc-not-today') .removeClass('fc-today') .removeClass(tm + '-state-highlight'); } addDays(d, dis); if (nwe) { skipWeekend(d, dis); } }); } } 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) { viewHeight = height; slotTopCache = {}; body.height(height - head.height()); 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); bodyTable.width(''); var topTDs = head.find('tr:first th'), stripeTDs = bg.find('td'), clientWidth = body[0].clientWidth; bodyTable.width(clientWidth); // time-axis width axisWidth = 0; setOuterWidth( head.find('tr:lt(2) th:first').add(body.find('tr:first th')) .width('') .each(function() { axisWidth = Math.max(axisWidth, $(this).outerWidth()); }), axisWidth ); // column width 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)); bg.css({ left: axisWidth, width: clientWidth - axisWidth }); } /* Slot/Day clicking and binding -----------------------------------------------------------------------*/ function dayBind(tds) { tds.click(slotClick) .mousedown(daySelectionMousedown); } function slotBind(tds) { tds.click(slotClick) .mousedown(slotSelectionMousedown); } function slotClick(ev) { if (!view.option('selectable')) { // SelectionManager will worry about dayClick var col = Math.floor((ev.pageX - bg.offset().left) / colWidth), date = addDays(cloneDate(view.visStart), dit + dis * col), rowMatch = this.className.match(/fc-slot(\d+)/); if (rowMatch) { var mins = parseInt(rowMatch[1]) * options.slotMinutes, hours = Math.floor(mins / 60); date.setHours(hours); date.setMinutes(mins % 60 + minMinute); view.trigger('dayClick', this, date, false, ev); } else { view.trigger('dayClick', this, date, true, ev); } } } /* Event Rendering -----------------------------------------------------------------------------*/ function renderEvents(events, modifiedEventId) { view.reportEvents(cachedEvents = events); var i, len = events.length, dayEvents = [], slotEvents = []; for (i = 0; i < len; i++) { if (events[i].allDay) { dayEvents.push(events[i]); } else { slotEvents.push(events[i]); } } renderDaySegs(compileDaySegs(dayEvents), modifiedEventId); renderSlotSegs(compileSlotSegs(slotEvents), modifiedEventId); } function rerenderEvents(modifiedEventId) { clearEvents(); renderEvents(cachedEvents, modifiedEventId); } function clearEvents() { view._clearEvents(); // only clears the hashes daySegmentContainer.empty(); slotSegmentContainer.empty(); } function compileDaySegs(events) { var levels = stackSegs(view.sliceSegs(events, $.map(events, exclEndDay), view.visStart, view.visEnd)), i, levelCnt = levels.length, level, j, seg, segs = []; for (i = 0; i < levelCnt; i++) { level = levels[i]; for (j = 0; j < level.length; j++) { seg = level[j]; seg.row = 0; seg.level = i; segs.push(seg); } } return segs; } 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 < colCnt; i++) { col = stackSegs(view.sliceSegs(events, visEventEnds, d, addMinutes(cloneDate(d), maxMinute - minMinute))); countForwardSegs(col); for (j = 0; j < col.length; j++) { level = col[j]; for (k = 0; k < level.length; k++) { seg = level[k]; seg.col = i; seg.level = j; segs.push(seg); } } addDays(d, 1, true); } return segs; } // 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 } } // renders events in the 'time slots' at the bottom 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 < segCnt; i++) { seg = segs[i]; event = seg.event; className = 'fc-event fc-event-vert '; if (seg.isStart) { className += 'fc-corner-top '; } if (seg.isEnd) { className += 'fc-corner-bottom '; } top = timePosition(seg.start, seg.start); bottom = timePosition(seg.start, seg.end); colI = seg.col; levelI = seg.level; forward = seg.forward || 0; leftmost = axisWidth + colContentPositions.left(colI * dis + dit); availWidth = axisWidth + colContentPositions.right(colI * dis + dit) - leftmost; availWidth = Math.min(availWidth - 6, availWidth * .95); // TODO: move this to CSS if (levelI) { // indented and thin outerWidth = availWidth / (levelI + forward + 1); } else { if (forward) { // moderately wide, aligned left still outerWidth = ((availWidth / (forward + 1)) - (12 / 2)) * 2; // 12 is the predicted width of resizer = } else { // can be entire width, aligned left outerWidth = availWidth; } } left = leftmost + // leftmost possible (availWidth / (levelI + forward + 1) * levelI) // indentation * dis + (rtl ? availWidth - outerWidth : 0); // rtl seg.top = top; seg.left = left; seg.outerWidth = outerWidth; seg.outerHeight = bottom - top; html += slotSegHtml(event, seg, className); } slotSegmentContainer[0].innerHTML = html; // faster than html() eventElements = slotSegmentContainer.children(); // retrieve elements, run through eventRender callback, bind event handlers for (i = 0; i < segCnt; i++) { seg = segs[i]; event = seg.event; eventElement = $(eventElements[i]); // faster than eq() triggerRes = view.trigger('eventRender', event, event, eventElement); if (triggerRes === false) { eventElement.remove(); } else { if (triggerRes && triggerRes !== true) { eventElement.remove(); eventElement = $(triggerRes) .css({ position: 'absolute', top: seg.top, left: seg.left }) .appendTo(slotSegmentContainer); } seg.element = eventElement; if (event._id === modifiedEventId) { slotSegBind(event, eventElement, seg); } else { eventElement[0]._fci = i; // for lazySegBind } view.reportEventElement(event, eventElement); } } lazySegBind(slotSegmentContainer, segs, slotSegBind); // record event sides and title positions for (i = 0; i < segCnt; i++) { seg = segs[i]; if (eventElement = seg.element) { val = vsideCache[key = seg.key = cssKey(eventElement[0])]; seg.vsides = val === undefined ? (vsideCache[key] = vsides(eventElement[0], true)) : val; val = hsideCache[key]; seg.hsides = val === undefined ? (hsideCache[key] = hsides(eventElement[0], true)) : val; titleSpan = eventElement.find('span.fc-event-title'); if (titleSpan.length) { seg.titleTop = titleSpan[0].offsetTop; } } } // set all positions/dimensions at once for (i = 0; i < segCnt; i++) { seg = segs[i]; if (eventElement = seg.element) { eventElement[0].style.width = seg.outerWidth - seg.hsides + 'px'; eventElement[0].style.height = (height = seg.outerHeight - seg.vsides) + 'px'; event = seg.event; if (seg.titleTop !== undefined && height - seg.titleTop < 10) { // not enough room for title, put it in the time header eventElement.find('span.fc-event-time') .text(formatDate(event.start, view.option('timeFormat')) + ' - ' + event.title); eventElement.find('span.fc-event-title') .remove(); } view.trigger('eventAfterRender', event, event, eventElement); } } } function slotSegHtml(event, seg, className) { return "
" + "" + "" + "" + htmlEscape(formatDates(event.start, event.end, view.option('timeFormat'))) + "" + "" + htmlEscape(event.title) + "" + "" + ((event.editable || event.editable === undefined && options.editable) && !options.disableResizing && $.fn.resizable ? "
=
" : '') + "
"; } function daySegBind(event, eventElement, seg) { view.eventElementHandlers(event, eventElement); if (event.editable || event.editable === undefined && options.editable) { draggableDayEvent(event, eventElement, seg.isStart); if (seg.isEnd) { view.resizableDayEvent(event, eventElement, colWidth); } } } function slotSegBind(event, eventElement, seg) { view.eventElementHandlers(event, eventElement); if (event.editable || event.editable === undefined && options.editable) { var timeElement = eventElement.find('span.fc-event-time'); draggableSlotEvent(event, eventElement, timeElement); if (seg.isEnd) { resizableSlotEvent(event, eventElement, timeElement); } } } /* Event Dragging -----------------------------------------------------------------------------*/ // when event starts out FULL-DAY function draggableDayEvent(event, eventElement, isStart) { if (!options.disableDragging && eventElement.draggable) { var origPosition, origWidth, resetElement, allDay = true, matrix; eventElement.draggable({ zIndex: 9, opacity: view.option('dragOpacity', 'month'), // use whatever the month view was using revertDuration: options.dragRevertDuration, start: function(ev, ui) { view.hideEvents(event, eventElement); view.trigger('eventDragStart', eventElement, event, ev, ui); origPosition = eventElement.position(); origWidth = eventElement.width(); resetElement = function() { if (!allDay) { eventElement .width(origWidth) .height('') .draggable('option', 'grid', null); allDay = true; } }; matrix = buildDayMatrix(function(cell) { eventElement.draggable('option', 'revert', !cell || !cell.rowDelta && !cell.colDelta); view.clearOverlays(); if (cell) { if (!cell.row) { // on full-days renderDayOverlay( matrix, addDays(cloneDate(event.start), cell.colDelta), addDays(exclEndDay(event), cell.colDelta) ); resetElement(); } else { // mouse is over bottom slots if (isStart && allDay) { // convert event to temporary slot-event 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) ); eventElement.draggable('option', 'grid', [colWidth, 1]); allDay = false; } } } }, true); matrix.mouse(ev); }, drag: function(ev, ui) { matrix.mouse(ev); }, stop: function(ev, ui) { view.trigger('eventDragStop', eventElement, event, ev, ui); view.clearOverlays(); var cell = matrix.cell; var dayDelta = dis * ( allDay ? // can't trust cell.colDelta when using slot grid (cell ? cell.colDelta : 0) : Math.floor((ui.position.left - origPosition.left) / colWidth) ); if (!cell || !dayDelta && !cell.rowDelta) { // over nothing (has reverted) resetElement(); if ($.browser.msie) { eventElement.css('filter', ''); // clear IE opacity side-effects } view.showEvents(event, eventElement); } else { eventElement.find('a').removeAttr('href'); // prevents safari from visiting the link view.eventDrop( this, event, dayDelta, allDay ? 0 : // minute delta Math.round((eventElement.offset().top - bodyContent.offset().top) / slotHeight) * options.slotMinutes + minMinute - (event.start.getHours() * 60 + event.start.getMinutes()), allDay, ev, ui ); } } }); } } // when event starts out IN TIMESLOTS function draggableSlotEvent(event, eventElement, timeElement) { if (!options.disableDragging && eventElement.draggable) { var origPosition, resetElement, prevSlotDelta, slotDelta, allDay = false, matrix; eventElement.draggable({ zIndex: 9, scroll: false, grid: [colWidth, slotHeight], axis: colCnt == 1 ? 'y' : false, opacity: view.option('dragOpacity'), revertDuration: options.dragRevertDuration, start: function(ev, ui) { view.hideEvents(event, eventElement); view.trigger('eventDragStart', eventElement, event, ev, ui); if ($.browser.msie) { eventElement.find('span.fc-event-bg').hide(); // nested opacities mess up in IE, just hide } origPosition = eventElement.position(); resetElement = function() { // convert back to original slot-event if (allDay) { timeElement.css('display', ''); // show() was causing display=inline eventElement.draggable('option', 'grid', [colWidth, slotHeight]); allDay = false; } }; prevSlotDelta = 0; matrix = buildDayMatrix(function(cell) { eventElement.draggable('option', 'revert', !cell); view.clearOverlays(); if (cell) { if (!cell.row && options.allDaySlot) { // over full days if (!allDay) { // convert to temporary all-day event allDay = true; timeElement.hide(); eventElement.draggable('option', 'grid', null); } renderDayOverlay( matrix, addDays(cloneDate(event.start), cell.colDelta), addDays(exclEndDay(event), cell.colDelta) ); } else { // on slots resetElement(); } } }, true); matrix.mouse(ev); }, drag: function(ev, ui) { slotDelta = Math.round((ui.position.top - origPosition.top) / slotHeight); if (slotDelta != prevSlotDelta) { if (!allDay) { // update time header var minuteDelta = slotDelta * options.slotMinutes, newStart = addMinutes(cloneDate(event.start), minuteDelta), newEnd; if (event.end) { newEnd = addMinutes(cloneDate(event.end), minuteDelta); } timeElement.text(formatDates(newStart, newEnd, view.option('timeFormat'))); } prevSlotDelta = slotDelta; } matrix.mouse(ev); }, stop: function(ev, ui) { view.clearOverlays(); view.trigger('eventDragStop', eventElement, event, ev, ui); var cell = matrix.cell, dayDelta = dis * ( allDay ? // can't trust cell.colDelta when using slot grid (cell ? cell.colDelta : 0) : Math.floor((ui.position.left - origPosition.left) / colWidth) ); if (!cell || !slotDelta && !dayDelta) { resetElement(); if ($.browser.msie) { eventElement .css('filter', '')// clear IE opacity side-effects .find('span.fc-event-bg').css('display', ''); // .show() made display=inline } eventElement.css(origPosition); // sometimes fast drags make event revert to wrong position view.showEvents(event, eventElement); } else { view.eventDrop( this, event, dayDelta, allDay ? 0 : slotDelta * options.slotMinutes, // minute delta allDay, ev, ui ); } } }); } } /* Event Resizing -----------------------------------------------------------------------------*/ // for TIMESLOT events function resizableSlotEvent(event, eventElement, timeElement) { if (!options.disableResizing && eventElement.resizable) { var slotDelta, prevSlotDelta; eventElement.resizable({ handles: { s: 'div.ui-resizable-s' }, grid: slotHeight, start: function(ev, ui) { slotDelta = prevSlotDelta = 0; view.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); }, 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 } } }); } } /* Selecting -----------------------------------------------------------------------------*/ daySelectionManager = new SelectionManager( view, unselect, function(startDate, endDate, allDay) { renderDayOverlay( selectionMatrix, startDate, addDays(cloneDate(endDate), 1) ); }, clearSelection ); function daySelectionMousedown(ev) { if (view.option('selectable')) { selectionMatrix = buildDayMatrix(function(cell) { if (cell) { var d = dayColDate(cell.col); daySelectionManager.drag(d, d, true); } else { daySelectionManager.drag(); } }); documentDragHelp( function(ev) { selectionMatrix.mouse(ev); }, function(ev) { daySelectionManager.dragStop(ev); } ); daySelectionManager.dragStart(ev); selectionMatrix.mouse(ev); return false; // prevent auto-unselect and text selection } } slotSelectionManager = new SelectionManager( view, unselect, renderSlotSelection, clearSelection ); function slotSelectionMousedown(ev) { if (view.option('selectable')) { selectionMatrix = buildSlotMatrix(function(cell) { if (cell) { var d = slotCellDate(cell.row, cell.origCol); slotSelectionManager.drag(d, addMinutes(cloneDate(d), options.slotMinutes), false); } else { slotSelectionManager.drag(); } }); documentDragHelp( function(ev) { selectionMatrix.mouse(ev); }, function(ev) { slotSelectionManager.dragStop(ev); } ); slotSelectionManager.dragStart(ev); selectionMatrix.mouse(ev); return false; // prevent auto-unselect and text selection } } documentUnselectAuto(view, unselect); this.select = function(start, end, allDay) { if (allDay) { if (options.allDaySlot) { if (!end) { end = cloneDate(start); } selectionMatrix = buildDayMatrix(); daySelectionManager.select(start, end, allDay); } } else { if (!end) { end = addMinutes(cloneDate(start), options.slotMinutes); } selectionMatrix = buildSlotMatrix(); slotSelectionManager.select(start, end, allDay); } }; function unselect() { slotSelectionManager.unselect(); daySelectionManager.unselect(); } this.unselect = unselect; /* Selecting drawing utils -----------------------------------------------------------------------------*/ function renderSlotSelection(startDate, endDate) { var helperOption = view.option('selectHelper'); if (helperOption) { var col = dayDiff(startDate, view.visStart); if (col >= 0 && col < colCnt) { // only works when times are on same day var rect = selectionMatrix.rect(0, col * dis + dit, 1, col * dis + dit + 1, 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(selectionMatrix, startDate, endDate); } } function clearSelection() { clearOverlays(); if (selectionHelper) { selectionHelper.remove(); selectionHelper = null; } } /* Semi-transparent Overlay Helpers -----------------------------------------------------*/ function renderDayOverlay(matrix, 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) { var rect = matrix.rect(0, startCol, 1, endCol, head); dayBind( view.renderOverlay(rect, head) ); } } function renderSlotOverlay(matrix, overlayStart, overlayEnd) { var dayStart = cloneDate(view.visStart); var dayEnd = addDays(cloneDate(dayStart), 1); for (var i = 0; i < colCnt; i++) { var stretchStart = new Date(Math.max(dayStart, overlayStart)); var stretchEnd = new Date(Math.min(dayEnd, overlayEnd)); if (stretchStart < stretchEnd) { var rect = matrix.rect(0, i * dis + dit, 1, i * dis + dit + 1, bodyContent); // only use it for horizontal coords var top = timePosition(dayStart, stretchStart); var bottom = timePosition(dayStart, stretchEnd); rect.top = top; rect.height = bottom - top; slotBind( view.renderOverlay(rect, bodyContent) ); } addDays(dayStart, 1); addDays(dayEnd, 1); } } function clearOverlays() { view.clearOverlays(); } /* Coordinate Utilities -----------------------------------------------------------------------------*/ // get the Y coordinate of the given time on the given day (both Date objects) function timePosition(day, time) { // both date objects. day holds 00:00 of current day day = cloneDate(day, true); if (time < addMinutes(cloneDate(day), minMinute)) { return 0; } if (time >= 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) )); } function buildDayMatrix(changeCallback, includeSlotArea) { var rowElements = options.allDaySlot ? head.find('td') : $([]); if (includeSlotArea) { rowElements = rowElements.add(body); } return new HoverMatrix(rowElements, bg.find('td'), changeCallback); } function buildSlotMatrix(changeCallback) { return new HoverMatrix(bodyTable.find('td'), bg.find('td'), changeCallback); } /* Date Utilities ----------------------------------------------------*/ function slotEventEnd(event) { if (event.end) { return cloneDate(event.end); } else { return addMinutes(cloneDate(event.start), options.defaultEventMinutes); } } function dayOfWeekCol(dayOfWeek) { return ((dayOfWeek - Math.max(firstDay, nwe) + colCnt) % colCnt) * dis + dit; } // generating dates from cell row & columns function dayColDate(col) { return addDays(cloneDate(view.visStart), col * dis + dit); } function slotCellDate(row, col) { var d = dayColDate(col); addMinutes(d, minMinute + row * options.slotMinutes); return d; } } // 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--) { level = levels[i]; for (j = 0; j < level.length; j++) { segForward = level[j]; for (k = 0; k < levels[i - 1].length; k++) { segBack = levels[i - 1][k]; if (segsCollide(segForward, segBack)) { segBack.forward = Math.max(segBack.forward || 0, (segForward.forward || 0) + 1); } } } } } /* 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 * */ init: function(element, options) { this.element = element; this.options = options; this.eventsByID = {}; this.eventElements = []; this.eventElementsByID = {}; this.usedOverlays = []; this.unusedOverlays = []; }, // 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 ? cloneDate(event.end) : this.defaultEventEnd(event); // TODO: make sure always using copies }, // 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 = {}; for (i = 0; i < len; i++) { event = events[i]; if (eventsByID[event._id]) { eventsByID[event._id].push(event); } else { eventsByID[event._id] = [event]; } } }, // report when view creates an element for an event reportEventElement: function(event, element) { this.eventElements.push(element); var eventElementsByID = this.eventElementsByID; if (eventElementsByID[event._id]) { eventElementsByID[event._id].push(element); } else { eventElementsByID[event._id] = [element]; } }, // event element manipulation _clearEvents: function() { // only resets hashes this.eventElements = []; this.eventElementsByID = {}; }, showEvents: function(event, exceptElement) { this._eee(event, exceptElement, 'show'); }, hideEvents: function(event, exceptElement) { this._eee(event, exceptElement, 'hide'); }, _eee: function(event, exceptElement, funcName) { // event-element-each var elements = this.eventElementsByID[event._id], i, len = elements.length; for (i = 0; i < len; i++) { if (elements[i][0] != exceptElement[0]) { // AHAHAHAHAHAHAHAH elements[i][funcName](); } } }, // event modification reporting eventDrop: function(e, event, dayDelta, minuteDelta, allDay, ev, ui) { var view = this, oldAllDay = event.allDay, eventId = event._id; view.moveEvents(view.eventsByID[eventId], dayDelta, minuteDelta, allDay); view.trigger('eventDrop', e, event, dayDelta, minuteDelta, allDay, function() { // TODO: change docs // TODO: investigate cases where this inverse technique might not work view.moveEvents(view.eventsByID[eventId], -dayDelta, -minuteDelta, oldAllDay); view.rerenderEvents(); }, ev, ui); view.eventsChanged = true; view.rerenderEvents(eventId); }, eventResize: function(e, event, dayDelta, minuteDelta, ev, ui) { var view = this, eventId = event._id; view.elongateEvents(view.eventsByID[eventId], dayDelta, minuteDelta); view.trigger('eventResize', e, event, dayDelta, minuteDelta, function() { // TODO: investigate cases where this inverse technique might not work view.elongateEvents(view.eventsByID[eventId], -dayDelta, -minuteDelta); view.rerenderEvents(); }, ev, ui); view.eventsChanged = true; view.rerenderEvents(eventId); }, // event modification moveEvents: function(events, dayDelta, minuteDelta, allDay) { minuteDelta = minuteDelta || 0; for (var e, len = events.length, i = 0; i < len; i++) { e = events[i]; if (allDay !== undefined) { e.allDay = allDay; } addMinutes(addDays(e.start, dayDelta, true), minuteDelta); if (e.end) { e.end = addMinutes(addDays(e.end, dayDelta, true), minuteDelta); } normalizeEvent(e, this.options); } }, elongateEvents: function(events, dayDelta, minuteDelta) { minuteDelta = minuteDelta || 0; for (var e, len = events.length, i = 0; i < len; i++) { e = events[i]; e.end = addMinutes(addDays(this.eventEnd(e), dayDelta, true), minuteDelta); normalizeEvent(e, this.options); } }, // semi-transparent overlay (while dragging or selecting) renderOverlay: function(rect, parent) { var e = this.unusedOverlays.shift(); if (!e) { e = $("
"); } 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()); } }, // 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); } } }); } }, // attaches eventClick, eventMouseover, eventMouseout eventElementHandlers: function(event, eventElement) { var view = this; eventElement .click(function(ev) { if (!eventElement.hasClass('ui-draggable-dragging') && !eventElement.hasClass('ui-resizable-resizing')) { return view.trigger('eventClick', this, event, ev); } }) .hover( function(ev) { view.trigger('eventMouseover', this, event, ev); }, function(ev) { view.trigger('eventMouseout', this, event, ev); } ); }, // 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); } return v; }, // event rendering utilities sliceSegs: function(events, visEventEnds, start, end) { var segs = [], i, len = events.length, event, eventStart, eventEnd, segStart, segEnd, isStart, isEnd; for (i = 0; i < len; i++) { event = events[i]; eventStart = event.start; eventEnd = visEventEnds[i]; if (eventEnd > 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); } }; function lazySegBind(container, segs, bindHandlers) { container.unbind('mouseover').mouseover(function(ev) { var parent = ev.target, e, i, seg; while (parent != this) { e = parent; parent = parent.parentNode; } if ((i = e._fci) !== undefined) { e._fci = undefined; seg = segs[i]; bindHandlers(seg.event, seg.element, seg); $(ev.target).trigger(ev); } ev.stopPropagation(); }); } // event rendering calculation utilities function stackSegs(segs) { var levels = [], i, len = segs.length, seg, j, collide, k; for (i = 0; i < len; i++) { seg = segs[i]; j = 0; // the level index where seg should belong while (true) { collide = false; if (levels[j]) { for (k = 0; k < levels[j].length; k++) { if (segsCollide(levels[j][k], seg)) { collide = true; break; } } } if (collide) { j++; } else { break; } } if (levels[j]) { levels[j].push(seg); } else { levels[j] = [seg]; } } return levels; } 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; } function SelectionManager(view, initFunc, displayFunc, clearFunc) { var t = this; var selected = false; var initialElement; var initialRange; var start; var end; var allDay; t.dragStart = function(ev) { initFunc(); start = end = undefined; initialRange = undefined; initialElement = ev.currentTarget; }; t.drag = function(currentStart, currentEnd, currentAllDay) { if (currentStart) { var range = [currentStart, currentEnd]; if (!initialRange) { initialRange = range; } var dates = initialRange.concat(range).sort(cmp); start = dates[0]; end = dates[3]; allDay = currentAllDay; clearFunc(); displayFunc(cloneDate(start), cloneDate(end), allDay); } else { // called with no arguments start = end = undefined; clearFunc(); } }; t.dragStop = function(ev) { if (start) { if (+initialRange[0] == +start && +initialRange[1] == +end) { view.trigger('dayClick', initialElement, start, allDay, ev); } _select(); } }; t.select = function(newStart, newEnd, newAllDay) { initFunc(); start = newStart; end = newEnd; allDay = newAllDay; displayFunc(cloneDate(start), cloneDate(end), allDay); _select(); }; function _select() { // just set the selected flag, and trigger selected = true; view.trigger('select', view, start, end, allDay); } function unselect() { if (selected) { selected = false; start = end = undefined; clearFunc(); view.trigger('unselect', view); } } t.unselect = unselect; } function documentDragHelp(mousemove, mouseup) { function _mouseup(ev) { mouseup(ev); $(document) .unbind('mousemove', mousemove) .unbind('mouseup', _mouseup); } $(document) .mousemove(mousemove) .mouseup(_mouseup); } function documentUnselectAuto(view, unselectFunc) { 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; } } unselectFunc(); }); } } /* Date Math -----------------------------------------------------------------------------*/ var DAY_MS = 86400000, HOUR_MS = 3600000, MINUTE_MS = 60000; function addYears(d, n, keepTime) { d.setFullYear(d.getFullYear() + n); if (!keepTime) { clearTime(d); } return d; } function addMonths(d, n, keepTime) { // prevents day overflow/underflow if (+d) { // prevent infinite looping on invalid dates var m = d.getMonth() + n, check = cloneDate(d); check.setDate(1); check.setMonth(m); d.setMonth(m); if (!keepTime) { clearTime(d); } while (d.getMonth() != check.getMonth()) { d.setDate(d.getDate() + (d < check ? 1 : -1)); } } return d; } function addDays(d, n, keepTime) { // deals with daylight savings if (+d) { var dd = d.getDate() + n, check = cloneDate(d); check.setHours(9); // set to middle of day check.setDate(dd); d.setDate(dd); if (!keepTime) { clearTime(d); } fixDate(d, check); } 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 while (d.getDate() != check.getDate()) { d.setTime(+d + (d < check ? 1 : -1) * HOUR_MS); } } } function addMinutes(d, n) { d.setMinutes(d.getMinutes() + n); return d; } function clearTime(d) { d.setHours(0); d.setMinutes(0); d.setSeconds(0); d.setMilliseconds(0); 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; do { d = new Date(1970, i++, 1); } while (d.getHours()); // != 0 return d; } function skipWeekend(date, inc, excl) { inc = inc || 1; while (!date.getDay() || (excl && date.getDay() == 1 || !excl && date.getDay() == 6)) { addDays(date, inc); } return date; } function dayDiff(d1, d2) { // d1 - d2 return Math.round((cloneDate(d1, true) - cloneDate(d2, true)) / DAY_MS); } /* Date Parsing -----------------------------------------------------------------------------*/ var parseDate = fc.parseDate = function(s) { if (typeof s == 'object') { // already a Date object return s; } if (typeof s == 'number') { // a UNIX timestamp return new Date(s * 1000); } if (typeof s == 'string') { if (s.match(/^\d+$/)) { // a UNIX timestamp return new Date(parseInt(s) * 1000); } return parseISO8601(s, true) || (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) { // 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; } offset -= date.getTimezoneOffset(); } return new Date(+date + (offset * 60 * 1000)); }; var parseTime = fc.parseTime = function(s) { // returns minutes since start of day if (typeof s == 'number') { // an hour return s * 60; } if (typeof s == 'object') { // a Date object return s.getHours() * 60 + s.getMinutes(); } var m = s.match(/(\d+)(?::(\d+))?\s*(\w+)?/); if (m) { var h = parseInt(m[1]); if (m[3]) { h %= 12; if (m[3].toLowerCase().charAt(0) == 'p') { h += 12; } } return h * 60 + (m[2] ? parseInt(m[2]) : 0); } }; /* Date Formatting -----------------------------------------------------------------------------*/ var formatDate = fc.formatDate = function(date, format, options) { return formatDates(date, null, format, options); }; var formatDates = fc.formatDates = function(date1, date2, format, options) { options = options || defaults; var date = date1, otherDate = date2, i, len = format.length, c, i2, formatter, res = ''; for (i = 0; i < len; i++) { c = format.charAt(i); if (c == "'") { for (i2 = i + 1; i2 < len; i2++) { if (format.charAt(i2) == "'") { if (date) { if (i2 == i + 1) { res += "'"; } else { res += format.substring(i + 1, i2); } i = i2; } break; } } } else if (c == '(') { for (i2 = i + 1; i2 < len; i2++) { if (format.charAt(i2) == ')') { var subres = formatDate(date, format.substring(i + 1, i2), options); if (parseInt(subres.replace(/\D/, ''))) { res += subres; } i = i2; break; } } } else if (c == '[') { for (i2 = i + 1; i2 < len; i2++) { if (format.charAt(i2) == ']') { var subformat = format.substring(i + 1, i2); var subres = formatDate(date, subformat, options); if (subres != formatDate(otherDate, subformat, options)) { res += subres; } i = i2; break; } } } else if (c == '{') { date = date2; otherDate = date1; } else if (c == '}') { date = date1; otherDate = date2; } else { for (i2 = len; i2 > i; i2--) { if (formatter = dateFormatters[format.substring(i, i2)]) { if (date) { res += formatter(date, options); } i = i2 - 1; break; } } if (i2 == i) { if (date) { res += c; } } } } return res; }; var dateFormatters = { s : function(d) { return d.getSeconds() }, ss : function(d) { return zeroPad(d.getSeconds()) }, m : function(d) { return d.getMinutes() }, mm : function(d) { return zeroPad(d.getMinutes()) }, h : function(d) { return d.getHours() % 12 || 12 }, hh : function(d) { return zeroPad(d.getHours() % 12 || 12) }, H : function(d) { return d.getHours() }, HH : function(d) { return zeroPad(d.getHours()) }, d : function(d) { return d.getDate() }, dd : function(d) { return zeroPad(d.getDate()) }, ddd : function(d, o) { return o.dayNamesShort[d.getDay()] }, dddd: function(d, o) { return o.dayNames[d.getDay()] }, M : function(d) { return d.getMonth() + 1 }, MM : function(d) { return zeroPad(d.getMonth() + 1) }, MMM : function(d, o) { return o.monthNamesShort[d.getMonth()] }, MMMM: function(d, o) { return o.monthNames[d.getMonth()] }, yy : function(d) { return (d.getFullYear() + '').substring(2) }, yyyy: function(d) { return d.getFullYear() }, t : function(d) { return d.getHours() < 12 ? 'a' : 'p' }, tt : function(d) { return d.getHours() < 12 ? 'am' : 'pm' }, T : function(d) { return d.getHours() < 12 ? 'A' : 'P' }, TT : function(d) { return d.getHours() < 12 ? 'AM' : 'PM' }, u : function(d) { return formatDate(d, "yyyy-MM-dd'T'HH:mm:ss'Z'") }, S : function(d) { var date = d.getDate(); if (date > 10 && date < 20) { return 'th'; } return ['st', 'nd', 'rd'][date % 10 - 1] || 'th'; } }; /* Element Dimensions -----------------------------------------------------------------------------*/ function setOuterWidth(element, width, includeMargins) { element.each(function(i, _element) { _element.style.width = width - hsides(_element, includeMargins) + 'px'; }); } function setOuterHeight(element, height, includeMargins) { element.each(function(i, _element) { _element.style.height = height - vsides(_element, includeMargins) + 'px'; }); } function hsides(_element, includeMargins) { return (parseFloat(jQuery.curCSS(_element, 'paddingLeft', true)) || 0) + (parseFloat(jQuery.curCSS(_element, 'paddingRight', true)) || 0) + (parseFloat(jQuery.curCSS(_element, 'borderLeftWidth', true)) || 0) + (parseFloat(jQuery.curCSS(_element, 'borderRightWidth', true)) || 0) + (includeMargins ? hmargins(_element) : 0); } function hmargins(_element) { return (parseFloat(jQuery.curCSS(_element, 'marginLeft', true)) || 0) + (parseFloat(jQuery.curCSS(_element, 'marginRight', true)) || 0); } function vsides(_element, includeMargins) { return (parseFloat(jQuery.curCSS(_element, 'paddingTop', true)) || 0) + (parseFloat(jQuery.curCSS(_element, 'paddingBottom', true)) || 0) + (parseFloat(jQuery.curCSS(_element, 'borderTopWidth', true)) || 0) + (parseFloat(jQuery.curCSS(_element, 'borderBottomWidth', true)) || 0) + (includeMargins ? vmargins(_element) : 0); } function vmargins(_element) { return (parseFloat(jQuery.curCSS(_element, 'marginTop', true)) || 0) + (parseFloat(jQuery.curCSS(_element, 'marginBottom', true)) || 0); } function setMinHeight(element, h) { h = typeof h == 'number' ? h + 'px' : h; element[0].style.cssText += ';min-height:' + h + ';_height:' + h; } /* Position Calculation -----------------------------------------------------------------------------*/ // nasty bugs in opera 9.25 // position()'s top returning incorrectly with TR/TD or elements within TD var topBug; function topCorrect(tr) { // tr/th/td or anything else if (topBug !== false) { var cell; if (tr.is('th,td')) { tr = (cell = tr).parent(); } if (topBug === undefined && tr.is('tr')) { topBug = tr.position().top != tr.children().position().top; } if (topBug) { return tr.parent().position().top + (cell ? tr.position().top - cell.position().top : 0); } } return 0; } /* Hover Matrix -----------------------------------------------------------------------------*/ function HoverMatrix(rowElements, colElements, changeCallback) { var t = this, tops = [], lefts = [], origRow, origCol, currRow, currCol, e; $.each(rowElements, function(i, _e) { e = $(_e); tops.push(e.offset().top + topCorrect(e)); }); tops.push(tops[tops.length - 1] + e.outerHeight()); $.each(colElements, function(i, _e) { e = $(_e); lefts.push(e.offset().left); }); lefts.push(lefts[lefts.length - 1] + e.outerWidth()); t.mouse = function(ev) { var x = ev.pageX; var y = ev.pageY; var r, c; for (r = 0; r < tops.length && y >= tops[r]; r++) { } for (c = 0; c < lefts.length && x >= lefts[c]; c++) { } r = r >= tops.length ? -1 : r - 1; c = c >= lefts.length ? -1 : c - 1; if (r != currRow || c != currCol) { currRow = r; currCol = c; if (r == -1 || c == -1) { t.cell = null; } else { if (origRow === undefined) { origRow = r; origCol = c; } t.cell = { row: r, col: c, top: tops[r], left: lefts[c], width: lefts[c + 1] - lefts[c], height: tops[r + 1] - tops[r], origRow: origRow, origCol: origCol, isOrig: r == origRow && c == origCol, rowDelta: r - origRow, colDelta: c - origCol }; } changeCallback(t.cell); } }; t.rect = function(row0, col0, row1, col1, originElement) { // row1,col1 are exclusive var origin = originElement.offset(); return { top: tops[row0] - origin.top, left: lefts[col0] - origin.left, width: lefts[col1] - lefts[col0], height: tops[row1] - tops[row0] }; }; } /* Misc Utils -----------------------------------------------------------------------------*/ var undefined, 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]; } var parts = name.split(/(?=[A-Z])/), i = parts.length - 1, res; for (; i >= 0; i--) { res = obj[parts[i].toLowerCase()]; if (res !== undefined) { return res; } } return obj['']; } function htmlEscape(s) { return s .replace(/&/g, '&') .replace(//g, '>') .replace(/'/g, ''') .replace(/"/g, '"'); } 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 = {}; }; } function cssKey(_element) { return _element.id + '/' + _element.className + '/' + _element.style.cssText.replace(/(^|;)\s*(top|left|width|height)\s*:[^;]*/ig, ''); } function cmp(a, b) { return a - b; } 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) : end; } function disableTextSelection(element) { element .attr('unselectable', 'on') .css('MozUserSelect', 'none') .bind('selectstart.ui', function() { return false; }); } /* function enableTextSelection(element) { element .attr('unselectable', 'off') .css('MozUserSelect', '') .unbind('selectstart.ui'); } */ })(jQuery);