mirror of
https://github.com/wassname/fullcalendar.git
synced 2026-07-02 17:00:06 +08:00
770 lines
17 KiB
JavaScript
770 lines
17 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 = 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
|
|
|
|
// 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);
|
|
ResourceManager.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;
|
|
}
|
|
}
|
|
|
|
|
|
}
|