Skip to content

Instantly share code, notes, and snippets.

@sachinsmc
Created March 11, 2020 18:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sachinsmc/67e80164187d506124446848d88ca66e to your computer and use it in GitHub Desktop.
Save sachinsmc/67e80164187d506124446848d88ca66e to your computer and use it in GitHub Desktop.
Calender
<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>
/*!
* 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)) {
var d1 = realCellToDate(origCell);
var d2 = realCellToDate(cell);
dates = [
d1,
addMinutes(cloneDate(d1), snapMinutes), // calculate minutes depending on selection slot minutes
d2,
addMinutes(cloneDate(d2), snapMinutes)
].sort(dateCompare);
renderSlotSelection(dates[0], dates[3]);
} else {
dates = null;
}
}, ev);
$(document).one("mouseup", function(ev) {
hoverListener.stop();
if (dates) {
if (+dates[0] == +dates[1]) {
reportDayClick(dates[0], false, ev);
}
reportSelection(dates[0], dates[3], false, ev);
}
});
}
}
function reportDayClick(date, allDay, ev) {
trigger("dayClick", dayBodyCells[dateToCell(date).col], date, allDay, ev);
}
/* External Dragging
--------------------------------------------------------------------------------*/
function dragStart(_dragElement, ev, ui) {
hoverListener.start(function(cell) {
clearOverlays();
if (cell) {
if (getIsCellAllDay(cell)) {
renderCellOverlay(cell.row, cell.col, cell.row, cell.col);
} else {
var d1 = realCellToDate(cell);
var d2 = addMinutes(cloneDate(d1), opt("defaultEventMinutes"));
renderSlotOverlay(d1, d2);
}
}
}, ev);
}
function dragStop(_dragElement, ev, ui) {
var cell = hoverListener.stop();
clearOverlays();
if (cell) {
trigger(
"drop",
_dragElement,
realCellToDate(cell),
getIsCellAllDay(cell),
ev,
ui
);
}
}
}
function AgendaEventRenderer() {
var t = this;
// exports
t.renderEvents = renderEvents;
t.clearEvents = clearEvents;
t.slotSegHtml = slotSegHtml;
// imports
DayEventRenderer.call(t);
var opt = t.opt;
var trigger = t.trigger;
var isEventDraggable = t.isEventDraggable;
var isEventResizable = t.isEventResizable;
var eventEnd = t.eventEnd;
var eventElementHandlers = t.eventElementHandlers;
var setHeight = t.setHeight;
var getDaySegmentContainer = t.getDaySegmentContainer;
var getSlotSegmentContainer = t.getSlotSegmentContainer;
var getHoverListener = t.getHoverListener;
var getMaxMinute = t.getMaxMinute;
var getMinMinute = t.getMinMinute;
var timePosition = t.timePosition;
var getIsCellAllDay = t.getIsCellAllDay;
var colContentLeft = t.colContentLeft;
var colContentRight = t.colContentRight;
var cellToDate = t.cellToDate;
var getColCnt = t.getColCnt;
var getColWidth = t.getColWidth;
var getSnapHeight = t.getSnapHeight;
var getSnapMinutes = t.getSnapMinutes;
var getSlotContainer = t.getSlotContainer;
var reportEventElement = t.reportEventElement;
var showEvents = t.showEvents;
var hideEvents = t.hideEvents;
var eventDrop = t.eventDrop;
var eventResize = t.eventResize;
var renderDayOverlay = t.renderDayOverlay;
var clearOverlays = t.clearOverlays;
var renderDayEvents = t.renderDayEvents;
var calendar = t.calendar;
var formatDate = calendar.formatDate;
var formatDates = calendar.formatDates;
// overrides
t.draggableDayEvent = draggableDayEvent;
/* Rendering
----------------------------------------------------------------------------*/
function renderEvents(events, modifiedEventId) {
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]);
}
}
if (opt("allDaySlot")) {
renderDayEvents(dayEvents, modifiedEventId);
setHeight(); // no params means set to viewHeight
}
renderSlotSegs(compileSlotSegs(slotEvents), modifiedEventId);
}
function clearEvents() {
getDaySegmentContainer().empty();
getSlotSegmentContainer().empty();
}
function compileSlotSegs(events) {
var colCnt = getColCnt(),
minMinute = getMinMinute(),
maxMinute = getMaxMinute(),
d,
visEventEnds = $.map(events, slotEventEnd),
i,
j,
seg,
colSegs,
segs = [];
for (i = 0; i < colCnt; i++) {
d = cellToDate(0, i);
addMinutes(d, minMinute);
colSegs = sliceSegs(
events,
visEventEnds,
d,
addMinutes(cloneDate(d), maxMinute - minMinute)
);
colSegs = placeSlotSegs(colSegs); // returns a new order
for (j = 0; j < colSegs.length; j++) {
seg = colSegs[j];
seg.col = i;
segs.push(seg);
}
}
return segs;
}
function sliceSegs(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
});
}
}
return segs.sort(compareSlotSegs);
}
function slotEventEnd(event) {
if (event.end) {
return cloneDate(event.end);
} else {
return addMinutes(cloneDate(event.start), opt("defaultEventMinutes"));
}
}
// renders events in the 'time slots' at the bottom
// TODO: when we refactor this, when user returns `false` eventRender, don't have empty space
// TODO: refactor will include using pixels to detect collisions instead of dates (handy for seg cmp)
function renderSlotSegs(segs, modifiedEventId) {
var i,
segCnt = segs.length,
seg,
event,
top,
bottom,
columnLeft,
columnRight,
columnWidth,
width,
left,
right,
html = "",
eventElements,
eventElement,
triggerRes,
titleElement,
height,
slotSegmentContainer = getSlotSegmentContainer(),
isRTL = opt("isRTL");
// calculate position/dimensions, create html
for (i = 0; i < segCnt; i++) {
seg = segs[i];
event = seg.event;
top = timePosition(seg.start, seg.start);
bottom = timePosition(seg.start, seg.end);
columnLeft = colContentLeft(seg.col);
columnRight = colContentRight(seg.col);
columnWidth = columnRight - columnLeft;
// shave off space on right near scrollbars (2.5%)
// TODO: move this to CSS somehow
columnRight -= columnWidth * 0.025;
columnWidth = columnRight - columnLeft;
width = columnWidth * (seg.forwardCoord - seg.backwardCoord);
if (opt("slotEventOverlap")) {
// double the width while making sure resize handle is visible
// (assumed to be 20px wide)
width = Math.max(
(width - 20 / 2) * 2,
width // narrow columns will want to make the segment smaller than
// the natural width. don't allow it
);
}
if (isRTL) {
right = columnRight - seg.backwardCoord * columnWidth;
left = right - width;
} else {
left = columnLeft + seg.backwardCoord * columnWidth;
right = left + width;
}
// make sure horizontal coordinates are in bounds
left = Math.max(left, columnLeft);
right = Math.min(right, columnRight);
width = right - left;
seg.top = top;
seg.left = left;
seg.outerWidth = width;
seg.outerHeight = bottom - top;
html += slotSegHtml(event, seg);
}
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 = 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) {
bindSlotSeg(event, eventElement, seg);
} else {
eventElement[0]._fci = i; // for lazySegBind
}
reportEventElement(event, eventElement);
}
}
lazySegBind(slotSegmentContainer, segs, bindSlotSeg);
// record event sides and title positions
for (i = 0; i < segCnt; i++) {
seg = segs[i];
if ((eventElement = seg.element)) {
seg.vsides = vsides(eventElement, true);
seg.hsides = hsides(eventElement, true);
titleElement = eventElement.find(".fc-event-title");
if (titleElement.length) {
seg.contentTop = titleElement[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 =
Math.max(0, seg.outerWidth - seg.hsides) + "px";
height = Math.max(0, seg.outerHeight - seg.vsides);
eventElement[0].style.height = height + "px";
event = seg.event;
if (seg.contentTop !== undefined && height - seg.contentTop < 10) {
// not enough room for title, put it in the time (TODO: maybe make both display:inline instead)
eventElement
.find("div.fc-event-time")
.text(formatDate(event.start, opt("timeFormat")) + " - " + event.title);
eventElement.find("div.fc-event-title").remove();
}
trigger("eventAfterRender", event, event, eventElement);
}
}
}
function slotSegHtml(event, seg) {
var html = "<";
var url = event.url;
var skinCss = getSkinCss(event, opt);
var classes = ["fc-event", "fc-event-vert"];
if (isEventDraggable(event)) {
classes.push("fc-event-draggable");
}
if (seg.isStart) {
classes.push("fc-event-start");
}
if (seg.isEnd) {
classes.push("fc-event-end");
}
classes = classes.concat(event.className);
if (event.source) {
classes = classes.concat(event.source.className || []);
}
if (url) {
html += "a href='" + htmlEscape(event.url) + "'";
} else {
html += "div";
}
html +=
" class='" +
classes.join(" ") +
"'" +
" style=" +
"'" +
"position:absolute;" +
"top:" +
seg.top +
"px;" +
"left:" +
seg.left +
"px;" +
skinCss +
"'" +
">" +
"<div class='fc-event-inner'>" +
"<div class='fc-event-time'>" +
htmlEscape(formatDates(event.start, event.end, opt("timeFormat"))) +
"</div>" +
"<div class='fc-event-title'>" +
htmlEscape(event.title || "") +
"</div>" +
"</div>" +
"<div class='fc-event-bg'></div>";
if (seg.isEnd && isEventResizable(event)) {
html += "<div class='ui-resizable-handle ui-resizable-s'>=</div>";
}
html += "</" + (url ? "a" : "div") + ">";
return html;
}
function bindSlotSeg(event, eventElement, seg) {
var timeElement = eventElement.find("div.fc-event-time");
if (isEventDraggable(event)) {
draggableSlotEvent(event, eventElement, timeElement);
}
if (seg.isEnd && isEventResizable(event)) {
resizableSlotEvent(event, eventElement, timeElement);
}
eventElementHandlers(event, eventElement);
}
/* Dragging
-----------------------------------------------------------------------------------*/
// when event starts out FULL-DAY
// overrides DayEventRenderer's version because it needs to account for dragging elements
// to and from the slot area.
function draggableDayEvent(event, eventElement, seg) {
var isStart = seg.isStart;
var origWidth;
var revert;
var allDay = true;
var dayDelta;
var hoverListener = getHoverListener();
var colWidth = getColWidth();
var snapHeight = getSnapHeight();
var snapMinutes = getSnapMinutes();
var minMinute = getMinMinute();
eventElement.draggable({
opacity: opt("dragOpacity", "month"), // use whatever the month view was using
revertDuration: opt("dragRevertDuration"),
start: function(ev, ui) {
trigger("eventDragStart", eventElement, event, ev, ui);
hideEvents(event, eventElement);
origWidth = eventElement.width();
hoverListener.start(
function(cell, origCell) {
clearOverlays();
if (cell) {
revert = false;
var origDate = cellToDate(0, origCell.col);
var date = cellToDate(0, cell.col);
dayDelta = dayDiff(date, origDate);
if (!cell.row) {
// on full-days
renderDayOverlay(
addDays(cloneDate(event.start), dayDelta),
addDays(exclEndDay(event), dayDelta)
);
resetElement();
} else {
// mouse is over bottom slots
if (isStart) {
if (allDay) {
// convert event to temporary slot-event
eventElement.width(colWidth - 10); // don't use entire width
setOuterHeight(
eventElement,
snapHeight *
Math.round(
(event.end
? (event.end - event.start) / MINUTE_MS
: opt("defaultEventMinutes")) / snapMinutes
)
);
eventElement.draggable("option", "grid", [colWidth, 1]);
allDay = false;
}
} else {
revert = true;
}
}
revert = revert || (allDay && !dayDelta);
} else {
resetElement();
revert = true;
}
eventElement.draggable("option", "revert", revert);
},
ev,
"drag"
);
},
stop: function(ev, ui) {
hoverListener.stop();
clearOverlays();
trigger("eventDragStop", eventElement, event, ev, ui);
if (revert) {
// hasn't moved or is out of bounds (draggable has already reverted)
resetElement();
eventElement.css("filter", ""); // clear IE opacity side-effects
showEvents(event, eventElement);
} else {
// changed!
var minuteDelta = 0;
if (!allDay) {
minuteDelta =
Math.round(
(eventElement.offset().top - getSlotContainer().offset().top) /
snapHeight
) *
snapMinutes +
minMinute -
(event.start.getHours() * 60 + event.start.getMinutes());
}
eventDrop(this, event, dayDelta, minuteDelta, allDay, ev, ui);
}
}
});
function resetElement() {
if (!allDay) {
eventElement
.width(origWidth)
.height("")
.draggable("option", "grid", null);
allDay = true;
}
}
}
// when event starts out IN TIMESLOTS
function draggableSlotEvent(event, eventElement, timeElement) {
var coordinateGrid = t.getCoordinateGrid();
var colCnt = getColCnt();
var colWidth = getColWidth();
var snapHeight = getSnapHeight();
var snapMinutes = getSnapMinutes();
// states
var origPosition; // original position of the element, not the mouse
var origCell;
var isInBounds, prevIsInBounds;
var isAllDay, prevIsAllDay;
var colDelta, prevColDelta;
var dayDelta; // derived from colDelta
var minuteDelta, prevMinuteDelta;
eventElement.draggable({
scroll: false,
grid: [colWidth, snapHeight],
axis: colCnt == 1 ? "y" : false,
opacity: opt("dragOpacity"),
revertDuration: opt("dragRevertDuration"),
start: function(ev, ui) {
trigger("eventDragStart", eventElement, event, ev, ui);
hideEvents(event, eventElement);
coordinateGrid.build();
// initialize states
origPosition = eventElement.position();
origCell = coordinateGrid.cell(ev.pageX, ev.pageY);
isInBounds = prevIsInBounds = true;
isAllDay = prevIsAllDay = getIsCellAllDay(origCell);
colDelta = prevColDelta = 0;
dayDelta = 0;
minuteDelta = prevMinuteDelta = 0;
},
drag: function(ev, ui) {
// NOTE: this `cell` value is only useful for determining in-bounds and all-day.
// Bad for anything else due to the discrepancy between the mouse position and the
// element position while snapping. (problem revealed in PR #55)
//
// PS- the problem exists for draggableDayEvent() when dragging an all-day event to a slot event.
// We should overhaul the dragging system and stop relying on jQuery UI.
var cell = coordinateGrid.cell(ev.pageX, ev.pageY);
// update states
isInBounds = !!cell;
if (isInBounds) {
isAllDay = getIsCellAllDay(cell);
// calculate column delta
colDelta = Math.round((ui.position.left - origPosition.left) / colWidth);
if (colDelta != prevColDelta) {
// calculate the day delta based off of the original clicked column and the column delta
var origDate = cellToDate(0, origCell.col);
var col = origCell.col + colDelta;
col = Math.max(0, col);
col = Math.min(colCnt - 1, col);
var date = cellToDate(0, col);
dayDelta = dayDiff(date, origDate);
}
// calculate minute delta (only if over slots)
if (!isAllDay) {
minuteDelta =
Math.round((ui.position.top - origPosition.top) / snapHeight) *
snapMinutes;
}
}
// any state changes?
if (
isInBounds != prevIsInBounds ||
isAllDay != prevIsAllDay ||
colDelta != prevColDelta ||
minuteDelta != prevMinuteDelta
) {
updateUI();
// update previous states for next time
prevIsInBounds = isInBounds;
prevIsAllDay = isAllDay;
prevColDelta = colDelta;
prevMinuteDelta = minuteDelta;
}
// if out-of-bounds, revert when done, and vice versa.
eventElement.draggable("option", "revert", !isInBounds);
},
stop: function(ev, ui) {
clearOverlays();
trigger("eventDragStop", eventElement, event, ev, ui);
if (isInBounds && (isAllDay || dayDelta || minuteDelta)) {
// changed!
eventDrop(
this,
event,
dayDelta,
isAllDay ? 0 : minuteDelta,
isAllDay,
ev,
ui
);
} else {
// either no change or out-of-bounds (draggable has already reverted)
// reset states for next time, and for updateUI()
isInBounds = true;
isAllDay = false;
colDelta = 0;
dayDelta = 0;
minuteDelta = 0;
updateUI();
eventElement.css("filter", ""); // clear IE opacity side-effects
// sometimes fast drags make event revert to wrong position, so reset.
// also, if we dragged the element out of the area because of snapping,
// but the *mouse* is still in bounds, we need to reset the position.
eventElement.css(origPosition);
showEvents(event, eventElement);
}
}
});
function updateUI() {
clearOverlays();
if (isInBounds) {
if (isAllDay) {
timeElement.hide();
eventElement.draggable("option", "grid", null); // disable grid snapping
renderDayOverlay(
addDays(cloneDate(event.start), dayDelta),
addDays(exclEndDay(event), dayDelta)
);
} else {
updateTimeText(minuteDelta);
timeElement.css("display", ""); // show() was causing display=inline
eventElement.draggable("option", "grid", [colWidth, snapHeight]); // re-enable grid snapping
}
}
}
function updateTimeText(minuteDelta) {
var newStart = addMinutes(cloneDate(event.start), minuteDelta);
var newEnd;
if (event.end) {
newEnd = addMinutes(cloneDate(event.end), minuteDelta);
}
timeElement.text(formatDates(newStart, newEnd, opt("timeFormat")));
}
}
/* Resizing
--------------------------------------------------------------------------------------*/
function resizableSlotEvent(event, eventElement, timeElement) {
var snapDelta, prevSnapDelta;
var snapHeight = getSnapHeight();
var snapMinutes = getSnapMinutes();
eventElement.resizable({
handles: {
s: ".ui-resizable-handle"
},
grid: snapHeight,
start: function(ev, ui) {
snapDelta = prevSnapDelta = 0;
hideEvents(event, eventElement);
trigger("eventResizeStart", this, event, ev, ui);
},
resize: function(ev, ui) {
// don't rely on ui.size.height, doesn't take grid into account
snapDelta = Math.round(
(Math.max(snapHeight, eventElement.height()) - ui.originalSize.height) /
snapHeight
);
if (snapDelta != prevSnapDelta) {
timeElement.text(
formatDates(
event.start,
!snapDelta && !event.end
? null // no change, so don't display time range
: addMinutes(eventEnd(event), snapMinutes * snapDelta),
opt("timeFormat")
)
);
prevSnapDelta = snapDelta;
}
},
stop: function(ev, ui) {
trigger("eventResizeStop", this, event, ev, ui);
if (snapDelta) {
eventResize(this, event, 0, snapMinutes * snapDelta, ev, ui);
} else {
showEvents(event, eventElement);
// BUG: if event was really short, need to put title back in span
}
}
});
}
}
/* Agenda Event Segment Utilities
-----------------------------------------------------------------------------*/
// Sets the seg.backwardCoord and seg.forwardCoord on each segment and returns a new
// list in the order they should be placed into the DOM (an implicit z-index).
function placeSlotSegs(segs) {
var levels = buildSlotSegLevels(segs);
var level0 = levels[0];
var i;
computeForwardSlotSegs(levels);
if (level0) {
for (i = 0; i < level0.length; i++) {
computeSlotSegPressures(level0[i]);
}
for (i = 0; i < level0.length; i++) {
computeSlotSegCoords(level0[i], 0, 0);
}
}
return flattenSlotSegLevels(levels);
}
// Builds an array of segments "levels". The first level will be the leftmost tier of segments
// if the calendar is left-to-right, or the rightmost if the calendar is right-to-left.
function buildSlotSegLevels(segs) {
var levels = [];
var i, seg;
var j;
for (i = 0; i < segs.length; i++) {
seg = segs[i];
// go through all the levels and stop on the first level where there are no collisions
for (j = 0; j < levels.length; j++) {
if (!computeSlotSegCollisions(seg, levels[j]).length) {
break;
}
}
(levels[j] || (levels[j] = [])).push(seg);
}
return levels;
}
// For every segment, figure out the other segments that are in subsequent
// levels that also occupy the same vertical space. Accumulate in seg.forwardSegs
function computeForwardSlotSegs(levels) {
var i, level;
var j, seg;
var k;
for (i = 0; i < levels.length; i++) {
level = levels[i];
for (j = 0; j < level.length; j++) {
seg = level[j];
seg.forwardSegs = [];
for (k = i + 1; k < levels.length; k++) {
computeSlotSegCollisions(seg, levels[k], seg.forwardSegs);
}
}
}
}
// Figure out which path forward (via seg.forwardSegs) results in the longest path until
// the furthest edge is reached. The number of segments in this path will be seg.forwardPressure
function computeSlotSegPressures(seg) {
var forwardSegs = seg.forwardSegs;
var forwardPressure = 0;
var i, forwardSeg;
if (seg.forwardPressure === undefined) {
// not already computed
for (i = 0; i < forwardSegs.length; i++) {
forwardSeg = forwardSegs[i];
// figure out the child's maximum forward path
computeSlotSegPressures(forwardSeg);
// either use the existing maximum, or use the child's forward pressure
// plus one (for the forwardSeg itself)
forwardPressure = Math.max(forwardPressure, 1 + forwardSeg.forwardPressure);
}
seg.forwardPressure = forwardPressure;
}
}
// Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
// from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
// seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
//
// The segment might be part of a "series", which means consecutive segments with the same pressure
// who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
// segments behind this one in the current series, and `seriesBackwardCoord` is the starting
// coordinate of the first segment in the series.
function computeSlotSegCoords(
seg,
seriesBackwardPressure,
seriesBackwardCoord
) {
var forwardSegs = seg.forwardSegs;
var i;
if (seg.forwardCoord === undefined) {
// not already computed
if (!forwardSegs.length) {
// if there are no forward segments, this segment should butt up against the edge
seg.forwardCoord = 1;
} else {
// sort highest pressure first
forwardSegs.sort(compareForwardSlotSegs);
// this segment's forwardCoord will be calculated from the backwardCoord of the
// highest-pressure forward segment.
computeSlotSegCoords(
forwardSegs[0],
seriesBackwardPressure + 1,
seriesBackwardCoord
);
seg.forwardCoord = forwardSegs[0].backwardCoord;
}
// calculate the backwardCoord from the forwardCoord. consider the series
seg.backwardCoord =
seg.forwardCoord -
(seg.forwardCoord - seriesBackwardCoord) / // available width for series
(seriesBackwardPressure + 1); // # of segments in the series
// use this segment's coordinates to computed the coordinates of the less-pressurized
// forward segments
for (i = 0; i < forwardSegs.length; i++) {
computeSlotSegCoords(forwardSegs[i], 0, seg.forwardCoord);
}
}
}
// Outputs a flat array of segments, from lowest to highest level
function flattenSlotSegLevels(levels) {
var segs = [];
var i, level;
var j;
for (i = 0; i < levels.length; i++) {
level = levels[i];
for (j = 0; j < level.length; j++) {
segs.push(level[j]);
}
}
return segs;
}
// Find all the segments in `otherSegs` that vertically collide with `seg`.
// Append into an optionally-supplied `results` array and return.
function computeSlotSegCollisions(seg, otherSegs, results) {
results = results || [];
for (var i = 0; i < otherSegs.length; i++) {
if (isSlotSegCollision(seg, otherSegs[i])) {
results.push(otherSegs[i]);
}
}
return results;
}
// Do these segments occupy the same vertical space?
function isSlotSegCollision(seg1, seg2) {
return seg1.end > seg2.start && seg1.start < seg2.end;
}
// A cmp function for determining which forward segment to rely on more when computing coordinates.
function compareForwardSlotSegs(seg1, seg2) {
// put higher-pressure first
return (
seg2.forwardPressure - seg1.forwardPressure ||
// put segments that are closer to initial edge first (and favor ones with no coords yet)
(seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) ||
// do normal sorting...
compareSlotSegs(seg1, seg2)
);
}
// A cmp function for determining which segment should be closer to the initial edge
// (the left edge on a left-to-right calendar).
function compareSlotSegs(seg1, seg2) {
return (
seg1.start - seg2.start || // earlier start time goes first
seg2.end - seg2.start - (seg1.end - seg1.start) || // tie? longer-duration goes first
(seg1.event.title || "").localeCompare(seg2.event.title)
); // tie? alphabetically by title
}
function View(element, calendar, viewName) {
var t = this;
// exports
t.element = element;
t.calendar = calendar;
t.name = viewName;
t.opt = opt;
t.trigger = trigger;
t.isEventDraggable = isEventDraggable;
t.isEventResizable = isEventResizable;
t.setEventData = setEventData;
t.clearEventData = clearEventData;
t.eventEnd = eventEnd;
t.reportEventElement = reportEventElement;
t.triggerEventDestroy = triggerEventDestroy;
t.eventElementHandlers = eventElementHandlers;
t.showEvents = showEvents;
t.hideEvents = hideEvents;
t.eventDrop = eventDrop;
t.eventResize = eventResize;
// t.title
// t.start, t.end
// t.visStart, t.visEnd
// imports
var defaultEventEnd = t.defaultEventEnd;
var normalizeEvent = calendar.normalizeEvent; // in EventManager
var reportEventChange = calendar.reportEventChange;
// locals
var eventsByID = {}; // eventID mapped to array of events (there can be multiple b/c of repeating events)
var eventElementsByID = {}; // eventID mapped to array of jQuery elements
var eventElementCouples = []; // array of objects, { event, element } // TODO: unify with segment system
var options = calendar.options;
function opt(name, viewNameOverride) {
var v = options[name];
if ($.isPlainObject(v)) {
return smartProperty(v, viewNameOverride || viewName);
}
return v;
}
function trigger(name, thisObj) {
return calendar.trigger.apply(
calendar,
[name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t])
);
}
/* Event Editable Boolean Calculations
------------------------------------------------------------------------------*/
function isEventDraggable(event) {
var source = event.source || {};
return (
firstDefined(
event.startEditable,
source.startEditable,
opt("eventStartEditable"),
event.editable,
source.editable,
opt("editable")
) && !opt("disableDragging")
); // deprecated
}
function isEventResizable(event) {
// but also need to make sure the seg.isEnd == true
var source = event.source || {};
return (
firstDefined(
event.durationEditable,
source.durationEditable,
opt("eventDurationEditable"),
event.editable,
source.editable,
opt("editable")
) && !opt("disableResizing")
); // deprecated
}
/* Event Data
------------------------------------------------------------------------------*/
function setEventData(events) {
// events are already normalized at this point
eventsByID = {};
var i,
len = events.length,
event;
for (i = 0; i < len; i++) {
event = events[i];
if (eventsByID[event._id]) {
eventsByID[event._id].push(event);
} else {
eventsByID[event._id] = [event];
}
}
}
function clearEventData() {
eventsByID = {};
eventElementsByID = {};
eventElementCouples = [];
}
// returns a Date object for an event's end
function eventEnd(event) {
return event.end ? cloneDate(event.end) : defaultEventEnd(event);
}
/* Event Elements
------------------------------------------------------------------------------*/
// report when view creates an element for an event
function reportEventElement(event, element) {
eventElementCouples.push({ event: event, element: element });
if (eventElementsByID[event._id]) {
eventElementsByID[event._id].push(element);
} else {
eventElementsByID[event._id] = [element];
}
}
function triggerEventDestroy() {
$.each(eventElementCouples, function(i, couple) {
t.trigger("eventDestroy", couple.event, couple.event, couple.element);
});
}
// attaches eventClick, eventMouseover, eventMouseout
function eventElementHandlers(event, eventElement) {
eventElement
.click(function(ev) {
if (
!eventElement.hasClass("ui-draggable-dragging") &&
!eventElement.hasClass("ui-resizable-resizing")
) {
return trigger("eventClick", this, event, ev);
}
})
.hover(
function(ev) {
trigger("eventMouseover", this, event, ev);
},
function(ev) {
trigger("eventMouseout", this, event, ev);
}
);
// TODO: don't fire eventMouseover/eventMouseout *while* dragging is occuring (on subject element)
// TODO: same for resizing
}
function showEvents(event, exceptElement) {
eachEventElement(event, exceptElement, "show");
}
function hideEvents(event, exceptElement) {
eachEventElement(event, exceptElement, "hide");
}
function eachEventElement(event, exceptElement, funcName) {
// NOTE: there may be multiple events per ID (repeating events)
// and multiple segments per event
var elements = eventElementsByID[event._id],
i,
len = elements.length;
for (i = 0; i < len; i++) {
if (!exceptElement || elements[i][0] != exceptElement[0]) {
elements[i][funcName]();
}
}
}
/* Event Modification Reporting
---------------------------------------------------------------------------------*/
function eventDrop(e, event, dayDelta, minuteDelta, allDay, ev, ui) {
var oldAllDay = event.allDay;
var eventId = event._id;
moveEvents(eventsByID[eventId], dayDelta, minuteDelta, allDay);
trigger(
"eventDrop",
e,
event,
dayDelta,
minuteDelta,
allDay,
function() {
// TODO: investigate cases where this inverse technique might not work
moveEvents(eventsByID[eventId], -dayDelta, -minuteDelta, oldAllDay);
reportEventChange(eventId);
},
ev,
ui
);
reportEventChange(eventId);
}
function eventResize(e, event, dayDelta, minuteDelta, ev, ui) {
var eventId = event._id;
elongateEvents(eventsByID[eventId], dayDelta, minuteDelta);
trigger(
"eventResize",
e,
event,
dayDelta,
minuteDelta,
function() {
// TODO: investigate cases where this inverse technique might not work
elongateEvents(eventsByID[eventId], -dayDelta, -minuteDelta);
reportEventChange(eventId);
},
ev,
ui
);
reportEventChange(eventId);
}
/* Event Modification Math
---------------------------------------------------------------------------------*/
function moveEvents(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, options);
}
}
function elongateEvents(events, dayDelta, minuteDelta) {
minuteDelta = minuteDelta || 0;
for (var e, len = events.length, i = 0; i < len; i++) {
e = events[i];
e.end = addMinutes(addDays(eventEnd(e), dayDelta, true), minuteDelta);
normalizeEvent(e, options);
}
}
// ====================================================================================================
// Utilities for day "cells"
// ====================================================================================================
// The "basic" views are completely made up of day cells.
// The "agenda" views have day cells at the top "all day" slot.
// This was the obvious common place to put these utilities, but they should be abstracted out into
// a more meaningful class (like DayEventRenderer).
// ====================================================================================================
// For determining how a given "cell" translates into a "date":
//
// 1. Convert the "cell" (row and column) into a "cell offset" (the # of the cell, cronologically from the first).
// Keep in mind that column indices are inverted with isRTL. This is taken into account.
//
// 2. Convert the "cell offset" to a "day offset" (the # of days since the first visible day in the view).
//
// 3. Convert the "day offset" into a "date" (a JavaScript Date object).
//
// The reverse transformation happens when transforming a date into a cell.
// exports
t.isHiddenDay = isHiddenDay;
t.skipHiddenDays = skipHiddenDays;
t.getCellsPerWeek = getCellsPerWeek;
t.dateToCell = dateToCell;
t.dateToDayOffset = dateToDayOffset;
t.dayOffsetToCellOffset = dayOffsetToCellOffset;
t.cellOffsetToCell = cellOffsetToCell;
t.cellToDate = cellToDate;
t.cellToCellOffset = cellToCellOffset;
t.cellOffsetToDayOffset = cellOffsetToDayOffset;
t.dayOffsetToDate = dayOffsetToDate;
t.rangeToSegments = rangeToSegments;
// internals
var hiddenDays = opt("hiddenDays") || []; // array of day-of-week indices that are hidden
var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
var cellsPerWeek;
var dayToCellMap = []; // hash from dayIndex -> cellIndex, for one week
var cellToDayMap = []; // hash from cellIndex -> dayIndex, for one week
var isRTL = opt("isRTL");
// initialize important internal variables
(function() {
if (opt("weekends") === false) {
hiddenDays.push(0, 6); // 0=sunday, 6=saturday
}
// Loop through a hypothetical week and determine which
// days-of-week are hidden. Record in both hashes (one is the reverse of the other).
for (var dayIndex = 0, cellIndex = 0; dayIndex < 7; dayIndex++) {
dayToCellMap[dayIndex] = cellIndex;
isHiddenDayHash[dayIndex] = $.inArray(dayIndex, hiddenDays) != -1;
if (!isHiddenDayHash[dayIndex]) {
cellToDayMap[cellIndex] = dayIndex;
cellIndex++;
}
}
cellsPerWeek = cellIndex;
if (!cellsPerWeek) {
throw "invalid hiddenDays"; // all days were hidden? bad.
}
})();
// Is the current day hidden?
// `day` is a day-of-week index (0-6), or a Date object
function isHiddenDay(day) {
if (typeof day == "object") {
day = day.getDay();
}
return isHiddenDayHash[day];
}
function getCellsPerWeek() {
return cellsPerWeek;
}
// Keep incrementing the current day until it is no longer a hidden day.
// If the initial value of `date` is not a hidden day, don't do anything.
// Pass `isExclusive` as `true` if you are dealing with an end date.
// `inc` defaults to `1` (increment one day forward each time)
function skipHiddenDays(date, inc, isExclusive) {
inc = inc || 1;
while (isHiddenDayHash[(date.getDay() + (isExclusive ? inc : 0) + 7) % 7]) {
addDays(date, inc);
}
}
//
// TRANSFORMATIONS: cell -> cell offset -> day offset -> date
//
// cell -> date (combines all transformations)
// Possible arguments:
// - row, col
// - { row:#, col: # }
function cellToDate() {
var cellOffset = cellToCellOffset.apply(null, arguments);
var dayOffset = cellOffsetToDayOffset(cellOffset);
var date = dayOffsetToDate(dayOffset);
return date;
}
// cell -> cell offset
// Possible arguments:
// - row, col
// - { row:#, col:# }
function cellToCellOffset(row, col) {
var colCnt = t.getColCnt();
// rtl variables. wish we could pre-populate these. but where?
var dis = isRTL ? -1 : 1;
var dit = isRTL ? colCnt - 1 : 0;
if (typeof row == "object") {
col = row.col;
row = row.row;
}
var cellOffset = row * colCnt + (col * dis + dit); // column, adjusted for RTL (dis & dit)
return cellOffset;
}
// cell offset -> day offset
function cellOffsetToDayOffset(cellOffset) {
var day0 = t.visStart.getDay(); // first date's day of week
cellOffset += dayToCellMap[day0]; // normlize cellOffset to beginning-of-week
return (
Math.floor(cellOffset / cellsPerWeek) * 7 + // # of days from full weeks
cellToDayMap[ // # of days from partial last week
((cellOffset % cellsPerWeek) + cellsPerWeek) % cellsPerWeek // crazy math to handle negative cellOffsets
] -
day0
); // adjustment for beginning-of-week normalization
}
// day offset -> date (JavaScript Date object)
function dayOffsetToDate(dayOffset) {
var date = cloneDate(t.visStart);
addDays(date, dayOffset);
return date;
}
//
// TRANSFORMATIONS: date -> day offset -> cell offset -> cell
//
// date -> cell (combines all transformations)
function dateToCell(date) {
var dayOffset = dateToDayOffset(date);
var cellOffset = dayOffsetToCellOffset(dayOffset);
var cell = cellOffsetToCell(cellOffset);
return cell;
}
// date -> day offset
function dateToDayOffset(date) {
return dayDiff(date, t.visStart);
}
// day offset -> cell offset
function dayOffsetToCellOffset(dayOffset) {
var day0 = t.visStart.getDay(); // first date's day of week
dayOffset += day0; // normalize dayOffset to beginning-of-week
return (
Math.floor(dayOffset / 7) * cellsPerWeek + // # of cells from full weeks
dayToCellMap[ // # of cells from partial last week
((dayOffset % 7) + 7) % 7 // crazy math to handle negative dayOffsets
] -
dayToCellMap[day0]
); // adjustment for beginning-of-week normalization
}
// cell offset -> cell (object with row & col keys)
function cellOffsetToCell(cellOffset) {
var colCnt = t.getColCnt();
// rtl variables. wish we could pre-populate these. but where?
var dis = isRTL ? -1 : 1;
var dit = isRTL ? colCnt - 1 : 0;
var row = Math.floor(cellOffset / colCnt);
var col = (((cellOffset % colCnt) + colCnt) % colCnt) * dis + dit; // column, adjusted for RTL (dis & dit)
return {
row: row,
col: col
};
}
//
// Converts a date range into an array of segment objects.
// "Segments" are horizontal stretches of time, sliced up by row.
// A segment object has the following properties:
// - row
// - cols
// - isStart
// - isEnd
//
function rangeToSegments(startDate, endDate) {
var rowCnt = t.getRowCnt();
var colCnt = t.getColCnt();
var segments = []; // array of segments to return
// day offset for given date range
var rangeDayOffsetStart = dateToDayOffset(startDate);
var rangeDayOffsetEnd = dateToDayOffset(endDate); // exclusive
// first and last cell offset for the given date range
// "last" implies inclusivity
var rangeCellOffsetFirst = dayOffsetToCellOffset(rangeDayOffsetStart);
var rangeCellOffsetLast = dayOffsetToCellOffset(rangeDayOffsetEnd) - 1;
// loop through all the rows in the view
for (var row = 0; row < rowCnt; row++) {
// first and last cell offset for the row
var rowCellOffsetFirst = row * colCnt;
var rowCellOffsetLast = rowCellOffsetFirst + colCnt - 1;
// get the segment's cell offsets by constraining the range's cell offsets to the bounds of the row
var segmentCellOffsetFirst = Math.max(
rangeCellOffsetFirst,
rowCellOffsetFirst
);
var segmentCellOffsetLast = Math.min(
rangeCellOffsetLast,
rowCellOffsetLast
);
// make sure segment's offsets are valid and in view
if (segmentCellOffsetFirst <= segmentCellOffsetLast) {
// translate to cells
var segmentCellFirst = cellOffsetToCell(segmentCellOffsetFirst);
var segmentCellLast = cellOffsetToCell(segmentCellOffsetLast);
// view might be RTL, so order by leftmost column
var cols = [segmentCellFirst.col, segmentCellLast.col].sort();
// Determine if segment's first/last cell is the beginning/end of the date range.
// We need to compare "day offset" because "cell offsets" are often ambiguous and
// can translate to multiple days, and an edge case reveals itself when we the
// range's first cell is hidden (we don't want isStart to be true).
var isStart =
cellOffsetToDayOffset(segmentCellOffsetFirst) == rangeDayOffsetStart;
var isEnd =
cellOffsetToDayOffset(segmentCellOffsetLast) + 1 == rangeDayOffsetEnd; // +1 for comparing exclusively
segments.push({
row: row,
leftCol: cols[0],
rightCol: cols[1],
isStart: isStart,
isEnd: isEnd
});
}
}
return segments;
}
}
function DayEventRenderer() {
var t = this;
// exports
t.renderDayEvents = renderDayEvents;
t.draggableDayEvent = draggableDayEvent; // made public so that subclasses can override
t.resizableDayEvent = resizableDayEvent; // "
// imports
var opt = t.opt;
var trigger = t.trigger;
var isEventDraggable = t.isEventDraggable;
var isEventResizable = t.isEventResizable;
var eventEnd = t.eventEnd;
var reportEventElement = t.reportEventElement;
var eventElementHandlers = t.eventElementHandlers;
var showEvents = t.showEvents;
var hideEvents = t.hideEvents;
var eventDrop = t.eventDrop;
var eventResize = t.eventResize;
var getRowCnt = t.getRowCnt;
var getColCnt = t.getColCnt;
var getColWidth = t.getColWidth;
var allDayRow = t.allDayRow; // TODO: rename
var colLeft = t.colLeft;
var colRight = t.colRight;
var colContentLeft = t.colContentLeft;
var colContentRight = t.colContentRight;
var dateToCell = t.dateToCell;
var getDaySegmentContainer = t.getDaySegmentContainer;
var formatDates = t.calendar.formatDates;
var renderDayOverlay = t.renderDayOverlay;
var clearOverlays = t.clearOverlays;
var clearSelection = t.clearSelection;
var getHoverListener = t.getHoverListener;
var rangeToSegments = t.rangeToSegments;
var cellToDate = t.cellToDate;
var cellToCellOffset = t.cellToCellOffset;
var cellOffsetToDayOffset = t.cellOffsetToDayOffset;
var dateToDayOffset = t.dateToDayOffset;
var dayOffsetToCellOffset = t.dayOffsetToCellOffset;
// Render `events` onto the calendar, attach mouse event handlers, and call the `eventAfterRender` callback for each.
// Mouse event will be lazily applied, except if the event has an ID of `modifiedEventId`.
// Can only be called when the event container is empty (because it wipes out all innerHTML).
function renderDayEvents(events, modifiedEventId) {
// do the actual rendering. Receive the intermediate "segment" data structures.
var segments = _renderDayEvents(
events,
false, // don't append event elements
true // set the heights of the rows
);
// report the elements to the View, for general drag/resize utilities
segmentElementEach(segments, function(segment, element) {
reportEventElement(segment.event, element);
});
// attach mouse handlers
attachHandlers(segments, modifiedEventId);
// call `eventAfterRender` callback for each event
segmentElementEach(segments, function(segment, element) {
trigger("eventAfterRender", segment.event, segment.event, element);
});
}
// Render an event on the calendar, but don't report them anywhere, and don't attach mouse handlers.
// Append this event element to the event container, which might already be populated with events.
// If an event's segment will have row equal to `adjustRow`, then explicitly set its top coordinate to `adjustTop`.
// This hack is used to maintain continuity when user is manually resizing an event.
// Returns an array of DOM elements for the event.
function renderTempDayEvent(event, adjustRow, adjustTop) {
// actually render the event. `true` for appending element to container.
// Recieve the intermediate "segment" data structures.
var segments = _renderDayEvents(
[event],
true, // append event elements
false // don't set the heights of the rows
);
var elements = [];
// Adjust certain elements' top coordinates
segmentElementEach(segments, function(segment, element) {
if (segment.row === adjustRow) {
element.css("top", adjustTop);
}
elements.push(element[0]); // accumulate DOM nodes
});
return elements;
}
// Render events onto the calendar. Only responsible for the VISUAL aspect.
// Not responsible for attaching handlers or calling callbacks.
// Set `doAppend` to `true` for rendering elements without clearing the existing container.
// Set `doRowHeights` to allow setting the height of each row, to compensate for vertical event overflow.
function _renderDayEvents(events, doAppend, doRowHeights) {
// where the DOM nodes will eventually end up
var finalContainer = getDaySegmentContainer();
// the container where the initial HTML will be rendered.
// If `doAppend`==true, uses a temporary container.
var renderContainer = doAppend ? $("<div/>") : finalContainer;
var segments = buildSegments(events);
var html;
var elements;
// calculate the desired `left` and `width` properties on each segment object
calculateHorizontals(segments);
// build the HTML string. relies on `left` property
html = buildHTML(segments);
// render the HTML. innerHTML is considerably faster than jQuery's .html()
renderContainer[0].innerHTML = html;
// retrieve the individual elements
elements = renderContainer.children();
// if we were appending, and thus using a temporary container,
// re-attach elements to the real container.
if (doAppend) {
finalContainer.append(elements);
}
// assigns each element to `segment.event`, after filtering them through user callbacks
resolveElements(segments, elements);
// Calculate the left and right padding+margin for each element.
// We need this for setting each element's desired outer width, because of the W3C box model.
// It's important we do this in a separate pass from acually setting the width on the DOM elements
// because alternating reading/writing dimensions causes reflow for every iteration.
segmentElementEach(segments, function(segment, element) {
segment.hsides = hsides(element, true); // include margins = `true`
});
// Set the width of each element
segmentElementEach(segments, function(segment, element) {
element.width(Math.max(0, segment.outerWidth - segment.hsides));
});
// Grab each element's outerHeight (setVerticals uses this).
// To get an accurate reading, it's important to have each element's width explicitly set already.
segmentElementEach(segments, function(segment, element) {
segment.outerHeight = element.outerHeight(true); // include margins = `true`
});
// Set the top coordinate on each element (requires segment.outerHeight)
setVerticals(segments, doRowHeights);
return segments;
}
// Generate an array of "segments" for all events.
function buildSegments(events) {
var segments = [];
for (var i = 0; i < events.length; i++) {
var eventSegments = buildSegmentsForEvent(events[i]);
segments.push.apply(segments, eventSegments); // append an array to an array
}
return segments;
}
// Generate an array of segments for a single event.
// A "segment" is the same data structure that View.rangeToSegments produces,
// with the addition of the `event` property being set to reference the original event.
function buildSegmentsForEvent(event) {
var startDate = event.start;
var endDate = exclEndDay(event);
var segments = rangeToSegments(startDate, endDate);
for (var i = 0; i < segments.length; i++) {
segments[i].event = event;
}
return segments;
}
// Sets the `left` and `outerWidth` property of each segment.
// These values are the desired dimensions for the eventual DOM elements.
function calculateHorizontals(segments) {
var isRTL = opt("isRTL");
for (var i = 0; i < segments.length; i++) {
var segment = segments[i];
// Determine functions used for calulating the elements left/right coordinates,
// depending on whether the view is RTL or not.
// NOTE:
// colLeft/colRight returns the coordinate butting up the edge of the cell.
// colContentLeft/colContentRight is indented a little bit from the edge.
var leftFunc = (isRTL
? segment.isEnd
: segment.isStart)
? colContentLeft
: colLeft;
var rightFunc = (isRTL
? segment.isStart
: segment.isEnd)
? colContentRight
: colRight;
var left = leftFunc(segment.leftCol);
var right = rightFunc(segment.rightCol);
segment.left = left;
segment.outerWidth = right - left;
}
}
// Build a concatenated HTML string for an array of segments
function buildHTML(segments) {
var html = "";
for (var i = 0; i < segments.length; i++) {
html += buildHTMLForSegment(segments[i]);
}
return html;
}
// Build an HTML string for a single segment.
// Relies on the following properties:
// - `segment.event` (from `buildSegmentsForEvent`)
// - `segment.left` (from `calculateHorizontals`)
function buildHTMLForSegment(segment) {
var html = "";
var isRTL = opt("isRTL");
var event = segment.event;
var url = event.url;
// generate the list of CSS classNames
var classNames = ["fc-event", "fc-event-hori"];
if (isEventDraggable(event)) {
classNames.push("fc-event-draggable");
}
if (segment.isStart) {
classNames.push("fc-event-start");
}
if (segment.isEnd) {
classNames.push("fc-event-end");
}
// use the event's configured classNames
// guaranteed to be an array via `normalizeEvent`
classNames = classNames.concat(event.className);
if (event.source) {
// use the event's source's classNames, if specified
classNames = classNames.concat(event.source.className || []);
}
// generate a semicolon delimited CSS string for any of the "skin" properties
// of the event object (`backgroundColor`, `borderColor` and such)
var skinCss = getSkinCss(event, opt);
if (url) {
html += "<a href='" + htmlEscape(url) + "'";
} else {
html += "<div";
}
html +=
" class='" +
classNames.join(" ") +
"'" +
" style=" +
"'" +
"position:absolute;" +
"left:" +
segment.left +
"px;" +
skinCss +
"'" +
">" +
"<div class='fc-event-inner'>";
if (!event.allDay && segment.isStart) {
html +=
"<span class='fc-event-time'>" +
htmlEscape(formatDates(event.start, event.end, opt("timeFormat"))) +
"</span>";
}
html +=
"<span class='fc-event-title'>" +
htmlEscape(event.title || "") +
"</span>" +
"</div>";
if (segment.isEnd && isEventResizable(event)) {
html +=
"<div class='ui-resizable-handle ui-resizable-" +
(isRTL ? "w" : "e") +
"'>" +
"   " + // makes hit area a lot better for IE6/7
"</div>";
}
html += "</" + (url ? "a" : "div") + ">";
// TODO:
// When these elements are initially rendered, they will be briefly visibile on the screen,
// even though their widths/heights are not set.
// SOLUTION: initially set them as visibility:hidden ?
return html;
}
// Associate each segment (an object) with an element (a jQuery object),
// by setting each `segment.element`.
// Run each element through the `eventRender` filter, which allows developers to
// modify an existing element, supply a new one, or cancel rendering.
function resolveElements(segments, elements) {
for (var i = 0; i < segments.length; i++) {
var segment = segments[i];
var event = segment.event;
var element = elements.eq(i);
// call the trigger with the original element
var triggerRes = trigger("eventRender", event, event, element);
if (triggerRes === false) {
// if `false`, remove the event from the DOM and don't assign it to `segment.event`
element.remove();
} else {
if (triggerRes && triggerRes !== true) {
// the trigger returned a new element, but not `true` (which means keep the existing element)
// re-assign the important CSS dimension properties that were already assigned in `buildHTMLForSegment`
triggerRes = $(triggerRes).css({
position: "absolute",
left: segment.left
});
element.replaceWith(triggerRes);
element = triggerRes;
}
segment.element = element;
}
}
}
/* Top-coordinate Methods
-------------------------------------------------------------------------------------------------*/
// Sets the "top" CSS property for each element.
// If `doRowHeights` is `true`, also sets each row's first cell to an explicit height,
// so that if elements vertically overflow, the cell expands vertically to compensate.
function setVerticals(segments, doRowHeights) {
var rowContentHeights = calculateVerticals(segments); // also sets segment.top
var rowContentElements = getRowContentElements(); // returns 1 inner div per row
var rowContentTops = [];
// Set each row's height by setting height of first inner div
if (doRowHeights) {
for (var i = 0; i < rowContentElements.length; i++) {
rowContentElements[i].height(rowContentHeights[i]);
}
}
// Get each row's top, relative to the views's origin.
// Important to do this after setting each row's height.
for (var i = 0; i < rowContentElements.length; i++) {
rowContentTops.push(rowContentElements[i].position().top);
}
// Set each segment element's CSS "top" property.
// Each segment object has a "top" property, which is relative to the row's top, but...
segmentElementEach(segments, function(segment, element) {
element.css(
"top",
rowContentTops[segment.row] + segment.top // ...now, relative to views's origin
);
});
}
// Calculate the "top" coordinate for each segment, relative to the "top" of the row.
// Also, return an array that contains the "content" height for each row
// (the height displaced by the vertically stacked events in the row).
// Requires segments to have their `outerHeight` property already set.
function calculateVerticals(segments) {
var rowCnt = getRowCnt();
var colCnt = getColCnt();
var rowContentHeights = []; // content height for each row
var segmentRows = buildSegmentRows(segments); // an array of segment arrays, one for each row
for (var rowI = 0; rowI < rowCnt; rowI++) {
var segmentRow = segmentRows[rowI];
// an array of running total heights for each column.
// initialize with all zeros.
var colHeights = [];
for (var colI = 0; colI < colCnt; colI++) {
colHeights.push(0);
}
// loop through every segment
for (var segmentI = 0; segmentI < segmentRow.length; segmentI++) {
var segment = segmentRow[segmentI];
// find the segment's top coordinate by looking at the max height
// of all the columns the segment will be in.
segment.top = arrayMax(
colHeights.slice(
segment.leftCol,
segment.rightCol + 1 // make exclusive for slice
)
);
// adjust the columns to account for the segment's height
for (var colI = segment.leftCol; colI <= segment.rightCol; colI++) {
colHeights[colI] = segment.top + segment.outerHeight;
}
}
// the tallest column in the row should be the "content height"
rowContentHeights.push(arrayMax(colHeights));
}
return rowContentHeights;
}
// Build an array of segment arrays, each representing the segments that will
// be in a row of the grid, sorted by which event should be closest to the top.
function buildSegmentRows(segments) {
var rowCnt = getRowCnt();
var segmentRows = [];
var segmentI;
var segment;
var rowI;
// group segments by row
for (segmentI = 0; segmentI < segments.length; segmentI++) {
segment = segments[segmentI];
rowI = segment.row;
if (segment.element) {
// was rendered?
if (segmentRows[rowI]) {
// already other segments. append to array
segmentRows[rowI].push(segment);
} else {
// first segment in row. create new array
segmentRows[rowI] = [segment];
}
}
}
// sort each row
for (rowI = 0; rowI < rowCnt; rowI++) {
segmentRows[rowI] = sortSegmentRow(
segmentRows[rowI] || [] // guarantee an array, even if no segments
);
}
return segmentRows;
}
// Sort an array of segments according to which segment should appear closest to the top
function sortSegmentRow(segments) {
var sortedSegments = [];
// build the subrow array
var subrows = buildSegmentSubrows(segments);
// flatten it
for (var i = 0; i < subrows.length; i++) {
sortedSegments.push.apply(sortedSegments, subrows[i]); // append an array to an array
}
return sortedSegments;
}
// Take an array of segments, which are all assumed to be in the same row,
// and sort into subrows.
function buildSegmentSubrows(segments) {
// Give preference to elements with certain criteria, so they have
// a chance to be closer to the top.
segments.sort(compareDaySegments);
var subrows = [];
for (var i = 0; i < segments.length; i++) {
var segment = segments[i];
// loop through subrows, starting with the topmost, until the segment
// doesn't collide with other segments.
for (var j = 0; j < subrows.length; j++) {
if (!isDaySegmentCollision(segment, subrows[j])) {
break;
}
}
// `j` now holds the desired subrow index
if (subrows[j]) {
subrows[j].push(segment);
} else {
subrows[j] = [segment];
}
}
return subrows;
}
// Return an array of jQuery objects for the placeholder content containers of each row.
// The content containers don't actually contain anything, but their dimensions should match
// the events that are overlaid on top.
function getRowContentElements() {
var i;
var rowCnt = getRowCnt();
var rowDivs = [];
for (i = 0; i < rowCnt; i++) {
rowDivs[i] = allDayRow(i).find("div.fc-day-content > div");
}
return rowDivs;
}
/* Mouse Handlers
---------------------------------------------------------------------------------------------------*/
// TODO: better documentation!
function attachHandlers(segments, modifiedEventId) {
var segmentContainer = getDaySegmentContainer();
segmentElementEach(segments, function(segment, element, i) {
var event = segment.event;
if (event._id === modifiedEventId) {
bindDaySeg(event, element, segment);
} else {
element[0]._fci = i; // for lazySegBind
}
});
lazySegBind(segmentContainer, segments, bindDaySeg);
}
function bindDaySeg(event, eventElement, segment) {
if (isEventDraggable(event)) {
t.draggableDayEvent(event, eventElement, segment); // use `t` so subclasses can override
}
if (
segment.isEnd && // only allow resizing on the final segment for an event
isEventResizable(event)
) {
t.resizableDayEvent(event, eventElement, segment); // use `t` so subclasses can override
}
// attach all other handlers.
// needs to be after, because resizableDayEvent might stopImmediatePropagation on click
eventElementHandlers(event, eventElement);
}
function draggableDayEvent(event, eventElement) {
var hoverListener = getHoverListener();
var dayDelta;
eventElement.draggable({
delay: 50,
opacity: opt("dragOpacity"),
revertDuration: opt("dragRevertDuration"),
start: function(ev, ui) {
trigger("eventDragStart", eventElement, event, ev, ui);
hideEvents(event, eventElement);
hoverListener.start(
function(cell, origCell, rowDelta, colDelta) {
eventElement.draggable(
"option",
"revert",
!cell || (!rowDelta && !colDelta)
);
clearOverlays();
if (cell) {
var origDate = cellToDate(origCell);
var date = cellToDate(cell);
dayDelta = dayDiff(date, origDate);
renderDayOverlay(
addDays(cloneDate(event.start), dayDelta),
addDays(exclEndDay(event), dayDelta)
);
} else {
dayDelta = 0;
}
},
ev,
"drag"
);
},
stop: function(ev, ui) {
hoverListener.stop();
clearOverlays();
trigger("eventDragStop", eventElement, event, ev, ui);
if (dayDelta) {
eventDrop(this, event, dayDelta, 0, event.allDay, ev, ui);
} else {
eventElement.css("filter", ""); // clear IE opacity side-effects
showEvents(event, eventElement);
}
}
});
}
function resizableDayEvent(event, element, segment) {
var isRTL = opt("isRTL");
var direction = isRTL ? "w" : "e";
var handle = element.find(".ui-resizable-" + direction); // TODO: stop using this class because we aren't using jqui for this
var isResizing = false;
// TODO: look into using jquery-ui mouse widget for this stuff
disableTextSelection(element); // prevent native <a> selection for IE
element
.mousedown(function(ev) {
// prevent native <a> selection for others
ev.preventDefault();
})
.click(function(ev) {
if (isResizing) {
ev.preventDefault(); // prevent link from being visited (only method that worked in IE6)
ev.stopImmediatePropagation(); // prevent fullcalendar eventClick handler from being called
// (eventElementHandlers needs to be bound after resizableDayEvent)
}
});
handle.mousedown(function(ev) {
if (ev.which != 1) {
return; // needs to be left mouse button
}
isResizing = true;
var hoverListener = getHoverListener();
var rowCnt = getRowCnt();
var colCnt = getColCnt();
var elementTop = element.css("top");
var dayDelta;
var helpers;
var eventCopy = $.extend({}, event);
var minCellOffset = dayOffsetToCellOffset(dateToDayOffset(event.start));
clearSelection();
$("body")
.css("cursor", direction + "-resize")
.one("mouseup", mouseup);
trigger("eventResizeStart", this, event, ev);
hoverListener.start(function(cell, origCell) {
if (cell) {
var origCellOffset = cellToCellOffset(origCell);
var cellOffset = cellToCellOffset(cell);
// don't let resizing move earlier than start date cell
cellOffset = Math.max(cellOffset, minCellOffset);
dayDelta =
cellOffsetToDayOffset(cellOffset) -
cellOffsetToDayOffset(origCellOffset);
if (dayDelta) {
eventCopy.end = addDays(eventEnd(event), dayDelta, true);
var oldHelpers = helpers;
helpers = renderTempDayEvent(eventCopy, segment.row, elementTop);
helpers = $(helpers); // turn array into a jQuery object
helpers.find("*").css("cursor", direction + "-resize");
if (oldHelpers) {
oldHelpers.remove();
}
hideEvents(event);
} else {
if (helpers) {
showEvents(event);
helpers.remove();
helpers = null;
}
}
clearOverlays();
renderDayOverlay(
// coordinate grid already rebuilt with hoverListener.start()
event.start,
addDays(exclEndDay(event), dayDelta)
// TODO: instead of calling renderDayOverlay() with dates,
// call _renderDayOverlay (or whatever) with cell offsets.
);
}
}, ev);
function mouseup(ev) {
trigger("eventResizeStop", this, event, ev);
$("body").css("cursor", "");
hoverListener.stop();
clearOverlays();
if (dayDelta) {
eventResize(this, event, dayDelta, 0, ev);
// event redraw will clear helpers
}
// otherwise, the drag handler already restored the old events
setTimeout(function() {
// make this happen after the element's click event
isResizing = false;
}, 0);
}
});
}
}
/* Generalized Segment Utilities
-------------------------------------------------------------------------------------------------*/
function isDaySegmentCollision(segment, otherSegments) {
for (var i = 0; i < otherSegments.length; i++) {
var otherSegment = otherSegments[i];
if (
otherSegment.leftCol <= segment.rightCol &&
otherSegment.rightCol >= segment.leftCol
) {
return true;
}
}
return false;
}
function segmentElementEach(segments, callback) {
// TODO: use in AgendaView?
for (var i = 0; i < segments.length; i++) {
var segment = segments[i];
var element = segment.element;
if (element) {
callback(segment, element, i);
}
}
}
// A cmp function for determining which segments should appear higher up
function compareDaySegments(a, b) {
return (
b.rightCol - b.leftCol - (a.rightCol - a.leftCol) || // put wider events first
b.event.allDay - a.event.allDay || // if tie, put all-day events first (booleans cast to 0/1)
a.event.start - b.event.start || // if a tie, sort by event start date
(a.event.title || "").localeCompare(b.event.title)
); // if a tie, sort by event title
}
//BUG: unselect needs to be triggered when events are dragged+dropped
function SelectionManager() {
var t = this;
// exports
t.select = select;
t.unselect = unselect;
t.reportSelection = reportSelection;
t.daySelectionMousedown = daySelectionMousedown;
// imports
var opt = t.opt;
var trigger = t.trigger;
var defaultSelectionEnd = t.defaultSelectionEnd;
var renderSelection = t.renderSelection;
var clearSelection = t.clearSelection;
// locals
var selected = false;
// unselectAuto
if (opt("selectable") && opt("unselectAuto")) {
$(document).mousedown(function(ev) {
var ignore = opt("unselectCancel");
if (ignore) {
if ($(ev.target).parents(ignore).length) {
// could be optimized to stop after first match
return;
}
}
unselect(ev);
});
}
function select(startDate, endDate, allDay) {
unselect();
if (!endDate) {
endDate = defaultSelectionEnd(startDate, allDay);
}
renderSelection(startDate, endDate, allDay);
reportSelection(startDate, endDate, allDay);
}
function unselect(ev) {
if (selected) {
selected = false;
clearSelection();
trigger("unselect", null, ev);
}
}
function reportSelection(startDate, endDate, allDay, ev) {
selected = true;
trigger("select", null, startDate, endDate, allDay, ev);
}
function daySelectionMousedown(ev) {
// not really a generic manager method, oh well
var cellToDate = t.cellToDate;
var getIsCellAllDay = t.getIsCellAllDay;
var hoverListener = t.getHoverListener();
var reportDayClick = t.reportDayClick; // this is hacky and sort of weird
if (ev.which == 1 && opt("selectable")) {
// which==1 means left mouse button
unselect(ev);
var _mousedownElement = this;
var dates;
hoverListener.start(function(cell, origCell) {
// TODO: maybe put cellToDate/getIsCellAllDay info in cell
clearSelection();
if (cell && getIsCellAllDay(cell)) {
dates = [cellToDate(origCell), cellToDate(cell)].sort(dateCompare);
renderSelection(dates[0], dates[1], true);
} else {
dates = null;
}
}, ev);
$(document).one("mouseup", function(ev) {
hoverListener.stop();
if (dates) {
if (+dates[0] == +dates[1]) {
reportDayClick(dates[0], true, ev);
}
reportSelection(dates[0], dates[1], true, ev);
}
});
}
}
}
function OverlayManager() {
var t = this;
// exports
t.renderOverlay = renderOverlay;
t.clearOverlays = clearOverlays;
// locals
var usedOverlays = [];
var unusedOverlays = [];
function renderOverlay(rect, parent) {
var e = unusedOverlays.shift();
if (!e) {
e = $("<div class='fc-cell-overlay' style='position:absolute;z-index:3'/>");
}
if (e[0].parentNode != parent[0]) {
e.appendTo(parent);
}
usedOverlays.push(e.css(rect).show());
return e;
}
function clearOverlays() {
var e;
while ((e = usedOverlays.shift())) {
unusedOverlays.push(e.hide().unbind());
}
}
}
function CoordinateGrid(buildFunc) {
var t = this;
var rows;
var cols;
t.build = function() {
rows = [];
cols = [];
buildFunc(rows, cols);
};
t.cell = function(x, y) {
var rowCnt = rows.length;
var colCnt = cols.length;
var i,
r = -1,
c = -1;
for (i = 0; i < rowCnt; i++) {
if (y >= rows[i][0] && y < rows[i][1]) {
r = i;
break;
}
}
for (i = 0; i < colCnt; i++) {
if (x >= cols[i][0] && x < cols[i][1]) {
c = i;
break;
}
}
return r >= 0 && c >= 0 ? { row: r, col: c } : null;
};
t.rect = function(row0, col0, row1, col1, originElement) {
// row1,col1 is inclusive
var origin = originElement.offset();
return {
top: rows[row0][0] - origin.top,
left: cols[col0][0] - origin.left,
width: cols[col1][1] - cols[col0][0],
height: rows[row1][1] - rows[row0][0]
};
};
}
function HoverListener(coordinateGrid) {
var t = this;
var bindType;
var change;
var firstCell;
var cell;
t.start = function(_change, ev, _bindType) {
change = _change;
firstCell = cell = null;
coordinateGrid.build();
mouse(ev);
bindType = _bindType || "mousemove";
$(document).bind(bindType, mouse);
};
function mouse(ev) {
_fixUIEvent(ev); // see below
var newCell = coordinateGrid.cell(ev.pageX, ev.pageY);
if (
!newCell != !cell ||
(newCell && (newCell.row != cell.row || newCell.col != cell.col))
) {
if (newCell) {
if (!firstCell) {
firstCell = newCell;
}
change(
newCell,
firstCell,
newCell.row - firstCell.row,
newCell.col - firstCell.col
);
} else {
change(newCell, firstCell);
}
cell = newCell;
}
}
t.stop = function() {
$(document).unbind(bindType, mouse);
return cell;
};
}
// this fix was only necessary for jQuery UI 1.8.16 (and jQuery 1.7 or 1.7.1)
// upgrading to jQuery UI 1.8.17 (and using either jQuery 1.7 or 1.7.1) fixed the problem
// but keep this in here for 1.8.16 users
// and maybe remove it down the line
function _fixUIEvent(event) {
// for issue 1168
if (event.pageX === undefined) {
event.pageX = event.originalEvent.pageX;
event.pageY = event.originalEvent.pageY;
}
}
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 = {};
};
}
})(jQuery);
/*!
* FullCalendar v1.6.4 Stylesheet
* Docs & License: http://arshaw.com/fullcalendar/
* (c) 2013 Adam Shaw
*/
@import url("https://fonts.googleapis.com/css?family=Roboto:100,100i,300,300i,400,400i,500,500i,700,700i,900,900i");
td.fc-day {
background: #fff !important;
font-family: "Roboto", sans-serif;
}
td.fc-today {
background: #fff !important;
position: relative;
}
.fc-first th {
font-family: "Roboto", sans-serif;
background: #9675ce !important;
color: #fff;
font-size: 14px !important;
font-weight: 500 !important;
}
.fc-event-inner {
font-family: "Roboto", sans-serif;
background: #03a9f3 !important;
color: #fff !important;
font-size: 12px !important;
font-weight: 500 !important;
padding: 5px 0px !important;
}
.fc {
direction: ltr;
text-align: left;
}
.fc table {
border-collapse: collapse;
border-spacing: 0;
}
html .fc,
.fc table {
font-size: 1em;
font-family: "Helvetica Neue", Helvetica;
}
.fc td,
.fc th {
padding: 0;
vertical-align: top;
}
/* Header
------------------------------------------------------------------------*/
.fc-header td {
white-space: nowrap;
padding: 15px 10px 0px;
}
.fc-header-left {
width: 25%;
text-align: left;
}
.fc-header-center {
text-align: center;
}
.fc-header-right {
width: 25%;
text-align: right;
}
.fc-header-title {
display: inline-block;
vertical-align: top;
margin-top: -5px;
}
.fc-header-title h2 {
margin-top: 0;
white-space: nowrap;
font-size: 32px;
font-weight: 100;
margin-bottom: 10px;
font-family: "Roboto", sans-serif;
}
span.fc-button {
font-family: "Roboto", sans-serif;
border-color: #9675ce;
color: #9675ce;
}
.fc-state-down,
.fc-state-active {
background-color: #9675ce !important;
color: #fff !important;
}
.fc .fc-header-space {
padding-left: 10px;
}
.fc-header .fc-button {
margin-bottom: 1em;
vertical-align: top;
}
/* buttons edges butting together */
.fc-header .fc-button {
margin-right: -1px;
}
.fc-header .fc-corner-right, /* non-theme */
.fc-header .ui-corner-right {
/* theme */
margin-right: 0; /* back to normal */
}
/* button layering (for border precedence) */
.fc-header .fc-state-hover,
.fc-header .ui-state-hover {
z-index: 2;
}
.fc-header .fc-state-down {
z-index: 3;
}
.fc-header .fc-state-active,
.fc-header .ui-state-active {
z-index: 4;
}
/* Content
------------------------------------------------------------------------*/
.fc-content {
clear: both;
zoom: 1; /* for IE7, gives accurate coordinates for [un]freezeContentHeight */
}
.fc-view {
width: 100%;
overflow: hidden;
}
/* Cell Styles
------------------------------------------------------------------------*/
/* <th>, usually */
.fc-widget-content {
/* <td>, usually */
border: 1px solid #e5e5e5;
}
.fc-widget-header {
border-bottom: 1px solid #eee;
}
.fc-state-highlight {
/* <td> today cell */ /* TODO: add .fc-today to <th> */
/* background: #fcf8e3; */
}
.fc-state-highlight > div > div.fc-day-number {
background-color: #ff3b30;
color: #ffffff;
border-radius: 50%;
margin: 4px;
}
.fc-cell-overlay {
/* semi-transparent rectangle while dragging */
background: #bce8f1;
opacity: 0.3;
filter: alpha(opacity=30); /* for IE */
}
/* Buttons
------------------------------------------------------------------------*/
.fc-button {
position: relative;
display: inline-block;
padding: 0 0.6em;
overflow: hidden;
height: 1.9em;
line-height: 1.9em;
white-space: nowrap;
cursor: pointer;
}
.fc-state-default {
/* non-theme */
border: 1px solid;
}
.fc-state-default.fc-corner-left {
/* non-theme */
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.fc-state-default.fc-corner-right {
/* non-theme */
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
/*
Our default prev/next buttons use HTML entities like ‹ › « »
and we'll try to make them look good cross-browser.
*/
.fc-text-arrow {
margin: 0 0.4em;
font-size: 2em;
line-height: 23px;
vertical-align: baseline; /* for IE7 */
}
.fc-button-prev .fc-text-arrow,
.fc-button-next .fc-text-arrow {
/* for ‹ › */
font-weight: bold;
}
/* icon (for jquery ui) */
.fc-button .fc-icon-wrap {
position: relative;
float: left;
top: 50%;
}
.fc-button .ui-icon {
position: relative;
float: left;
margin-top: -50%;
*margin-top: 0;
*top: -50%;
}
.fc-state-default {
border-color: #ff3b30;
color: #ff3b30;
}
.fc-button-month.fc-state-default,
.fc-button-agendaWeek.fc-state-default,
.fc-button-agendaDay.fc-state-default {
min-width: 67px;
text-align: center;
transition: all 0.2s;
-webkit-transition: all 0.2s;
}
.fc-state-hover,
.fc-state-down,
.fc-state-active,
.fc-state-disabled {
color: #333333;
background-color: #ffe3e3;
}
.fc-state-hover {
color: #ff3b30;
text-decoration: none;
background-position: 0 -15px;
-webkit-transition: background-position 0.1s linear;
-moz-transition: background-position 0.1s linear;
-o-transition: background-position 0.1s linear;
transition: background-position 0.1s linear;
}
.fc-state-down,
.fc-state-active {
background-color: #ff3b30;
background-image: none;
outline: 0;
color: #ffffff;
}
.fc-state-disabled {
cursor: default;
background-image: none;
background-color: #ffe3e3;
filter: alpha(opacity=65);
box-shadow: none;
border: 1px solid #ffe3e3;
color: #ff3b30;
}
/* Global Event Styles
------------------------------------------------------------------------*/
.fc-event-container > * {
z-index: 8;
}
.fc-event-container > .ui-draggable-dragging,
.fc-event-container > .ui-resizable-resizing {
z-index: 9;
}
.fc-event {
border: 1px solid #fff; /* default BORDER color */
background-color: #fff; /* default BACKGROUND color */
color: #919191; /* default TEXT color */
font-size: 12px;
cursor: default;
}
.fc-event.chill {
background-color: #f3dcf8;
}
.fc-event.info {
background-color: #c6ebfe;
}
.fc-event.important {
background-color: #ffbebe;
}
.fc-event.success {
background-color: #beffbf;
}
.fc-event:hover {
opacity: 0.7;
}
a.fc-event {
text-decoration: none;
}
a.fc-event,
.fc-event-draggable {
cursor: pointer;
}
.fc-rtl .fc-event {
text-align: right;
}
.fc-event-inner {
width: 100%;
height: 100%;
overflow: hidden;
line-height: 15px;
}
.fc-event-time,
.fc-event-title {
padding: 0 1px;
}
.fc .ui-resizable-handle {
display: block;
position: absolute;
z-index: 99999;
overflow: hidden; /* hacky spaces (IE6/7) */
font-size: 300%; /* */
line-height: 50%; /* */
}
/* Horizontal Events
------------------------------------------------------------------------*/
.fc-event-hori {
border-width: 1px 0;
margin-bottom: 1px;
}
.fc-ltr .fc-event-hori.fc-event-start,
.fc-rtl .fc-event-hori.fc-event-end {
border-left-width: 1px;
/*
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
*/
}
.fc-ltr .fc-event-hori.fc-event-end,
.fc-rtl .fc-event-hori.fc-event-start {
border-right-width: 1px;
/*
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
*/
}
/* resizable */
.fc-event-hori .ui-resizable-e {
top: 0 !important; /* importants override pre jquery ui 1.7 styles */
right: -3px !important;
width: 7px !important;
height: 100% !important;
cursor: e-resize;
}
.fc-event-hori .ui-resizable-w {
top: 0 !important;
left: -3px !important;
width: 7px !important;
height: 100% !important;
cursor: w-resize;
}
.fc-event-hori .ui-resizable-handle {
_padding-bottom: 14px; /* IE6 had 0 height */
}
/* Reusable Separate-border Table
------------------------------------------------------------*/
table.fc-border-separate {
border-collapse: separate;
}
.fc-border-separate th,
.fc-border-separate td {
border-width: 1px 0 0 1px;
}
.fc-border-separate th.fc-last,
.fc-border-separate td.fc-last {
border-right-width: 1px;
}
.fc-border-separate tr.fc-last td {
}
.fc-border-separate .fc-week .fc-first {
border-left: 0;
}
.fc-border-separate .fc-week .fc-last {
border-right: 0;
}
.fc-border-separate tr.fc-last th {
border-bottom-width: 1px;
border-color: #cdcdcd;
font-size: 16px;
font-weight: 300;
line-height: 30px;
}
.fc-border-separate tbody tr.fc-first td,
.fc-border-separate tbody tr.fc-first th {
border-top-width: 0;
}
/* Month View, Basic Week View, Basic Day View
------------------------------------------------------------------------*/
.fc-grid th {
text-align: center;
}
.fc .fc-week-number {
width: 22px;
text-align: center;
}
.fc .fc-week-number div {
padding: 0 2px;
}
.fc-grid .fc-day-number {
float: right;
padding: 0 2px;
}
.fc-grid .fc-other-month .fc-day-number {
opacity: 0.3;
filter: alpha(opacity=30); /* for IE */
/* opacity with small font can sometimes look too faded
might want to set the 'color' property instead
making day-numbers bold also fixes the problem */
}
.fc-grid .fc-day-content {
clear: both;
padding: 2px 2px 1px; /* distance between events and day edges */
}
/* event styles */
.fc-grid .fc-event-time {
font-weight: bold;
}
/* right-to-left */
.fc-rtl .fc-grid .fc-day-number {
float: left;
}
.fc-rtl .fc-grid .fc-event-time {
float: right;
}
/* Agenda Week View, Agenda Day View
------------------------------------------------------------------------*/
.fc-agenda table {
border-collapse: separate;
}
.fc-agenda-days th {
text-align: center;
}
.fc-agenda .fc-agenda-axis {
width: 50px;
padding: 0 4px;
vertical-align: middle;
text-align: right;
white-space: nowrap;
font-weight: normal;
}
.fc-agenda .fc-week-number {
font-weight: bold;
}
.fc-agenda .fc-day-content {
padding: 2px 2px 1px;
}
/* make axis border take precedence */
.fc-agenda-days .fc-agenda-axis {
border-right-width: 1px;
}
.fc-agenda-days .fc-col0 {
border-left-width: 0;
}
/* all-day area */
.fc-agenda-allday th {
border-width: 0 1px;
}
.fc-agenda-allday .fc-day-content {
min-height: 34px; /* TODO: doesnt work well in quirksmode */
_height: 34px;
}
/* divider (between all-day and slots) */
.fc-agenda-divider-inner {
height: 2px;
overflow: hidden;
}
.fc-widget-header .fc-agenda-divider-inner {
background: #eee;
}
/* slot rows */
.fc-agenda-slots th {
border-width: 1px 1px 0;
}
.fc-agenda-slots td {
border-width: 1px 0 0;
background: none;
}
.fc-agenda-slots td div {
height: 20px;
}
.fc-agenda-slots tr.fc-slot0 th,
.fc-agenda-slots tr.fc-slot0 td {
border-top-width: 0;
}
.fc-agenda-slots tr.fc-minor th.ui-widget-header {
*border-top-style: solid; /* doesn't work with background in IE6/7 */
}
/* Vertical Events
------------------------------------------------------------------------*/
.fc-event-vert {
border-width: 0 1px;
}
.fc-event-vert.fc-event-start {
border-top-width: 1px;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
.fc-event-vert.fc-event-end {
border-bottom-width: 1px;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
.fc-event-vert .fc-event-time {
white-space: nowrap;
font-size: 10px;
}
.fc-event-vert .fc-event-inner {
position: relative;
z-index: 2;
}
.fc-event-vert .fc-event-bg {
/* makes the event lighter w/ a semi-transparent overlay */
position: absolute;
z-index: 1;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #fff;
opacity: 0.25;
filter: alpha(opacity=25);
}
.fc .ui-draggable-dragging .fc-event-bg, /* TODO: something nicer like .fc-opacity */
.fc-select-helper .fc-event-bg {
display: none\9; /* for IE6/7/8. nested opacity filters while dragging don't work */
}
/* resizable */
.fc-event-vert .ui-resizable-s {
bottom: 0 !important; /* importants override pre jquery ui 1.7 styles */
width: 100% !important;
height: 8px !important;
overflow: hidden !important;
line-height: 8px !important;
font-size: 11px !important;
font-family: monospace;
text-align: center;
cursor: s-resize;
}
.fc-agenda .ui-resizable-resizing {
/* TODO: better selector */
_overflow: hidden;
}
thead tr.fc-first {
background-color: #f7f7f7;
}
table.fc-header {
background-color: #ffffff;
border-radius: 6px 6px 0 0;
}
.fc-week .fc-day > div .fc-day-number {
font-size: 15px;
margin: 2px;
min-width: 19px;
padding: 6px;
text-align: center;
width: 30px;
height: 30px;
}
.fc-sun,
.fc-sat {
color: #b8b8b8;
}
.fc-week .fc-day:hover .fc-day-number {
background-color: #b8b8b8;
border-radius: 50%;
color: #ffffff;
transition: background-color 0.2s;
}
.fc-week .fc-day.fc-state-highlight:hover .fc-day-number {
background-color: #ff3b30;
}
.fc-button-today {
border: 1px solid rgba(255, 255, 255, 0);
}
.fc-view-agendaDay thead tr.fc-first .fc-widget-header {
text-align: right;
padding-right: 10px;
}
/*!
* FullCalendar v1.6.4 Print Stylesheet
* Docs & License: http://arshaw.com/fullcalendar/
* (c) 2013 Adam Shaw
*/
/*
* Include this stylesheet on your page to get a more printer-friendly calendar.
* When including this stylesheet, use the media='print' attribute of the <link> tag.
* Make sure to include this stylesheet IN ADDITION to the regular fullcalendar.css.
*/
/* Events
-----------------------------------------------------*/
.fc-event {
background: #fff !important;
color: #000 !important;
}
/* for vertical events */
.fc-event-bg {
display: none !important;
}
.fc-event .ui-resizable-handle {
display: none !important;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment