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.localeData(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); 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 = $("
") .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]( $("") .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; } } }