simple-coder
Version:
tool to generate code and framework
2,066 lines (1,607 loc) • 185 kB
JavaScript
/*!
* FullCalendar v2.0.2
* Docs & License: http://arshaw.com/fullcalendar/
* (c) 2013 Adam Shaw
*/
(function(factory) {
if (typeof define === 'function' && define.amd) {
define([ 'jquery', 'moment' ], factory);
}
else {
factory(jQuery, moment);
}
})(function($, moment) {
;;
var defaults = {
lang: 'en',
defaultTimedEventDuration: '02:00:00',
defaultAllDayEventDuration: { days: 1 },
forceEventDuration: false,
nextDayThreshold: '09:00:00', // 9am
// display
defaultView: 'month',
aspectRatio: 1.35,
header: {
left: 'title',
center: '',
right: 'today prev,next'
},
weekends: true,
weekNumbers: false,
weekNumberTitle: 'W',
weekNumberCalculation: 'local',
//editable: false,
// event ajax
lazyFetching: true,
startParam: 'start',
endParam: 'end',
timezoneParam: 'timezone',
timezone: false,
//allDayDefault: undefined,
// time formats
titleFormat: {
month: 'MMMM YYYY', // like "September 1986". each language will override this
week: 'll', // like "Sep 4 1986"
day: 'LL' // like "September 4 1986"
},
columnFormat: {
month: 'ddd', // like "Sat"
week: generateWeekColumnFormat,
day: 'dddd' // like "Saturday"
},
timeFormat: { // for event elements
'default': generateShortTimeFormat
},
displayEventEnd: {
month: false,
basicWeek: false,
'default': true
},
// locale
isRTL: false,
defaultButtonText: {
prev: "prev",
next: "next",
prevYear: "prev year",
nextYear: "next year",
today: 'today',
month: 'month',
week: 'week',
day: 'day'
},
buttonIcons: {
prev: 'left-single-arrow',
next: 'right-single-arrow',
prevYear: 'left-double-arrow',
nextYear: 'right-double-arrow'
},
// jquery-ui theming
theme: false,
themeButtonIcons: {
prev: 'circle-triangle-w',
next: 'circle-triangle-e',
prevYear: 'seek-prev',
nextYear: 'seek-next'
},
//selectable: false,
unselectAuto: true,
dropAccept: '*',
handleWindowResize: true,
windowResizeDelay: 200 // milliseconds before a rerender happens
};
function generateShortTimeFormat(options, langData) {
return langData.longDateFormat('LT')
.replace(':mm', '(:mm)')
.replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs
.replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand
}
function generateWeekColumnFormat(options, langData) {
var format = langData.longDateFormat('L'); // for the format like "MM/DD/YYYY"
format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, ''); // strip the year off the edge, as well as other misc non-whitespace chars
if (options.isRTL) {
format += ' ddd'; // for RTL, add day-of-week to end
}
else {
format = 'ddd ' + format; // for LTR, add day-of-week to beginning
}
return format;
}
var langOptionHash = {
en: {
columnFormat: {
week: 'ddd M/D' // override for english. different from the generated default, which is MM/DD
}
}
};
// right-to-left defaults
var rtlDefaults = {
header: {
left: 'next,prev today',
center: '',
right: 'title'
},
buttonIcons: {
prev: 'right-single-arrow',
next: 'left-single-arrow',
prevYear: 'right-double-arrow',
nextYear: 'left-double-arrow'
},
themeButtonIcons: {
prev: 'circle-triangle-e',
next: 'circle-triangle-w',
nextYear: 'seek-prev',
prevYear: 'seek-next'
}
};
;;
var fc = $.fullCalendar = { version: "2.0.2" };
var fcViews = fc.views = {};
$.fn.fullCalendar = function(options) {
var args = Array.prototype.slice.call(arguments, 1); // for a possible method call
var res = this; // what this function will return (this jQuery object by default)
this.each(function(i, _element) { // loop each DOM element involved
var element = $(_element);
var calendar = element.data('fullCalendar'); // get the existing calendar object (if any)
var singleRes; // the returned value of this single method call
// a method call
if (typeof options === 'string') {
if (calendar && $.isFunction(calendar[options])) {
singleRes = calendar[options].apply(calendar, args);
if (!i) {
res = singleRes; // record the first method call result
}
if (options === 'destroy') { // for the destroy method, must remove Calendar object data
element.removeData('fullCalendar');
}
}
}
// a new calendar initialization
else if (!calendar) { // don't initialize twice
calendar = new Calendar(element, options);
element.data('fullCalendar', calendar);
calendar.render();
}
});
return res;
};
// function for adding/overriding defaults
function setDefaults(d) {
mergeOptions(defaults, d);
}
// Recursively combines option hash-objects.
// Better than `$.extend(true, ...)` because arrays are not traversed/copied.
//
// called like:
// mergeOptions(target, obj1, obj2, ...)
//
function mergeOptions(target) {
function mergeIntoTarget(name, value) {
if ($.isPlainObject(value) && $.isPlainObject(target[name]) && !isForcedAtomicOption(name)) {
// merge into a new object to avoid destruction
target[name] = mergeOptions({}, target[name], value); // combine. `value` object takes precedence
}
else if (value !== undefined) { // only use values that are set and not undefined
target[name] = value;
}
}
for (var i=1; i<arguments.length; i++) {
$.each(arguments[i], mergeIntoTarget);
}
return target;
}
// overcome sucky view-option-hash and option-merging behavior messing with options it shouldn't
function isForcedAtomicOption(name) {
// Any option that ends in "Time" or "Duration" is probably a Duration,
// and these will commonly be specified as plain objects, which we don't want to mess up.
return /(Time|Duration)$/.test(name);
}
// FIX: find a different solution for view-option-hashes and have a whitelist
// for options that can be recursively merged.
;;
//var langOptionHash = {}; // initialized in defaults.js
fc.langs = langOptionHash; // expose
// Initialize jQuery UI Datepicker translations while using some of the translations
// for our own purposes. Will set this as the default language for datepicker.
// Called from a translation file.
fc.datepickerLang = function(langCode, datepickerLangCode, options) {
var langOptions = langOptionHash[langCode];
// initialize FullCalendar's lang hash for this language
if (!langOptions) {
langOptions = langOptionHash[langCode] = {};
}
// merge certain Datepicker options into FullCalendar's options
mergeOptions(langOptions, {
isRTL: options.isRTL,
weekNumberTitle: options.weekHeader,
titleFormat: {
month: options.showMonthAfterYear ?
'YYYY[' + options.yearSuffix + '] MMMM' :
'MMMM YYYY[' + options.yearSuffix + ']'
},
defaultButtonText: {
// the translations sometimes wrongly contain HTML entities
prev: stripHTMLEntities(options.prevText),
next: stripHTMLEntities(options.nextText),
today: stripHTMLEntities(options.currentText)
}
});
// is jQuery UI Datepicker is on the page?
if ($.datepicker) {
// Register the language data.
// FullCalendar and MomentJS use language codes like "pt-br" but Datepicker
// does it like "pt-BR" or if it doesn't have the language, maybe just "pt".
// Make an alias so the language can be referenced either way.
$.datepicker.regional[datepickerLangCode] =
$.datepicker.regional[langCode] = // alias
options;
// Alias 'en' to the default language data. Do this every time.
$.datepicker.regional.en = $.datepicker.regional[''];
// Set as Datepicker's global defaults.
$.datepicker.setDefaults(options);
}
};
// Sets FullCalendar-specific translations. Also sets the language as the global default.
// Called from a translation file.
fc.lang = function(langCode, options) {
var langOptions;
if (options) {
langOptions = langOptionHash[langCode];
// initialize the hash for this language
if (!langOptions) {
langOptions = langOptionHash[langCode] = {};
}
mergeOptions(langOptions, options || {});
}
// set it as the default language for FullCalendar
defaults.lang = langCode;
};
;;
function Calendar(element, instanceOptions) {
var t = this;
// Build options object
// -----------------------------------------------------------------------------------
// Precedence (lowest to highest): defaults, rtlDefaults, langOptions, instanceOptions
instanceOptions = instanceOptions || {};
var options = mergeOptions({}, defaults, instanceOptions);
var langOptions;
// determine language options
if (options.lang in langOptionHash) {
langOptions = langOptionHash[options.lang];
}
else {
langOptions = langOptionHash[defaults.lang];
}
if (langOptions) { // if language options exist, rebuild...
options = mergeOptions({}, defaults, langOptions, instanceOptions);
}
if (options.isRTL) { // is isRTL, rebuild...
options = mergeOptions({}, defaults, rtlDefaults, langOptions || {}, instanceOptions);
}
// 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.getDate = getDate;
t.getCalendar = getCalendar;
t.getView = getView;
t.option = option;
t.trigger = trigger;
// Language-data Internals
// -----------------------------------------------------------------------------------
// Apply overrides to the current language's data
var langData = createObject( // make a cheap clone
moment.langData(options.lang)
);
if (options.monthNames) {
langData._months = options.monthNames;
}
if (options.monthNamesShort) {
langData._monthsShort = options.monthNamesShort;
}
if (options.dayNames) {
langData._weekdays = options.dayNames;
}
if (options.dayNamesShort) {
langData._weekdaysShort = options.dayNamesShort;
}
if (options.firstDay != null) {
var _week = createObject(langData._week); // _week: { dow: # }
_week.dow = options.firstDay;
langData._week = _week;
}
// Calendar-specific Date Utilities
// -----------------------------------------------------------------------------------
t.defaultAllDayEventDuration = moment.duration(options.defaultAllDayEventDuration);
t.defaultTimedEventDuration = moment.duration(options.defaultTimedEventDuration);
// Builds a moment using the settings of the current calendar: timezone and language.
// Accepts anything the vanilla moment() constructor accepts.
t.moment = function() {
var mom;
if (options.timezone === 'local') {
mom = fc.moment.apply(null, arguments);
// Force the moment to be local, because fc.moment doesn't guarantee it.
if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone
mom.local();
}
}
else if (options.timezone === 'UTC') {
mom = fc.moment.utc.apply(null, arguments); // process as UTC
}
else {
mom = fc.moment.parseZone.apply(null, arguments); // let the input decide the zone
}
mom._lang = langData;
return mom;
};
// Returns a boolean about whether or not the calendar knows how to calculate
// the timezone offset of arbitrary dates in the current timezone.
t.getIsAmbigTimezone = function() {
return options.timezone !== 'local' && options.timezone !== 'UTC';
};
// Returns a copy of the given date in the current timezone of it is ambiguously zoned.
// This will also give the date an unambiguous time.
t.rezoneDate = function(date) {
return t.moment(date.toArray());
};
// Returns a moment for the current date, as defined by the client's computer,
// or overridden by the `now` option.
t.getNow = function() {
var now = options.now;
if (typeof now === 'function') {
now = now();
}
return t.moment(now);
};
// Calculates the week number for a moment according to the calendar's
// `weekNumberCalculation` setting.
t.calculateWeekNumber = function(mom) {
var calc = options.weekNumberCalculation;
if (typeof calc === 'function') {
return calc(mom);
}
else if (calc === 'local') {
return mom.week();
}
else if (calc.toUpperCase() === 'ISO') {
return mom.isoWeek();
}
};
// Get an event's normalized end date. If not present, calculate it from the defaults.
t.getEventEnd = function(event) {
if (event.end) {
return event.end.clone();
}
else {
return t.getDefaultEventEnd(event.allDay, event.start);
}
};
// Given an event's allDay status and start date, return swhat its fallback end date should be.
t.getDefaultEventEnd = function(allDay, start) { // TODO: rename to computeDefaultEventEnd
var end = start.clone();
if (allDay) {
end.stripTime().add(t.defaultAllDayEventDuration);
}
else {
end.add(t.defaultTimedEventDuration);
}
if (t.getIsAmbigTimezone()) {
end.stripZone(); // we don't know what the tzo should be
}
return end;
};
// Date-formatting Utilities
// -----------------------------------------------------------------------------------
// Like the vanilla formatRange, but with calendar-specific settings applied.
t.formatRange = function(m1, m2, formatStr) {
// a function that returns a formatStr // TODO: in future, precompute this
if (typeof formatStr === 'function') {
formatStr = formatStr.call(t, options, langData);
}
return formatRange(m1, m2, formatStr, null, options.isRTL);
};
// Like the vanilla formatDate, but with calendar-specific settings applied.
t.formatDate = function(mom, formatStr) {
// a function that returns a formatStr // TODO: in future, precompute this
if (typeof formatStr === 'function') {
formatStr = formatStr.call(t, options, langData);
}
return formatDate(mom, formatStr);
};
// Imports
// -----------------------------------------------------------------------------------
EventManager.call(t, options);
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;
var events = [];
var _dragElement;
// Main Rendering
// -----------------------------------------------------------------------------------
if (options.defaultDate != null) {
date = t.moment(options.defaultDate);
}
else {
date = t.getNow();
}
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' />")
.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);
if (options.droppable) {
$(document)
.off('dragstart', droppableDragStart)
.off('dragstop', droppableDragStop);
}
if (currentView.selectionManagerDestroy) {
currentView.selectionManagerDestroy();
}
header.destroy();
content.remove();
element.removeClass('fc fc-ltr 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 + "' />")
.appendTo(content),
t // the calendar object
);
renderView();
unfreezeContentHeight();
ignoreWindowResize--;
}
function renderView(inc) {
if (
!currentView.start || // never rendered before
inc || // explicit date window change
!date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change
) {
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();
if (inc) {
date = currentView.incrementDate(date, inc);
}
currentView.render(date.clone()); // the view's render method ONLY renders the skeleton, nothing else
setSize();
unfreezeContentHeight();
(currentView.afterRender || noop)();
updateTitle();
updateTodayButton();
trigger('viewRender', currentView, currentView, currentView.element);
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, .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(ev) {
if (
!ignoreWindowResize &&
ev.target === window // so we don't process jqui "resize" events that have bubbled up
) {
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--;
}
}
}, options.windowResizeDelay);
}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.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.start, currentView.end)) {
fetchAndRenderEvents();
}
else {
renderEvents();
}
}
function fetchAndRenderEvents() {
fetchEvents(currentView.start, currentView.end);
// ... 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 now = t.getNow();
if (now.isWithin(currentView.intervalStart, currentView.intervalEnd)) {
header.disableButton('today');
}
else {
header.enableButton('today');
}
}
/* Selection
-----------------------------------------------------------------------------*/
function select(start, end) {
currentView.select(start, end);
}
function unselect() { // safe to be called before renderView
if (currentView) {
currentView.unselect();
}
}
/* Date
-----------------------------------------------------------------------------*/
function prev() {
renderView(-1);
}
function next() {
renderView(1);
}
function prevYear() {
date.add('years', -1);
renderView();
}
function nextYear() {
date.add('years', 1);
renderView();
}
function today() {
date = t.getNow();
renderView();
}
function gotoDate(dateInput) {
date = t.moment(dateInput);
renderView();
}
function incrementDate(delta) {
date.add(moment.duration(delta));
renderView();
}
function getDate() {
return date.clone();
}
/* Height "Freezing"
-----------------------------------------------------------------------------*/
function freezeContentHeight() {
content.css({
width: '100%',
height: content.height(),
overflow: 'hidden'
});
}
function unfreezeContentHeight() {
content.css({
width: '',
height: '',
overflow: ''
});
}
/* Misc
-----------------------------------------------------------------------------*/
function getCalendar() {
return t;
}
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) {
// TODO: unbind on destroy
$(document)
.on('dragstart', droppableDragStart)
.on('dragstop', droppableDragStop);
// this is undone in destroy
}
function droppableDragStart(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);
}
}
}
function droppableDragStop(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) {
// smartProperty allows different text per view button (ex: "Agenda Week" vs "Basic Week")
var themeIcon = smartProperty(options.themeButtonIcons, buttonName);
var normalIcon = smartProperty(options.buttonIcons, buttonName);
var defaultText = smartProperty(options.defaultButtonText, buttonName);
var customText = smartProperty(options.buttonText, buttonName);
var html;
if (customText) {
html = htmlEscape(customText);
}
else if (themeIcon && options.theme) {
html = "<span class='ui-icon ui-icon-" + themeIcon + "'></span>";
}
else if (normalIcon && !options.theme) {
html = "<span class='fc-icon fc-icon-" + normalIcon + "'></span>";
}
else {
html = htmlEscape(defaultText || buttonName);
}
var button = $(
"<span class='fc-button fc-button-" + buttonName + " " + tm + "-state-default'>" +
html +
"</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) { // assumed to be a calendar
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.mutateEvent = mutateEvent;
// imports
var trigger = t.trigger;
var getView = t.getView;
var reportEvents = t.reportEvents;
var getEventEnd = t.getEventEnd;
// locals
var stickySource = { events: [] };
var sources = [ stickySource ];
var rangeStart, rangeEnd;
var currentFetchID = 0;
var pendingSourceCnt = 0;
var loadingLevel = 0;
var cache = [];
$.each(
(options.events ? [ options.events ] : []).concat(options.eventSources || []),
function(i, sourceInput) {
var source = buildEventSource(sourceInput);
if (source) {
sources.push(source);
}
}
);
/* Fetching
-----------------------------------------------------------------------------*/
function isFetchNeeded(start, end) {
return !rangeStart || // nothing has been fetched yet?
// or, a part of the new range is outside of the old range? (after normalizing)
start.clone().stripZone() < rangeStart.clone().stripZone() ||
end.clone().stripZone() > rangeEnd.clone().stripZone();
}
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) {
var isArraySource = $.isArray(source.events);
var i;
var event;
if (fetchID == currentFetchID) {
if (events) {
for (i=0; i<events.length; i++) {
event = events[i];
// event array sources have already been convert to Event Objects
if (!isArraySource) {
event = buildEvent(event, source);
}
if (event) {
cache.push(event);
}
}
}
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].call(
t, // this, the Calendar object
source,
rangeStart.clone(),
rangeEnd.clone(),
options.timezone,
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.call(
t, // this, the Calendar object
rangeStart.clone(),
rangeEnd.clone(),
options.timezone,
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);
var timezoneParam = firstDefined(source.timezoneParam, options.timezoneParam);
if (startParam) {
data[startParam] = rangeStart.format();
}
if (endParam) {
data[endParam] = rangeEnd.format();
}
if (options.timezone && options.timezone != 'local') {
data[timezoneParam] = options.timezone;
}
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(sourceInput) {
var source = buildEventSource(sourceInput);
if (source) {
sources.push(source);
pendingSourceCnt++;
fetchEventSource(source, currentFetchID); // will eventually call reportEvents
}
}
function buildEventSource(sourceInput) { // will return undefined if invalid source
var normalizers = fc.sourceNormalizers;
var source;
var i;
if ($.isFunction(sourceInput) || $.isArray(sourceInput)) {
source = { events: sourceInput };
}
else if (typeof sourceInput === 'string') {
source = { url: sourceInput };
}
else if (typeof sourceInput === 'object') {
source = $.extend({}, sourceInput); // shallow copy
if (typeof source.className === 'string') {
// TODO: repeat code, same code for event classNames
source.className = source.className.split(/\s+/);
}
}
if (source) {
// for array sources, we convert to standard Event Objects up front
if ($.isArray(source.events)) {
source.events = $.map(source.events, function(eventInput) {
return buildEvent(eventInput, source);
});
}
for (i=0; i<normalizers.length; i++) {
normalizers[i].call(t, 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);
}
function isSourcesEqual(source1, source2) {
return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2);
}
function getSourcePrimitive(source) {
return ((typeof source == 'object') ? (source.events || source.url) : '') || source;
}
/* Manipulation
-----------------------------------------------------------------------------*/
function updateEvent(event) {
event.start = t.moment(event.start);
if (event.end) {
event.end = t.moment(event.end);
}
mutateEvent(event);
propagateMiscProperties(event);
reportEvents(cache); // reports event modifications (so we can redraw)
}
var miscCopyableProps = [
'title',
'url',
'allDay',
'className',
'editable',
'color',
'backgroundColor',
'borderColor',
'textColor'
];
function propagateMiscProperties(event) {
var i;
var cachedEvent;
var j;
var prop;
for (i=0; i<cache.length; i++) {
cachedEvent = cache[i];
if (cachedEvent._id == event._id && cachedEvent !== event) {
for (j=0; j<miscCopyableProps.length; j++) {
prop = miscCopyableProps[j];
if (event[prop] !== undefined) {
cachedEvent[prop] = event[prop];
}
}
}
}
}
function renderEvent(eventData, stick) {
var event = buildEvent(eventData);
if (event) {
if (!event.source) {
if (stick) {
stickySource.events.push(event);
event.source = stickySource;
}
cache.push(event);
}
reportEvents(cache);
}
}
function removeEvents(filter) {
var eventID;
var i;
if (filter == null) { // null or undefined. remove all events
filter = function() { return true; }; // will always match
}
else if (!$.isFunction(filter)) { // an event ID
eventID = filter + '';
filter = function(event) {
return event._id == eventID;
};
}
// Purge event(s) from our local cache
cache = $.grep(cache, filter, true); // inverse=true
// Remove events from array sources.
// This works because they have been converted to official Event Objects up front.
// (and as a result, event._id has been calculated).
for (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 != null) { // not null, not undefined. 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 buildEvent(data, source) { // source may be undefined!
var out = {};
var start;
var end;
var allDay;
var allDayDefault;
if (options.eventDataTransform) {
data = options.eventDataTransform(data);
}
if (source && source.eventDataTransform) {
data = source.eventDataTransform(data);
}
start = t.moment(data.start || data.date); // "date" is an alias for "start"
if (!start.isValid()) {
return;
}
end = null;
if (data.end) {
end = t.moment(data.end);
if (!end.isValid()) {
return;
}
}
allDay = data.allDay;
if (allDay === undefined) {
allDayDefault = firstDefined(
source ? source.allDayDefault : undefined,
options.allDayDefault
);
if (allDayDefault !== undefined) {
// use the default
allDay = allDayDefault;
}
else {
// all dates need to have ambig time for the event to be considered allDay
allDay = !start.hasTime() && (!end || !end.hasTime());
}
}
// normalize the date based on allDay
if (allDay) {
// neither date should have a time
if (start.hasTime()) {
start.stripTime();
}
if (end && end.hasTime()) {
end.stripTime();
}
}
else {
// force a time/zone up the dates
if (!start.hasTime()) {
start = t.rezoneDate(start);
}
if (end && !end.hasTime()) {
end = t.rezoneDate(end);
}
}
// Copy all properties over to the resulting object.
// The special-case properties will be copied over afterwards.
$.extend(out, data);
if (source) {
out.source = source;
}
out._id = data._id || (data.id === undefined ? '_fc' + eventGUID++ : data.id + '');
if (data.className) {
if (typeof data.className == 'string') {
out.className = data.className.split(/\s+/);
}
else { // assumed to be an array
out.className = data.className;
}
}
else {
out.className = [];
}
out.allDay = allDay;
out.start = start;
out.end = end;
if (options.forceEventDuration && !out.end) {
out.end = getEventEnd(out);
}
backupEventDates(out);
return out;
}
/* Event Modification Math
-----------------------------------------------------------------------------------------*/
// Modify the date(s) of an event and make this change propagate to all other events with
// the same ID (related repeating events).
//
// If `newStart`/`newEnd` are not specified, the "new" dates are assumed to be `event.start` and `event.end`.
// The "old" dates to be compare against are always `event._start` and `event._end` (set by EventManager).
//
// Returns an object with delta information and a function to undo all operations.
//
function mutateEvent(event, newStart, newEnd) {
var oldAllDay = event._allDay;
var oldStart = event._start;
var oldEnd = event._end;
var clearEnd = false;
var newAllDay;
var dateDelta;
var durationDelta;
var undoFunc;
// if no new dates were passed in, compare against the event's existing dates
if (!newStart && !newEnd) {
newStart = event.start;
newEnd = event.end;
}
// NOTE: throughout this function, the initial values of `newStart` and `newEnd` are
// preserved. These values may be undefined.
// detect new allDay
if (event.allDay != oldAllDay) { // if value has changed, use it
newAllDay = event.allDay;
}
else { // otherwise, see if any of the new dates are allDay
newAllDay = !(newStart || newEnd).hasTime();
}
// normalize the new dates based on allDay
if (newAllDay) {
if (newStart) {
newStart = newStart.clone().stripTime();
}
if (newEnd) {
newEnd = newEnd.clone().stripTime();
}
}
// compute dateDelta
if (newStart) {
if (newAllDay) {
dateDelta = dayishDiff(newStart, oldStart.clone().stripTime()); // treat oldStart as allDay
}
else {
dateDelta = dayishDiff(newStart, oldStart);
}
}
if (newAllDay != oldAllDay) {
// if allDay has changed, always throw away the end
clearEnd = true;
}
else if (newEnd) {
durationDelta = dayishDiff(
// new duration
newEnd || t.getDefaultEventEnd(newAllDay, newStart || oldStart),
newStart || oldStart
).subtract(dayishDiff(
// subtract old duration
oldEnd || t.getDefaultEventEnd(oldAllDay, oldStart),
oldStart
));
}
undoFunc = mutateEvents(
clientEvents(event._id), // get events with this ID
clearEnd,
newAllDay,
dateDelta,
durationDelta
);
return {
dateDelta: dateDelta,
durationDelta: durationDelta,
undo: undoFunc
};
}
// Modifies an array of events in the following ways (operations are in order):
// - clear the event's `end`
// - convert the event to allDay
// - add `dateDelta` to the start and end
// - add `durationDelta` to the event's duration
//
// Returns a function that can be called to undo all the operations.
//
function mutateEvents(events, clearEnd, forceAllDay, dateDelta, durationDelta) {
var isAmbigTimezone = t.getIsAmbigTimezone();
var undoFunctions = [];
$.each(events, function(i, event) {
var oldAllDay = event._allDay;
var oldStart = event._start;
var oldEnd = event._end;
var newAllDay = forceAllDay != null ? forceAllDay : oldAllDay;
var newStart = oldStart.clone();
var newEnd = (!clearEnd && oldEnd) ? oldEnd.clone() : null;
// NOTE: this function is responsible for transforming `newStart` and `newEnd`,
// which were initialized to the OLD values first. `newEnd` may be null.
// normlize newStart/newEnd to be consistent with newAllDay
if (newAllDay) {
newStart.stripTime();
if (newEnd) {
newEnd.stripTime();
}
}
else {
if (!newStart.hasTime()) {
newStart = t.rezoneDate(newStart);
}
if (newEnd && !newEnd.hasTime()) {
newEnd = t.rezoneDate(newEnd);
}
}
// ensure we have an end date if necessary
if (!newEnd && (options.forceEventDuration || +durationDelta)) {
newEnd = t.getDefaultEventEnd(newAllDay, newStart);
}
// translate the dates
newStart.add(dateDelta);
if (newEnd) {
newEnd.add(dateDelta).add(durationDelta);
}
// if the dates have changed, and we know it is impossible to recompute the
// timezone offsets, strip the zone.
if (isAmbigTimezone) {
if (+dateDelta || +durationDelta) {
newStart.stripZone();
if (newEnd) {
newEnd.stripZone();
}
}
}
event.allDay = newAllDay;
event.start = newStart;
event.end = newEnd;
backupEventDates(event);
undoFunctions.push(function() {
event.allDay = oldAllDay;
event.start = oldStart;
event.end = oldEnd;
backupEventDates(event);
});
});
return function() {
for (var i=0; i<undoFunctions.length; i++) {
undoFunctions[i]();
}
};
}
}
// updates the "backup" properties, which are preserved in order to compute diffs later on.
function backupEventDates(event) {
event._allDay = event.allDay;
event._start = event.start.clone();
event._end = event.end ? event.end.clone() : null;
}
;;
fc.applyAll = applyAll;
// Create an object that has the given prototype.
// Just like Object.create
function createObject(proto) {
var f = function() {};
f.prototype = proto;
return new f();
}
// Copies specifically-owned (non-protoype) properties of `b` onto `a`.
// FYI, $.extend would copy *all* properties of `b` onto `a`.
function extend(a, b) {
for (var i in b) {
if (b.hasOwnProperty(i)) {
a[i] = b[i];
}
}
}
/* Date
-----------------------------------------------------------------------------*/
var dayIDs = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
// diffs the two moments into a Duration where full-days are recorded first,
// then the remaining time.
function dayishDiff(d1, d0) {
return moment.duration({
days: d1.clone().stripTime().diff(d0.clone().stripTime(), 'days'),
ms: d1.time() - d0.time()
});
}
function isNativeDate(input) {
return Object.prototype.toString.call(input) === '[object Date]' ||
input instanceof Date;
}
/* 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);
}
functi