UNPKG

simple-coder

Version:

tool to generate code and framework

2,066 lines (1,607 loc) 185 kB
/*! * 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>&nbsp;</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