mirror of
https://github.com/wassname/fullcalendar.git
synced 2026-07-10 00:30:28 +08:00
726 lines
16 KiB
JavaScript
726 lines
16 KiB
JavaScript
|
|
|
|
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 = renderEvents; // `renderEvents` serves as a rerender. an API method
|
|
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.zoomTo = zoomTo;
|
|
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
|
|
|
|
|
|
// Returns moment's internal locale data. If doesn't exist, returns English.
|
|
// Works with moment-pre-2.8
|
|
function getLocaleData(langCode) {
|
|
var f = moment.localeData || moment.langData;
|
|
return f.call(moment, langCode) ||
|
|
f.call(moment, 'en'); // the newer localData could return null, so fall back to en
|
|
}
|
|
|
|
|
|
var localeData = createObject(getLocaleData(options.lang)); // make a cheap copy
|
|
|
|
if (options.monthNames) {
|
|
localeData._months = options.monthNames;
|
|
}
|
|
if (options.monthNamesShort) {
|
|
localeData._monthsShort = options.monthNamesShort;
|
|
}
|
|
if (options.dayNames) {
|
|
localeData._weekdays = options.dayNames;
|
|
}
|
|
if (options.dayNamesShort) {
|
|
localeData._weekdaysShort = options.dayNamesShort;
|
|
}
|
|
if (options.firstDay != null) {
|
|
var _week = createObject(localeData._week); // _week: { dow: # }
|
|
_week.dow = options.firstDay;
|
|
localeData._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
|
|
}
|
|
|
|
if ('_locale' in mom) { // moment 2.8 and above
|
|
mom._locale = localeData;
|
|
}
|
|
else { // pre-moment-2.8
|
|
mom._lang = localeData;
|
|
}
|
|
|
|
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, localeData);
|
|
}
|
|
|
|
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, localeData);
|
|
}
|
|
|
|
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 suggestedViewHeight;
|
|
var windowResizeProxy; // wraps the windowResize function
|
|
var ignoreWindowResize = 0;
|
|
var date;
|
|
var events = [];
|
|
|
|
|
|
|
|
// 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');
|
|
}
|
|
else {
|
|
element.addClass('fc-unthemed');
|
|
}
|
|
|
|
content = $("<div class='fc-view-container'/>").prependTo(element);
|
|
|
|
header = new Header(t, options);
|
|
headerElement = header.render();
|
|
if (headerElement) {
|
|
element.prepend(headerElement);
|
|
}
|
|
|
|
changeView(options.defaultView);
|
|
|
|
if (options.handleWindowResize) {
|
|
windowResizeProxy = debounce(windowResize, options.windowResizeDelay); // prevents rapid calls
|
|
$(window).resize(windowResizeProxy);
|
|
}
|
|
}
|
|
|
|
|
|
function destroy() {
|
|
|
|
if (currentView) {
|
|
currentView.destroy();
|
|
}
|
|
|
|
header.destroy();
|
|
content.remove();
|
|
element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');
|
|
|
|
$(window).unbind('resize', windowResizeProxy);
|
|
}
|
|
|
|
|
|
function elementVisible() {
|
|
return element.is(':visible');
|
|
}
|
|
|
|
|
|
|
|
// View Rendering
|
|
// -----------------------------------------------------------------------------------
|
|
|
|
|
|
function changeView(viewName) {
|
|
renderView(0, viewName);
|
|
}
|
|
|
|
|
|
// Renders a view because of a date change, view-type change, or for the first time
|
|
function renderView(delta, viewName) {
|
|
ignoreWindowResize++;
|
|
|
|
// if viewName is changing, destroy the old view
|
|
if (currentView && viewName && currentView.name !== viewName) {
|
|
header.deactivateButton(currentView.name);
|
|
freezeContentHeight(); // prevent a scroll jump when view element is removed
|
|
if (currentView.start) { // rendered before?
|
|
currentView.destroy();
|
|
}
|
|
currentView.el.remove();
|
|
currentView = null;
|
|
}
|
|
|
|
// if viewName changed, or the view was never created, create a fresh view
|
|
if (!currentView && viewName) {
|
|
currentView = new fcViews[viewName](t);
|
|
currentView.el = $("<div class='fc-view fc-" + viewName + "-view' />").appendTo(content);
|
|
header.activateButton(viewName);
|
|
}
|
|
|
|
if (currentView) {
|
|
|
|
// let the view determine what the delta means
|
|
if (delta) {
|
|
date = currentView.incrementDate(date, delta);
|
|
}
|
|
|
|
// render or rerender the view
|
|
if (
|
|
!currentView.start || // never rendered before
|
|
delta || // explicit date window change
|
|
!date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change
|
|
) {
|
|
if (elementVisible()) {
|
|
|
|
freezeContentHeight();
|
|
if (currentView.start) { // rendered before?
|
|
currentView.destroy();
|
|
}
|
|
currentView.render(date);
|
|
unfreezeContentHeight();
|
|
|
|
// need to do this after View::render, so dates are calculated
|
|
updateTitle();
|
|
updateTodayButton();
|
|
|
|
getAndRenderEvents();
|
|
}
|
|
}
|
|
}
|
|
|
|
unfreezeContentHeight(); // undo any lone freezeContentHeight calls
|
|
ignoreWindowResize--;
|
|
}
|
|
|
|
|
|
|
|
// Resizing
|
|
// -----------------------------------------------------------------------------------
|
|
|
|
|
|
t.getSuggestedViewHeight = function() {
|
|
if (suggestedViewHeight === undefined) {
|
|
calcSize();
|
|
}
|
|
return suggestedViewHeight;
|
|
};
|
|
|
|
|
|
t.isHeightAuto = function() {
|
|
return options.contentHeight === 'auto' || options.height === 'auto';
|
|
};
|
|
|
|
|
|
function updateSize(shouldRecalc) {
|
|
if (elementVisible()) {
|
|
|
|
if (shouldRecalc) {
|
|
_calcSize();
|
|
}
|
|
|
|
ignoreWindowResize++;
|
|
currentView.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto()
|
|
ignoreWindowResize--;
|
|
|
|
return true; // signal success
|
|
}
|
|
}
|
|
|
|
|
|
function calcSize() {
|
|
if (elementVisible()) {
|
|
_calcSize();
|
|
}
|
|
}
|
|
|
|
|
|
function _calcSize() { // assumes elementVisible
|
|
if (typeof options.contentHeight === 'number') { // exists and not 'auto'
|
|
suggestedViewHeight = options.contentHeight;
|
|
}
|
|
else if (typeof options.height === 'number') { // exists and not 'auto'
|
|
suggestedViewHeight = options.height - (headerElement ? headerElement.outerHeight(true) : 0);
|
|
}
|
|
else {
|
|
suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5));
|
|
}
|
|
}
|
|
|
|
|
|
function windowResize(ev) {
|
|
if (
|
|
!ignoreWindowResize &&
|
|
ev.target === window && // so we don't process jqui "resize" events that have bubbled up
|
|
currentView.start // view has already been rendered
|
|
) {
|
|
if (updateSize(true)) {
|
|
currentView.trigger('windowResize', _element);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* 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
|
|
destroyEvents(); // so that events are cleared before user starts waiting for AJAX
|
|
fetchAndRenderEvents();
|
|
}
|
|
|
|
|
|
function renderEvents() { // destroys old events if previously rendered
|
|
if (elementVisible()) {
|
|
freezeContentHeight();
|
|
currentView.destroyEvents(); // no performance cost if never rendered
|
|
currentView.renderEvents(events);
|
|
unfreezeContentHeight();
|
|
}
|
|
}
|
|
|
|
|
|
function destroyEvents() {
|
|
freezeContentHeight();
|
|
currentView.destroyEvents();
|
|
unfreezeContentHeight();
|
|
}
|
|
|
|
|
|
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() {
|
|
renderEvents();
|
|
}
|
|
|
|
|
|
|
|
/* 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) {
|
|
|
|
start = t.moment(start);
|
|
if (end) {
|
|
end = t.moment(end);
|
|
}
|
|
else if (start.hasTime()) {
|
|
end = start.clone().add(t.defaultTimedEventDuration);
|
|
}
|
|
else {
|
|
end = start.clone().add(t.defaultAllDayEventDuration);
|
|
}
|
|
|
|
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(-1, 'years');
|
|
renderView();
|
|
}
|
|
|
|
|
|
function nextYear() {
|
|
date.add(1, 'years');
|
|
renderView();
|
|
}
|
|
|
|
|
|
function today() {
|
|
date = t.getNow();
|
|
renderView();
|
|
}
|
|
|
|
|
|
function gotoDate(dateInput) {
|
|
date = t.moment(dateInput);
|
|
renderView();
|
|
}
|
|
|
|
|
|
function incrementDate(delta) {
|
|
date.add(moment.duration(delta));
|
|
renderView();
|
|
}
|
|
|
|
|
|
// Forces navigation to a view for the given date.
|
|
// `viewName` can be a specific view name or a generic one like "week" or "day".
|
|
function zoomTo(newDate, viewName) {
|
|
var viewStr;
|
|
var match;
|
|
|
|
if (!viewName || fcViews[viewName] === undefined) { // a general view name, or "auto"
|
|
viewName = viewName || 'day';
|
|
viewStr = header.getViewsWithButtons().join(' '); // space-separated string of all the views in the header
|
|
|
|
// try to match a general view name, like "week", against a specific one, like "agendaWeek"
|
|
match = viewStr.match(new RegExp('\\w+' + capitaliseFirstLetter(viewName)));
|
|
|
|
// fall back to the day view being used in the header
|
|
if (!match) {
|
|
match = viewStr.match(/\w+Day/);
|
|
}
|
|
|
|
viewName = match ? match[0] : 'agendaDay'; // fall back to agendaDay
|
|
}
|
|
|
|
date = newDate;
|
|
changeView(viewName);
|
|
}
|
|
|
|
|
|
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(true); // true = allow recalculation of height
|
|
}
|
|
}
|
|
|
|
|
|
function trigger(name, thisObj) {
|
|
if (options[name]) {
|
|
return options[name].apply(
|
|
thisObj || _element,
|
|
Array.prototype.slice.call(arguments, 2)
|
|
);
|
|
}
|
|
}
|
|
|
|
}
|