A Pen by Sachin Chavan on CodePen.
Created
March 11, 2020 18:56
-
-
Save sachinsmc/67e80164187d506124446848d88ca66e to your computer and use it in GitHub Desktop.
Calender
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.0/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css"> | |
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.0/js/bootstrap.min.js"></script> | |
<script src="//code.jquery.com/jquery-1.11.1.min.js"></script> | |
<!------ Include the above in your HEAD tag ----------> | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<link href="https://fonts.googleapis.com/css?family=Roboto:100,100i,300,300i,400,400i,500,500i,700,700i,900,900i" | |
rel="stylesheet"> | |
<script> | |
$(document).ready(function() { | |
var date = new Date(); | |
var d = date.getDate(); | |
var m = date.getMonth(); | |
var y = date.getFullYear(); | |
/* className colors | |
className: default(transparent), important(red), chill(pink), success(green), info(blue) | |
*/ | |
/* initialize the external events | |
-----------------------------------------------------------------*/ | |
$('#external-events div.external-event').each(function() { | |
// create an Event Object (http://arshaw.com/fullcalendar/docs/event_data/Event_Object/) | |
// it doesn't need to have a start or end | |
var eventObject = { | |
title: $.trim($(this).text()) // use the element's text as the event title | |
}; | |
// store the Event Object in the DOM element so we can get to it later | |
$(this).data('eventObject', eventObject); | |
// make the event draggable using jQuery UI | |
$(this).draggable({ | |
zIndex: 999, | |
revert: true, // will cause the event to go back to its | |
revertDuration: 0 // original position after the drag | |
}); | |
}); | |
/* initialize the calendar | |
-----------------------------------------------------------------*/ | |
var calendar = $('#calendar').fullCalendar({ | |
header: { | |
left: 'title', | |
center: 'agendaDay,agendaWeek,month', | |
right: 'prev,next today' | |
}, | |
editable: true, | |
firstDay: 1, // 1(Monday) this can be changed to 0(Sunday) for the USA system | |
selectable: true, | |
defaultView: 'month', | |
axisFormat: 'h:mm', | |
columnFormat: { | |
month: 'ddd', // Mon | |
week: 'ddd d', // Mon 7 | |
day: 'dddd M/d', // Monday 9/7 | |
agendaDay: 'dddd d' | |
}, | |
titleFormat: { | |
month: 'MMMM yyyy', // September 2009 | |
week: "MMMM yyyy", // September 2009 | |
day: 'MMMM yyyy' // Tuesday, Sep 8, 2009 | |
}, | |
allDaySlot: false, | |
selectHelper: true, | |
select: function(start, end, allDay) { | |
var title = prompt('Event Title:'); | |
if (title) { | |
calendar.fullCalendar('renderEvent', { | |
title: title, | |
start: start, | |
end: end, | |
allDay: allDay | |
}, | |
true // make the event "stick" | |
); | |
} | |
calendar.fullCalendar('unselect'); | |
}, | |
droppable: true, // this allows things to be dropped onto the calendar !!! | |
drop: function(date, allDay) { // this function is called when something is dropped | |
// retrieve the dropped element's stored Event Object | |
var originalEventObject = $(this).data('eventObject'); | |
// we need to copy it, so that multiple events don't have a reference to the same object | |
var copiedEventObject = $.extend({}, originalEventObject); | |
// assign it the date that was reported | |
copiedEventObject.start = date; | |
copiedEventObject.allDay = allDay; | |
// render the event on the calendar | |
// the last `true` argument determines if the event "sticks" (http://arshaw.com/fullcalendar/docs/event_rendering/renderEvent/) | |
$('#calendar').fullCalendar('renderEvent', copiedEventObject, true); | |
// is the "remove after drop" checkbox checked? | |
if ($('#drop-remove').is(':checked')) { | |
// if so, remove the element from the "Draggable Events" list | |
$(this).remove(); | |
} | |
}, | |
events: [{ | |
title: 'All Day Event', | |
start: new Date(y, m, 1) | |
}, | |
{ | |
id: 999, | |
title: 'Repeating Event', | |
start: new Date(y, m, d - 3, 16, 0), | |
allDay: false, | |
className: 'info' | |
}, | |
{ | |
id: 999, | |
title: 'Repeating Event', | |
start: new Date(y, m, d + 4, 16, 0), | |
allDay: false, | |
className: 'info' | |
}, | |
{ | |
title: 'Meeting', | |
start: new Date(y, m, d, 10, 30), | |
allDay: false, | |
className: 'important' | |
}, | |
{ | |
title: 'Lunch', | |
start: new Date(y, m, d, 12, 0), | |
end: new Date(y, m, d, 14, 0), | |
allDay: false, | |
className: 'important' | |
}, | |
{ | |
title: 'Birthday Party', | |
start: new Date(y, m, d + 1, 19, 0), | |
end: new Date(y, m, d + 1, 22, 30), | |
allDay: false, | |
}, | |
{ | |
title: 'Click for Google', | |
start: new Date(y, m, 28), | |
end: new Date(y, m, 29), | |
url: 'https://ccp.cloudaccess.net/aff.php?aff=5188', | |
className: 'success' | |
} | |
], | |
}); | |
}); | |
</script> | |
<style> | |
body { | |
margin-bottom: 40px; | |
margin-top: 40px; | |
text-align: center; | |
font-size: 14px; | |
font-family: 'Roboto', sans-serif; | |
background: url(http://www.digiphotohub.com/wp-content/uploads/2015/09/bigstock-Abstract-Blurred-Background-Of-92820527.jpg); | |
} | |
#wrap { | |
width: 1100px; | |
margin: 0 auto; | |
} | |
#external-events { | |
float: left; | |
width: 150px; | |
padding: 0 10px; | |
text-align: left; | |
} | |
#external-events h4 { | |
font-size: 16px; | |
margin-top: 0; | |
padding-top: 1em; | |
} | |
.external-event { | |
/* try to mimick the look of a real event */ | |
margin: 10px 0; | |
padding: 2px 4px; | |
background: #3366CC; | |
color: #fff; | |
font-size: .85em; | |
cursor: pointer; | |
} | |
#external-events p { | |
margin: 1.5em 0; | |
font-size: 11px; | |
color: #666; | |
} | |
#external-events p input { | |
margin: 0; | |
vertical-align: middle; | |
} | |
#calendar { | |
/* float: right; */ | |
margin: 0 auto; | |
width: 900px; | |
background-color: #FFFFFF; | |
border-radius: 6px; | |
box-shadow: 0 1px 2px #C3C3C3; | |
-webkit-box-shadow: 0px 0px 21px 2px rgba(0, 0, 0, 0.18); | |
-moz-box-shadow: 0px 0px 21px 2px rgba(0, 0, 0, 0.18); | |
box-shadow: 0px 0px 21px 2px rgba(0, 0, 0, 0.18); | |
} | |
</style> | |
</head> | |
<body> | |
<div id='wrap'> | |
<div id='calendar'></div> | |
<div style='clear:both'></div> | |
</div> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! | |
* FullCalendar v1.6.4 | |
* Docs & License: http://arshaw.com/fullcalendar/ | |
* (c) 2013 Adam Shaw | |
*/ | |
/* | |
* Use fullcalendar.css for basic styling. | |
* For event drag & drop, requires jQuery UI draggable. | |
* For event resizing, requires jQuery UI resizable. | |
*/ | |
(function($, undefined) { | |
var defaults = { | |
// display | |
defaultView: "month", | |
aspectRatio: 1.35, | |
header: { | |
left: "title", | |
center: "", | |
right: "today prev,next" | |
}, | |
weekends: true, | |
weekNumbers: false, | |
weekNumberCalculation: "iso", | |
weekNumberTitle: "W", | |
// editing | |
//editable: false, | |
//disableDragging: false, | |
//disableResizing: false, | |
allDayDefault: true, | |
ignoreTimezone: 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: "<span class='fc-text-arrow'>‹</span>", | |
next: "<span class='fc-text-arrow'>›</span>", | |
prevYear: "<span class='fc-text-arrow'>«</span>", | |
nextYear: "<span class='fc-text-arrow'>»</span>", | |
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, | |
dropAccept: "*", | |
handleWindowResize: true | |
}; | |
// right-to-left defaults | |
var rtlDefaults = { | |
header: { | |
left: "next,prev today", | |
center: "", | |
right: "title" | |
}, | |
buttonText: { | |
prev: "<span class='fc-text-arrow'>›</span>", | |
next: "<span class='fc-text-arrow'>‹</span>", | |
prevYear: "<span class='fc-text-arrow'>»</span>", | |
nextYear: "<span class='fc-text-arrow'>«</span>" | |
}, | |
buttonIcons: { | |
prev: "circle-triangle-e", | |
next: "circle-triangle-w" | |
} | |
}; | |
var fc = ($.fullCalendar = { version: "1.6.4" }); | |
var fcViews = (fc.views = {}); | |
$.fn.fullCalendar = function(options) { | |
// method calling | |
if (typeof options == "string") { | |
var args = Array.prototype.slice.call(arguments, 1); | |
var res; | |
this.each(function() { | |
var calendar = $.data(this, "fullCalendar"); | |
if (calendar && $.isFunction(calendar[options])) { | |
var r = calendar[options].apply(calendar, args); | |
if (res === undefined) { | |
res = r; | |
} | |
if (options == "destroy") { | |
$.removeData(this, "fullCalendar"); | |
} | |
} | |
}); | |
if (res !== undefined) { | |
return res; | |
} | |
return this; | |
} | |
options = options || {}; | |
// would like to have this logic in EventManager, but needs to happen before options are recursively extended | |
var eventSources = options.eventSources || []; | |
delete options.eventSources; | |
if (options.events) { | |
eventSources.push(options.events); | |
delete options.events; | |
} | |
options = $.extend( | |
true, | |
{}, | |
defaults, | |
options.isRTL || (options.isRTL === undefined && defaults.isRTL) | |
? rtlDefaults | |
: {}, | |
options | |
); | |
this.each(function(i, _element) { | |
var element = $(_element); | |
var calendar = new Calendar(element, options, eventSources); | |
element.data("fullCalendar", calendar); // TODO: look into memory leak implications | |
calendar.render(); | |
}); | |
return this; | |
}; | |
// function for adding/overriding defaults | |
function setDefaults(d) { | |
$.extend(true, defaults, d); | |
} | |
function Calendar(element, options, eventSources) { | |
var t = this; | |
// exports | |
t.options = options; | |
t.render = render; | |
t.destroy = destroy; | |
t.refetchEvents = refetchEvents; | |
t.reportEvents = reportEvents; | |
t.reportEventChange = reportEventChange; | |
t.rerenderEvents = rerenderEvents; | |
t.changeView = changeView; | |
t.select = select; | |
t.unselect = unselect; | |
t.prev = prev; | |
t.next = next; | |
t.prevYear = prevYear; | |
t.nextYear = nextYear; | |
t.today = today; | |
t.gotoDate = gotoDate; | |
t.incrementDate = incrementDate; | |
t.formatDate = function(format, date) { | |
return formatDate(format, date, options); | |
}; | |
t.formatDates = function(format, date1, date2) { | |
return formatDates(format, date1, date2, options); | |
}; | |
t.getDate = getDate; | |
t.getView = getView; | |
t.option = option; | |
t.trigger = trigger; | |
// imports | |
EventManager.call(t, options, eventSources); | |
var isFetchNeeded = t.isFetchNeeded; | |
var fetchEvents = t.fetchEvents; | |
// locals | |
var _element = element[0]; | |
var header; | |
var headerElement; | |
var content; | |
var tm; // for making theme classes | |
var currentView; | |
var elementOuterWidth; | |
var suggestedViewHeight; | |
var resizeUID = 0; | |
var ignoreWindowResize = 0; | |
var date = new Date(); | |
var events = []; | |
var _dragElement; | |
/* Main Rendering | |
-----------------------------------------------------------------------------*/ | |
setYMD(date, options.year, options.month, options.date); | |
function render(inc) { | |
if (!content) { | |
initialRender(); | |
} else if (elementVisible()) { | |
// mainly for the public API | |
calcSize(); | |
_renderView(inc); | |
} | |
} | |
function initialRender() { | |
tm = options.theme ? "ui" : "fc"; | |
element.addClass("fc"); | |
if (options.isRTL) { | |
element.addClass("fc-rtl"); | |
} else { | |
element.addClass("fc-ltr"); | |
} | |
if (options.theme) { | |
element.addClass("ui-widget"); | |
} | |
content = $("<div class='fc-content' style='position:relative'/>").prependTo( | |
element | |
); | |
header = new Header(t, options); | |
headerElement = header.render(); | |
if (headerElement) { | |
element.prepend(headerElement); | |
} | |
changeView(options.defaultView); | |
if (options.handleWindowResize) { | |
$(window).resize(windowResize); | |
} | |
// needed for IE in a 0x0 iframe, b/c when it is resized, never triggers a windowResize | |
if (!bodyVisible()) { | |
lateRender(); | |
} | |
} | |
// called when we know the calendar couldn't be rendered when it was initialized, | |
// but we think it's ready now | |
function lateRender() { | |
setTimeout(function() { | |
// IE7 needs this so dimensions are calculated correctly | |
if (!currentView.start && bodyVisible()) { | |
// !currentView.start makes sure this never happens more than once | |
renderView(); | |
} | |
}, 0); | |
} | |
function destroy() { | |
if (currentView) { | |
trigger("viewDestroy", currentView, currentView, currentView.element); | |
currentView.triggerEventDestroy(); | |
} | |
$(window).unbind("resize", windowResize); | |
header.destroy(); | |
content.remove(); | |
element.removeClass("fc fc-rtl ui-widget"); | |
} | |
function elementVisible() { | |
return element.is(":visible"); | |
} | |
function bodyVisible() { | |
return $("body").is(":visible"); | |
} | |
/* View Rendering | |
-----------------------------------------------------------------------------*/ | |
function changeView(newViewName) { | |
if (!currentView || newViewName != currentView.name) { | |
_changeView(newViewName); | |
} | |
} | |
function _changeView(newViewName) { | |
ignoreWindowResize++; | |
if (currentView) { | |
trigger("viewDestroy", currentView, currentView, currentView.element); | |
unselect(); | |
currentView.triggerEventDestroy(); // trigger 'eventDestroy' for each event | |
freezeContentHeight(); | |
currentView.element.remove(); | |
header.deactivateButton(currentView.name); | |
} | |
header.activateButton(newViewName); | |
currentView = new fcViews[newViewName]( | |
$( | |
"<div class='fc-view fc-view-" + | |
newViewName + | |
"' style='position:relative'/>" | |
).appendTo(content), | |
t // the calendar object | |
); | |
renderView(); | |
unfreezeContentHeight(); | |
ignoreWindowResize--; | |
} | |
function renderView(inc) { | |
if ( | |
!currentView.start || // never rendered before | |
inc || | |
date < currentView.start || | |
date >= currentView.end // or new date range | |
) { | |
if (elementVisible()) { | |
_renderView(inc); | |
} | |
} | |
} | |
function _renderView(inc) { | |
// assumes elementVisible | |
ignoreWindowResize++; | |
if (currentView.start) { | |
// already been rendered? | |
trigger("viewDestroy", currentView, currentView, currentView.element); | |
unselect(); | |
clearEvents(); | |
} | |
freezeContentHeight(); | |
currentView.render(date, inc || 0); // the view's render method ONLY renders the skeleton, nothing else | |
setSize(); | |
unfreezeContentHeight(); | |
(currentView.afterRender || noop)(); | |
updateTitle(); | |
updateTodayButton(); | |
trigger("viewRender", currentView, currentView, currentView.element); | |
currentView.trigger("viewDisplay", _element); // deprecated | |
ignoreWindowResize--; | |
getAndRenderEvents(); | |
} | |
/* Resizing | |
-----------------------------------------------------------------------------*/ | |
function updateSize() { | |
if (elementVisible()) { | |
unselect(); | |
clearEvents(); | |
calcSize(); | |
setSize(); | |
renderEvents(); | |
} | |
} | |
function calcSize() { | |
// assumes elementVisible | |
if (options.contentHeight) { | |
suggestedViewHeight = options.contentHeight; | |
} else if (options.height) { | |
suggestedViewHeight = | |
options.height - | |
(headerElement ? headerElement.height() : 0) - | |
vsides(content); | |
} else { | |
suggestedViewHeight = Math.round( | |
content.width() / Math.max(options.aspectRatio, 0.5) | |
); | |
} | |
} | |
function setSize() { | |
// assumes elementVisible | |
if (suggestedViewHeight === undefined) { | |
calcSize(); // for first time | |
// NOTE: we don't want to recalculate on every renderView because | |
// it could result in oscillating heights due to scrollbars. | |
} | |
ignoreWindowResize++; | |
currentView.setHeight(suggestedViewHeight); | |
currentView.setWidth(content.width()); | |
ignoreWindowResize--; | |
elementOuterWidth = element.outerWidth(); | |
} | |
function windowResize() { | |
if (!ignoreWindowResize) { | |
if (currentView.start) { | |
// view has already been rendered | |
var uid = ++resizeUID; | |
setTimeout(function() { | |
// add a delay | |
if (uid == resizeUID && !ignoreWindowResize && elementVisible()) { | |
if (elementOuterWidth != (elementOuterWidth = element.outerWidth())) { | |
ignoreWindowResize++; // in case the windowResize callback changes the height | |
updateSize(); | |
currentView.trigger("windowResize", _element); | |
ignoreWindowResize--; | |
} | |
} | |
}, 200); | |
} else { | |
// calendar must have been initialized in a 0x0 iframe that has just been resized | |
lateRender(); | |
} | |
} | |
} | |
/* Event Fetching/Rendering | |
-----------------------------------------------------------------------------*/ | |
// TODO: going forward, most of this stuff should be directly handled by the view | |
function refetchEvents() { | |
// can be called as an API method | |
clearEvents(); | |
fetchAndRenderEvents(); | |
} | |
function rerenderEvents(modifiedEventID) { | |
// can be called as an API method | |
clearEvents(); | |
renderEvents(modifiedEventID); | |
} | |
function renderEvents(modifiedEventID) { | |
// TODO: remove modifiedEventID hack | |
if (elementVisible()) { | |
currentView.setEventData(events); // for View.js, TODO: unify with renderEvents | |
currentView.renderEvents(events, modifiedEventID); // actually render the DOM elements | |
currentView.trigger("eventAfterAllRender"); | |
} | |
} | |
function clearEvents() { | |
currentView.triggerEventDestroy(); // trigger 'eventDestroy' for each event | |
currentView.clearEvents(); // actually remove the DOM elements | |
currentView.clearEventData(); // for View.js, TODO: unify with clearEvents | |
} | |
function getAndRenderEvents() { | |
if ( | |
!options.lazyFetching || | |
isFetchNeeded(currentView.visStart, currentView.visEnd) | |
) { | |
fetchAndRenderEvents(); | |
} else { | |
renderEvents(); | |
} | |
} | |
function fetchAndRenderEvents() { | |
fetchEvents(currentView.visStart, currentView.visEnd); | |
// ... will call reportEvents | |
// ... which will call renderEvents | |
} | |
// called when event data arrives | |
function reportEvents(_events) { | |
events = _events; | |
renderEvents(); | |
} | |
// called when a single event's data has been changed | |
function reportEventChange(eventID) { | |
rerenderEvents(eventID); | |
} | |
/* Header Updating | |
-----------------------------------------------------------------------------*/ | |
function updateTitle() { | |
header.updateTitle(currentView.title); | |
} | |
function updateTodayButton() { | |
var today = new Date(); | |
if (today >= currentView.start && today < currentView.end) { | |
header.disableButton("today"); | |
} else { | |
header.enableButton("today"); | |
} | |
} | |
/* Selection | |
-----------------------------------------------------------------------------*/ | |
function select(start, end, allDay) { | |
currentView.select(start, end, allDay === undefined ? true : allDay); | |
} | |
function unselect() { | |
// safe to be called before renderView | |
if (currentView) { | |
currentView.unselect(); | |
} | |
} | |
/* Date | |
-----------------------------------------------------------------------------*/ | |
function prev() { | |
renderView(-1); | |
} | |
function next() { | |
renderView(1); | |
} | |
function prevYear() { | |
addYears(date, -1); | |
renderView(); | |
} | |
function nextYear() { | |
addYears(date, 1); | |
renderView(); | |
} | |
function today() { | |
date = new Date(); | |
renderView(); | |
} | |
function gotoDate(year, month, dateOfMonth) { | |
if (year instanceof Date) { | |
date = cloneDate(year); // provided 1 argument, a Date | |
} else { | |
setYMD(date, year, month, dateOfMonth); | |
} | |
renderView(); | |
} | |
function incrementDate(years, months, days) { | |
if (years !== undefined) { | |
addYears(date, years); | |
} | |
if (months !== undefined) { | |
addMonths(date, months); | |
} | |
if (days !== undefined) { | |
addDays(date, days); | |
} | |
renderView(); | |
} | |
function getDate() { | |
return cloneDate(date); | |
} | |
/* Height "Freezing" | |
-----------------------------------------------------------------------------*/ | |
function freezeContentHeight() { | |
content.css({ | |
width: "100%", | |
height: content.height(), | |
overflow: "hidden" | |
}); | |
} | |
function unfreezeContentHeight() { | |
content.css({ | |
width: "", | |
height: "", | |
overflow: "" | |
}); | |
} | |
/* Misc | |
-----------------------------------------------------------------------------*/ | |
function getView() { | |
return currentView; | |
} | |
function option(name, value) { | |
if (value === undefined) { | |
return options[name]; | |
} | |
if (name == "height" || name == "contentHeight" || name == "aspectRatio") { | |
options[name] = value; | |
updateSize(); | |
} | |
} | |
function trigger(name, thisObj) { | |
if (options[name]) { | |
return options[name].apply( | |
thisObj || _element, | |
Array.prototype.slice.call(arguments, 2) | |
); | |
} | |
} | |
/* External Dragging | |
------------------------------------------------------------------------*/ | |
if (options.droppable) { | |
$(document) | |
.bind("dragstart", function(ev, ui) { | |
var _e = ev.target; | |
var e = $(_e); | |
if (!e.parents(".fc").length) { | |
// not already inside a calendar | |
var accept = options.dropAccept; | |
if ($.isFunction(accept) ? accept.call(_e, e) : e.is(accept)) { | |
_dragElement = _e; | |
currentView.dragStart(_dragElement, ev, ui); | |
} | |
} | |
}) | |
.bind("dragstop", function(ev, ui) { | |
if (_dragElement) { | |
currentView.dragStop(_dragElement, ev, ui); | |
_dragElement = null; | |
} | |
}); | |
} | |
} | |
function Header(calendar, options) { | |
var t = this; | |
// exports | |
t.render = render; | |
t.destroy = destroy; | |
t.updateTitle = updateTitle; | |
t.activateButton = activateButton; | |
t.deactivateButton = deactivateButton; | |
t.disableButton = disableButton; | |
t.enableButton = enableButton; | |
// locals | |
var element = $([]); | |
var tm; | |
function render() { | |
tm = options.theme ? "ui" : "fc"; | |
var sections = options.header; | |
if (sections) { | |
element = $("<table class='fc-header' style='width:100%'/>").append( | |
$("<tr/>") | |
.append(renderSection("left")) | |
.append(renderSection("center")) | |
.append(renderSection("right")) | |
); | |
return element; | |
} | |
} | |
function destroy() { | |
element.remove(); | |
} | |
function renderSection(position) { | |
var e = $("<td class='fc-header-" + position + "'/>"); | |
var buttonStr = options.header[position]; | |
if (buttonStr) { | |
$.each(buttonStr.split(" "), function(i) { | |
if (i > 0) { | |
e.append("<span class='fc-header-space'/>"); | |
} | |
var prevButton; | |
$.each(this.split(","), function(j, buttonName) { | |
if (buttonName == "title") { | |
e.append("<span class='fc-header-title'><h2> </h2></span>"); | |
if (prevButton) { | |
prevButton.addClass(tm + "-corner-right"); | |
} | |
prevButton = null; | |
} else { | |
var buttonClick; | |
if (calendar[buttonName]) { | |
buttonClick = calendar[buttonName]; // calendar method | |
} else if (fcViews[buttonName]) { | |
buttonClick = function() { | |
button.removeClass(tm + "-state-hover"); // forget why | |
calendar.changeView(buttonName); | |
}; | |
} | |
if (buttonClick) { | |
var icon = options.theme | |
? smartProperty(options.buttonIcons, buttonName) | |
: null; // why are we using smartProperty here? | |
var text = smartProperty(options.buttonText, buttonName); // why are we using smartProperty here? | |
var button = $( | |
"<span class='fc-button fc-button-" + | |
buttonName + | |
" " + | |
tm + | |
"-state-default'>" + | |
(icon | |
? "<span class='fc-icon-wrap'>" + | |
"<span class='ui-icon ui-icon-" + | |
icon + | |
"'/>" + | |
"</span>" | |
: text) + | |
"</span>" | |
) | |
.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(e); | |
disableTextSelection(button); | |
if (!prevButton) { | |
button.addClass(tm + "-corner-left"); | |
} | |
prevButton = button; | |
} | |
} | |
}); | |
if (prevButton) { | |
prevButton.addClass(tm + "-corner-right"); | |
} | |
}); | |
} | |
return e; | |
} | |
function updateTitle(html) { | |
element.find("h2").html(html); | |
} | |
function activateButton(buttonName) { | |
element.find("span.fc-button-" + buttonName).addClass(tm + "-state-active"); | |
} | |
function deactivateButton(buttonName) { | |
element | |
.find("span.fc-button-" + buttonName) | |
.removeClass(tm + "-state-active"); | |
} | |
function disableButton(buttonName) { | |
element | |
.find("span.fc-button-" + buttonName) | |
.addClass(tm + "-state-disabled"); | |
} | |
function enableButton(buttonName) { | |
element | |
.find("span.fc-button-" + buttonName) | |
.removeClass(tm + "-state-disabled"); | |
} | |
} | |
fc.sourceNormalizers = []; | |
fc.sourceFetchers = []; | |
var ajaxDefaults = { | |
dataType: "json", | |
cache: false | |
}; | |
var eventGUID = 1; | |
function EventManager(options, _sources) { | |
var t = this; | |
// exports | |
t.isFetchNeeded = isFetchNeeded; | |
t.fetchEvents = fetchEvents; | |
t.addEventSource = addEventSource; | |
t.removeEventSource = removeEventSource; | |
t.updateEvent = updateEvent; | |
t.renderEvent = renderEvent; | |
t.removeEvents = removeEvents; | |
t.clientEvents = clientEvents; | |
t.normalizeEvent = normalizeEvent; | |
// imports | |
var trigger = t.trigger; | |
var getView = t.getView; | |
var reportEvents = t.reportEvents; | |
// locals | |
var stickySource = { events: [] }; | |
var sources = [stickySource]; | |
var rangeStart, rangeEnd; | |
var currentFetchID = 0; | |
var pendingSourceCnt = 0; | |
var loadingLevel = 0; | |
var cache = []; | |
for (var i = 0; i < _sources.length; i++) { | |
_addEventSource(_sources[i]); | |
} | |
/* Fetching | |
-----------------------------------------------------------------------------*/ | |
function isFetchNeeded(start, end) { | |
return !rangeStart || start < rangeStart || end > rangeEnd; | |
} | |
function fetchEvents(start, end) { | |
rangeStart = start; | |
rangeEnd = end; | |
cache = []; | |
var fetchID = ++currentFetchID; | |
var len = sources.length; | |
pendingSourceCnt = len; | |
for (var i = 0; i < len; i++) { | |
fetchEventSource(sources[i], fetchID); | |
} | |
} | |
function fetchEventSource(source, fetchID) { | |
_fetchEventSource(source, function(events) { | |
if (fetchID == currentFetchID) { | |
if (events) { | |
if (options.eventDataTransform) { | |
events = $.map(events, options.eventDataTransform); | |
} | |
if (source.eventDataTransform) { | |
events = $.map(events, source.eventDataTransform); | |
} | |
// TODO: this technique is not ideal for static array event sources. | |
// For arrays, we'll want to process all events right in the beginning, then never again. | |
for (var i = 0; i < events.length; i++) { | |
events[i].source = source; | |
normalizeEvent(events[i]); | |
} | |
cache = cache.concat(events); | |
} | |
pendingSourceCnt--; | |
if (!pendingSourceCnt) { | |
reportEvents(cache); | |
} | |
} | |
}); | |
} | |
function _fetchEventSource(source, callback) { | |
var i; | |
var fetchers = fc.sourceFetchers; | |
var res; | |
for (i = 0; i < fetchers.length; i++) { | |
res = fetchers[i](source, rangeStart, rangeEnd, callback); | |
if (res === true) { | |
// the fetcher is in charge. made its own async request | |
return; | |
} else if (typeof res == "object") { | |
// the fetcher returned a new source. process it | |
_fetchEventSource(res, callback); | |
return; | |
} | |
} | |
var events = source.events; | |
if (events) { | |
if ($.isFunction(events)) { | |
pushLoading(); | |
events(cloneDate(rangeStart), cloneDate(rangeEnd), function(events) { | |
callback(events); | |
popLoading(); | |
}); | |
} else if ($.isArray(events)) { | |
callback(events); | |
} else { | |
callback(); | |
} | |
} else { | |
var url = source.url; | |
if (url) { | |
var success = source.success; | |
var error = source.error; | |
var complete = source.complete; | |
// retrieve any outbound GET/POST $.ajax data from the options | |
var customData; | |
if ($.isFunction(source.data)) { | |
// supplied as a function that returns a key/value object | |
customData = source.data(); | |
} else { | |
// supplied as a straight key/value object | |
customData = source.data; | |
} | |
// use a copy of the custom data so we can modify the parameters | |
// and not affect the passed-in object. | |
var data = $.extend({}, customData || {}); | |
var startParam = firstDefined(source.startParam, options.startParam); | |
var endParam = firstDefined(source.endParam, options.endParam); | |
if (startParam) { | |
data[startParam] = Math.round(+rangeStart / 1000); | |
} | |
if (endParam) { | |
data[endParam] = Math.round(+rangeEnd / 1000); | |
} | |
pushLoading(); | |
$.ajax( | |
$.extend({}, ajaxDefaults, source, { | |
data: data, | |
success: function(events) { | |
events = events || []; | |
var res = applyAll(success, this, arguments); | |
if ($.isArray(res)) { | |
events = res; | |
} | |
callback(events); | |
}, | |
error: function() { | |
applyAll(error, this, arguments); | |
callback(); | |
}, | |
complete: function() { | |
applyAll(complete, this, arguments); | |
popLoading(); | |
} | |
}) | |
); | |
} else { | |
callback(); | |
} | |
} | |
} | |
/* Sources | |
-----------------------------------------------------------------------------*/ | |
function addEventSource(source) { | |
source = _addEventSource(source); | |
if (source) { | |
pendingSourceCnt++; | |
fetchEventSource(source, currentFetchID); // will eventually call reportEvents | |
} | |
} | |
function _addEventSource(source) { | |
if ($.isFunction(source) || $.isArray(source)) { | |
source = { events: source }; | |
} else if (typeof source == "string") { | |
source = { url: source }; | |
} | |
if (typeof source == "object") { | |
normalizeSource(source); | |
sources.push(source); | |
return source; | |
} | |
} | |
function removeEventSource(source) { | |
sources = $.grep(sources, function(src) { | |
return !isSourcesEqual(src, source); | |
}); | |
// remove all client events from that source | |
cache = $.grep(cache, function(e) { | |
return !isSourcesEqual(e.source, source); | |
}); | |
reportEvents(cache); | |
} | |
/* Manipulation | |
-----------------------------------------------------------------------------*/ | |
function updateEvent(event) { | |
// update an existing event | |
var i, | |
len = cache.length, | |
e, | |
defaultEventEnd = getView().defaultEventEnd, // getView??? | |
startDelta = event.start - event._start, | |
endDelta = event.end | |
? event.end - (event._end || 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 = cache[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(+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; | |
e.color = event.color; | |
e.backgroundColor = event.backgroundColor; | |
e.borderColor = event.borderColor; | |
e.textColor = event.textColor; | |
normalizeEvent(e); | |
} | |
} | |
normalizeEvent(event); | |
reportEvents(cache); | |
} | |
function renderEvent(event, stick) { | |
normalizeEvent(event); | |
if (!event.source) { | |
if (stick) { | |
stickySource.events.push(event); | |
event.source = stickySource; | |
} | |
cache.push(event); | |
} | |
reportEvents(cache); | |
} | |
function removeEvents(filter) { | |
if (!filter) { | |
// remove all | |
cache = []; | |
// clear all array sources | |
for (var i = 0; i < sources.length; i++) { | |
if ($.isArray(sources[i].events)) { | |
sources[i].events = []; | |
} | |
} | |
} else { | |
if (!$.isFunction(filter)) { | |
// an event ID | |
var id = filter + ""; | |
filter = function(e) { | |
return e._id == id; | |
}; | |
} | |
cache = $.grep(cache, filter, true); | |
// remove events from array sources | |
for (var i = 0; i < sources.length; i++) { | |
if ($.isArray(sources[i].events)) { | |
sources[i].events = $.grep(sources[i].events, filter, true); | |
} | |
} | |
} | |
reportEvents(cache); | |
} | |
function clientEvents(filter) { | |
if ($.isFunction(filter)) { | |
return $.grep(cache, filter); | |
} else if (filter) { | |
// an event ID | |
filter += ""; | |
return $.grep(cache, function(e) { | |
return e._id == filter; | |
}); | |
} | |
return cache; // else, return all | |
} | |
/* Loading State | |
-----------------------------------------------------------------------------*/ | |
function pushLoading() { | |
if (!loadingLevel++) { | |
trigger("loading", null, true, getView()); | |
} | |
} | |
function popLoading() { | |
if (!--loadingLevel) { | |
trigger("loading", null, false, getView()); | |
} | |
} | |
/* Event Normalization | |
-----------------------------------------------------------------------------*/ | |
function normalizeEvent(event) { | |
var source = event.source || {}; | |
var ignoreTimezone = firstDefined( | |
source.ignoreTimezone, | |
options.ignoreTimezone | |
); | |
event._id = | |
event._id || (event.id === undefined ? "_fc" + eventGUID++ : event.id + ""); | |
if (event.date) { | |
if (!event.start) { | |
event.start = event.date; | |
} | |
delete event.date; | |
} | |
event._start = cloneDate( | |
(event.start = parseDate(event.start, ignoreTimezone)) | |
); | |
event.end = parseDate(event.end, ignoreTimezone); | |
if (event.end && event.end <= event.start) { | |
event.end = null; | |
} | |
event._end = event.end ? cloneDate(event.end) : null; | |
if (event.allDay === undefined) { | |
event.allDay = firstDefined(source.allDayDefault, 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 | |
} | |
/* Utils | |
------------------------------------------------------------------------------*/ | |
function normalizeSource(source) { | |
if (source.className) { | |
// TODO: repeat code, same code for event classNames | |
if (typeof source.className == "string") { | |
source.className = source.className.split(/\s+/); | |
} | |
} else { | |
source.className = []; | |
} | |
var normalizers = fc.sourceNormalizers; | |
for (var i = 0; i < normalizers.length; i++) { | |
normalizers[i](source); | |
} | |
} | |
function isSourcesEqual(source1, source2) { | |
return ( | |
source1 && | |
source2 && | |
getSourcePrimitive(source1) == getSourcePrimitive(source2) | |
); | |
} | |
function getSourcePrimitive(source) { | |
return ( | |
(typeof source == "object" ? source.events || source.url : "") || source | |
); | |
} | |
} | |
fc.addDays = addDays; | |
fc.cloneDate = cloneDate; | |
fc.parseDate = parseDate; | |
fc.parseISO8601 = parseISO8601; | |
fc.parseTime = parseTime; | |
fc.formatDate = formatDate; | |
fc.formatDates = formatDates; | |
/* Date Math | |
-----------------------------------------------------------------------------*/ | |
var dayIDs = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"], | |
DAY_MS = 86400000, | |
HOUR_MS = 3600000, | |
MINUTE_MS = 60000; | |
function addYears(d, n, keepTime) { | |
d.setFullYear(d.getFullYear() + n); | |
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; | |
} | |
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); | |
} | |
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 dayDiff(d1, d2) { | |
// d1 - d2 | |
return Math.round((cloneDate(d1, true) - cloneDate(d2, true)) / DAY_MS); | |
} | |
function setYMD(date, y, m, d) { | |
if (y !== undefined && y != date.getFullYear()) { | |
date.setDate(1); | |
date.setMonth(0); | |
date.setFullYear(y); | |
} | |
if (m !== undefined && m != date.getMonth()) { | |
date.setDate(1); | |
date.setMonth(m); | |
} | |
if (d !== undefined) { | |
date.setDate(d); | |
} | |
} | |
/* Date Parsing | |
-----------------------------------------------------------------------------*/ | |
function parseDate(s, ignoreTimezone) { | |
// ignoreTimezone defaults to true | |
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+(\.\d+)?$/)) { | |
// a UNIX timestamp | |
return new Date(parseFloat(s) * 1000); | |
} | |
if (ignoreTimezone === undefined) { | |
ignoreTimezone = true; | |
} | |
return parseISO8601(s, ignoreTimezone) || (s ? new Date(s) : null); | |
} | |
// TODO: never return invalid dates (like from new Date(<string>)), return null instead | |
return null; | |
} | |
function parseISO8601(s, ignoreTimezone) { | |
// ignoreTimezone defaults to false | |
// derived from http://delete.me.uk/2005/03/iso8601.html | |
// TODO: for a know glitch/feature, read tests/issue_206_parseDate_dst.html | |
var m = s.match( | |
/^([0-9]{4})(-([0-9]{2})(-([0-9]{2})([T ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2})(:?([0-9]{2}))?))?)?)?)?$/ | |
); | |
if (!m) { | |
return null; | |
} | |
var date = new Date(m[1], 0, 1); | |
if (ignoreTimezone || !m[13]) { | |
var check = new Date(m[1], 0, 1, 9, 0); | |
if (m[3]) { | |
date.setMonth(m[3] - 1); | |
check.setMonth(m[3] - 1); | |
} | |
if (m[5]) { | |
date.setDate(m[5]); | |
check.setDate(m[5]); | |
} | |
fixDate(date, check); | |
if (m[7]) { | |
date.setHours(m[7]); | |
} | |
if (m[8]) { | |
date.setMinutes(m[8]); | |
} | |
if (m[10]) { | |
date.setSeconds(m[10]); | |
} | |
if (m[12]) { | |
date.setMilliseconds(Number("0." + m[12]) * 1000); | |
} | |
fixDate(date, check); | |
} else { | |
date.setUTCFullYear(m[1], m[3] ? m[3] - 1 : 0, m[5] || 1); | |
date.setUTCHours( | |
m[7] || 0, | |
m[8] || 0, | |
m[10] || 0, | |
m[12] ? Number("0." + m[12]) * 1000 : 0 | |
); | |
if (m[14]) { | |
var offset = Number(m[16]) * 60 + (m[18] ? Number(m[18]) : 0); | |
offset *= m[15] == "-" ? 1 : -1; | |
date = new Date(+date + offset * 60 * 1000); | |
} | |
} | |
return date; | |
} | |
function parseTime(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], 10); | |
if (m[3]) { | |
h %= 12; | |
if (m[3].toLowerCase().charAt(0) == "p") { | |
h += 12; | |
} | |
} | |
return h * 60 + (m[2] ? parseInt(m[2], 10) : 0); | |
} | |
} | |
/* Date Formatting | |
-----------------------------------------------------------------------------*/ | |
// TODO: use same function formatDate(date, [date2], format, [options]) | |
function formatDate(date, format, options) { | |
return formatDates(date, null, format, options); | |
} | |
function formatDates(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/, ""), 10)) { | |
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"; | |
}, | |
w: function(d, o) { | |
// local | |
return o.weekNumberCalculation(d); | |
}, | |
W: function(d) { | |
// ISO | |
return iso8601Week(d); | |
} | |
}; | |
fc.dateFormatters = dateFormatters; | |
/* thanks jQuery UI (https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.datepicker.js) | |
* | |
* Set as calculateWeek to determine the week of the year based on the ISO 8601 definition. | |
* `date` - the date to get the week for | |
* `number` - the number of the week within the year that contains this date | |
*/ | |
function iso8601Week(date) { | |
var time; | |
var checkDate = new Date(date.getTime()); | |
// Find Thursday of this week starting on Monday | |
checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); | |
time = checkDate.getTime(); | |
checkDate.setMonth(0); // Compare with Jan 1 | |
checkDate.setDate(1); | |
return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; | |
} | |
fc.applyAll = applyAll; | |
/* Event Date Math | |
-----------------------------------------------------------------------------*/ | |
function exclEndDay(event) { | |
if (event.end) { | |
return _exclEndDay(event.end, event.allDay); | |
} else { | |
return addDays(cloneDate(event.start), 1); | |
} | |
} | |
function _exclEndDay(end, allDay) { | |
end = cloneDate(end); | |
return allDay || end.getHours() || end.getMinutes() | |
? addDays(end, 1) | |
: clearTime(end); | |
// why don't we check for seconds/ms too? | |
} | |
/* Event Element Binding | |
-----------------------------------------------------------------------------*/ | |
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(); | |
}); | |
} | |
/* Element Dimensions | |
-----------------------------------------------------------------------------*/ | |
function setOuterWidth(element, width, includeMargins) { | |
for (var i = 0, e; i < element.length; i++) { | |
e = $(element[i]); | |
e.width(Math.max(0, width - hsides(e, includeMargins))); | |
} | |
} | |
function setOuterHeight(element, height, includeMargins) { | |
for (var i = 0, e; i < element.length; i++) { | |
e = $(element[i]); | |
e.height(Math.max(0, height - vsides(e, includeMargins))); | |
} | |
} | |
function hsides(element, includeMargins) { | |
return ( | |
hpadding(element) + | |
hborders(element) + | |
(includeMargins ? hmargins(element) : 0) | |
); | |
} | |
function hpadding(element) { | |
return ( | |
(parseFloat($.css(element[0], "paddingLeft", true)) || 0) + | |
(parseFloat($.css(element[0], "paddingRight", true)) || 0) | |
); | |
} | |
function hmargins(element) { | |
return ( | |
(parseFloat($.css(element[0], "marginLeft", true)) || 0) + | |
(parseFloat($.css(element[0], "marginRight", true)) || 0) | |
); | |
} | |
function hborders(element) { | |
return ( | |
(parseFloat($.css(element[0], "borderLeftWidth", true)) || 0) + | |
(parseFloat($.css(element[0], "borderRightWidth", true)) || 0) | |
); | |
} | |
function vsides(element, includeMargins) { | |
return ( | |
vpadding(element) + | |
vborders(element) + | |
(includeMargins ? vmargins(element) : 0) | |
); | |
} | |
function vpadding(element) { | |
return ( | |
(parseFloat($.css(element[0], "paddingTop", true)) || 0) + | |
(parseFloat($.css(element[0], "paddingBottom", true)) || 0) | |
); | |
} | |
function vmargins(element) { | |
return ( | |
(parseFloat($.css(element[0], "marginTop", true)) || 0) + | |
(parseFloat($.css(element[0], "marginBottom", true)) || 0) | |
); | |
} | |
function vborders(element) { | |
return ( | |
(parseFloat($.css(element[0], "borderTopWidth", true)) || 0) + | |
(parseFloat($.css(element[0], "borderBottomWidth", true)) || 0) | |
); | |
} | |
/* Misc Utils | |
-----------------------------------------------------------------------------*/ | |
//TODO: arraySlice | |
//TODO: isFunction, grep ? | |
function noop() {} | |
function dateCompare(a, b) { | |
return a - b; | |
} | |
function arrayMax(a) { | |
return Math.max.apply(Math, a); | |
} | |
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, "'") | |
.replace(/"/g, '"') | |
.replace(/\n/g, "<br />"); | |
} | |
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'); | |
} | |
*/ | |
function markFirstLast(e) { | |
e.children() | |
.removeClass("fc-first fc-last") | |
.filter(":first-child") | |
.addClass("fc-first") | |
.end() | |
.filter(":last-child") | |
.addClass("fc-last"); | |
} | |
function setDayID(cell, date) { | |
cell.each(function(i, _cell) { | |
_cell.className = _cell.className.replace( | |
/^fc-\w*/, | |
"fc-" + dayIDs[date.getDay()] | |
); | |
// TODO: make a way that doesn't rely on order of classes | |
}); | |
} | |
function getSkinCss(event, opt) { | |
var source = event.source || {}; | |
var eventColor = event.color; | |
var sourceColor = source.color; | |
var optionColor = opt("eventColor"); | |
var backgroundColor = | |
event.backgroundColor || | |
eventColor || | |
source.backgroundColor || | |
sourceColor || | |
opt("eventBackgroundColor") || | |
optionColor; | |
var borderColor = | |
event.borderColor || | |
eventColor || | |
source.borderColor || | |
sourceColor || | |
opt("eventBorderColor") || | |
optionColor; | |
var textColor = event.textColor || source.textColor || opt("eventTextColor"); | |
var statements = []; | |
if (backgroundColor) { | |
statements.push("background-color:" + backgroundColor); | |
} | |
if (borderColor) { | |
statements.push("border-color:" + borderColor); | |
} | |
if (textColor) { | |
statements.push("color:" + textColor); | |
} | |
return statements.join(";"); | |
} | |
function applyAll(functions, thisObj, args) { | |
if ($.isFunction(functions)) { | |
functions = [functions]; | |
} | |
if (functions) { | |
var i; | |
var ret; | |
for (i = 0; i < functions.length; i++) { | |
ret = functions[i].apply(thisObj, args) || ret; | |
} | |
return ret; | |
} | |
} | |
function firstDefined() { | |
for (var i = 0; i < arguments.length; i++) { | |
if (arguments[i] !== undefined) { | |
return arguments[i]; | |
} | |
} | |
} | |
fcViews.month = MonthView; | |
function MonthView(element, calendar) { | |
var t = this; | |
// exports | |
t.render = render; | |
// imports | |
BasicView.call(t, element, calendar, "month"); | |
var opt = t.opt; | |
var renderBasic = t.renderBasic; | |
var skipHiddenDays = t.skipHiddenDays; | |
var getCellsPerWeek = t.getCellsPerWeek; | |
var formatDate = calendar.formatDate; | |
function render(date, delta) { | |
if (delta) { | |
addMonths(date, delta); | |
date.setDate(1); | |
} | |
var firstDay = opt("firstDay"); | |
var start = cloneDate(date, true); | |
start.setDate(1); | |
var end = addMonths(cloneDate(start), 1); | |
var visStart = cloneDate(start); | |
addDays(visStart, -((visStart.getDay() - firstDay + 7) % 7)); | |
skipHiddenDays(visStart); | |
var visEnd = cloneDate(end); | |
addDays(visEnd, (7 - visEnd.getDay() + firstDay) % 7); | |
skipHiddenDays(visEnd, -1, true); | |
var colCnt = getCellsPerWeek(); | |
var rowCnt = Math.round(dayDiff(visEnd, visStart) / 7); // should be no need for Math.round | |
if (opt("weekMode") == "fixed") { | |
addDays(visEnd, (6 - rowCnt) * 7); // add weeks to make up for it | |
rowCnt = 6; | |
} | |
t.title = formatDate(start, opt("titleFormat")); | |
t.start = start; | |
t.end = end; | |
t.visStart = visStart; | |
t.visEnd = visEnd; | |
renderBasic(rowCnt, colCnt, true); | |
} | |
} | |
fcViews.basicWeek = BasicWeekView; | |
function BasicWeekView(element, calendar) { | |
var t = this; | |
// exports | |
t.render = render; | |
// imports | |
BasicView.call(t, element, calendar, "basicWeek"); | |
var opt = t.opt; | |
var renderBasic = t.renderBasic; | |
var skipHiddenDays = t.skipHiddenDays; | |
var getCellsPerWeek = t.getCellsPerWeek; | |
var formatDates = calendar.formatDates; | |
function render(date, delta) { | |
if (delta) { | |
addDays(date, delta * 7); | |
} | |
var start = addDays( | |
cloneDate(date), | |
-((date.getDay() - opt("firstDay") + 7) % 7) | |
); | |
var end = addDays(cloneDate(start), 7); | |
var visStart = cloneDate(start); | |
skipHiddenDays(visStart); | |
var visEnd = cloneDate(end); | |
skipHiddenDays(visEnd, -1, true); | |
var colCnt = getCellsPerWeek(); | |
t.start = start; | |
t.end = end; | |
t.visStart = visStart; | |
t.visEnd = visEnd; | |
t.title = formatDates( | |
visStart, | |
addDays(cloneDate(visEnd), -1), | |
opt("titleFormat") | |
); | |
renderBasic(1, colCnt, false); | |
} | |
} | |
fcViews.basicDay = BasicDayView; | |
function BasicDayView(element, calendar) { | |
var t = this; | |
// exports | |
t.render = render; | |
// imports | |
BasicView.call(t, element, calendar, "basicDay"); | |
var opt = t.opt; | |
var renderBasic = t.renderBasic; | |
var skipHiddenDays = t.skipHiddenDays; | |
var formatDate = calendar.formatDate; | |
function render(date, delta) { | |
if (delta) { | |
addDays(date, delta); | |
} | |
skipHiddenDays(date, delta < 0 ? -1 : 1); | |
var start = cloneDate(date, true); | |
var end = addDays(cloneDate(start), 1); | |
t.title = formatDate(date, opt("titleFormat")); | |
t.start = t.visStart = start; | |
t.end = t.visEnd = end; | |
renderBasic(1, 1, false); | |
} | |
} | |
setDefaults({ | |
weekMode: "fixed" | |
}); | |
function BasicView(element, calendar, viewName) { | |
var t = this; | |
// exports | |
t.renderBasic = renderBasic; | |
t.setHeight = setHeight; | |
t.setWidth = setWidth; | |
t.renderDayOverlay = renderDayOverlay; | |
t.defaultSelectionEnd = defaultSelectionEnd; | |
t.renderSelection = renderSelection; | |
t.clearSelection = clearSelection; | |
t.reportDayClick = reportDayClick; // for selection (kinda hacky) | |
t.dragStart = dragStart; | |
t.dragStop = dragStop; | |
t.defaultEventEnd = defaultEventEnd; | |
t.getHoverListener = function() { | |
return hoverListener; | |
}; | |
t.colLeft = colLeft; | |
t.colRight = colRight; | |
t.colContentLeft = colContentLeft; | |
t.colContentRight = colContentRight; | |
t.getIsCellAllDay = function() { | |
return true; | |
}; | |
t.allDayRow = allDayRow; | |
t.getRowCnt = function() { | |
return rowCnt; | |
}; | |
t.getColCnt = function() { | |
return colCnt; | |
}; | |
t.getColWidth = function() { | |
return colWidth; | |
}; | |
t.getDaySegmentContainer = function() { | |
return daySegmentContainer; | |
}; | |
// imports | |
View.call(t, element, calendar, viewName); | |
OverlayManager.call(t); | |
SelectionManager.call(t); | |
BasicEventRenderer.call(t); | |
var opt = t.opt; | |
var trigger = t.trigger; | |
var renderOverlay = t.renderOverlay; | |
var clearOverlays = t.clearOverlays; | |
var daySelectionMousedown = t.daySelectionMousedown; | |
var cellToDate = t.cellToDate; | |
var dateToCell = t.dateToCell; | |
var rangeToSegments = t.rangeToSegments; | |
var formatDate = calendar.formatDate; | |
// locals | |
var table; | |
var head; | |
var headCells; | |
var body; | |
var bodyRows; | |
var bodyCells; | |
var bodyFirstCells; | |
var firstRowCellInners; | |
var firstRowCellContentInners; | |
var daySegmentContainer; | |
var viewWidth; | |
var viewHeight; | |
var colWidth; | |
var weekNumberWidth; | |
var rowCnt, colCnt; | |
var showNumbers; | |
var coordinateGrid; | |
var hoverListener; | |
var colPositions; | |
var colContentPositions; | |
var tm; | |
var colFormat; | |
var showWeekNumbers; | |
var weekNumberTitle; | |
var weekNumberFormat; | |
/* Rendering | |
------------------------------------------------------------*/ | |
disableTextSelection(element.addClass("fc-grid")); | |
function renderBasic(_rowCnt, _colCnt, _showNumbers) { | |
rowCnt = _rowCnt; | |
colCnt = _colCnt; | |
showNumbers = _showNumbers; | |
updateOptions(); | |
if (!body) { | |
buildEventContainer(); | |
} | |
buildTable(); | |
} | |
function updateOptions() { | |
tm = opt("theme") ? "ui" : "fc"; | |
colFormat = opt("columnFormat"); | |
// week # options. (TODO: bad, logic also in other views) | |
showWeekNumbers = opt("weekNumbers"); | |
weekNumberTitle = opt("weekNumberTitle"); | |
if (opt("weekNumberCalculation") != "iso") { | |
weekNumberFormat = "w"; | |
} else { | |
weekNumberFormat = "W"; | |
} | |
} | |
function buildEventContainer() { | |
daySegmentContainer = $( | |
"<div class='fc-event-container' style='position:absolute;z-index:8;top:0;left:0'/>" | |
).appendTo(element); | |
} | |
function buildTable() { | |
var html = buildTableHTML(); | |
if (table) { | |
table.remove(); | |
} | |
table = $(html).appendTo(element); | |
head = table.find("thead"); | |
headCells = head.find(".fc-day-header"); | |
body = table.find("tbody"); | |
bodyRows = body.find("tr"); | |
bodyCells = body.find(".fc-day"); | |
bodyFirstCells = bodyRows.find("td:first-child"); | |
firstRowCellInners = bodyRows.eq(0).find(".fc-day > div"); | |
firstRowCellContentInners = bodyRows.eq(0).find(".fc-day-content > div"); | |
markFirstLast(head.add(head.find("tr"))); // marks first+last tr/th's | |
markFirstLast(bodyRows); // marks first+last td's | |
bodyRows.eq(0).addClass("fc-first"); | |
bodyRows.filter(":last").addClass("fc-last"); | |
bodyCells.each(function(i, _cell) { | |
var date = cellToDate(Math.floor(i / colCnt), i % colCnt); | |
trigger("dayRender", t, date, $(_cell)); | |
}); | |
dayBind(bodyCells); | |
} | |
/* HTML Building | |
-----------------------------------------------------------*/ | |
function buildTableHTML() { | |
var html = | |
"<table class='fc-border-separate' style='width:100%' cellspacing='0'>" + | |
buildHeadHTML() + | |
buildBodyHTML() + | |
"</table>"; | |
return html; | |
} | |
function buildHeadHTML() { | |
var headerClass = tm + "-widget-header"; | |
var html = ""; | |
var col; | |
var date; | |
html += "<thead><tr>"; | |
if (showWeekNumbers) { | |
html += | |
"<th class='fc-week-number " + | |
headerClass + | |
"'>" + | |
htmlEscape(weekNumberTitle) + | |
"</th>"; | |
} | |
for (col = 0; col < colCnt; col++) { | |
date = cellToDate(0, col); | |
html += | |
"<th class='fc-day-header fc-" + | |
dayIDs[date.getDay()] + | |
" " + | |
headerClass + | |
"'>" + | |
htmlEscape(formatDate(date, colFormat)) + | |
"</th>"; | |
} | |
html += "</tr></thead>"; | |
return html; | |
} | |
function buildBodyHTML() { | |
var contentClass = tm + "-widget-content"; | |
var html = ""; | |
var row; | |
var col; | |
var date; | |
html += "<tbody>"; | |
for (row = 0; row < rowCnt; row++) { | |
html += "<tr class='fc-week'>"; | |
if (showWeekNumbers) { | |
date = cellToDate(row, 0); | |
html += | |
"<td class='fc-week-number " + | |
contentClass + | |
"'>" + | |
"<div>" + | |
htmlEscape(formatDate(date, weekNumberFormat)) + | |
"</div>" + | |
"</td>"; | |
} | |
for (col = 0; col < colCnt; col++) { | |
date = cellToDate(row, col); | |
html += buildCellHTML(date); | |
} | |
html += "</tr>"; | |
} | |
html += "</tbody>"; | |
return html; | |
} | |
function buildCellHTML(date) { | |
var contentClass = tm + "-widget-content"; | |
var month = t.start.getMonth(); | |
var today = clearTime(new Date()); | |
var html = ""; | |
var classNames = ["fc-day", "fc-" + dayIDs[date.getDay()], contentClass]; | |
if (date.getMonth() != month) { | |
classNames.push("fc-other-month"); | |
} | |
if (+date == +today) { | |
classNames.push("fc-today", tm + "-state-highlight"); | |
} else if (date < today) { | |
classNames.push("fc-past"); | |
} else { | |
classNames.push("fc-future"); | |
} | |
html += | |
"<td" + | |
" class='" + | |
classNames.join(" ") + | |
"'" + | |
" data-date='" + | |
formatDate(date, "yyyy-MM-dd") + | |
"'" + | |
">" + | |
"<div>"; | |
if (showNumbers) { | |
html += "<div class='fc-day-number'>" + date.getDate() + "</div>"; | |
} | |
html += | |
"<div class='fc-day-content'>" + | |
"<div style='position:relative'> </div>" + | |
"</div>" + | |
"</div>" + | |
"</td>"; | |
return html; | |
} | |
/* Dimensions | |
-----------------------------------------------------------*/ | |
function setHeight(height) { | |
viewHeight = height; | |
var bodyHeight = viewHeight - head.height(); | |
var rowHeight; | |
var rowHeightLast; | |
var cell; | |
if (opt("weekMode") == "variable") { | |
rowHeight = rowHeightLast = Math.floor(bodyHeight / (rowCnt == 1 ? 2 : 6)); | |
} else { | |
rowHeight = Math.floor(bodyHeight / rowCnt); | |
rowHeightLast = bodyHeight - rowHeight * (rowCnt - 1); | |
} | |
bodyFirstCells.each(function(i, _cell) { | |
if (i < rowCnt) { | |
cell = $(_cell); | |
cell | |
.find("> div") | |
.css( | |
"min-height", | |
(i == rowCnt - 1 ? rowHeightLast : rowHeight) - vsides(cell) | |
); | |
} | |
}); | |
} | |
function setWidth(width) { | |
viewWidth = width; | |
colPositions.clear(); | |
colContentPositions.clear(); | |
weekNumberWidth = 0; | |
if (showWeekNumbers) { | |
weekNumberWidth = head.find("th.fc-week-number").outerWidth(); | |
} | |
colWidth = Math.floor((viewWidth - weekNumberWidth) / colCnt); | |
setOuterWidth(headCells.slice(0, -1), colWidth); | |
} | |
/* Day clicking and binding | |
-----------------------------------------------------------*/ | |
function dayBind(days) { | |
days.click(dayClick).mousedown(daySelectionMousedown); | |
} | |
function dayClick(ev) { | |
if (!opt("selectable")) { | |
// if selectable, SelectionManager will worry about dayClick | |
var date = parseISO8601($(this).data("date")); | |
trigger("dayClick", this, date, true, ev); | |
} | |
} | |
/* Semi-transparent Overlay Helpers | |
------------------------------------------------------*/ | |
// TODO: should be consolidated with AgendaView's methods | |
function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { | |
// overlayEnd is exclusive | |
if (refreshCoordinateGrid) { | |
coordinateGrid.build(); | |
} | |
var segments = rangeToSegments(overlayStart, overlayEnd); | |
for (var i = 0; i < segments.length; i++) { | |
var segment = segments[i]; | |
dayBind( | |
renderCellOverlay( | |
segment.row, | |
segment.leftCol, | |
segment.row, | |
segment.rightCol | |
) | |
); | |
} | |
} | |
function renderCellOverlay(row0, col0, row1, col1) { | |
// row1,col1 is inclusive | |
var rect = coordinateGrid.rect(row0, col0, row1, col1, element); | |
return renderOverlay(rect, element); | |
} | |
/* Selection | |
-----------------------------------------------------------------------*/ | |
function defaultSelectionEnd(startDate, allDay) { | |
return cloneDate(startDate); | |
} | |
function renderSelection(startDate, endDate, allDay) { | |
renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true); // rebuild every time??? | |
} | |
function clearSelection() { | |
clearOverlays(); | |
} | |
function reportDayClick(date, allDay, ev) { | |
var cell = dateToCell(date); | |
var _element = bodyCells[cell.row * colCnt + cell.col]; | |
trigger("dayClick", _element, date, allDay, ev); | |
} | |
/* External Dragging | |
-----------------------------------------------------------------------*/ | |
function dragStart(_dragElement, ev, ui) { | |
hoverListener.start(function(cell) { | |
clearOverlays(); | |
if (cell) { | |
renderCellOverlay(cell.row, cell.col, cell.row, cell.col); | |
} | |
}, ev); | |
} | |
function dragStop(_dragElement, ev, ui) { | |
var cell = hoverListener.stop(); | |
clearOverlays(); | |
if (cell) { | |
var d = cellToDate(cell); | |
trigger("drop", _dragElement, d, true, ev, ui); | |
} | |
} | |
/* Utilities | |
--------------------------------------------------------*/ | |
function defaultEventEnd(event) { | |
return cloneDate(event.start); | |
} | |
coordinateGrid = new CoordinateGrid(function(rows, cols) { | |
var e, n, p; | |
headCells.each(function(i, _e) { | |
e = $(_e); | |
n = e.offset().left; | |
if (i) { | |
p[1] = n; | |
} | |
p = [n]; | |
cols[i] = p; | |
}); | |
p[1] = n + e.outerWidth(); | |
bodyRows.each(function(i, _e) { | |
if (i < rowCnt) { | |
e = $(_e); | |
n = e.offset().top; | |
if (i) { | |
p[1] = n; | |
} | |
p = [n]; | |
rows[i] = p; | |
} | |
}); | |
p[1] = n + e.outerHeight(); | |
}); | |
hoverListener = new HoverListener(coordinateGrid); | |
colPositions = new HorizontalPositionCache(function(col) { | |
return firstRowCellInners.eq(col); | |
}); | |
colContentPositions = new HorizontalPositionCache(function(col) { | |
return firstRowCellContentInners.eq(col); | |
}); | |
function colLeft(col) { | |
return colPositions.left(col); | |
} | |
function colRight(col) { | |
return colPositions.right(col); | |
} | |
function colContentLeft(col) { | |
return colContentPositions.left(col); | |
} | |
function colContentRight(col) { | |
return colContentPositions.right(col); | |
} | |
function allDayRow(i) { | |
return bodyRows.eq(i); | |
} | |
} | |
function BasicEventRenderer() { | |
var t = this; | |
// exports | |
t.renderEvents = renderEvents; | |
t.clearEvents = clearEvents; | |
// imports | |
DayEventRenderer.call(t); | |
function renderEvents(events, modifiedEventId) { | |
t.renderDayEvents(events, modifiedEventId); | |
} | |
function clearEvents() { | |
t.getDaySegmentContainer().empty(); | |
} | |
// TODO: have this class (and AgendaEventRenderer) be responsible for creating the event container div | |
} | |
fcViews.agendaWeek = AgendaWeekView; | |
function AgendaWeekView(element, calendar) { | |
var t = this; | |
// exports | |
t.render = render; | |
// imports | |
AgendaView.call(t, element, calendar, "agendaWeek"); | |
var opt = t.opt; | |
var renderAgenda = t.renderAgenda; | |
var skipHiddenDays = t.skipHiddenDays; | |
var getCellsPerWeek = t.getCellsPerWeek; | |
var formatDates = calendar.formatDates; | |
function render(date, delta) { | |
if (delta) { | |
addDays(date, delta * 7); | |
} | |
var start = addDays( | |
cloneDate(date), | |
-((date.getDay() - opt("firstDay") + 7) % 7) | |
); | |
var end = addDays(cloneDate(start), 7); | |
var visStart = cloneDate(start); | |
skipHiddenDays(visStart); | |
var visEnd = cloneDate(end); | |
skipHiddenDays(visEnd, -1, true); | |
var colCnt = getCellsPerWeek(); | |
t.title = formatDates( | |
visStart, | |
addDays(cloneDate(visEnd), -1), | |
opt("titleFormat") | |
); | |
t.start = start; | |
t.end = end; | |
t.visStart = visStart; | |
t.visEnd = visEnd; | |
renderAgenda(colCnt); | |
} | |
} | |
fcViews.agendaDay = AgendaDayView; | |
function AgendaDayView(element, calendar) { | |
var t = this; | |
// exports | |
t.render = render; | |
// imports | |
AgendaView.call(t, element, calendar, "agendaDay"); | |
var opt = t.opt; | |
var renderAgenda = t.renderAgenda; | |
var skipHiddenDays = t.skipHiddenDays; | |
var formatDate = calendar.formatDate; | |
function render(date, delta) { | |
if (delta) { | |
addDays(date, delta); | |
} | |
skipHiddenDays(date, delta < 0 ? -1 : 1); | |
var start = cloneDate(date, true); | |
var end = addDays(cloneDate(start), 1); | |
t.title = formatDate(date, opt("titleFormat")); | |
t.start = t.visStart = start; | |
t.end = t.visEnd = end; | |
renderAgenda(1); | |
} | |
} | |
setDefaults({ | |
allDaySlot: true, | |
allDayText: "all-day", | |
firstHour: 6, | |
slotMinutes: 30, | |
defaultEventMinutes: 120, | |
axisFormat: "h(:mm)tt", | |
timeFormat: { | |
agenda: "h:mm{ - h:mm}" | |
}, | |
dragOpacity: { | |
agenda: 0.5 | |
}, | |
minTime: 0, | |
maxTime: 24, | |
slotEventOverlap: true | |
}); | |
// TODO: make it work in quirks mode (event corners, all-day height) | |
// TODO: test liquid width, especially in IE6 | |
function AgendaView(element, calendar, viewName) { | |
var t = this; | |
// exports | |
t.renderAgenda = renderAgenda; | |
t.setWidth = setWidth; | |
t.setHeight = setHeight; | |
t.afterRender = afterRender; | |
t.defaultEventEnd = defaultEventEnd; | |
t.timePosition = timePosition; | |
t.getIsCellAllDay = getIsCellAllDay; | |
t.allDayRow = getAllDayRow; | |
t.getCoordinateGrid = function() { | |
return coordinateGrid; | |
}; // specifically for AgendaEventRenderer | |
t.getHoverListener = function() { | |
return hoverListener; | |
}; | |
t.colLeft = colLeft; | |
t.colRight = colRight; | |
t.colContentLeft = colContentLeft; | |
t.colContentRight = colContentRight; | |
t.getDaySegmentContainer = function() { | |
return daySegmentContainer; | |
}; | |
t.getSlotSegmentContainer = function() { | |
return slotSegmentContainer; | |
}; | |
t.getMinMinute = function() { | |
return minMinute; | |
}; | |
t.getMaxMinute = function() { | |
return maxMinute; | |
}; | |
t.getSlotContainer = function() { | |
return slotContainer; | |
}; | |
t.getRowCnt = function() { | |
return 1; | |
}; | |
t.getColCnt = function() { | |
return colCnt; | |
}; | |
t.getColWidth = function() { | |
return colWidth; | |
}; | |
t.getSnapHeight = function() { | |
return snapHeight; | |
}; | |
t.getSnapMinutes = function() { | |
return snapMinutes; | |
}; | |
t.defaultSelectionEnd = defaultSelectionEnd; | |
t.renderDayOverlay = renderDayOverlay; | |
t.renderSelection = renderSelection; | |
t.clearSelection = clearSelection; | |
t.reportDayClick = reportDayClick; // selection mousedown hack | |
t.dragStart = dragStart; | |
t.dragStop = dragStop; | |
// imports | |
View.call(t, element, calendar, viewName); | |
OverlayManager.call(t); | |
SelectionManager.call(t); | |
AgendaEventRenderer.call(t); | |
var opt = t.opt; | |
var trigger = t.trigger; | |
var renderOverlay = t.renderOverlay; | |
var clearOverlays = t.clearOverlays; | |
var reportSelection = t.reportSelection; | |
var unselect = t.unselect; | |
var daySelectionMousedown = t.daySelectionMousedown; | |
var slotSegHtml = t.slotSegHtml; | |
var cellToDate = t.cellToDate; | |
var dateToCell = t.dateToCell; | |
var rangeToSegments = t.rangeToSegments; | |
var formatDate = calendar.formatDate; | |
// locals | |
var dayTable; | |
var dayHead; | |
var dayHeadCells; | |
var dayBody; | |
var dayBodyCells; | |
var dayBodyCellInners; | |
var dayBodyCellContentInners; | |
var dayBodyFirstCell; | |
var dayBodyFirstCellStretcher; | |
var slotLayer; | |
var daySegmentContainer; | |
var allDayTable; | |
var allDayRow; | |
var slotScroller; | |
var slotContainer; | |
var slotSegmentContainer; | |
var slotTable; | |
var selectionHelper; | |
var viewWidth; | |
var viewHeight; | |
var axisWidth; | |
var colWidth; | |
var gutterWidth; | |
var slotHeight; // TODO: what if slotHeight changes? (see issue 650) | |
var snapMinutes; | |
var snapRatio; // ratio of number of "selection" slots to normal slots. (ex: 1, 2, 4) | |
var snapHeight; // holds the pixel hight of a "selection" slot | |
var colCnt; | |
var slotCnt; | |
var coordinateGrid; | |
var hoverListener; | |
var colPositions; | |
var colContentPositions; | |
var slotTopCache = {}; | |
var tm; | |
var rtl; | |
var minMinute, maxMinute; | |
var colFormat; | |
var showWeekNumbers; | |
var weekNumberTitle; | |
var weekNumberFormat; | |
/* Rendering | |
-----------------------------------------------------------------------------*/ | |
disableTextSelection(element.addClass("fc-agenda")); | |
function renderAgenda(c) { | |
colCnt = c; | |
updateOptions(); | |
if (!dayTable) { | |
// first time rendering? | |
buildSkeleton(); // builds day table, slot area, events containers | |
} else { | |
buildDayTable(); // rebuilds day table | |
} | |
} | |
function updateOptions() { | |
tm = opt("theme") ? "ui" : "fc"; | |
rtl = opt("isRTL"); | |
minMinute = parseTime(opt("minTime")); | |
maxMinute = parseTime(opt("maxTime")); | |
colFormat = opt("columnFormat"); | |
// week # options. (TODO: bad, logic also in other views) | |
showWeekNumbers = opt("weekNumbers"); | |
weekNumberTitle = opt("weekNumberTitle"); | |
if (opt("weekNumberCalculation") != "iso") { | |
weekNumberFormat = "w"; | |
} else { | |
weekNumberFormat = "W"; | |
} | |
snapMinutes = opt("snapMinutes") || opt("slotMinutes"); | |
} | |
/* Build DOM | |
-----------------------------------------------------------------------*/ | |
function buildSkeleton() { | |
var headerClass = tm + "-widget-header"; | |
var contentClass = tm + "-widget-content"; | |
var s; | |
var d; | |
var i; | |
var maxd; | |
var minutes; | |
var slotNormal = opt("slotMinutes") % 15 == 0; | |
buildDayTable(); | |
slotLayer = $( | |
"<div style='position:absolute;z-index:2;left:0;width:100%'/>" | |
).appendTo(element); | |
if (opt("allDaySlot")) { | |
daySegmentContainer = $( | |
"<div class='fc-event-container' style='position:absolute;z-index:8;top:0;left:0'/>" | |
).appendTo(slotLayer); | |
s = | |
"<table style='width:100%' class='fc-agenda-allday' cellspacing='0'>" + | |
"<tr>" + | |
"<th class='" + | |
headerClass + | |
" fc-agenda-axis'>" + | |
opt("allDayText") + | |
"</th>" + | |
"<td>" + | |
"<div class='fc-day-content'><div style='position:relative'/></div>" + | |
"</td>" + | |
"<th class='" + | |
headerClass + | |
" fc-agenda-gutter'> </th>" + | |
"</tr>" + | |
"</table>"; | |
allDayTable = $(s).appendTo(slotLayer); | |
allDayRow = allDayTable.find("tr"); | |
dayBind(allDayRow.find("td")); | |
slotLayer.append( | |
"<div class='fc-agenda-divider " + | |
headerClass + | |
"'>" + | |
"<div class='fc-agenda-divider-inner'/>" + | |
"</div>" | |
); | |
} else { | |
daySegmentContainer = $([]); // in jQuery 1.4, we can just do $() | |
} | |
slotScroller = $( | |
"<div style='position:absolute;width:100%;overflow-x:hidden;overflow-y:auto'/>" | |
).appendTo(slotLayer); | |
slotContainer = $( | |
"<div style='position:relative;width:100%;overflow:hidden'/>" | |
).appendTo(slotScroller); | |
slotSegmentContainer = $( | |
"<div class='fc-event-container' style='position:absolute;z-index:8;top:0;left:0'/>" | |
).appendTo(slotContainer); | |
s = | |
"<table class='fc-agenda-slots' style='width:100%' cellspacing='0'>" + | |
"<tbody>"; | |
d = zeroDate(); | |
maxd = addMinutes(cloneDate(d), maxMinute); | |
addMinutes(d, minMinute); | |
slotCnt = 0; | |
for (i = 0; d < maxd; i++) { | |
minutes = d.getMinutes(); | |
s += | |
"<tr class='fc-slot" + | |
i + | |
" " + | |
(!minutes ? "" : "fc-minor") + | |
"'>" + | |
"<th class='fc-agenda-axis " + | |
headerClass + | |
"'>" + | |
(!slotNormal || !minutes ? formatDate(d, opt("axisFormat")) : " ") + | |
"</th>" + | |
"<td class='" + | |
contentClass + | |
"'>" + | |
"<div style='position:relative'> </div>" + | |
"</td>" + | |
"</tr>"; | |
addMinutes(d, opt("slotMinutes")); | |
slotCnt++; | |
} | |
s += "</tbody>" + "</table>"; | |
slotTable = $(s).appendTo(slotContainer); | |
slotBind(slotTable.find("td")); | |
} | |
/* Build Day Table | |
-----------------------------------------------------------------------*/ | |
function buildDayTable() { | |
var html = buildDayTableHTML(); | |
if (dayTable) { | |
dayTable.remove(); | |
} | |
dayTable = $(html).appendTo(element); | |
dayHead = dayTable.find("thead"); | |
dayHeadCells = dayHead.find("th").slice(1, -1); // exclude gutter | |
dayBody = dayTable.find("tbody"); | |
dayBodyCells = dayBody.find("td").slice(0, -1); // exclude gutter | |
dayBodyCellInners = dayBodyCells.find("> div"); | |
dayBodyCellContentInners = dayBodyCells.find(".fc-day-content > div"); | |
dayBodyFirstCell = dayBodyCells.eq(0); | |
dayBodyFirstCellStretcher = dayBodyCellInners.eq(0); | |
markFirstLast(dayHead.add(dayHead.find("tr"))); | |
markFirstLast(dayBody.add(dayBody.find("tr"))); | |
// TODO: now that we rebuild the cells every time, we should call dayRender | |
} | |
function buildDayTableHTML() { | |
var html = | |
"<table style='width:100%' class='fc-agenda-days fc-border-separate' cellspacing='0'>" + | |
buildDayTableHeadHTML() + | |
buildDayTableBodyHTML() + | |
"</table>"; | |
return html; | |
} | |
function buildDayTableHeadHTML() { | |
var headerClass = tm + "-widget-header"; | |
var date; | |
var html = ""; | |
var weekText; | |
var col; | |
html += "<thead>" + "<tr>"; | |
if (showWeekNumbers) { | |
date = cellToDate(0, 0); | |
weekText = formatDate(date, weekNumberFormat); | |
if (rtl) { | |
weekText += weekNumberTitle; | |
} else { | |
weekText = weekNumberTitle + weekText; | |
} | |
html += | |
"<th class='fc-agenda-axis fc-week-number " + | |
headerClass + | |
"'>" + | |
htmlEscape(weekText) + | |
"</th>"; | |
} else { | |
html += "<th class='fc-agenda-axis " + headerClass + "'> </th>"; | |
} | |
for (col = 0; col < colCnt; col++) { | |
date = cellToDate(0, col); | |
html += | |
"<th class='fc-" + | |
dayIDs[date.getDay()] + | |
" fc-col" + | |
col + | |
" " + | |
headerClass + | |
"'>" + | |
htmlEscape(formatDate(date, colFormat)) + | |
"</th>"; | |
} | |
html += | |
"<th class='fc-agenda-gutter " + | |
headerClass + | |
"'> </th>" + | |
"</tr>" + | |
"</thead>"; | |
return html; | |
} | |
function buildDayTableBodyHTML() { | |
var headerClass = tm + "-widget-header"; // TODO: make these when updateOptions() called | |
var contentClass = tm + "-widget-content"; | |
var date; | |
var today = clearTime(new Date()); | |
var col; | |
var cellsHTML; | |
var cellHTML; | |
var classNames; | |
var html = ""; | |
html += | |
"<tbody>" + | |
"<tr>" + | |
"<th class='fc-agenda-axis " + | |
headerClass + | |
"'> </th>"; | |
cellsHTML = ""; | |
for (col = 0; col < colCnt; col++) { | |
date = cellToDate(0, col); | |
classNames = ["fc-col" + col, "fc-" + dayIDs[date.getDay()], contentClass]; | |
if (+date == +today) { | |
classNames.push(tm + "-state-highlight", "fc-today"); | |
} else if (date < today) { | |
classNames.push("fc-past"); | |
} else { | |
classNames.push("fc-future"); | |
} | |
cellHTML = | |
"<td class='" + | |
classNames.join(" ") + | |
"'>" + | |
"<div>" + | |
"<div class='fc-day-content'>" + | |
"<div style='position:relative'> </div>" + | |
"</div>" + | |
"</div>" + | |
"</td>"; | |
cellsHTML += cellHTML; | |
} | |
html += cellsHTML; | |
html += | |
"<td class='fc-agenda-gutter " + | |
contentClass + | |
"'> </td>" + | |
"</tr>" + | |
"</tbody>"; | |
return html; | |
} | |
// TODO: data-date on the cells | |
/* Dimensions | |
-----------------------------------------------------------------------*/ | |
function setHeight(height) { | |
if (height === undefined) { | |
height = viewHeight; | |
} | |
viewHeight = height; | |
slotTopCache = {}; | |
var headHeight = dayBody.position().top; | |
var allDayHeight = slotScroller.position().top; // including divider | |
var bodyHeight = Math.min( | |
// total body height, including borders | |
height - headHeight, // when scrollbars | |
slotTable.height() + allDayHeight + 1 // when no scrollbars. +1 for bottom border | |
); | |
dayBodyFirstCellStretcher.height(bodyHeight - vsides(dayBodyFirstCell)); | |
slotLayer.css("top", headHeight); | |
slotScroller.height(bodyHeight - allDayHeight - 1); | |
// the stylesheet guarantees that the first row has no border. | |
// this allows .height() to work well cross-browser. | |
slotHeight = slotTable.find("tr:first").height() + 1; // +1 for bottom border | |
snapRatio = opt("slotMinutes") / snapMinutes; | |
snapHeight = slotHeight / snapRatio; | |
} | |
function setWidth(width) { | |
viewWidth = width; | |
colPositions.clear(); | |
colContentPositions.clear(); | |
var axisFirstCells = dayHead.find("th:first"); | |
if (allDayTable) { | |
axisFirstCells = axisFirstCells.add(allDayTable.find("th:first")); | |
} | |
axisFirstCells = axisFirstCells.add(slotTable.find("th:first")); | |
axisWidth = 0; | |
setOuterWidth( | |
axisFirstCells.width("").each(function(i, _cell) { | |
axisWidth = Math.max(axisWidth, $(_cell).outerWidth()); | |
}), | |
axisWidth | |
); | |
var gutterCells = dayTable.find(".fc-agenda-gutter"); | |
if (allDayTable) { | |
gutterCells = gutterCells.add(allDayTable.find("th.fc-agenda-gutter")); | |
} | |
var slotTableWidth = slotScroller[0].clientWidth; // needs to be done after axisWidth (for IE7) | |
gutterWidth = slotScroller.width() - slotTableWidth; | |
if (gutterWidth) { | |
setOuterWidth(gutterCells, gutterWidth); | |
gutterCells | |
.show() | |
.prev() | |
.removeClass("fc-last"); | |
} else { | |
gutterCells | |
.hide() | |
.prev() | |
.addClass("fc-last"); | |
} | |
colWidth = Math.floor((slotTableWidth - axisWidth) / colCnt); | |
setOuterWidth(dayHeadCells.slice(0, -1), colWidth); | |
} | |
/* Scrolling | |
-----------------------------------------------------------------------*/ | |
function resetScroll() { | |
var d0 = zeroDate(); | |
var scrollDate = cloneDate(d0); | |
scrollDate.setHours(opt("firstHour")); | |
var top = timePosition(d0, scrollDate) + 1; // +1 for the border | |
function scroll() { | |
slotScroller.scrollTop(top); | |
} | |
scroll(); | |
setTimeout(scroll, 0); // overrides any previous scroll state made by the browser | |
} | |
function afterRender() { | |
// after the view has been freshly rendered and sized | |
resetScroll(); | |
} | |
/* Slot/Day clicking and binding | |
-----------------------------------------------------------------------*/ | |
function dayBind(cells) { | |
cells.click(slotClick).mousedown(daySelectionMousedown); | |
} | |
function slotBind(cells) { | |
cells.click(slotClick).mousedown(slotSelectionMousedown); | |
} | |
function slotClick(ev) { | |
if (!opt("selectable")) { | |
// if selectable, SelectionManager will worry about dayClick | |
var col = Math.min( | |
colCnt - 1, | |
Math.floor((ev.pageX - dayTable.offset().left - axisWidth) / colWidth) | |
); | |
var date = cellToDate(0, col); | |
var rowMatch = this.parentNode.className.match(/fc-slot(\d+)/); // TODO: maybe use data | |
if (rowMatch) { | |
var mins = parseInt(rowMatch[1]) * opt("slotMinutes"); | |
var hours = Math.floor(mins / 60); | |
date.setHours(hours); | |
date.setMinutes((mins % 60) + minMinute); | |
trigger("dayClick", dayBodyCells[col], date, false, ev); | |
} else { | |
trigger("dayClick", dayBodyCells[col], date, true, ev); | |
} | |
} | |
} | |
/* Semi-transparent Overlay Helpers | |
-----------------------------------------------------*/ | |
// TODO: should be consolidated with BasicView's methods | |
function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { | |
// overlayEnd is exclusive | |
if (refreshCoordinateGrid) { | |
coordinateGrid.build(); | |
} | |
var segments = rangeToSegments(overlayStart, overlayEnd); | |
for (var i = 0; i < segments.length; i++) { | |
var segment = segments[i]; | |
dayBind( | |
renderCellOverlay( | |
segment.row, | |
segment.leftCol, | |
segment.row, | |
segment.rightCol | |
) | |
); | |
} | |
} | |
function renderCellOverlay(row0, col0, row1, col1) { | |
// only for all-day? | |
var rect = coordinateGrid.rect(row0, col0, row1, col1, slotLayer); | |
return renderOverlay(rect, slotLayer); | |
} | |
function renderSlotOverlay(overlayStart, overlayEnd) { | |
for (var i = 0; i < colCnt; i++) { | |
var dayStart = cellToDate(0, i); | |
var dayEnd = addDays(cloneDate(dayStart), 1); | |
var stretchStart = new Date(Math.max(dayStart, overlayStart)); | |
var stretchEnd = new Date(Math.min(dayEnd, overlayEnd)); | |
if (stretchStart < stretchEnd) { | |
var rect = coordinateGrid.rect(0, i, 0, i, slotContainer); // only use it for horizontal coords | |
var top = timePosition(dayStart, stretchStart); | |
var bottom = timePosition(dayStart, stretchEnd); | |
rect.top = top; | |
rect.height = bottom - top; | |
slotBind(renderOverlay(rect, slotContainer)); | |
} | |
} | |
} | |
/* Coordinate Utilities | |
-----------------------------------------------------------------------------*/ | |
coordinateGrid = new CoordinateGrid(function(rows, cols) { | |
var e, n, p; | |
dayHeadCells.each(function(i, _e) { | |
e = $(_e); | |
n = e.offset().left; | |
if (i) { | |
p[1] = n; | |
} | |
p = [n]; | |
cols[i] = p; | |
}); | |
p[1] = n + e.outerWidth(); | |
if (opt("allDaySlot")) { | |
e = allDayRow; | |
n = e.offset().top; | |
rows[0] = [n, n + e.outerHeight()]; | |
} | |
var slotTableTop = slotContainer.offset().top; | |
var slotScrollerTop = slotScroller.offset().top; | |
var slotScrollerBottom = slotScrollerTop + slotScroller.outerHeight(); | |
function constrain(n) { | |
return Math.max(slotScrollerTop, Math.min(slotScrollerBottom, n)); | |
} | |
for (var i = 0; i < slotCnt * snapRatio; i++) { | |
// adapt slot count to increased/decreased selection slot count | |
rows.push([ | |
constrain(slotTableTop + snapHeight * i), | |
constrain(slotTableTop + snapHeight * (i + 1)) | |
]); | |
} | |
}); | |
hoverListener = new HoverListener(coordinateGrid); | |
colPositions = new HorizontalPositionCache(function(col) { | |
return dayBodyCellInners.eq(col); | |
}); | |
colContentPositions = new HorizontalPositionCache(function(col) { | |
return dayBodyCellContentInners.eq(col); | |
}); | |
function colLeft(col) { | |
return colPositions.left(col); | |
} | |
function colContentLeft(col) { | |
return colContentPositions.left(col); | |
} | |
function colRight(col) { | |
return colPositions.right(col); | |
} | |
function colContentRight(col) { | |
return colContentPositions.right(col); | |
} | |
function getIsCellAllDay(cell) { | |
return opt("allDaySlot") && !cell.row; | |
} | |
function realCellToDate(cell) { | |
// ugh "real" ... but blame it on our abuse of the "cell" system | |
var d = cellToDate(0, cell.col); | |
var slotIndex = cell.row; | |
if (opt("allDaySlot")) { | |
slotIndex--; | |
} | |
if (slotIndex >= 0) { | |
addMinutes(d, minMinute + slotIndex * snapMinutes); | |
} | |
return d; | |
} | |
// 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 slotTable.height(); | |
} | |
var slotMinutes = opt("slotMinutes"), | |
minutes = time.getHours() * 60 + time.getMinutes() - minMinute, | |
slotI = Math.floor(minutes / slotMinutes), | |
slotTop = slotTopCache[slotI]; | |
if (slotTop === undefined) { | |
slotTop = slotTopCache[slotI] = slotTable | |
.find("tr") | |
.eq(slotI) | |
.find("td div")[0].offsetTop; | |
// .eq() is faster than ":eq()" selector | |
// [0].offsetTop is faster than .position().top (do we really need this optimization?) | |
// a better optimization would be to cache all these divs | |
} | |
return Math.max( | |
0, | |
Math.round( | |
slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes) | |
) | |
); | |
} | |
function getAllDayRow(index) { | |
return allDayRow; | |
} | |
function defaultEventEnd(event) { | |
var start = cloneDate(event.start); | |
if (event.allDay) { | |
return start; | |
} | |
return addMinutes(start, opt("defaultEventMinutes")); | |
} | |
/* Selection | |
---------------------------------------------------------------------------------*/ | |
function defaultSelectionEnd(startDate, allDay) { | |
if (allDay) { | |
return cloneDate(startDate); | |
} | |
return addMinutes(cloneDate(startDate), opt("slotMinutes")); | |
} | |
function renderSelection(startDate, endDate, allDay) { | |
// only for all-day | |
if (allDay) { | |
if (opt("allDaySlot")) { | |
renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true); | |
} | |
} else { | |
renderSlotSelection(startDate, endDate); | |
} | |
} | |
function renderSlotSelection(startDate, endDate) { | |
var helperOption = opt("selectHelper"); | |
coordinateGrid.build(); | |
if (helperOption) { | |
var col = dateToCell(startDate).col; | |
if (col >= 0 && col < colCnt) { | |
// only works when times are on same day | |
var rect = coordinateGrid.rect(0, col, 0, col, slotContainer); // 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"; | |
selectionHelper = $(helperRes) | |
.css(rect) | |
.appendTo(slotContainer); | |
} | |
} else { | |
rect.isStart = true; // conside rect a "seg" now | |
rect.isEnd = true; // | |
selectionHelper = $( | |
slotSegHtml( | |
{ | |
title: "", | |
start: startDate, | |
end: endDate, | |
className: ["fc-select-helper"], | |
editable: false | |
}, | |
rect | |
) | |
); | |
selectionHelper.css("opacity", opt("dragOpacity")); | |
} | |
if (selectionHelper) { | |
slotBind(selectionHelper); | |
slotContainer.append(selectionHelper); | |
setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended | |
setOuterHeight(selectionHelper, rect.height, true); | |
} | |
} | |
} | |
} else { | |
renderSlotOverlay(startDate, endDate); | |
} | |
} | |
function clearSelection() { | |
clearOverlays(); | |
if (selectionHelper) { | |
selectionHelper.remove(); | |
selectionHelper = null; | |
} | |
} | |
function slotSelectionMousedown(ev) { | |
if (ev.which == 1 && opt("selectable")) { | |
// ev.which==1 means left mouse button | |
unselect(ev); | |
var dates; | |
hoverListener.start(function(cell, origCell) { | |
clearSelection(); | |
if (cell && cell.col == origCell.col && !getIsCellAllDay(cell)) { | |