Path: blob/master/web-gui/buildyourownbotnet/assets/js/fullcalendar-2/fullcalendar.js
1293 views
/*!1* FullCalendar v2.2.32* Docs & License: http://arshaw.com/fullcalendar/3* (c) 2013 Adam Shaw4*/56(function(factory) {7if (typeof define === 'function' && define.amd) {8define([ 'jquery', 'moment' ], factory);9}10else {11factory(jQuery, moment);12}13})(function($, moment) {1415;;1617var defaults = {1819lang: 'en',2021defaultTimedEventDuration: '02:00:00',22defaultAllDayEventDuration: { days: 1 },23forceEventDuration: false,24nextDayThreshold: '09:00:00', // 9am2526// display27defaultView: 'month',28aspectRatio: 1.35,29header: {30left: 'title',31center: '',32right: 'today prev,next'33},34weekends: true,35weekNumbers: false,3637weekNumberTitle: 'W',38weekNumberCalculation: 'local',3940//editable: false,4142// event ajax43lazyFetching: true,44startParam: 'start',45endParam: 'end',46timezoneParam: 'timezone',4748timezone: false,4950//allDayDefault: undefined,5152// time formats53titleFormat: {54month: 'MMMM YYYY', // like "September 1986". each language will override this55week: 'll', // like "Sep 4 1986"56day: 'LL' // like "September 4 1986"57},58columnFormat: {59month: 'ddd', // like "Sat"60week: generateWeekColumnFormat,61day: 'dddd' // like "Saturday"62},63timeFormat: { // for event elements64'default': generateShortTimeFormat65},6667displayEventEnd: {68month: false,69basicWeek: false,70'default': true71},7273// locale74isRTL: false,75defaultButtonText: {76prev: "prev",77next: "next",78prevYear: "prev year",79nextYear: "next year",80today: 'today',81month: 'month',82week: 'week',83day: 'day'84},8586buttonIcons: {87prev: 'left-single-arrow',88next: 'right-single-arrow',89prevYear: 'left-double-arrow',90nextYear: 'right-double-arrow'91},9293// jquery-ui theming94theme: false,95themeButtonIcons: {96prev: 'circle-triangle-w',97next: 'circle-triangle-e',98prevYear: 'seek-prev',99nextYear: 'seek-next'100},101102dragOpacity: .75,103dragRevertDuration: 500,104dragScroll: true,105106//selectable: false,107unselectAuto: true,108109dropAccept: '*',110111eventLimit: false,112eventLimitText: 'more',113eventLimitClick: 'popover',114dayPopoverFormat: 'LL',115116handleWindowResize: true,117windowResizeDelay: 200 // milliseconds before a rerender happens118119};120121122function generateShortTimeFormat(options, langData) {123return langData.longDateFormat('LT')124.replace(':mm', '(:mm)')125.replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs126.replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand127}128129130function generateWeekColumnFormat(options, langData) {131var format = langData.longDateFormat('L'); // for the format like "MM/DD/YYYY"132format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, ''); // strip the year off the edge, as well as other misc non-whitespace chars133if (options.isRTL) {134format += ' ddd'; // for RTL, add day-of-week to end135}136else {137format = 'ddd ' + format; // for LTR, add day-of-week to beginning138}139return format;140}141142143var langOptionHash = {144en: {145columnFormat: {146week: 'ddd M/D' // override for english. different from the generated default, which is MM/DD147},148dayPopoverFormat: 'dddd, MMMM D'149}150};151152153// right-to-left defaults154var rtlDefaults = {155header: {156left: 'next,prev today',157center: '',158right: 'title'159},160buttonIcons: {161prev: 'right-single-arrow',162next: 'left-single-arrow',163prevYear: 'right-double-arrow',164nextYear: 'left-double-arrow'165},166themeButtonIcons: {167prev: 'circle-triangle-e',168next: 'circle-triangle-w',169nextYear: 'seek-prev',170prevYear: 'seek-next'171}172};173174;;175176var fc = $.fullCalendar = { version: "2.2.3" };177var fcViews = fc.views = {};178179180$.fn.fullCalendar = function(options) {181var args = Array.prototype.slice.call(arguments, 1); // for a possible method call182var res = this; // what this function will return (this jQuery object by default)183184this.each(function(i, _element) { // loop each DOM element involved185var element = $(_element);186var calendar = element.data('fullCalendar'); // get the existing calendar object (if any)187var singleRes; // the returned value of this single method call188189// a method call190if (typeof options === 'string') {191if (calendar && $.isFunction(calendar[options])) {192singleRes = calendar[options].apply(calendar, args);193if (!i) {194res = singleRes; // record the first method call result195}196if (options === 'destroy') { // for the destroy method, must remove Calendar object data197element.removeData('fullCalendar');198}199}200}201// a new calendar initialization202else if (!calendar) { // don't initialize twice203calendar = new Calendar(element, options);204element.data('fullCalendar', calendar);205calendar.render();206}207});208209return res;210};211212213// function for adding/overriding defaults214function setDefaults(d) {215mergeOptions(defaults, d);216}217218219// Recursively combines option hash-objects.220// Better than `$.extend(true, ...)` because arrays are not traversed/copied.221//222// called like:223// mergeOptions(target, obj1, obj2, ...)224//225function mergeOptions(target) {226227function mergeIntoTarget(name, value) {228if ($.isPlainObject(value) && $.isPlainObject(target[name]) && !isForcedAtomicOption(name)) {229// merge into a new object to avoid destruction230target[name] = mergeOptions({}, target[name], value); // combine. `value` object takes precedence231}232else if (value !== undefined) { // only use values that are set and not undefined233target[name] = value;234}235}236237for (var i=1; i<arguments.length; i++) {238$.each(arguments[i], mergeIntoTarget);239}240241return target;242}243244245// overcome sucky view-option-hash and option-merging behavior messing with options it shouldn't246function isForcedAtomicOption(name) {247// Any option that ends in "Time" or "Duration" is probably a Duration,248// and these will commonly be specified as plain objects, which we don't want to mess up.249return /(Time|Duration)$/.test(name);250}251// FIX: find a different solution for view-option-hashes and have a whitelist252// for options that can be recursively merged.253254;;255256//var langOptionHash = {}; // initialized in defaults.js257fc.langs = langOptionHash; // expose258259260// Initialize jQuery UI Datepicker translations while using some of the translations261// for our own purposes. Will set this as the default language for datepicker.262// Called from a translation file.263fc.datepickerLang = function(langCode, datepickerLangCode, options) {264var langOptions = langOptionHash[langCode];265266// initialize FullCalendar's lang hash for this language267if (!langOptions) {268langOptions = langOptionHash[langCode] = {};269}270271// merge certain Datepicker options into FullCalendar's options272mergeOptions(langOptions, {273isRTL: options.isRTL,274weekNumberTitle: options.weekHeader,275titleFormat: {276month: options.showMonthAfterYear ?277'YYYY[' + options.yearSuffix + '] MMMM' :278'MMMM YYYY[' + options.yearSuffix + ']'279},280defaultButtonText: {281// the translations sometimes wrongly contain HTML entities282prev: stripHtmlEntities(options.prevText),283next: stripHtmlEntities(options.nextText),284today: stripHtmlEntities(options.currentText)285}286});287288// is jQuery UI Datepicker is on the page?289if ($.datepicker) {290291// Register the language data.292// FullCalendar and MomentJS use language codes like "pt-br" but Datepicker293// does it like "pt-BR" or if it doesn't have the language, maybe just "pt".294// Make an alias so the language can be referenced either way.295$.datepicker.regional[datepickerLangCode] =296$.datepicker.regional[langCode] = // alias297options;298299// Alias 'en' to the default language data. Do this every time.300$.datepicker.regional.en = $.datepicker.regional[''];301302// Set as Datepicker's global defaults.303$.datepicker.setDefaults(options);304}305};306307308// Sets FullCalendar-specific translations. Also sets the language as the global default.309// Called from a translation file.310fc.lang = function(langCode, options) {311var langOptions;312313if (options) {314langOptions = langOptionHash[langCode];315316// initialize the hash for this language317if (!langOptions) {318langOptions = langOptionHash[langCode] = {};319}320321mergeOptions(langOptions, options || {});322}323324// set it as the default language for FullCalendar325defaults.lang = langCode;326};327;;328329330function Calendar(element, instanceOptions) {331var t = this;332333334335// Build options object336// -----------------------------------------------------------------------------------337// Precedence (lowest to highest): defaults, rtlDefaults, langOptions, instanceOptions338339instanceOptions = instanceOptions || {};340341var options = mergeOptions({}, defaults, instanceOptions);342var langOptions;343344// determine language options345if (options.lang in langOptionHash) {346langOptions = langOptionHash[options.lang];347}348else {349langOptions = langOptionHash[defaults.lang];350}351352if (langOptions) { // if language options exist, rebuild...353options = mergeOptions({}, defaults, langOptions, instanceOptions);354}355356if (options.isRTL) { // is isRTL, rebuild...357options = mergeOptions({}, defaults, rtlDefaults, langOptions || {}, instanceOptions);358}359360361362// Exports363// -----------------------------------------------------------------------------------364365t.options = options;366t.render = render;367t.destroy = destroy;368t.refetchEvents = refetchEvents;369t.reportEvents = reportEvents;370t.reportEventChange = reportEventChange;371t.rerenderEvents = renderEvents; // `renderEvents` serves as a rerender. an API method372t.changeView = changeView;373t.select = select;374t.unselect = unselect;375t.prev = prev;376t.next = next;377t.prevYear = prevYear;378t.nextYear = nextYear;379t.today = today;380t.gotoDate = gotoDate;381t.incrementDate = incrementDate;382t.zoomTo = zoomTo;383t.getDate = getDate;384t.getCalendar = getCalendar;385t.getView = getView;386t.option = option;387t.trigger = trigger;388389390391// Language-data Internals392// -----------------------------------------------------------------------------------393// Apply overrides to the current language's data394395396// Returns moment's internal locale data. If doesn't exist, returns English.397// Works with moment-pre-2.8398function getLocaleData(langCode) {399var f = moment.localeData || moment.langData;400return f.call(moment, langCode) ||401f.call(moment, 'en'); // the newer localData could return null, so fall back to en402}403404405var localeData = createObject(getLocaleData(options.lang)); // make a cheap copy406407if (options.monthNames) {408localeData._months = options.monthNames;409}410if (options.monthNamesShort) {411localeData._monthsShort = options.monthNamesShort;412}413if (options.dayNames) {414localeData._weekdays = options.dayNames;415}416if (options.dayNamesShort) {417localeData._weekdaysShort = options.dayNamesShort;418}419if (options.firstDay != null) {420var _week = createObject(localeData._week); // _week: { dow: # }421_week.dow = options.firstDay;422localeData._week = _week;423}424425426427// Calendar-specific Date Utilities428// -----------------------------------------------------------------------------------429430431t.defaultAllDayEventDuration = moment.duration(options.defaultAllDayEventDuration);432t.defaultTimedEventDuration = moment.duration(options.defaultTimedEventDuration);433434435// Builds a moment using the settings of the current calendar: timezone and language.436// Accepts anything the vanilla moment() constructor accepts.437t.moment = function() {438var mom;439440if (options.timezone === 'local') {441mom = fc.moment.apply(null, arguments);442443// Force the moment to be local, because fc.moment doesn't guarantee it.444if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone445mom.local();446}447}448else if (options.timezone === 'UTC') {449mom = fc.moment.utc.apply(null, arguments); // process as UTC450}451else {452mom = fc.moment.parseZone.apply(null, arguments); // let the input decide the zone453}454455if ('_locale' in mom) { // moment 2.8 and above456mom._locale = localeData;457}458else { // pre-moment-2.8459mom._lang = localeData;460}461462return mom;463};464465466// Returns a boolean about whether or not the calendar knows how to calculate467// the timezone offset of arbitrary dates in the current timezone.468t.getIsAmbigTimezone = function() {469return options.timezone !== 'local' && options.timezone !== 'UTC';470};471472473// Returns a copy of the given date in the current timezone of it is ambiguously zoned.474// This will also give the date an unambiguous time.475t.rezoneDate = function(date) {476return t.moment(date.toArray());477};478479480// Returns a moment for the current date, as defined by the client's computer,481// or overridden by the `now` option.482t.getNow = function() {483var now = options.now;484if (typeof now === 'function') {485now = now();486}487return t.moment(now);488};489490491// Calculates the week number for a moment according to the calendar's492// `weekNumberCalculation` setting.493t.calculateWeekNumber = function(mom) {494var calc = options.weekNumberCalculation;495496if (typeof calc === 'function') {497return calc(mom);498}499else if (calc === 'local') {500return mom.week();501}502else if (calc.toUpperCase() === 'ISO') {503return mom.isoWeek();504}505};506507508// Get an event's normalized end date. If not present, calculate it from the defaults.509t.getEventEnd = function(event) {510if (event.end) {511return event.end.clone();512}513else {514return t.getDefaultEventEnd(event.allDay, event.start);515}516};517518519// Given an event's allDay status and start date, return swhat its fallback end date should be.520t.getDefaultEventEnd = function(allDay, start) { // TODO: rename to computeDefaultEventEnd521var end = start.clone();522523if (allDay) {524end.stripTime().add(t.defaultAllDayEventDuration);525}526else {527end.add(t.defaultTimedEventDuration);528}529530if (t.getIsAmbigTimezone()) {531end.stripZone(); // we don't know what the tzo should be532}533534return end;535};536537538539// Date-formatting Utilities540// -----------------------------------------------------------------------------------541542543// Like the vanilla formatRange, but with calendar-specific settings applied.544t.formatRange = function(m1, m2, formatStr) {545546// a function that returns a formatStr // TODO: in future, precompute this547if (typeof formatStr === 'function') {548formatStr = formatStr.call(t, options, localeData);549}550551return formatRange(m1, m2, formatStr, null, options.isRTL);552};553554555// Like the vanilla formatDate, but with calendar-specific settings applied.556t.formatDate = function(mom, formatStr) {557558// a function that returns a formatStr // TODO: in future, precompute this559if (typeof formatStr === 'function') {560formatStr = formatStr.call(t, options, localeData);561}562563return formatDate(mom, formatStr);564};565566567568// Imports569// -----------------------------------------------------------------------------------570571572EventManager.call(t, options);573var isFetchNeeded = t.isFetchNeeded;574var fetchEvents = t.fetchEvents;575576577578// Locals579// -----------------------------------------------------------------------------------580581582var _element = element[0];583var header;584var headerElement;585var content;586var tm; // for making theme classes587var currentView;588var suggestedViewHeight;589var windowResizeProxy; // wraps the windowResize function590var ignoreWindowResize = 0;591var date;592var events = [];593594595596// Main Rendering597// -----------------------------------------------------------------------------------598599600if (options.defaultDate != null) {601date = t.moment(options.defaultDate);602}603else {604date = t.getNow();605}606607608function render(inc) {609if (!content) {610initialRender();611}612else if (elementVisible()) {613// mainly for the public API614calcSize();615renderView(inc);616}617}618619620function initialRender() {621tm = options.theme ? 'ui' : 'fc';622element.addClass('fc');623624if (options.isRTL) {625element.addClass('fc-rtl');626}627else {628element.addClass('fc-ltr');629}630631if (options.theme) {632element.addClass('ui-widget');633}634else {635element.addClass('fc-unthemed');636}637638content = $("<div class='fc-view-container'/>").prependTo(element);639640header = new Header(t, options);641headerElement = header.render();642if (headerElement) {643element.prepend(headerElement);644}645646changeView(options.defaultView);647648if (options.handleWindowResize) {649windowResizeProxy = debounce(windowResize, options.windowResizeDelay); // prevents rapid calls650$(window).resize(windowResizeProxy);651}652}653654655function destroy() {656657if (currentView) {658currentView.destroy();659}660661header.destroy();662content.remove();663element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');664665$(window).unbind('resize', windowResizeProxy);666}667668669function elementVisible() {670return element.is(':visible');671}672673674675// View Rendering676// -----------------------------------------------------------------------------------677678679function changeView(viewName) {680renderView(0, viewName);681}682683684// Renders a view because of a date change, view-type change, or for the first time685function renderView(delta, viewName) {686ignoreWindowResize++;687688// if viewName is changing, destroy the old view689if (currentView && viewName && currentView.name !== viewName) {690header.deactivateButton(currentView.name);691freezeContentHeight(); // prevent a scroll jump when view element is removed692if (currentView.start) { // rendered before?693currentView.destroy();694}695currentView.el.remove();696currentView = null;697}698699// if viewName changed, or the view was never created, create a fresh view700if (!currentView && viewName) {701currentView = new fcViews[viewName](t);702currentView.el = $("<div class='fc-view fc-" + viewName + "-view' />").appendTo(content);703header.activateButton(viewName);704}705706if (currentView) {707708// let the view determine what the delta means709if (delta) {710date = currentView.incrementDate(date, delta);711}712713// render or rerender the view714if (715!currentView.start || // never rendered before716delta || // explicit date window change717!date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change718) {719if (elementVisible()) {720721freezeContentHeight();722if (currentView.start) { // rendered before?723currentView.destroy();724}725currentView.render(date);726unfreezeContentHeight();727728// need to do this after View::render, so dates are calculated729updateTitle();730updateTodayButton();731732getAndRenderEvents();733}734}735}736737unfreezeContentHeight(); // undo any lone freezeContentHeight calls738ignoreWindowResize--;739}740741742743// Resizing744// -----------------------------------------------------------------------------------745746747t.getSuggestedViewHeight = function() {748if (suggestedViewHeight === undefined) {749calcSize();750}751return suggestedViewHeight;752};753754755t.isHeightAuto = function() {756return options.contentHeight === 'auto' || options.height === 'auto';757};758759760function updateSize(shouldRecalc) {761if (elementVisible()) {762763if (shouldRecalc) {764_calcSize();765}766767ignoreWindowResize++;768currentView.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto()769ignoreWindowResize--;770771return true; // signal success772}773}774775776function calcSize() {777if (elementVisible()) {778_calcSize();779}780}781782783function _calcSize() { // assumes elementVisible784if (typeof options.contentHeight === 'number') { // exists and not 'auto'785suggestedViewHeight = options.contentHeight;786}787else if (typeof options.height === 'number') { // exists and not 'auto'788suggestedViewHeight = options.height - (headerElement ? headerElement.outerHeight(true) : 0);789}790else {791suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5));792}793}794795796function windowResize(ev) {797if (798!ignoreWindowResize &&799ev.target === window && // so we don't process jqui "resize" events that have bubbled up800currentView.start // view has already been rendered801) {802if (updateSize(true)) {803currentView.trigger('windowResize', _element);804}805}806}807808809810/* Event Fetching/Rendering811-----------------------------------------------------------------------------*/812// TODO: going forward, most of this stuff should be directly handled by the view813814815function refetchEvents() { // can be called as an API method816destroyEvents(); // so that events are cleared before user starts waiting for AJAX817fetchAndRenderEvents();818}819820821function renderEvents() { // destroys old events if previously rendered822if (elementVisible()) {823freezeContentHeight();824currentView.destroyEvents(); // no performance cost if never rendered825currentView.renderEvents(events);826unfreezeContentHeight();827}828}829830831function destroyEvents() {832freezeContentHeight();833currentView.destroyEvents();834unfreezeContentHeight();835}836837838function getAndRenderEvents() {839if (!options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) {840fetchAndRenderEvents();841}842else {843renderEvents();844}845}846847848function fetchAndRenderEvents() {849fetchEvents(currentView.start, currentView.end);850// ... will call reportEvents851// ... which will call renderEvents852}853854855// called when event data arrives856function reportEvents(_events) {857events = _events;858renderEvents();859}860861862// called when a single event's data has been changed863function reportEventChange() {864renderEvents();865}866867868869/* Header Updating870-----------------------------------------------------------------------------*/871872873function updateTitle() {874header.updateTitle(currentView.title);875}876877878function updateTodayButton() {879var now = t.getNow();880if (now.isWithin(currentView.intervalStart, currentView.intervalEnd)) {881header.disableButton('today');882}883else {884header.enableButton('today');885}886}887888889890/* Selection891-----------------------------------------------------------------------------*/892893894function select(start, end) {895896start = t.moment(start);897if (end) {898end = t.moment(end);899}900else if (start.hasTime()) {901end = start.clone().add(t.defaultTimedEventDuration);902}903else {904end = start.clone().add(t.defaultAllDayEventDuration);905}906907currentView.select(start, end);908}909910911function unselect() { // safe to be called before renderView912if (currentView) {913currentView.unselect();914}915}916917918919/* Date920-----------------------------------------------------------------------------*/921922923function prev() {924renderView(-1);925}926927928function next() {929renderView(1);930}931932933function prevYear() {934date.add(-1, 'years');935renderView();936}937938939function nextYear() {940date.add(1, 'years');941renderView();942}943944945function today() {946date = t.getNow();947renderView();948}949950951function gotoDate(dateInput) {952date = t.moment(dateInput);953renderView();954}955956957function incrementDate(delta) {958date.add(moment.duration(delta));959renderView();960}961962963// Forces navigation to a view for the given date.964// `viewName` can be a specific view name or a generic one like "week" or "day".965function zoomTo(newDate, viewName) {966var viewStr;967var match;968969if (!viewName || fcViews[viewName] === undefined) { // a general view name, or "auto"970viewName = viewName || 'day';971viewStr = header.getViewsWithButtons().join(' '); // space-separated string of all the views in the header972973// try to match a general view name, like "week", against a specific one, like "agendaWeek"974match = viewStr.match(new RegExp('\\w+' + capitaliseFirstLetter(viewName)));975976// fall back to the day view being used in the header977if (!match) {978match = viewStr.match(/\w+Day/);979}980981viewName = match ? match[0] : 'agendaDay'; // fall back to agendaDay982}983984date = newDate;985changeView(viewName);986}987988989function getDate() {990return date.clone();991}992993994995/* Height "Freezing"996-----------------------------------------------------------------------------*/997998999function freezeContentHeight() {1000content.css({1001width: '100%',1002height: content.height(),1003overflow: 'hidden'1004});1005}100610071008function unfreezeContentHeight() {1009content.css({1010width: '',1011height: '',1012overflow: ''1013});1014}1015101610171018/* Misc1019-----------------------------------------------------------------------------*/102010211022function getCalendar() {1023return t;1024}102510261027function getView() {1028return currentView;1029}103010311032function option(name, value) {1033if (value === undefined) {1034return options[name];1035}1036if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') {1037options[name] = value;1038updateSize(true); // true = allow recalculation of height1039}1040}104110421043function trigger(name, thisObj) {1044if (options[name]) {1045return options[name].apply(1046thisObj || _element,1047Array.prototype.slice.call(arguments, 2)1048);1049}1050}10511052}10531054;;10551056/* Top toolbar area with buttons and title1057----------------------------------------------------------------------------------------------------------------------*/1058// TODO: rename all header-related things to "toolbar"10591060function Header(calendar, options) {1061var t = this;10621063// exports1064t.render = render;1065t.destroy = destroy;1066t.updateTitle = updateTitle;1067t.activateButton = activateButton;1068t.deactivateButton = deactivateButton;1069t.disableButton = disableButton;1070t.enableButton = enableButton;1071t.getViewsWithButtons = getViewsWithButtons;10721073// locals1074var el = $();1075var viewsWithButtons = [];1076var tm;107710781079function render() {1080var sections = options.header;10811082tm = options.theme ? 'ui' : 'fc';10831084if (sections) {1085el = $("<div class='fc-toolbar'/>")1086.append(renderSection('left'))1087.append(renderSection('right'))1088.append(renderSection('center'))1089.append('<div class="fc-clear"/>');10901091return el;1092}1093}109410951096function destroy() {1097el.remove();1098}109911001101function renderSection(position) {1102var sectionEl = $('<div class="fc-' + position + '"/>');1103var buttonStr = options.header[position];11041105if (buttonStr) {1106$.each(buttonStr.split(' '), function(i) {1107var groupChildren = $();1108var isOnlyButtons = true;1109var groupEl;11101111$.each(this.split(','), function(j, buttonName) {1112var buttonClick;1113var themeIcon;1114var normalIcon;1115var defaultText;1116var customText;1117var innerHtml;1118var classes;1119var button;11201121if (buttonName == 'title') {1122groupChildren = groupChildren.add($('<h2> </h2>')); // we always want it to take up height1123isOnlyButtons = false;1124}1125else {1126if (calendar[buttonName]) { // a calendar method1127buttonClick = function() {1128calendar[buttonName]();1129};1130}1131else if (fcViews[buttonName]) { // a view name1132buttonClick = function() {1133calendar.changeView(buttonName);1134};1135viewsWithButtons.push(buttonName);1136}1137if (buttonClick) {11381139// smartProperty allows different text per view button (ex: "Agenda Week" vs "Basic Week")1140themeIcon = smartProperty(options.themeButtonIcons, buttonName);1141normalIcon = smartProperty(options.buttonIcons, buttonName);1142defaultText = smartProperty(options.defaultButtonText, buttonName);1143customText = smartProperty(options.buttonText, buttonName);11441145if (customText) {1146innerHtml = htmlEscape(customText);1147}1148else if (themeIcon && options.theme) {1149innerHtml = "<span class='ui-icon ui-icon-" + themeIcon + "'></span>";1150}1151else if (normalIcon && !options.theme) {1152innerHtml = "<span class='fc-icon fc-icon-" + normalIcon + "'></span>";1153}1154else {1155innerHtml = htmlEscape(defaultText || buttonName);1156}11571158classes = [1159'fc-' + buttonName + '-button',1160tm + '-button',1161tm + '-state-default'1162];11631164button = $( // type="button" so that it doesn't submit a form1165'<button type="button" class="' + classes.join(' ') + '">' +1166innerHtml +1167'</button>'1168)1169.click(function() {1170// don't process clicks for disabled buttons1171if (!button.hasClass(tm + '-state-disabled')) {11721173buttonClick();11741175// after the click action, if the button becomes the "active" tab, or disabled,1176// it should never have a hover class, so remove it now.1177if (1178button.hasClass(tm + '-state-active') ||1179button.hasClass(tm + '-state-disabled')1180) {1181button.removeClass(tm + '-state-hover');1182}1183}1184})1185.mousedown(function() {1186// the *down* effect (mouse pressed in).1187// only on buttons that are not the "active" tab, or disabled1188button1189.not('.' + tm + '-state-active')1190.not('.' + tm + '-state-disabled')1191.addClass(tm + '-state-down');1192})1193.mouseup(function() {1194// undo the *down* effect1195button.removeClass(tm + '-state-down');1196})1197.hover(1198function() {1199// the *hover* effect.1200// only on buttons that are not the "active" tab, or disabled1201button1202.not('.' + tm + '-state-active')1203.not('.' + tm + '-state-disabled')1204.addClass(tm + '-state-hover');1205},1206function() {1207// undo the *hover* effect1208button1209.removeClass(tm + '-state-hover')1210.removeClass(tm + '-state-down'); // if mouseleave happens before mouseup1211}1212);12131214groupChildren = groupChildren.add(button);1215}1216}1217});12181219if (isOnlyButtons) {1220groupChildren1221.first().addClass(tm + '-corner-left').end()1222.last().addClass(tm + '-corner-right').end();1223}12241225if (groupChildren.length > 1) {1226groupEl = $('<div/>');1227if (isOnlyButtons) {1228groupEl.addClass('fc-button-group');1229}1230groupEl.append(groupChildren);1231sectionEl.append(groupEl);1232}1233else {1234sectionEl.append(groupChildren); // 1 or 0 children1235}1236});1237}12381239return sectionEl;1240}124112421243function updateTitle(text) {1244el.find('h2').text(text);1245}124612471248function activateButton(buttonName) {1249el.find('.fc-' + buttonName + '-button')1250.addClass(tm + '-state-active');1251}125212531254function deactivateButton(buttonName) {1255el.find('.fc-' + buttonName + '-button')1256.removeClass(tm + '-state-active');1257}125812591260function disableButton(buttonName) {1261el.find('.fc-' + buttonName + '-button')1262.attr('disabled', 'disabled')1263.addClass(tm + '-state-disabled');1264}126512661267function enableButton(buttonName) {1268el.find('.fc-' + buttonName + '-button')1269.removeAttr('disabled')1270.removeClass(tm + '-state-disabled');1271}127212731274function getViewsWithButtons() {1275return viewsWithButtons;1276}12771278}12791280;;12811282fc.sourceNormalizers = [];1283fc.sourceFetchers = [];12841285var ajaxDefaults = {1286dataType: 'json',1287cache: false1288};12891290var eventGUID = 1;129112921293function EventManager(options) { // assumed to be a calendar1294var t = this;129512961297// exports1298t.isFetchNeeded = isFetchNeeded;1299t.fetchEvents = fetchEvents;1300t.addEventSource = addEventSource;1301t.removeEventSource = removeEventSource;1302t.updateEvent = updateEvent;1303t.renderEvent = renderEvent;1304t.removeEvents = removeEvents;1305t.clientEvents = clientEvents;1306t.mutateEvent = mutateEvent;130713081309// imports1310var trigger = t.trigger;1311var getView = t.getView;1312var reportEvents = t.reportEvents;1313var getEventEnd = t.getEventEnd;131413151316// locals1317var stickySource = { events: [] };1318var sources = [ stickySource ];1319var rangeStart, rangeEnd;1320var currentFetchID = 0;1321var pendingSourceCnt = 0;1322var loadingLevel = 0;1323var cache = []; // holds events that have already been expanded132413251326$.each(1327(options.events ? [ options.events ] : []).concat(options.eventSources || []),1328function(i, sourceInput) {1329var source = buildEventSource(sourceInput);1330if (source) {1331sources.push(source);1332}1333}1334);1335133613371338/* Fetching1339-----------------------------------------------------------------------------*/134013411342function isFetchNeeded(start, end) {1343return !rangeStart || // nothing has been fetched yet?1344// or, a part of the new range is outside of the old range? (after normalizing)1345start.clone().stripZone() < rangeStart.clone().stripZone() ||1346end.clone().stripZone() > rangeEnd.clone().stripZone();1347}134813491350function fetchEvents(start, end) {1351rangeStart = start;1352rangeEnd = end;1353cache = [];1354var fetchID = ++currentFetchID;1355var len = sources.length;1356pendingSourceCnt = len;1357for (var i=0; i<len; i++) {1358fetchEventSource(sources[i], fetchID);1359}1360}136113621363function fetchEventSource(source, fetchID) {1364_fetchEventSource(source, function(eventInputs) {1365var isArraySource = $.isArray(source.events);1366var i, eventInput;1367var abstractEvent;13681369if (fetchID == currentFetchID) {13701371if (eventInputs) {1372for (i = 0; i < eventInputs.length; i++) {1373eventInput = eventInputs[i];13741375if (isArraySource) { // array sources have already been convert to Event Objects1376abstractEvent = eventInput;1377}1378else {1379abstractEvent = buildEventFromInput(eventInput, source);1380}13811382if (abstractEvent) { // not false (an invalid event)1383cache.push.apply(1384cache,1385expandEvent(abstractEvent) // add individual expanded events to the cache1386);1387}1388}1389}13901391pendingSourceCnt--;1392if (!pendingSourceCnt) {1393reportEvents(cache);1394}1395}1396});1397}139813991400function _fetchEventSource(source, callback) {1401var i;1402var fetchers = fc.sourceFetchers;1403var res;14041405for (i=0; i<fetchers.length; i++) {1406res = fetchers[i].call(1407t, // this, the Calendar object1408source,1409rangeStart.clone(),1410rangeEnd.clone(),1411options.timezone,1412callback1413);14141415if (res === true) {1416// the fetcher is in charge. made its own async request1417return;1418}1419else if (typeof res == 'object') {1420// the fetcher returned a new source. process it1421_fetchEventSource(res, callback);1422return;1423}1424}14251426var events = source.events;1427if (events) {1428if ($.isFunction(events)) {1429pushLoading();1430events.call(1431t, // this, the Calendar object1432rangeStart.clone(),1433rangeEnd.clone(),1434options.timezone,1435function(events) {1436callback(events);1437popLoading();1438}1439);1440}1441else if ($.isArray(events)) {1442callback(events);1443}1444else {1445callback();1446}1447}else{1448var url = source.url;1449if (url) {1450var success = source.success;1451var error = source.error;1452var complete = source.complete;14531454// retrieve any outbound GET/POST $.ajax data from the options1455var customData;1456if ($.isFunction(source.data)) {1457// supplied as a function that returns a key/value object1458customData = source.data();1459}1460else {1461// supplied as a straight key/value object1462customData = source.data;1463}14641465// use a copy of the custom data so we can modify the parameters1466// and not affect the passed-in object.1467var data = $.extend({}, customData || {});14681469var startParam = firstDefined(source.startParam, options.startParam);1470var endParam = firstDefined(source.endParam, options.endParam);1471var timezoneParam = firstDefined(source.timezoneParam, options.timezoneParam);14721473if (startParam) {1474data[startParam] = rangeStart.format();1475}1476if (endParam) {1477data[endParam] = rangeEnd.format();1478}1479if (options.timezone && options.timezone != 'local') {1480data[timezoneParam] = options.timezone;1481}14821483pushLoading();1484$.ajax($.extend({}, ajaxDefaults, source, {1485data: data,1486success: function(events) {1487events = events || [];1488var res = applyAll(success, this, arguments);1489if ($.isArray(res)) {1490events = res;1491}1492callback(events);1493},1494error: function() {1495applyAll(error, this, arguments);1496callback();1497},1498complete: function() {1499applyAll(complete, this, arguments);1500popLoading();1501}1502}));1503}else{1504callback();1505}1506}1507}1508150915101511/* Sources1512-----------------------------------------------------------------------------*/151315141515function addEventSource(sourceInput) {1516var source = buildEventSource(sourceInput);1517if (source) {1518sources.push(source);1519pendingSourceCnt++;1520fetchEventSource(source, currentFetchID); // will eventually call reportEvents1521}1522}152315241525function buildEventSource(sourceInput) { // will return undefined if invalid source1526var normalizers = fc.sourceNormalizers;1527var source;1528var i;15291530if ($.isFunction(sourceInput) || $.isArray(sourceInput)) {1531source = { events: sourceInput };1532}1533else if (typeof sourceInput === 'string') {1534source = { url: sourceInput };1535}1536else if (typeof sourceInput === 'object') {1537source = $.extend({}, sourceInput); // shallow copy1538}15391540if (source) {15411542// TODO: repeat code, same code for event classNames1543if (source.className) {1544if (typeof source.className === 'string') {1545source.className = source.className.split(/\s+/);1546}1547// otherwise, assumed to be an array1548}1549else {1550source.className = [];1551}15521553// for array sources, we convert to standard Event Objects up front1554if ($.isArray(source.events)) {1555source.origArray = source.events; // for removeEventSource1556source.events = $.map(source.events, function(eventInput) {1557return buildEventFromInput(eventInput, source);1558});1559}15601561for (i=0; i<normalizers.length; i++) {1562normalizers[i].call(t, source);1563}15641565return source;1566}1567}156815691570function removeEventSource(source) {1571sources = $.grep(sources, function(src) {1572return !isSourcesEqual(src, source);1573});1574// remove all client events from that source1575cache = $.grep(cache, function(e) {1576return !isSourcesEqual(e.source, source);1577});1578reportEvents(cache);1579}158015811582function isSourcesEqual(source1, source2) {1583return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2);1584}158515861587function getSourcePrimitive(source) {1588return (1589(typeof source === 'object') ? // a normalized event source?1590(source.origArray || source.googleCalendarId || source.url || source.events) : // get the primitive1591null1592) ||1593source; // the given argument *is* the primitive1594}1595159615971598/* Manipulation1599-----------------------------------------------------------------------------*/160016011602function updateEvent(event) {16031604event.start = t.moment(event.start);1605if (event.end) {1606event.end = t.moment(event.end);1607}16081609mutateEvent(event);1610propagateMiscProperties(event);1611reportEvents(cache); // reports event modifications (so we can redraw)1612}161316141615var miscCopyableProps = [1616'title',1617'url',1618'allDay',1619'className',1620'editable',1621'color',1622'backgroundColor',1623'borderColor',1624'textColor'1625];16261627function propagateMiscProperties(event) {1628var i;1629var cachedEvent;1630var j;1631var prop;16321633for (i=0; i<cache.length; i++) {1634cachedEvent = cache[i];1635if (cachedEvent._id == event._id && cachedEvent !== event) {1636for (j=0; j<miscCopyableProps.length; j++) {1637prop = miscCopyableProps[j];1638if (event[prop] !== undefined) {1639cachedEvent[prop] = event[prop];1640}1641}1642}1643}1644}164516461647// returns the expanded events that were created1648function renderEvent(eventInput, stick) {1649var abstractEvent = buildEventFromInput(eventInput);1650var events;1651var i, event;16521653if (abstractEvent) { // not false (a valid input)1654events = expandEvent(abstractEvent);16551656for (i = 0; i < events.length; i++) {1657event = events[i];16581659if (!event.source) {1660if (stick) {1661stickySource.events.push(event);1662event.source = stickySource;1663}1664cache.push(event);1665}1666}16671668reportEvents(cache);16691670return events;1671}16721673return [];1674}167516761677function removeEvents(filter) {1678var eventID;1679var i;16801681if (filter == null) { // null or undefined. remove all events1682filter = function() { return true; }; // will always match1683}1684else if (!$.isFunction(filter)) { // an event ID1685eventID = filter + '';1686filter = function(event) {1687return event._id == eventID;1688};1689}16901691// Purge event(s) from our local cache1692cache = $.grep(cache, filter, true); // inverse=true16931694// Remove events from array sources.1695// This works because they have been converted to official Event Objects up front.1696// (and as a result, event._id has been calculated).1697for (i=0; i<sources.length; i++) {1698if ($.isArray(sources[i].events)) {1699sources[i].events = $.grep(sources[i].events, filter, true);1700}1701}17021703reportEvents(cache);1704}170517061707function clientEvents(filter) {1708if ($.isFunction(filter)) {1709return $.grep(cache, filter);1710}1711else if (filter != null) { // not null, not undefined. an event ID1712filter += '';1713return $.grep(cache, function(e) {1714return e._id == filter;1715});1716}1717return cache; // else, return all1718}1719172017211722/* Loading State1723-----------------------------------------------------------------------------*/172417251726function pushLoading() {1727if (!(loadingLevel++)) {1728trigger('loading', null, true, getView());1729}1730}173117321733function popLoading() {1734if (!(--loadingLevel)) {1735trigger('loading', null, false, getView());1736}1737}1738173917401741/* Event Normalization1742-----------------------------------------------------------------------------*/174317441745// Given a raw object with key/value properties, returns an "abstract" Event object.1746// An "abstract" event is an event that, if recurring, will not have been expanded yet.1747// Will return `false` when input is invalid.1748// `source` is optional1749function buildEventFromInput(input, source) {1750var out = {};1751var start, end;1752var allDay;1753var allDayDefault;17541755if (options.eventDataTransform) {1756input = options.eventDataTransform(input);1757}1758if (source && source.eventDataTransform) {1759input = source.eventDataTransform(input);1760}17611762// Copy all properties over to the resulting object.1763// The special-case properties will be copied over afterwards.1764$.extend(out, input);17651766if (source) {1767out.source = source;1768}17691770out._id = input._id || (input.id === undefined ? '_fc' + eventGUID++ : input.id + '');17711772if (input.className) {1773if (typeof input.className == 'string') {1774out.className = input.className.split(/\s+/);1775}1776else { // assumed to be an array1777out.className = input.className;1778}1779}1780else {1781out.className = [];1782}17831784start = input.start || input.date; // "date" is an alias for "start"1785end = input.end;17861787// parse as a time (Duration) if applicable1788if (isTimeString(start)) {1789start = moment.duration(start);1790}1791if (isTimeString(end)) {1792end = moment.duration(end);1793}17941795if (input.dow || moment.isDuration(start) || moment.isDuration(end)) {17961797// the event is "abstract" (recurring) so don't calculate exact start/end dates just yet1798out.start = start ? moment.duration(start) : null; // will be a Duration or null1799out.end = end ? moment.duration(end) : null; // will be a Duration or null1800out._recurring = true; // our internal marker1801}1802else {18031804if (start) {1805start = t.moment(start);1806if (!start.isValid()) {1807return false;1808}1809}18101811if (end) {1812end = t.moment(end);1813if (!end.isValid()) {1814end = null; // let defaults take over1815}1816}18171818allDay = input.allDay;1819if (allDay === undefined) {1820allDayDefault = firstDefined(1821source ? source.allDayDefault : undefined,1822options.allDayDefault1823);1824if (allDayDefault !== undefined) {1825// use the default1826allDay = allDayDefault;1827}1828else {1829// if a single date has a time, the event should not be all-day1830allDay = !start.hasTime() && (!end || !end.hasTime());1831}1832}18331834assignDatesToEvent(start, end, allDay, out);1835}18361837return out;1838}183918401841// Normalizes and assigns the given dates to the given partially-formed event object.1842// Requires an explicit `allDay` boolean parameter.1843// NOTE: mutates the given start/end moments. does not make an internal copy1844function assignDatesToEvent(start, end, allDay, event) {18451846// normalize the date based on allDay1847if (allDay) {1848// neither date should have a time1849if (start.hasTime()) {1850start.stripTime();1851}1852if (end && end.hasTime()) {1853end.stripTime();1854}1855}1856else {1857// force a time/zone up the dates1858if (!start.hasTime()) {1859start = t.rezoneDate(start);1860}1861if (end && !end.hasTime()) {1862end = t.rezoneDate(end);1863}1864}18651866if (end && end <= start) { // end is exclusive. must be after start1867end = null; // let defaults take over1868}18691870event.allDay = allDay;1871event.start = start;1872event.end = end || null; // ensure null if falsy18731874if (options.forceEventDuration && !event.end) {1875event.end = getEventEnd(event);1876}18771878backupEventDates(event);1879}188018811882// If the given event is a recurring event, break it down into an array of individual instances.1883// If not a recurring event, return an array with the single original event.1884// If given a falsy input (probably because of a failed buildEventFromInput call), returns an empty array.1885// HACK: can override the recurring window by providing custom rangeStart/rangeEnd (for businessHours).1886function expandEvent(abstractEvent, _rangeStart, _rangeEnd) {1887var events = [];1888var dowHash;1889var dow;1890var i;1891var date;1892var startTime, endTime;1893var start, end;1894var event;18951896_rangeStart = _rangeStart || rangeStart;1897_rangeEnd = _rangeEnd || rangeEnd;18981899if (abstractEvent) {1900if (abstractEvent._recurring) {19011902// make a boolean hash as to whether the event occurs on each day-of-week1903if ((dow = abstractEvent.dow)) {1904dowHash = {};1905for (i = 0; i < dow.length; i++) {1906dowHash[dow[i]] = true;1907}1908}19091910// iterate through every day in the current range1911date = _rangeStart.clone().stripTime(); // holds the date of the current day1912while (date.isBefore(_rangeEnd)) {19131914if (!dowHash || dowHash[date.day()]) { // if everyday, or this particular day-of-week19151916startTime = abstractEvent.start; // the stored start and end properties are times (Durations)1917endTime = abstractEvent.end; // "1918start = date.clone();1919end = null;19201921if (startTime) {1922start = start.time(startTime);1923}1924if (endTime) {1925end = date.clone().time(endTime);1926}19271928event = $.extend({}, abstractEvent); // make a copy of the original1929assignDatesToEvent(1930start, end,1931!startTime && !endTime, // allDay?1932event1933);1934events.push(event);1935}19361937date.add(1, 'days');1938}1939}1940else {1941events.push(abstractEvent); // return the original event. will be a one-item array1942}1943}19441945return events;1946}1947194819491950/* Event Modification Math1951-----------------------------------------------------------------------------------------*/195219531954// Modify the date(s) of an event and make this change propagate to all other events with1955// the same ID (related repeating events).1956//1957// If `newStart`/`newEnd` are not specified, the "new" dates are assumed to be `event.start` and `event.end`.1958// The "old" dates to be compare against are always `event._start` and `event._end` (set by EventManager).1959//1960// Returns an object with delta information and a function to undo all operations.1961//1962function mutateEvent(event, newStart, newEnd) {1963var oldAllDay = event._allDay;1964var oldStart = event._start;1965var oldEnd = event._end;1966var clearEnd = false;1967var newAllDay;1968var dateDelta;1969var durationDelta;1970var undoFunc;19711972// if no new dates were passed in, compare against the event's existing dates1973if (!newStart && !newEnd) {1974newStart = event.start;1975newEnd = event.end;1976}19771978// NOTE: throughout this function, the initial values of `newStart` and `newEnd` are1979// preserved. These values may be undefined.19801981// detect new allDay1982if (event.allDay != oldAllDay) { // if value has changed, use it1983newAllDay = event.allDay;1984}1985else { // otherwise, see if any of the new dates are allDay1986newAllDay = !(newStart || newEnd).hasTime();1987}19881989// normalize the new dates based on allDay1990if (newAllDay) {1991if (newStart) {1992newStart = newStart.clone().stripTime();1993}1994if (newEnd) {1995newEnd = newEnd.clone().stripTime();1996}1997}19981999// compute dateDelta2000if (newStart) {2001if (newAllDay) {2002dateDelta = dayishDiff(newStart, oldStart.clone().stripTime()); // treat oldStart as allDay2003}2004else {2005dateDelta = dayishDiff(newStart, oldStart);2006}2007}20082009if (newAllDay != oldAllDay) {2010// if allDay has changed, always throw away the end2011clearEnd = true;2012}2013else if (newEnd) {2014durationDelta = dayishDiff(2015// new duration2016newEnd || t.getDefaultEventEnd(newAllDay, newStart || oldStart),2017newStart || oldStart2018).subtract(dayishDiff(2019// subtract old duration2020oldEnd || t.getDefaultEventEnd(oldAllDay, oldStart),2021oldStart2022));2023}20242025undoFunc = mutateEvents(2026clientEvents(event._id), // get events with this ID2027clearEnd,2028newAllDay,2029dateDelta,2030durationDelta2031);20322033return {2034dateDelta: dateDelta,2035durationDelta: durationDelta,2036undo: undoFunc2037};2038}203920402041// Modifies an array of events in the following ways (operations are in order):2042// - clear the event's `end`2043// - convert the event to allDay2044// - add `dateDelta` to the start and end2045// - add `durationDelta` to the event's duration2046//2047// Returns a function that can be called to undo all the operations.2048//2049function mutateEvents(events, clearEnd, forceAllDay, dateDelta, durationDelta) {2050var isAmbigTimezone = t.getIsAmbigTimezone();2051var undoFunctions = [];20522053$.each(events, function(i, event) {2054var oldAllDay = event._allDay;2055var oldStart = event._start;2056var oldEnd = event._end;2057var newAllDay = forceAllDay != null ? forceAllDay : oldAllDay;2058var newStart = oldStart.clone();2059var newEnd = (!clearEnd && oldEnd) ? oldEnd.clone() : null;20602061// NOTE: this function is responsible for transforming `newStart` and `newEnd`,2062// which were initialized to the OLD values first. `newEnd` may be null.20632064// normlize newStart/newEnd to be consistent with newAllDay2065if (newAllDay) {2066newStart.stripTime();2067if (newEnd) {2068newEnd.stripTime();2069}2070}2071else {2072if (!newStart.hasTime()) {2073newStart = t.rezoneDate(newStart);2074}2075if (newEnd && !newEnd.hasTime()) {2076newEnd = t.rezoneDate(newEnd);2077}2078}20792080// ensure we have an end date if necessary2081if (!newEnd && (options.forceEventDuration || +durationDelta)) {2082newEnd = t.getDefaultEventEnd(newAllDay, newStart);2083}20842085// translate the dates2086newStart.add(dateDelta);2087if (newEnd) {2088newEnd.add(dateDelta).add(durationDelta);2089}20902091// if the dates have changed, and we know it is impossible to recompute the2092// timezone offsets, strip the zone.2093if (isAmbigTimezone) {2094if (+dateDelta || +durationDelta) {2095newStart.stripZone();2096if (newEnd) {2097newEnd.stripZone();2098}2099}2100}21012102event.allDay = newAllDay;2103event.start = newStart;2104event.end = newEnd;2105backupEventDates(event);21062107undoFunctions.push(function() {2108event.allDay = oldAllDay;2109event.start = oldStart;2110event.end = oldEnd;2111backupEventDates(event);2112});2113});21142115return function() {2116for (var i=0; i<undoFunctions.length; i++) {2117undoFunctions[i]();2118}2119};2120}212121222123/* Business Hours2124-----------------------------------------------------------------------------------------*/21252126t.getBusinessHoursEvents = getBusinessHoursEvents;212721282129// Returns an array of events as to when the business hours occur in the given view.2130// Abuse of our event system :(2131function getBusinessHoursEvents() {2132var optionVal = options.businessHours;2133var defaultVal = {2134className: 'fc-nonbusiness',2135start: '09:00',2136end: '17:00',2137dow: [ 1, 2, 3, 4, 5 ], // monday - friday2138rendering: 'inverse-background'2139};2140var view = t.getView();2141var eventInput;21422143if (optionVal) {2144if (typeof optionVal === 'object') {2145// option value is an object that can override the default business hours2146eventInput = $.extend({}, defaultVal, optionVal);2147}2148else {2149// option value is `true`. use default business hours2150eventInput = defaultVal;2151}2152}21532154if (eventInput) {2155return expandEvent(2156buildEventFromInput(eventInput),2157view.start,2158view.end2159);2160}21612162return [];2163}216421652166/* Overlapping / Constraining2167-----------------------------------------------------------------------------------------*/21682169t.isEventAllowedInRange = isEventAllowedInRange;2170t.isSelectionAllowedInRange = isSelectionAllowedInRange;2171t.isExternalDragAllowedInRange = isExternalDragAllowedInRange;217221732174function isEventAllowedInRange(event, start, end) {2175var source = event.source || {};2176var constraint = firstDefined(2177event.constraint,2178source.constraint,2179options.eventConstraint2180);2181var overlap = firstDefined(2182event.overlap,2183source.overlap,2184options.eventOverlap2185);21862187return isRangeAllowed(start, end, constraint, overlap, event);2188}218921902191function isSelectionAllowedInRange(start, end) {2192return isRangeAllowed(2193start,2194end,2195options.selectConstraint,2196options.selectOverlap2197);2198}219922002201function isExternalDragAllowedInRange(start, end, eventInput) { // eventInput is optional associated event data2202var event;22032204if (eventInput) {2205event = expandEvent(buildEventFromInput(eventInput))[0];2206if (event) {2207return isEventAllowedInRange(event, start, end);2208}2209}22102211return isSelectionAllowedInRange(start, end); // treat it as a selection2212}221322142215// Returns true if the given range (caused by an event drop/resize or a selection) is allowed to exist2216// according to the constraint/overlap settings.2217// `event` is not required if checking a selection.2218function isRangeAllowed(start, end, constraint, overlap, event) {2219var constraintEvents;2220var anyContainment;2221var i, otherEvent;2222var otherOverlap;22232224// normalize. fyi, we're normalizing in too many places :(2225start = start.clone().stripZone();2226end = end.clone().stripZone();22272228// the range must be fully contained by at least one of produced constraint events2229if (constraint != null) {2230constraintEvents = constraintToEvents(constraint);2231anyContainment = false;22322233for (i = 0; i < constraintEvents.length; i++) {2234if (eventContainsRange(constraintEvents[i], start, end)) {2235anyContainment = true;2236break;2237}2238}22392240if (!anyContainment) {2241return false;2242}2243}22442245for (i = 0; i < cache.length; i++) { // loop all events and detect overlap2246otherEvent = cache[i];22472248// don't compare the event to itself or other related [repeating] events2249if (event && event._id === otherEvent._id) {2250continue;2251}22522253// there needs to be an actual intersection before disallowing anything2254if (eventIntersectsRange(otherEvent, start, end)) {22552256// evaluate overlap for the given range and short-circuit if necessary2257if (overlap === false) {2258return false;2259}2260else if (typeof overlap === 'function' && !overlap(otherEvent, event)) {2261return false;2262}22632264// if we are computing if the given range is allowable for an event, consider the other event's2265// EventObject-specific or Source-specific `overlap` property2266if (event) {2267otherOverlap = firstDefined(2268otherEvent.overlap,2269(otherEvent.source || {}).overlap2270// we already considered the global `eventOverlap`2271);2272if (otherOverlap === false) {2273return false;2274}2275if (typeof otherOverlap === 'function' && !otherOverlap(event, otherEvent)) {2276return false;2277}2278}2279}2280}22812282return true;2283}228422852286// Given an event input from the API, produces an array of event objects. Possible event inputs:2287// 'businessHours'2288// An event ID (number or string)2289// An object with specific start/end dates or a recurring event (like what businessHours accepts)2290function constraintToEvents(constraintInput) {22912292if (constraintInput === 'businessHours') {2293return getBusinessHoursEvents();2294}22952296if (typeof constraintInput === 'object') {2297return expandEvent(buildEventFromInput(constraintInput));2298}22992300return clientEvents(constraintInput); // probably an ID2301}230223032304// Is the event's date ranged fully contained by the given range?2305// start/end already assumed to have stripped zones :(2306function eventContainsRange(event, start, end) {2307var eventStart = event.start.clone().stripZone();2308var eventEnd = t.getEventEnd(event).stripZone();23092310return start >= eventStart && end <= eventEnd;2311}231223132314// Does the event's date range intersect with the given range?2315// start/end already assumed to have stripped zones :(2316function eventIntersectsRange(event, start, end) {2317var eventStart = event.start.clone().stripZone();2318var eventEnd = t.getEventEnd(event).stripZone();23192320return start < eventEnd && end > eventStart;2321}23222323}232423252326// updates the "backup" properties, which are preserved in order to compute diffs later on.2327function backupEventDates(event) {2328event._allDay = event.allDay;2329event._start = event.start.clone();2330event._end = event.end ? event.end.clone() : null;2331}23322333;;23342335/* FullCalendar-specific DOM Utilities2336----------------------------------------------------------------------------------------------------------------------*/233723382339// Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left2340// and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that.2341function compensateScroll(rowEls, scrollbarWidths) {2342if (scrollbarWidths.left) {2343rowEls.css({2344'border-left-width': 1,2345'margin-left': scrollbarWidths.left - 12346});2347}2348if (scrollbarWidths.right) {2349rowEls.css({2350'border-right-width': 1,2351'margin-right': scrollbarWidths.right - 12352});2353}2354}235523562357// Undoes compensateScroll and restores all borders/margins2358function uncompensateScroll(rowEls) {2359rowEls.css({2360'margin-left': '',2361'margin-right': '',2362'border-left-width': '',2363'border-right-width': ''2364});2365}236623672368// Make the mouse cursor express that an event is not allowed in the current area2369function disableCursor() {2370$('body').addClass('fc-not-allowed');2371}237223732374// Returns the mouse cursor to its original look2375function enableCursor() {2376$('body').removeClass('fc-not-allowed');2377}237823792380// Given a total available height to fill, have `els` (essentially child rows) expand to accomodate.2381// By default, all elements that are shorter than the recommended height are expanded uniformly, not considering2382// any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and2383// reduces the available height.2384function distributeHeight(els, availableHeight, shouldRedistribute) {23852386// *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions,2387// and it is better to be shorter than taller, to avoid creating unnecessary scrollbars.23882389var minOffset1 = Math.floor(availableHeight / els.length); // for non-last element2390var minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)); // for last element *FLOORING NOTE*2391var flexEls = []; // elements that are allowed to expand. array of DOM nodes2392var flexOffsets = []; // amount of vertical space it takes up2393var flexHeights = []; // actual css height2394var usedHeight = 0;23952396undistributeHeight(els); // give all elements their natural height23972398// find elements that are below the recommended height (expandable).2399// important to query for heights in a single first pass (to avoid reflow oscillation).2400els.each(function(i, el) {2401var minOffset = i === els.length - 1 ? minOffset2 : minOffset1;2402var naturalOffset = $(el).outerHeight(true);24032404if (naturalOffset < minOffset) {2405flexEls.push(el);2406flexOffsets.push(naturalOffset);2407flexHeights.push($(el).height());2408}2409else {2410// this element stretches past recommended height (non-expandable). mark the space as occupied.2411usedHeight += naturalOffset;2412}2413});24142415// readjust the recommended height to only consider the height available to non-maxed-out rows.2416if (shouldRedistribute) {2417availableHeight -= usedHeight;2418minOffset1 = Math.floor(availableHeight / flexEls.length);2419minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)); // *FLOORING NOTE*2420}24212422// assign heights to all expandable elements2423$(flexEls).each(function(i, el) {2424var minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1;2425var naturalOffset = flexOffsets[i];2426var naturalHeight = flexHeights[i];2427var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding24282429if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things2430$(el).height(newHeight);2431}2432});2433}243424352436// Undoes distrubuteHeight, restoring all els to their natural height2437function undistributeHeight(els) {2438els.height('');2439}244024412442// Given `els`, a jQuery set of <td> cells, find the cell with the largest natural width and set the widths of all the2443// cells to be that width.2444// PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline2445function matchCellWidths(els) {2446var maxInnerWidth = 0;24472448els.find('> *').each(function(i, innerEl) {2449var innerWidth = $(innerEl).outerWidth();2450if (innerWidth > maxInnerWidth) {2451maxInnerWidth = innerWidth;2452}2453});24542455maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance24562457els.width(maxInnerWidth);24582459return maxInnerWidth;2460}246124622463// Turns a container element into a scroller if its contents is taller than the allotted height.2464// Returns true if the element is now a scroller, false otherwise.2465// NOTE: this method is best because it takes weird zooming dimensions into account2466function setPotentialScroller(containerEl, height) {2467containerEl.height(height).addClass('fc-scroller');24682469// are scrollbars needed?2470if (containerEl[0].scrollHeight - 1 > containerEl[0].clientHeight) { // !!! -1 because IE is often off-by-one :(2471return true;2472}24732474unsetScroller(containerEl); // undo2475return false;2476}247724782479// Takes an element that might have been a scroller, and turns it back into a normal element.2480function unsetScroller(containerEl) {2481containerEl.height('').removeClass('fc-scroller');2482}248324842485/* General DOM Utilities2486----------------------------------------------------------------------------------------------------------------------*/248724882489// borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L512490function getScrollParent(el) {2491var position = el.css('position'),2492scrollParent = el.parents().filter(function() {2493var parent = $(this);2494return (/(auto|scroll)/).test(2495parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x')2496);2497}).eq(0);24982499return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent;2500}250125022503// Given a container element, return an object with the pixel values of the left/right scrollbars.2504// Left scrollbars might occur on RTL browsers (IE maybe?) but I have not tested.2505// PREREQUISITE: container element must have a single child with display:block2506function getScrollbarWidths(container) {2507var containerLeft = container.offset().left;2508var containerRight = containerLeft + container.width();2509var inner = container.children();2510var innerLeft = inner.offset().left;2511var innerRight = innerLeft + inner.outerWidth();25122513return {2514left: innerLeft - containerLeft,2515right: containerRight - innerRight2516};2517}251825192520// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)2521function isPrimaryMouseButton(ev) {2522return ev.which == 1 && !ev.ctrlKey;2523}252425252526/* FullCalendar-specific Misc Utilities2527----------------------------------------------------------------------------------------------------------------------*/252825292530// Creates a basic segment with the intersection of the two ranges. Returns undefined if no intersection.2531// Expects all dates to be normalized to the same timezone beforehand.2532function intersectionToSeg(subjectStart, subjectEnd, intervalStart, intervalEnd) {2533var segStart, segEnd;2534var isStart, isEnd;25352536if (subjectEnd > intervalStart && subjectStart < intervalEnd) { // in bounds at all?25372538if (subjectStart >= intervalStart) {2539segStart = subjectStart.clone();2540isStart = true;2541}2542else {2543segStart = intervalStart.clone();2544isStart = false;2545}25462547if (subjectEnd <= intervalEnd) {2548segEnd = subjectEnd.clone();2549isEnd = true;2550}2551else {2552segEnd = intervalEnd.clone();2553isEnd = false;2554}25552556return {2557start: segStart,2558end: segEnd,2559isStart: isStart,2560isEnd: isEnd2561};2562}2563}256425652566function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object2567obj = obj || {};2568if (obj[name] !== undefined) {2569return obj[name];2570}2571var parts = name.split(/(?=[A-Z])/),2572i = parts.length - 1, res;2573for (; i>=0; i--) {2574res = obj[parts[i].toLowerCase()];2575if (res !== undefined) {2576return res;2577}2578}2579return obj['default'];2580}258125822583/* Date Utilities2584----------------------------------------------------------------------------------------------------------------------*/25852586var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ];258725882589// Diffs the two moments into a Duration where full-days are recorded first, then the remaining time.2590// Moments will have their timezones normalized.2591function dayishDiff(a, b) {2592return moment.duration({2593days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'),2594ms: a.time() - b.time()2595});2596}259725982599function isNativeDate(input) {2600return Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date;2601}260226032604// Returns a boolean about whether the given input is a time string, like "06:40:00" or "06:00"2605function isTimeString(str) {2606return /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str);2607}260826092610/* General Utilities2611----------------------------------------------------------------------------------------------------------------------*/26122613fc.applyAll = applyAll; // export261426152616// Create an object that has the given prototype. Just like Object.create2617function createObject(proto) {2618var f = function() {};2619f.prototype = proto;2620return new f();2621}262226232624function applyAll(functions, thisObj, args) {2625if ($.isFunction(functions)) {2626functions = [ functions ];2627}2628if (functions) {2629var i;2630var ret;2631for (i=0; i<functions.length; i++) {2632ret = functions[i].apply(thisObj, args) || ret;2633}2634return ret;2635}2636}263726382639function firstDefined() {2640for (var i=0; i<arguments.length; i++) {2641if (arguments[i] !== undefined) {2642return arguments[i];2643}2644}2645}264626472648function htmlEscape(s) {2649return (s + '').replace(/&/g, '&')2650.replace(/</g, '<')2651.replace(/>/g, '>')2652.replace(/'/g, ''')2653.replace(/"/g, '"')2654.replace(/\n/g, '<br />');2655}265626572658function stripHtmlEntities(text) {2659return text.replace(/&.*?;/g, '');2660}266126622663function capitaliseFirstLetter(str) {2664return str.charAt(0).toUpperCase() + str.slice(1);2665}266626672668function compareNumbers(a, b) { // for .sort()2669return a - b;2670}267126722673// Returns a function, that, as long as it continues to be invoked, will not2674// be triggered. The function will be called after it stops being called for2675// N milliseconds.2676// https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L7142677function debounce(func, wait) {2678var timeoutId;2679var args;2680var context;2681var timestamp; // of most recent call2682var later = function() {2683var last = +new Date() - timestamp;2684if (last < wait && last > 0) {2685timeoutId = setTimeout(later, wait - last);2686}2687else {2688timeoutId = null;2689func.apply(context, args);2690if (!timeoutId) {2691context = args = null;2692}2693}2694};26952696return function() {2697context = this;2698args = arguments;2699timestamp = +new Date();2700if (!timeoutId) {2701timeoutId = setTimeout(later, wait);2702}2703};2704}27052706;;27072708var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/;2709var ambigTimeOrZoneRegex =2710/^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/;2711var newMomentProto = moment.fn; // where we will attach our new methods2712var oldMomentProto = $.extend({}, newMomentProto); // copy of original moment methods2713var allowValueOptimization;2714var setUTCValues; // function defined below2715var setLocalValues; // function defined below271627172718// Creating2719// -------------------------------------------------------------------------------------------------27202721// Creates a new moment, similar to the vanilla moment(...) constructor, but with2722// extra features (ambiguous time, enhanced formatting). When given an existing moment,2723// it will function as a clone (and retain the zone of the moment). Anything else will2724// result in a moment in the local zone.2725fc.moment = function() {2726return makeMoment(arguments);2727};27282729// Sames as fc.moment, but forces the resulting moment to be in the UTC timezone.2730fc.moment.utc = function() {2731var mom = makeMoment(arguments, true);27322733// Force it into UTC because makeMoment doesn't guarantee it2734// (if given a pre-existing moment for example)2735if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone2736mom.utc();2737}27382739return mom;2740};27412742// Same as fc.moment, but when given an ISO8601 string, the timezone offset is preserved.2743// ISO8601 strings with no timezone offset will become ambiguously zoned.2744fc.moment.parseZone = function() {2745return makeMoment(arguments, true, true);2746};27472748// Builds an enhanced moment from args. When given an existing moment, it clones. When given a2749// native Date, or called with no arguments (the current time), the resulting moment will be local.2750// Anything else needs to be "parsed" (a string or an array), and will be affected by:2751// parseAsUTC - if there is no zone information, should we parse the input in UTC?2752// parseZone - if there is zone information, should we force the zone of the moment?2753function makeMoment(args, parseAsUTC, parseZone) {2754var input = args[0];2755var isSingleString = args.length == 1 && typeof input === 'string';2756var isAmbigTime;2757var isAmbigZone;2758var ambigMatch;2759var mom;27602761if (moment.isMoment(input)) {2762mom = moment.apply(null, args); // clone it2763transferAmbigs(input, mom); // the ambig flags weren't transfered with the clone2764}2765else if (isNativeDate(input) || input === undefined) {2766mom = moment.apply(null, args); // will be local2767}2768else { // "parsing" is required2769isAmbigTime = false;2770isAmbigZone = false;27712772if (isSingleString) {2773if (ambigDateOfMonthRegex.test(input)) {2774// accept strings like '2014-05', but convert to the first of the month2775input += '-01';2776args = [ input ]; // for when we pass it on to moment's constructor2777isAmbigTime = true;2778isAmbigZone = true;2779}2780else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) {2781isAmbigTime = !ambigMatch[5]; // no time part?2782isAmbigZone = true;2783}2784}2785else if ($.isArray(input)) {2786// arrays have no timezone information, so assume ambiguous zone2787isAmbigZone = true;2788}2789// otherwise, probably a string with a format27902791if (parseAsUTC) {2792mom = moment.utc.apply(moment, args);2793}2794else {2795mom = moment.apply(null, args);2796}27972798if (isAmbigTime) {2799mom._ambigTime = true;2800mom._ambigZone = true; // ambiguous time always means ambiguous zone2801}2802else if (parseZone) { // let's record the inputted zone somehow2803if (isAmbigZone) {2804mom._ambigZone = true;2805}2806else if (isSingleString) {2807mom.zone(input); // if not a valid zone, will assign UTC2808}2809}2810}28112812mom._fullCalendar = true; // flag for extended functionality28132814return mom;2815}281628172818// A clone method that works with the flags related to our enhanced functionality.2819// In the future, use moment.momentProperties2820newMomentProto.clone = function() {2821var mom = oldMomentProto.clone.apply(this, arguments);28222823// these flags weren't transfered with the clone2824transferAmbigs(this, mom);2825if (this._fullCalendar) {2826mom._fullCalendar = true;2827}28282829return mom;2830};283128322833// Time-of-day2834// -------------------------------------------------------------------------------------------------28352836// GETTER2837// Returns a Duration with the hours/minutes/seconds/ms values of the moment.2838// If the moment has an ambiguous time, a duration of 00:00 will be returned.2839//2840// SETTER2841// You can supply a Duration, a Moment, or a Duration-like argument.2842// When setting the time, and the moment has an ambiguous time, it then becomes unambiguous.2843newMomentProto.time = function(time) {28442845// Fallback to the original method (if there is one) if this moment wasn't created via FullCalendar.2846// `time` is a generic enough method name where this precaution is necessary to avoid collisions w/ other plugins.2847if (!this._fullCalendar) {2848return oldMomentProto.time.apply(this, arguments);2849}28502851if (time == null) { // getter2852return moment.duration({2853hours: this.hours(),2854minutes: this.minutes(),2855seconds: this.seconds(),2856milliseconds: this.milliseconds()2857});2858}2859else { // setter28602861this._ambigTime = false; // mark that the moment now has a time28622863if (!moment.isDuration(time) && !moment.isMoment(time)) {2864time = moment.duration(time);2865}28662867// The day value should cause overflow (so 24 hours becomes 00:00:00 of next day).2868// Only for Duration times, not Moment times.2869var dayHours = 0;2870if (moment.isDuration(time)) {2871dayHours = Math.floor(time.asDays()) * 24;2872}28732874// We need to set the individual fields.2875// Can't use startOf('day') then add duration. In case of DST at start of day.2876return this.hours(dayHours + time.hours())2877.minutes(time.minutes())2878.seconds(time.seconds())2879.milliseconds(time.milliseconds());2880}2881};28822883// Converts the moment to UTC, stripping out its time-of-day and timezone offset,2884// but preserving its YMD. A moment with a stripped time will display no time2885// nor timezone offset when .format() is called.2886newMomentProto.stripTime = function() {2887var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array28882889this.utc(); // set the internal UTC flag (will clear the ambig flags)2890setUTCValues(this, a.slice(0, 3)); // set the year/month/date. time will be zero28912892// Mark the time as ambiguous. This needs to happen after the .utc() call, which calls .zone(),2893// which clears all ambig flags. Same with setUTCValues with moment-timezone.2894this._ambigTime = true;2895this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset28962897return this; // for chaining2898};28992900// Returns if the moment has a non-ambiguous time (boolean)2901newMomentProto.hasTime = function() {2902return !this._ambigTime;2903};290429052906// Timezone2907// -------------------------------------------------------------------------------------------------29082909// Converts the moment to UTC, stripping out its timezone offset, but preserving its2910// YMD and time-of-day. A moment with a stripped timezone offset will display no2911// timezone offset when .format() is called.2912newMomentProto.stripZone = function() {2913var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array2914var wasAmbigTime = this._ambigTime;29152916this.utc(); // set the internal UTC flag (will clear the ambig flags)2917setUTCValues(this, a); // will set the year/month/date/hours/minutes/seconds/ms29182919if (wasAmbigTime) {2920// the above call to .utc()/.zone() unfortunately clears the ambig flags, so reassign2921this._ambigTime = true;2922}29232924// Mark the zone as ambiguous. This needs to happen after the .utc() call, which calls .zone(),2925// which clears all ambig flags. Same with setUTCValues with moment-timezone.2926this._ambigZone = true;29272928return this; // for chaining2929};29302931// Returns of the moment has a non-ambiguous timezone offset (boolean)2932newMomentProto.hasZone = function() {2933return !this._ambigZone;2934};29352936// this method implicitly marks a zone (will get called upon .utc() and .local())2937newMomentProto.zone = function(tzo) {29382939if (tzo != null) { // setter2940// these assignments needs to happen before the original zone method is called.2941// I forget why, something to do with a browser crash.2942this._ambigTime = false;2943this._ambigZone = false;2944}29452946return oldMomentProto.zone.apply(this, arguments);2947};29482949// this method implicitly marks a zone2950newMomentProto.local = function() {2951var a = this.toArray(); // year,month,date,hours,minutes,seconds,ms as an array2952var wasAmbigZone = this._ambigZone;29532954oldMomentProto.local.apply(this, arguments); // will clear ambig flags29552956if (wasAmbigZone) {2957// If the moment was ambiguously zoned, the date fields were stored as UTC.2958// We want to preserve these, but in local time.2959setLocalValues(this, a);2960}29612962return this; // for chaining2963};296429652966// Formatting2967// -------------------------------------------------------------------------------------------------29682969newMomentProto.format = function() {2970if (this._fullCalendar && arguments[0]) { // an enhanced moment? and a format string provided?2971return formatDate(this, arguments[0]); // our extended formatting2972}2973if (this._ambigTime) {2974return oldMomentFormat(this, 'YYYY-MM-DD');2975}2976if (this._ambigZone) {2977return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');2978}2979return oldMomentProto.format.apply(this, arguments);2980};29812982newMomentProto.toISOString = function() {2983if (this._ambigTime) {2984return oldMomentFormat(this, 'YYYY-MM-DD');2985}2986if (this._ambigZone) {2987return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');2988}2989return oldMomentProto.toISOString.apply(this, arguments);2990};299129922993// Querying2994// -------------------------------------------------------------------------------------------------29952996// Is the moment within the specified range? `end` is exclusive.2997// FYI, this method is not a standard Moment method, so always do our enhanced logic.2998newMomentProto.isWithin = function(start, end) {2999var a = commonlyAmbiguate([ this, start, end ]);3000return a[0] >= a[1] && a[0] < a[2];3001};30023003// When isSame is called with units, timezone ambiguity is normalized before the comparison happens.3004// If no units specified, the two moments must be identically the same, with matching ambig flags.3005newMomentProto.isSame = function(input, units) {3006var a;30073008// only do custom logic if this is an enhanced moment3009if (!this._fullCalendar) {3010return oldMomentProto.isSame.apply(this, arguments);3011}30123013if (units) {3014a = commonlyAmbiguate([ this, input ], true); // normalize timezones but don't erase times3015return oldMomentProto.isSame.call(a[0], a[1], units);3016}3017else {3018input = fc.moment.parseZone(input); // normalize input3019return oldMomentProto.isSame.call(this, input) &&3020Boolean(this._ambigTime) === Boolean(input._ambigTime) &&3021Boolean(this._ambigZone) === Boolean(input._ambigZone);3022}3023};30243025// Make these query methods work with ambiguous moments3026$.each([3027'isBefore',3028'isAfter'3029], function(i, methodName) {3030newMomentProto[methodName] = function(input, units) {3031var a;30323033// only do custom logic if this is an enhanced moment3034if (!this._fullCalendar) {3035return oldMomentProto[methodName].apply(this, arguments);3036}30373038a = commonlyAmbiguate([ this, input ]);3039return oldMomentProto[methodName].call(a[0], a[1], units);3040};3041});304230433044// Misc Internals3045// -------------------------------------------------------------------------------------------------30463047// given an array of moment-like inputs, return a parallel array w/ moments similarly ambiguated.3048// for example, of one moment has ambig time, but not others, all moments will have their time stripped.3049// set `preserveTime` to `true` to keep times, but only normalize zone ambiguity.3050function commonlyAmbiguate(inputs, preserveTime) {3051var outputs = [];3052var anyAmbigTime = false;3053var anyAmbigZone = false;3054var i;30553056for (i=0; i<inputs.length; i++) {3057outputs.push(fc.moment.parseZone(inputs[i]));3058anyAmbigTime = anyAmbigTime || outputs[i]._ambigTime;3059anyAmbigZone = anyAmbigZone || outputs[i]._ambigZone;3060}30613062for (i=0; i<outputs.length; i++) {3063if (anyAmbigTime && !preserveTime) {3064outputs[i].stripTime();3065}3066else if (anyAmbigZone) {3067outputs[i].stripZone();3068}3069}30703071return outputs;3072}30733074// Transfers all the flags related to ambiguous time/zone from the `src` moment to the `dest` moment3075function transferAmbigs(src, dest) {3076if (src._ambigTime) {3077dest._ambigTime = true;3078}3079else if (dest._ambigTime) {3080dest._ambigTime = false;3081}30823083if (src._ambigZone) {3084dest._ambigZone = true;3085}3086else if (dest._ambigZone) {3087dest._ambigZone = false;3088}3089}309030913092// Sets the year/month/date/etc values of the moment from the given array.3093// Inefficient because it calls each individual setter.3094function setMomentValues(mom, a) {3095mom.year(a[0] || 0)3096.month(a[1] || 0)3097.date(a[2] || 0)3098.hours(a[3] || 0)3099.minutes(a[4] || 0)3100.seconds(a[5] || 0)3101.milliseconds(a[6] || 0);3102}31033104// Can we set the moment's internal date directly?3105allowValueOptimization = '_d' in moment() && 'updateOffset' in moment;31063107// Utility function. Accepts a moment and an array of the UTC year/month/date/etc values to set.3108// Assumes the given moment is already in UTC mode.3109setUTCValues = allowValueOptimization ? function(mom, a) {3110// simlate what moment's accessors do3111mom._d.setTime(Date.UTC.apply(Date, a));3112moment.updateOffset(mom, false); // keepTime=false3113} : setMomentValues;31143115// Utility function. Accepts a moment and an array of the local year/month/date/etc values to set.3116// Assumes the given moment is already in local mode.3117setLocalValues = allowValueOptimization ? function(mom, a) {3118// simlate what moment's accessors do3119mom._d.setTime(+new Date( // FYI, there is now way to apply an array of args to a constructor3120a[0] || 0,3121a[1] || 0,3122a[2] || 0,3123a[3] || 0,3124a[4] || 0,3125a[5] || 0,3126a[6] || 03127));3128moment.updateOffset(mom, false); // keepTime=false3129} : setMomentValues;31303131;;31323133// Single Date Formatting3134// -------------------------------------------------------------------------------------------------313531363137// call this if you want Moment's original format method to be used3138function oldMomentFormat(mom, formatStr) {3139return oldMomentProto.format.call(mom, formatStr); // oldMomentProto defined in moment-ext.js3140}314131423143// Formats `date` with a Moment formatting string, but allow our non-zero areas and3144// additional token.3145function formatDate(date, formatStr) {3146return formatDateWithChunks(date, getFormatStringChunks(formatStr));3147}314831493150function formatDateWithChunks(date, chunks) {3151var s = '';3152var i;31533154for (i=0; i<chunks.length; i++) {3155s += formatDateWithChunk(date, chunks[i]);3156}31573158return s;3159}316031613162// addition formatting tokens we want recognized3163var tokenOverrides = {3164t: function(date) { // "a" or "p"3165return oldMomentFormat(date, 'a').charAt(0);3166},3167T: function(date) { // "A" or "P"3168return oldMomentFormat(date, 'A').charAt(0);3169}3170};317131723173function formatDateWithChunk(date, chunk) {3174var token;3175var maybeStr;31763177if (typeof chunk === 'string') { // a literal string3178return chunk;3179}3180else if ((token = chunk.token)) { // a token, like "YYYY"3181if (tokenOverrides[token]) {3182return tokenOverrides[token](date); // use our custom token3183}3184return oldMomentFormat(date, token);3185}3186else if (chunk.maybe) { // a grouping of other chunks that must be non-zero3187maybeStr = formatDateWithChunks(date, chunk.maybe);3188if (maybeStr.match(/[1-9]/)) {3189return maybeStr;3190}3191}31923193return '';3194}319531963197// Date Range Formatting3198// -------------------------------------------------------------------------------------------------3199// TODO: make it work with timezone offset32003201// Using a formatting string meant for a single date, generate a range string, like3202// "Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ.3203// If the dates are the same as far as the format string is concerned, just return a single3204// rendering of one date, without any separator.3205function formatRange(date1, date2, formatStr, separator, isRTL) {3206var localeData;32073208date1 = fc.moment.parseZone(date1);3209date2 = fc.moment.parseZone(date2);32103211localeData = (date1.localeData || date1.lang).call(date1); // works with moment-pre-2.832123213// Expand localized format strings, like "LL" -> "MMMM D YYYY"3214formatStr = localeData.longDateFormat(formatStr) || formatStr;3215// BTW, this is not important for `formatDate` because it is impossible to put custom tokens3216// or non-zero areas in Moment's localized format strings.32173218separator = separator || ' - ';32193220return formatRangeWithChunks(3221date1,3222date2,3223getFormatStringChunks(formatStr),3224separator,3225isRTL3226);3227}3228fc.formatRange = formatRange; // expose322932303231function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) {3232var chunkStr; // the rendering of the chunk3233var leftI;3234var leftStr = '';3235var rightI;3236var rightStr = '';3237var middleI;3238var middleStr1 = '';3239var middleStr2 = '';3240var middleStr = '';32413242// Start at the leftmost side of the formatting string and continue until you hit a token3243// that is not the same between dates.3244for (leftI=0; leftI<chunks.length; leftI++) {3245chunkStr = formatSimilarChunk(date1, date2, chunks[leftI]);3246if (chunkStr === false) {3247break;3248}3249leftStr += chunkStr;3250}32513252// Similarly, start at the rightmost side of the formatting string and move left3253for (rightI=chunks.length-1; rightI>leftI; rightI--) {3254chunkStr = formatSimilarChunk(date1, date2, chunks[rightI]);3255if (chunkStr === false) {3256break;3257}3258rightStr = chunkStr + rightStr;3259}32603261// The area in the middle is different for both of the dates.3262// Collect them distinctly so we can jam them together later.3263for (middleI=leftI; middleI<=rightI; middleI++) {3264middleStr1 += formatDateWithChunk(date1, chunks[middleI]);3265middleStr2 += formatDateWithChunk(date2, chunks[middleI]);3266}32673268if (middleStr1 || middleStr2) {3269if (isRTL) {3270middleStr = middleStr2 + separator + middleStr1;3271}3272else {3273middleStr = middleStr1 + separator + middleStr2;3274}3275}32763277return leftStr + middleStr + rightStr;3278}327932803281var similarUnitMap = {3282Y: 'year',3283M: 'month',3284D: 'day', // day of month3285d: 'day', // day of week3286// prevents a separator between anything time-related...3287A: 'second', // AM/PM3288a: 'second', // am/pm3289T: 'second', // A/P3290t: 'second', // a/p3291H: 'second', // hour (24)3292h: 'second', // hour (12)3293m: 'second', // minute3294s: 'second' // second3295};3296// TODO: week maybe?329732983299// Given a formatting chunk, and given that both dates are similar in the regard the3300// formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`.3301function formatSimilarChunk(date1, date2, chunk) {3302var token;3303var unit;33043305if (typeof chunk === 'string') { // a literal string3306return chunk;3307}3308else if ((token = chunk.token)) {3309unit = similarUnitMap[token.charAt(0)];3310// are the dates the same for this unit of measurement?3311if (unit && date1.isSame(date2, unit)) {3312return oldMomentFormat(date1, token); // would be the same if we used `date2`3313// BTW, don't support custom tokens3314}3315}33163317return false; // the chunk is NOT the same for the two dates3318// BTW, don't support splitting on non-zero areas3319}332033213322// Chunking Utils3323// -------------------------------------------------------------------------------------------------332433253326var formatStringChunkCache = {};332733283329function getFormatStringChunks(formatStr) {3330if (formatStr in formatStringChunkCache) {3331return formatStringChunkCache[formatStr];3332}3333return (formatStringChunkCache[formatStr] = chunkFormatString(formatStr));3334}333533363337// Break the formatting string into an array of chunks3338function chunkFormatString(formatStr) {3339var chunks = [];3340var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LT|(\w)\4*o?)|([^\w\[\(]+)/g; // TODO: more descrimination3341var match;33423343while ((match = chunker.exec(formatStr))) {3344if (match[1]) { // a literal string inside [ ... ]3345chunks.push(match[1]);3346}3347else if (match[2]) { // non-zero formatting inside ( ... )3348chunks.push({ maybe: chunkFormatString(match[2]) });3349}3350else if (match[3]) { // a formatting token3351chunks.push({ token: match[3] });3352}3353else if (match[5]) { // an unenclosed literal string3354chunks.push(match[5]);3355}3356}33573358return chunks;3359}33603361;;33623363/* A rectangular panel that is absolutely positioned over other content3364------------------------------------------------------------------------------------------------------------------------3365Options:3366- className (string)3367- content (HTML string or jQuery element set)3368- parentEl3369- top3370- left3371- right (the x coord of where the right edge should be. not a "CSS" right)3372- autoHide (boolean)3373- show (callback)3374- hide (callback)3375*/33763377function Popover(options) {3378this.options = options || {};3379}338033813382Popover.prototype = {33833384isHidden: true,3385options: null,3386el: null, // the container element for the popover. generated by this object3387documentMousedownProxy: null, // document mousedown handler bound to `this`3388margin: 10, // the space required between the popover and the edges of the scroll container338933903391// Shows the popover on the specified position. Renders it if not already3392show: function() {3393if (this.isHidden) {3394if (!this.el) {3395this.render();3396}3397this.el.show();3398this.position();3399this.isHidden = false;3400this.trigger('show');3401}3402},340334043405// Hides the popover, through CSS, but does not remove it from the DOM3406hide: function() {3407if (!this.isHidden) {3408this.el.hide();3409this.isHidden = true;3410this.trigger('hide');3411}3412},341334143415// Creates `this.el` and renders content inside of it3416render: function() {3417var _this = this;3418var options = this.options;34193420this.el = $('<div class="fc-popover"/>')3421.addClass(options.className || '')3422.css({3423// position initially to the top left to avoid creating scrollbars3424top: 0,3425left: 03426})3427.append(options.content)3428.appendTo(options.parentEl);34293430// when a click happens on anything inside with a 'fc-close' className, hide the popover3431this.el.on('click', '.fc-close', function() {3432_this.hide();3433});34343435if (options.autoHide) {3436$(document).on('mousedown', this.documentMousedownProxy = $.proxy(this, 'documentMousedown'));3437}3438},343934403441// Triggered when the user clicks *anywhere* in the document, for the autoHide feature3442documentMousedown: function(ev) {3443// only hide the popover if the click happened outside the popover3444if (this.el && !$(ev.target).closest(this.el).length) {3445this.hide();3446}3447},344834493450// Hides and unregisters any handlers3451destroy: function() {3452this.hide();34533454if (this.el) {3455this.el.remove();3456this.el = null;3457}34583459$(document).off('mousedown', this.documentMousedownProxy);3460},346134623463// Positions the popover optimally, using the top/left/right options3464position: function() {3465var options = this.options;3466var origin = this.el.offsetParent().offset();3467var width = this.el.outerWidth();3468var height = this.el.outerHeight();3469var windowEl = $(window);3470var viewportEl = getScrollParent(this.el);3471var viewportTop;3472var viewportLeft;3473var viewportOffset;3474var top; // the "position" (not "offset") values for the popover3475var left; //34763477// compute top and left3478top = options.top || 0;3479if (options.left !== undefined) {3480left = options.left;3481}3482else if (options.right !== undefined) {3483left = options.right - width; // derive the left value from the right value3484}3485else {3486left = 0;3487}34883489if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result3490viewportEl = windowEl;3491viewportTop = 0; // the window is always at the top left3492viewportLeft = 0; // (and .offset() won't work if called here)3493}3494else {3495viewportOffset = viewportEl.offset();3496viewportTop = viewportOffset.top;3497viewportLeft = viewportOffset.left;3498}34993500// if the window is scrolled, it causes the visible area to be further down3501viewportTop += windowEl.scrollTop();3502viewportLeft += windowEl.scrollLeft();35033504// constrain to the view port. if constrained by two edges, give precedence to top/left3505if (options.viewportConstrain !== false) {3506top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin);3507top = Math.max(top, viewportTop + this.margin);3508left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin);3509left = Math.max(left, viewportLeft + this.margin);3510}35113512this.el.css({3513top: top - origin.top,3514left: left - origin.left3515});3516},351735183519// Triggers a callback. Calls a function in the option hash of the same name.3520// Arguments beyond the first `name` are forwarded on.3521// TODO: better code reuse for this. Repeat code3522trigger: function(name) {3523if (this.options[name]) {3524this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));3525}3526}35273528};35293530;;35313532/* A "coordinate map" converts pixel coordinates into an associated cell, which has an associated date3533------------------------------------------------------------------------------------------------------------------------3534Common interface:35353536CoordMap.prototype = {3537build: function() {},3538getCell: function(x, y) {}3539};35403541*/35423543/* Coordinate map for a grid component3544----------------------------------------------------------------------------------------------------------------------*/35453546function GridCoordMap(grid) {3547this.grid = grid;3548}354935503551GridCoordMap.prototype = {35523553grid: null, // reference to the Grid3554rows: null, // the top-to-bottom y coordinates. including the bottom of the last item3555cols: null, // the left-to-right x coordinates. including the right of the last item35563557containerEl: null, // container element that all coordinates are constrained to. optionally assigned3558minX: null,3559maxX: null, // exclusive3560minY: null,3561maxY: null, // exclusive356235633564// Queries the grid for the coordinates of all the cells3565build: function() {3566this.grid.buildCoords(3567this.rows = [],3568this.cols = []3569);3570this.computeBounds();3571},357235733574// Given a coordinate of the document, gets the associated cell. If no cell is underneath, returns null3575getCell: function(x, y) {3576var cell = null;3577var rows = this.rows;3578var cols = this.cols;3579var r = -1;3580var c = -1;3581var i;35823583if (this.inBounds(x, y)) {35843585for (i = 0; i < rows.length; i++) {3586if (y >= rows[i][0] && y < rows[i][1]) {3587r = i;3588break;3589}3590}35913592for (i = 0; i < cols.length; i++) {3593if (x >= cols[i][0] && x < cols[i][1]) {3594c = i;3595break;3596}3597}35983599if (r >= 0 && c >= 0) {3600cell = { row: r, col: c };3601cell.grid = this.grid;3602cell.date = this.grid.getCellDate(cell);3603}3604}36053606return cell;3607},360836093610// If there is a containerEl, compute the bounds into min/max values3611computeBounds: function() {3612var containerOffset;36133614if (this.containerEl) {3615containerOffset = this.containerEl.offset();3616this.minX = containerOffset.left;3617this.maxX = containerOffset.left + this.containerEl.outerWidth();3618this.minY = containerOffset.top;3619this.maxY = containerOffset.top + this.containerEl.outerHeight();3620}3621},362236233624// Determines if the given coordinates are in bounds. If no `containerEl`, always true3625inBounds: function(x, y) {3626if (this.containerEl) {3627return x >= this.minX && x < this.maxX && y >= this.minY && y < this.maxY;3628}3629return true;3630}36313632};363336343635/* Coordinate map that is a combination of multiple other coordinate maps3636----------------------------------------------------------------------------------------------------------------------*/36373638function ComboCoordMap(coordMaps) {3639this.coordMaps = coordMaps;3640}364136423643ComboCoordMap.prototype = {36443645coordMaps: null, // an array of CoordMaps364636473648// Builds all coordMaps3649build: function() {3650var coordMaps = this.coordMaps;3651var i;36523653for (i = 0; i < coordMaps.length; i++) {3654coordMaps[i].build();3655}3656},365736583659// Queries all coordMaps for the cell underneath the given coordinates, returning the first result3660getCell: function(x, y) {3661var coordMaps = this.coordMaps;3662var cell = null;3663var i;36643665for (i = 0; i < coordMaps.length && !cell; i++) {3666cell = coordMaps[i].getCell(x, y);3667}36683669return cell;3670}36713672};36733674;;36753676/* Tracks mouse movements over a CoordMap and raises events about which cell the mouse is over.3677----------------------------------------------------------------------------------------------------------------------*/3678// TODO: very useful to have a handler that gets called upon cellOut OR when dragging stops (for cleanup)36793680function DragListener(coordMap, options) {3681this.coordMap = coordMap;3682this.options = options || {};3683}368436853686DragListener.prototype = {36873688coordMap: null,3689options: null,36903691isListening: false,3692isDragging: false,36933694// the cell/date the mouse was over when listening started3695origCell: null,3696origDate: null,36973698// the cell/date the mouse is over3699cell: null,3700date: null,37013702// coordinates of the initial mousedown3703mouseX0: null,3704mouseY0: null,37053706// handler attached to the document, bound to the DragListener's `this`3707mousemoveProxy: null,3708mouseupProxy: null,37093710scrollEl: null,3711scrollBounds: null, // { top, bottom, left, right }3712scrollTopVel: null, // pixels per second3713scrollLeftVel: null, // pixels per second3714scrollIntervalId: null, // ID of setTimeout for scrolling animation loop3715scrollHandlerProxy: null, // this-scoped function for handling when scrollEl is scrolled37163717scrollSensitivity: 30, // pixels from edge for scrolling to start3718scrollSpeed: 200, // pixels per second, at maximum speed3719scrollIntervalMs: 50, // millisecond wait between scroll increment372037213722// Call this when the user does a mousedown. Will probably lead to startListening3723mousedown: function(ev) {3724if (isPrimaryMouseButton(ev)) {37253726ev.preventDefault(); // prevents native selection in most browsers37273728this.startListening(ev);37293730// start the drag immediately if there is no minimum distance for a drag start3731if (!this.options.distance) {3732this.startDrag(ev);3733}3734}3735},373637373738// Call this to start tracking mouse movements3739startListening: function(ev) {3740var scrollParent;3741var cell;37423743if (!this.isListening) {37443745// grab scroll container and attach handler3746if (ev && this.options.scroll) {3747scrollParent = getScrollParent($(ev.target));3748if (!scrollParent.is(window) && !scrollParent.is(document)) {3749this.scrollEl = scrollParent;37503751// scope to `this`, and use `debounce` to make sure rapid calls don't happen3752this.scrollHandlerProxy = debounce($.proxy(this, 'scrollHandler'), 100);3753this.scrollEl.on('scroll', this.scrollHandlerProxy);3754}3755}37563757this.computeCoords(); // relies on `scrollEl`37583759// get info on the initial cell, date, and coordinates3760if (ev) {3761cell = this.getCell(ev);3762this.origCell = cell;3763this.origDate = cell ? cell.date : null;37643765this.mouseX0 = ev.pageX;3766this.mouseY0 = ev.pageY;3767}37683769$(document)3770.on('mousemove', this.mousemoveProxy = $.proxy(this, 'mousemove'))3771.on('mouseup', this.mouseupProxy = $.proxy(this, 'mouseup'))3772.on('selectstart', this.preventDefault); // prevents native selection in IE<=837733774this.isListening = true;3775this.trigger('listenStart', ev);3776}3777},377837793780// Recomputes the drag-critical positions of elements3781computeCoords: function() {3782this.coordMap.build();3783this.computeScrollBounds();3784},378537863787// Called when the user moves the mouse3788mousemove: function(ev) {3789var minDistance;3790var distanceSq; // current distance from mouseX0/mouseY0, squared37913792if (!this.isDragging) { // if not already dragging...3793// then start the drag if the minimum distance criteria is met3794minDistance = this.options.distance || 1;3795distanceSq = Math.pow(ev.pageX - this.mouseX0, 2) + Math.pow(ev.pageY - this.mouseY0, 2);3796if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem3797this.startDrag(ev);3798}3799}38003801if (this.isDragging) {3802this.drag(ev); // report a drag, even if this mousemove initiated the drag3803}3804},380538063807// Call this to initiate a legitimate drag.3808// This function is called internally from this class, but can also be called explicitly from outside3809startDrag: function(ev) {3810var cell;38113812if (!this.isListening) { // startDrag must have manually initiated3813this.startListening();3814}38153816if (!this.isDragging) {3817this.isDragging = true;3818this.trigger('dragStart', ev);38193820// report the initial cell the mouse is over3821cell = this.getCell(ev);3822if (cell) {3823this.cellOver(cell, true);3824}3825}3826},382738283829// Called while the mouse is being moved and when we know a legitimate drag is taking place3830drag: function(ev) {3831var cell;38323833if (this.isDragging) {3834cell = this.getCell(ev);38353836if (!isCellsEqual(cell, this.cell)) { // a different cell than before?3837if (this.cell) {3838this.cellOut();3839}3840if (cell) {3841this.cellOver(cell);3842}3843}38443845this.dragScroll(ev); // will possibly cause scrolling3846}3847},384838493850// Called when a the mouse has just moved over a new cell3851cellOver: function(cell) {3852this.cell = cell;3853this.date = cell.date;3854this.trigger('cellOver', cell, cell.date);3855},385638573858// Called when the mouse has just moved out of a cell3859cellOut: function() {3860if (this.cell) {3861this.trigger('cellOut', this.cell);3862this.cell = null;3863this.date = null;3864}3865},386638673868// Called when the user does a mouseup3869mouseup: function(ev) {3870this.stopDrag(ev);3871this.stopListening(ev);3872},387338743875// Called when the drag is over. Will not cause listening to stop however.3876// A concluding 'cellOut' event will NOT be triggered.3877stopDrag: function(ev) {3878if (this.isDragging) {3879this.stopScrolling();3880this.trigger('dragStop', ev);3881this.isDragging = false;3882}3883},388438853886// Call this to stop listening to the user's mouse events3887stopListening: function(ev) {3888if (this.isListening) {38893890// remove the scroll handler if there is a scrollEl3891if (this.scrollEl) {3892this.scrollEl.off('scroll', this.scrollHandlerProxy);3893this.scrollHandlerProxy = null;3894}38953896$(document)3897.off('mousemove', this.mousemoveProxy)3898.off('mouseup', this.mouseupProxy)3899.off('selectstart', this.preventDefault);39003901this.mousemoveProxy = null;3902this.mouseupProxy = null;39033904this.isListening = false;3905this.trigger('listenStop', ev);39063907this.origCell = this.cell = null;3908this.origDate = this.date = null;3909}3910},391139123913// Gets the cell underneath the coordinates for the given mouse event3914getCell: function(ev) {3915return this.coordMap.getCell(ev.pageX, ev.pageY);3916},391739183919// Triggers a callback. Calls a function in the option hash of the same name.3920// Arguments beyond the first `name` are forwarded on.3921trigger: function(name) {3922if (this.options[name]) {3923this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));3924}3925},392639273928// Stops a given mouse event from doing it's native browser action. In our case, text selection.3929preventDefault: function(ev) {3930ev.preventDefault();3931},393239333934/* Scrolling3935------------------------------------------------------------------------------------------------------------------*/393639373938// Computes and stores the bounding rectangle of scrollEl3939computeScrollBounds: function() {3940var el = this.scrollEl;3941var offset;39423943if (el) {3944offset = el.offset();3945this.scrollBounds = {3946top: offset.top,3947left: offset.left,3948bottom: offset.top + el.outerHeight(),3949right: offset.left + el.outerWidth()3950};3951}3952},395339543955// Called when the dragging is in progress and scrolling should be updated3956dragScroll: function(ev) {3957var sensitivity = this.scrollSensitivity;3958var bounds = this.scrollBounds;3959var topCloseness, bottomCloseness;3960var leftCloseness, rightCloseness;3961var topVel = 0;3962var leftVel = 0;39633964if (bounds) { // only scroll if scrollEl exists39653966// compute closeness to edges. valid range is from 0.0 - 1.03967topCloseness = (sensitivity - (ev.pageY - bounds.top)) / sensitivity;3968bottomCloseness = (sensitivity - (bounds.bottom - ev.pageY)) / sensitivity;3969leftCloseness = (sensitivity - (ev.pageX - bounds.left)) / sensitivity;3970rightCloseness = (sensitivity - (bounds.right - ev.pageX)) / sensitivity;39713972// translate vertical closeness into velocity.3973// mouse must be completely in bounds for velocity to happen.3974if (topCloseness >= 0 && topCloseness <= 1) {3975topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up3976}3977else if (bottomCloseness >= 0 && bottomCloseness <= 1) {3978topVel = bottomCloseness * this.scrollSpeed;3979}39803981// translate horizontal closeness into velocity3982if (leftCloseness >= 0 && leftCloseness <= 1) {3983leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left3984}3985else if (rightCloseness >= 0 && rightCloseness <= 1) {3986leftVel = rightCloseness * this.scrollSpeed;3987}3988}39893990this.setScrollVel(topVel, leftVel);3991},399239933994// Sets the speed-of-scrolling for the scrollEl3995setScrollVel: function(topVel, leftVel) {39963997this.scrollTopVel = topVel;3998this.scrollLeftVel = leftVel;39994000this.constrainScrollVel(); // massages into realistic values40014002// if there is non-zero velocity, and an animation loop hasn't already started, then START4003if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) {4004this.scrollIntervalId = setInterval(4005$.proxy(this, 'scrollIntervalFunc'), // scope to `this`4006this.scrollIntervalMs4007);4008}4009},401040114012// Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way4013constrainScrollVel: function() {4014var el = this.scrollEl;40154016if (this.scrollTopVel < 0) { // scrolling up?4017if (el.scrollTop() <= 0) { // already scrolled all the way up?4018this.scrollTopVel = 0;4019}4020}4021else if (this.scrollTopVel > 0) { // scrolling down?4022if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) { // already scrolled all the way down?4023this.scrollTopVel = 0;4024}4025}40264027if (this.scrollLeftVel < 0) { // scrolling left?4028if (el.scrollLeft() <= 0) { // already scrolled all the left?4029this.scrollLeftVel = 0;4030}4031}4032else if (this.scrollLeftVel > 0) { // scrolling right?4033if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) { // already scrolled all the way right?4034this.scrollLeftVel = 0;4035}4036}4037},403840394040// This function gets called during every iteration of the scrolling animation loop4041scrollIntervalFunc: function() {4042var el = this.scrollEl;4043var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by40444045// change the value of scrollEl's scroll4046if (this.scrollTopVel) {4047el.scrollTop(el.scrollTop() + this.scrollTopVel * frac);4048}4049if (this.scrollLeftVel) {4050el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac);4051}40524053this.constrainScrollVel(); // since the scroll values changed, recompute the velocities40544055// if scrolled all the way, which causes the vels to be zero, stop the animation loop4056if (!this.scrollTopVel && !this.scrollLeftVel) {4057this.stopScrolling();4058}4059},406040614062// Kills any existing scrolling animation loop4063stopScrolling: function() {4064if (this.scrollIntervalId) {4065clearInterval(this.scrollIntervalId);4066this.scrollIntervalId = null;40674068// when all done with scrolling, recompute positions since they probably changed4069this.computeCoords();4070}4071},407240734074// Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce)4075scrollHandler: function() {4076// recompute all coordinates, but *only* if this is *not* part of our scrolling animation4077if (!this.scrollIntervalId) {4078this.computeCoords();4079}4080}40814082};408340844085// Returns `true` if the cells are identically equal. `false` otherwise.4086// They must have the same row, col, and be from the same grid.4087// Two null values will be considered equal, as two "out of the grid" states are the same.4088function isCellsEqual(cell1, cell2) {40894090if (!cell1 && !cell2) {4091return true;4092}40934094if (cell1 && cell2) {4095return cell1.grid === cell2.grid &&4096cell1.row === cell2.row &&4097cell1.col === cell2.col;4098}40994100return false;4101}41024103;;41044105/* Creates a clone of an element and lets it track the mouse as it moves4106----------------------------------------------------------------------------------------------------------------------*/41074108function MouseFollower(sourceEl, options) {4109this.options = options = options || {};4110this.sourceEl = sourceEl;4111this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent4112}411341144115MouseFollower.prototype = {41164117options: null,41184119sourceEl: null, // the element that will be cloned and made to look like it is dragging4120el: null, // the clone of `sourceEl` that will track the mouse4121parentEl: null, // the element that `el` (the clone) will be attached to41224123// the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl4124top0: null,4125left0: null,41264127// the initial position of the mouse4128mouseY0: null,4129mouseX0: null,41304131// the number of pixels the mouse has moved from its initial position4132topDelta: null,4133leftDelta: null,41344135mousemoveProxy: null, // document mousemove handler, bound to the MouseFollower's `this`41364137isFollowing: false,4138isHidden: false,4139isAnimating: false, // doing the revert animation?414041414142// Causes the element to start following the mouse4143start: function(ev) {4144if (!this.isFollowing) {4145this.isFollowing = true;41464147this.mouseY0 = ev.pageY;4148this.mouseX0 = ev.pageX;4149this.topDelta = 0;4150this.leftDelta = 0;41514152if (!this.isHidden) {4153this.updatePosition();4154}41554156$(document).on('mousemove', this.mousemoveProxy = $.proxy(this, 'mousemove'));4157}4158},415941604161// Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position.4162// `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately.4163stop: function(shouldRevert, callback) {4164var _this = this;4165var revertDuration = this.options.revertDuration;41664167function complete() {4168this.isAnimating = false;4169_this.destroyEl();41704171this.top0 = this.left0 = null; // reset state for future updatePosition calls41724173if (callback) {4174callback();4175}4176}41774178if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time4179this.isFollowing = false;41804181$(document).off('mousemove', this.mousemoveProxy);41824183if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation?4184this.isAnimating = true;4185this.el.animate({4186top: this.top0,4187left: this.left04188}, {4189duration: revertDuration,4190complete: complete4191});4192}4193else {4194complete();4195}4196}4197},419841994200// Gets the tracking element. Create it if necessary4201getEl: function() {4202var el = this.el;42034204if (!el) {4205this.sourceEl.width(); // hack to force IE8 to compute correct bounding box4206el = this.el = this.sourceEl.clone()4207.css({4208position: 'absolute',4209visibility: '', // in case original element was hidden (commonly through hideEvents())4210display: this.isHidden ? 'none' : '', // for when initially hidden4211margin: 0,4212right: 'auto', // erase and set width instead4213bottom: 'auto', // erase and set height instead4214width: this.sourceEl.width(), // explicit height in case there was a 'right' value4215height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value4216opacity: this.options.opacity || '',4217zIndex: this.options.zIndex4218})4219.appendTo(this.parentEl);4220}42214222return el;4223},422442254226// Removes the tracking element if it has already been created4227destroyEl: function() {4228if (this.el) {4229this.el.remove();4230this.el = null;4231}4232},423342344235// Update the CSS position of the tracking element4236updatePosition: function() {4237var sourceOffset;4238var origin;42394240this.getEl(); // ensure this.el42414242// make sure origin info was computed4243if (this.top0 === null) {4244this.sourceEl.width(); // hack to force IE8 to compute correct bounding box4245sourceOffset = this.sourceEl.offset();4246origin = this.el.offsetParent().offset();4247this.top0 = sourceOffset.top - origin.top;4248this.left0 = sourceOffset.left - origin.left;4249}42504251this.el.css({4252top: this.top0 + this.topDelta,4253left: this.left0 + this.leftDelta4254});4255},425642574258// Gets called when the user moves the mouse4259mousemove: function(ev) {4260this.topDelta = ev.pageY - this.mouseY0;4261this.leftDelta = ev.pageX - this.mouseX0;42624263if (!this.isHidden) {4264this.updatePosition();4265}4266},426742684269// Temporarily makes the tracking element invisible. Can be called before following starts4270hide: function() {4271if (!this.isHidden) {4272this.isHidden = true;4273if (this.el) {4274this.el.hide();4275}4276}4277},427842794280// Show the tracking element after it has been temporarily hidden4281show: function() {4282if (this.isHidden) {4283this.isHidden = false;4284this.updatePosition();4285this.getEl().show();4286}4287}42884289};42904291;;42924293/* A utility class for rendering <tr> rows.4294----------------------------------------------------------------------------------------------------------------------*/4295// It leverages methods of the subclass and the View to determine custom rendering behavior for each row "type"4296// (such as highlight rows, day rows, helper rows, etc).42974298function RowRenderer(view) {4299this.view = view;4300}430143024303RowRenderer.prototype = {43044305view: null, // a View object4306cellHtml: '<td/>', // plain default HTML used for a cell when no other is available430743084309// Renders the HTML for a row, leveraging custom cell-HTML-renderers based on the `rowType`.4310// Also applies the "intro" and "outro" cells, which are specified by the subclass and views.4311// `row` is an optional row number.4312rowHtml: function(rowType, row) {4313var view = this.view;4314var renderCell = this.getHtmlRenderer('cell', rowType);4315var cellHtml = '';4316var col;4317var date;43184319row = row || 0;43204321for (col = 0; col < view.colCnt; col++) {4322date = view.cellToDate(row, col);4323cellHtml += renderCell(row, col, date);4324}43254326cellHtml = this.bookendCells(cellHtml, rowType, row); // apply intro and outro43274328return '<tr>' + cellHtml + '</tr>';4329},433043314332// Applies the "intro" and "outro" HTML to the given cells.4333// Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro.4334// `cells` can be an HTML string of <td>'s or a jQuery <tr> element4335// `row` is an optional row number.4336bookendCells: function(cells, rowType, row) {4337var view = this.view;4338var intro = this.getHtmlRenderer('intro', rowType)(row || 0);4339var outro = this.getHtmlRenderer('outro', rowType)(row || 0);4340var isRTL = view.opt('isRTL');4341var prependHtml = isRTL ? outro : intro;4342var appendHtml = isRTL ? intro : outro;43434344if (typeof cells === 'string') {4345return prependHtml + cells + appendHtml;4346}4347else { // a jQuery <tr> element4348return cells.prepend(prependHtml).append(appendHtml);4349}4350},435143524353// Returns an HTML-rendering function given a specific `rendererName` (like cell, intro, or outro) and a specific4354// `rowType` (like day, eventSkeleton, helperSkeleton), which is optional.4355// If a renderer for the specific rowType doesn't exist, it will fall back to a generic renderer.4356// We will query the View object first for any custom rendering functions, then the methods of the subclass.4357getHtmlRenderer: function(rendererName, rowType) {4358var view = this.view;4359var generalName; // like "cellHtml"4360var specificName; // like "dayCellHtml". based on rowType4361var provider; // either the View or the RowRenderer subclass, whichever provided the method4362var renderer;43634364generalName = rendererName + 'Html';4365if (rowType) {4366specificName = rowType + capitaliseFirstLetter(rendererName) + 'Html';4367}43684369if (specificName && (renderer = view[specificName])) {4370provider = view;4371}4372else if (specificName && (renderer = this[specificName])) {4373provider = this;4374}4375else if ((renderer = view[generalName])) {4376provider = view;4377}4378else if ((renderer = this[generalName])) {4379provider = this;4380}43814382if (typeof renderer === 'function') {4383return function() {4384return renderer.apply(provider, arguments) || ''; // use correct `this` and always return a string4385};4386}43874388// the rendered can be a plain string as well. if not specified, always an empty string.4389return function() {4390return renderer || '';4391};4392}43934394};43954396;;43974398/* An abstract class comprised of a "grid" of cells that each represent a specific datetime4399----------------------------------------------------------------------------------------------------------------------*/44004401function Grid(view) {4402RowRenderer.call(this, view); // call the super-constructor4403this.coordMap = new GridCoordMap(this);4404this.elsByFill = {};4405}440644074408Grid.prototype = createObject(RowRenderer.prototype); // declare the super-class4409$.extend(Grid.prototype, {44104411el: null, // the containing element4412coordMap: null, // a GridCoordMap that converts pixel values to datetimes4413cellDuration: null, // a cell's duration. subclasses must assign this ASAP4414elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.441544164417// Renders the grid into the `el` element.4418// Subclasses should override and call this super-method when done.4419render: function() {4420this.bindHandlers();4421},442244234424// Called when the grid's resources need to be cleaned up4425destroy: function() {4426// subclasses can implement4427},442844294430/* Coordinates & Cells4431------------------------------------------------------------------------------------------------------------------*/443244334434// Populates the given empty arrays with the y and x coordinates of the cells4435buildCoords: function(rows, cols) {4436// subclasses must implement4437},443844394440// Given a cell object, returns the date for that cell4441getCellDate: function(cell) {4442// subclasses must implement4443},444444454446// Given a cell object, returns the element that represents the cell's whole-day4447getCellDayEl: function(cell) {4448// subclasses must implement4449},445044514452// Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects4453rangeToSegs: function(start, end) {4454// subclasses must implement4455},445644574458/* Handlers4459------------------------------------------------------------------------------------------------------------------*/446044614462// Attach handlers to `this.el`, using bubbling to listen to all ancestors.4463// We don't need to undo any of this in a "destroy" method, because the view will simply remove `this.el` from the4464// DOM and jQuery will be smart enough to garbage collect the handlers.4465bindHandlers: function() {4466var _this = this;44674468this.el.on('mousedown', function(ev) {4469if (4470!$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link4471!$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one)4472) {4473_this.dayMousedown(ev);4474}4475});44764477this.bindSegHandlers(); // attach event-element-related handlers. in Grid.events.js4478},447944804481// Process a mousedown on an element that represents a day. For day clicking and selecting.4482dayMousedown: function(ev) {4483var _this = this;4484var view = this.view;4485var calendar = view.calendar;4486var isSelectable = view.opt('selectable');4487var dates = null; // the inclusive dates of the selection. will be null if no selection4488var start; // the inclusive start of the selection4489var end; // the *exclusive* end of the selection4490var dayEl;44914492// this listener tracks a mousedown on a day element, and a subsequent drag.4493// if the drag ends on the same day, it is a 'dayClick'.4494// if 'selectable' is enabled, this listener also detects selections.4495var dragListener = new DragListener(this.coordMap, {4496//distance: 5, // needs more work if we want dayClick to fire correctly4497scroll: view.opt('dragScroll'),4498dragStart: function() {4499view.unselect(); // since we could be rendering a new selection, we want to clear any old one4500},4501cellOver: function(cell, date) {4502if (dragListener.origDate) { // click needs to have started on a cell45034504dayEl = _this.getCellDayEl(cell);45054506dates = [ date, dragListener.origDate ].sort(compareNumbers); // works with Moments4507start = dates[0];4508end = dates[1].clone().add(_this.cellDuration);45094510if (isSelectable) {4511if (calendar.isSelectionAllowedInRange(start, end)) { // allowed to select within this range?4512_this.renderSelection(start, end);4513}4514else {4515dates = null; // flag for an invalid selection4516disableCursor();4517}4518}4519}4520},4521cellOut: function(cell, date) {4522dates = null;4523_this.destroySelection();4524enableCursor();4525},4526listenStop: function(ev) {4527if (dates) { // started and ended on a cell?4528if (dates[0].isSame(dates[1])) {4529view.trigger('dayClick', dayEl[0], start, ev);4530}4531if (isSelectable) {4532// the selection will already have been rendered. just report it4533view.reportSelection(start, end, ev);4534}4535}4536enableCursor();4537}4538});45394540dragListener.mousedown(ev); // start listening, which will eventually initiate a dragStart4541},454245434544/* Event Dragging4545------------------------------------------------------------------------------------------------------------------*/454645474548// Renders a visual indication of a event being dragged over the given date(s).4549// `end` can be null, as well as `seg`. See View's documentation on renderDrag for more info.4550// A returned value of `true` signals that a mock "helper" event has been rendered.4551renderDrag: function(start, end, seg) {4552// subclasses must implement4553},455445554556// Unrenders a visual indication of an event being dragged4557destroyDrag: function() {4558// subclasses must implement4559},456045614562/* Event Resizing4563------------------------------------------------------------------------------------------------------------------*/456445654566// Renders a visual indication of an event being resized.4567// `start` and `end` are the updated dates of the event. `seg` is the original segment object involved in the drag.4568renderResize: function(start, end, seg) {4569// subclasses must implement4570},457145724573// Unrenders a visual indication of an event being resized.4574destroyResize: function() {4575// subclasses must implement4576},457745784579/* Event Helper4580------------------------------------------------------------------------------------------------------------------*/458145824583// Renders a mock event over the given date(s).4584// `end` can be null, in which case the mock event that is rendered will have a null end time.4585// `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging.4586renderRangeHelper: function(start, end, sourceSeg) {4587var view = this.view;4588var fakeEvent;45894590// compute the end time if forced to do so (this is what EventManager does)4591if (!end && view.opt('forceEventDuration')) {4592end = view.calendar.getDefaultEventEnd(!start.hasTime(), start);4593}45944595fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible4596fakeEvent.start = start;4597fakeEvent.end = end;4598fakeEvent.allDay = !(start.hasTime() || (end && end.hasTime())); // freshly compute allDay45994600// this extra className will be useful for differentiating real events from mock events in CSS4601fakeEvent.className = (fakeEvent.className || []).concat('fc-helper');46024603// if something external is being dragged in, don't render a resizer4604if (!sourceSeg) {4605fakeEvent.editable = false;4606}46074608this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering4609},461046114612// Renders a mock event4613renderHelper: function(event, sourceSeg) {4614// subclasses must implement4615},461646174618// Unrenders a mock event4619destroyHelper: function() {4620// subclasses must implement4621},462246234624/* Selection4625------------------------------------------------------------------------------------------------------------------*/462646274628// Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses.4629renderSelection: function(start, end) {4630this.renderHighlight(start, end);4631},463246334634// Unrenders any visual indications of a selection. Will unrender a highlight by default.4635destroySelection: function() {4636this.destroyHighlight();4637},463846394640/* Highlight4641------------------------------------------------------------------------------------------------------------------*/464246434644// Renders an emphasis on the given date range. `start` is inclusive. `end` is exclusive.4645renderHighlight: function(start, end) {4646this.renderFill('highlight', this.rangeToSegs(start, end));4647},464846494650// Unrenders the emphasis on a date range4651destroyHighlight: function() {4652this.destroyFill('highlight');4653},465446554656// Generates an array of classNames for rendering the highlight. Used by the fill system.4657highlightSegClasses: function() {4658return [ 'fc-highlight' ];4659},466046614662/* Fill System (highlight, background events, business hours)4663------------------------------------------------------------------------------------------------------------------*/466446654666// Renders a set of rectangles over the given segments of time.4667// Returns a subset of segs, the segs that were actually rendered.4668// Responsible for populating this.elsByFill4669renderFill: function(type, segs) {4670// subclasses must implement4671},467246734674// Unrenders a specific type of fill that is currently rendered on the grid4675destroyFill: function(type) {4676var el = this.elsByFill[type];46774678if (el) {4679el.remove();4680delete this.elsByFill[type];4681}4682},468346844685// Renders and assigns an `el` property for each fill segment. Generic enough to work with different types.4686// Only returns segments that successfully rendered.4687// To be harnessed by renderFill (implemented by subclasses).4688// Analagous to renderFgSegEls.4689renderFillSegEls: function(type, segs) {4690var _this = this;4691var segElMethod = this[type + 'SegEl'];4692var html = '';4693var renderedSegs = [];4694var i;46954696if (segs.length) {46974698// build a large concatenation of segment HTML4699for (i = 0; i < segs.length; i++) {4700html += this.fillSegHtml(type, segs[i]);4701}47024703// Grab individual elements from the combined HTML string. Use each as the default rendering.4704// Then, compute the 'el' for each segment.4705$(html).each(function(i, node) {4706var seg = segs[i];4707var el = $(node);47084709// allow custom filter methods per-type4710if (segElMethod) {4711el = segElMethod.call(_this, seg, el);4712}47134714if (el) { // custom filters did not cancel the render4715el = $(el); // allow custom filter to return raw DOM node47164717// correct element type? (would be bad if a non-TD were inserted into a table for example)4718if (el.is(_this.fillSegTag)) {4719seg.el = el;4720renderedSegs.push(seg);4721}4722}4723});4724}47254726return renderedSegs;4727},472847294730fillSegTag: 'div', // subclasses can override473147324733// Builds the HTML needed for one fill segment. Generic enought o work with different types.4734fillSegHtml: function(type, seg) {4735var classesMethod = this[type + 'SegClasses']; // custom hooks per-type4736var stylesMethod = this[type + 'SegStyles']; //4737var classes = classesMethod ? classesMethod.call(this, seg) : [];4738var styles = stylesMethod ? stylesMethod.call(this, seg) : ''; // a semi-colon separated CSS property string47394740return '<' + this.fillSegTag +4741(classes.length ? ' class="' + classes.join(' ') + '"' : '') +4742(styles ? ' style="' + styles + '"' : '') +4743' />';4744},474547464747/* Generic rendering utilities for subclasses4748------------------------------------------------------------------------------------------------------------------*/474947504751// Renders a day-of-week header row4752headHtml: function() {4753return '' +4754'<div class="fc-row ' + this.view.widgetHeaderClass + '">' +4755'<table>' +4756'<thead>' +4757this.rowHtml('head') + // leverages RowRenderer4758'</thead>' +4759'</table>' +4760'</div>';4761},476247634764// Used by the `headHtml` method, via RowRenderer, for rendering the HTML of a day-of-week header cell4765headCellHtml: function(row, col, date) {4766var view = this.view;4767var calendar = view.calendar;4768var colFormat = view.opt('columnFormat');47694770return '' +4771'<th class="fc-day-header ' + view.widgetHeaderClass + ' fc-' + dayIDs[date.day()] + '">' +4772htmlEscape(calendar.formatDate(date, colFormat)) +4773'</th>';4774},477547764777// Renders the HTML for a single-day background cell4778bgCellHtml: function(row, col, date) {4779var view = this.view;4780var classes = this.getDayClasses(date);47814782classes.unshift('fc-day', view.widgetContentClass);47834784return '<td class="' + classes.join(' ') + '" data-date="' + date.format() + '"></td>';4785},478647874788// Computes HTML classNames for a single-day cell4789getDayClasses: function(date) {4790var view = this.view;4791var today = view.calendar.getNow().stripTime();4792var classes = [ 'fc-' + dayIDs[date.day()] ];47934794if (4795view.name === 'month' &&4796date.month() != view.intervalStart.month()4797) {4798classes.push('fc-other-month');4799}48004801if (date.isSame(today, 'day')) {4802classes.push(4803'fc-today',4804view.highlightStateClass4805);4806}4807else if (date < today) {4808classes.push('fc-past');4809}4810else {4811classes.push('fc-future');4812}48134814return classes;4815}48164817});48184819;;48204821/* Event-rendering and event-interaction methods for the abstract Grid class4822----------------------------------------------------------------------------------------------------------------------*/48234824$.extend(Grid.prototype, {48254826mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing4827isDraggingSeg: false, // is a segment being dragged? boolean4828isResizingSeg: false, // is a segment being resized? boolean4829segs: null, // the event segments currently rendered in the grid483048314832// Renders the given events onto the grid4833renderEvents: function(events) {4834var segs = this.eventsToSegs(events);4835var bgSegs = [];4836var fgSegs = [];4837var i, seg;48384839for (i = 0; i < segs.length; i++) {4840seg = segs[i];48414842if (isBgEvent(seg.event)) {4843bgSegs.push(seg);4844}4845else {4846fgSegs.push(seg);4847}4848}48494850// Render each different type of segment.4851// Each function may return a subset of the segs, segs that were actually rendered.4852bgSegs = this.renderBgSegs(bgSegs) || bgSegs;4853fgSegs = this.renderFgSegs(fgSegs) || fgSegs;48544855this.segs = bgSegs.concat(fgSegs);4856},485748584859// Unrenders all events currently rendered on the grid4860destroyEvents: function() {4861this.triggerSegMouseout(); // trigger an eventMouseout if user's mouse is over an event48624863this.destroyFgSegs();4864this.destroyBgSegs();48654866this.segs = null;4867},486848694870// Retrieves all rendered segment objects currently rendered on the grid4871getSegs: function() {4872return this.segs || [];4873},487448754876/* Foreground Segment Rendering4877------------------------------------------------------------------------------------------------------------------*/487848794880// Renders foreground event segments onto the grid. May return a subset of segs that were rendered.4881renderFgSegs: function(segs) {4882// subclasses must implement4883},488448854886// Unrenders all currently rendered foreground segments4887destroyFgSegs: function() {4888// subclasses must implement4889},489048914892// Renders and assigns an `el` property for each foreground event segment.4893// Only returns segments that successfully rendered.4894// A utility that subclasses may use.4895renderFgSegEls: function(segs, disableResizing) {4896var view = this.view;4897var html = '';4898var renderedSegs = [];4899var i;49004901if (segs.length) { // don't build an empty html string49024903// build a large concatenation of event segment HTML4904for (i = 0; i < segs.length; i++) {4905html += this.fgSegHtml(segs[i], disableResizing);4906}49074908// Grab individual elements from the combined HTML string. Use each as the default rendering.4909// Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.4910$(html).each(function(i, node) {4911var seg = segs[i];4912var el = view.resolveEventEl(seg.event, $(node));49134914if (el) {4915el.data('fc-seg', seg); // used by handlers4916seg.el = el;4917renderedSegs.push(seg);4918}4919});4920}49214922return renderedSegs;4923},492449254926// Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls()4927fgSegHtml: function(seg, disableResizing) {4928// subclasses should implement4929},493049314932/* Background Segment Rendering4933------------------------------------------------------------------------------------------------------------------*/493449354936// Renders the given background event segments onto the grid.4937// Returns a subset of the segs that were actually rendered.4938renderBgSegs: function(segs) {4939return this.renderFill('bgEvent', segs);4940},494149424943// Unrenders all the currently rendered background event segments4944destroyBgSegs: function() {4945this.destroyFill('bgEvent');4946},494749484949// Renders a background event element, given the default rendering. Called by the fill system.4950bgEventSegEl: function(seg, el) {4951return this.view.resolveEventEl(seg.event, el); // will filter through eventRender4952},495349544955// Generates an array of classNames to be used for the default rendering of a background event.4956// Called by the fill system.4957bgEventSegClasses: function(seg) {4958var event = seg.event;4959var source = event.source || {};49604961return [ 'fc-bgevent' ].concat(4962event.className,4963source.className || []4964);4965},496649674968// Generates a semicolon-separated CSS string to be used for the default rendering of a background event.4969// Called by the fill system.4970// TODO: consolidate with getEventSkinCss?4971bgEventSegStyles: function(seg) {4972var view = this.view;4973var event = seg.event;4974var source = event.source || {};4975var eventColor = event.color;4976var sourceColor = source.color;4977var optionColor = view.opt('eventColor');4978var backgroundColor =4979event.backgroundColor ||4980eventColor ||4981source.backgroundColor ||4982sourceColor ||4983view.opt('eventBackgroundColor') ||4984optionColor;49854986if (backgroundColor) {4987return 'background-color:' + backgroundColor;4988}49894990return '';4991},499249934994// Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system.4995businessHoursSegClasses: function(seg) {4996return [ 'fc-nonbusiness', 'fc-bgevent' ];4997},499849995000/* Handlers5001------------------------------------------------------------------------------------------------------------------*/500250035004// Attaches event-element-related handlers to the container element and leverage bubbling5005bindSegHandlers: function() {5006var _this = this;5007var view = this.view;50085009$.each(5010{5011mouseenter: function(seg, ev) {5012_this.triggerSegMouseover(seg, ev);5013},5014mouseleave: function(seg, ev) {5015_this.triggerSegMouseout(seg, ev);5016},5017click: function(seg, ev) {5018return view.trigger('eventClick', this, seg.event, ev); // can return `false` to cancel5019},5020mousedown: function(seg, ev) {5021if ($(ev.target).is('.fc-resizer') && view.isEventResizable(seg.event)) {5022_this.segResizeMousedown(seg, ev);5023}5024else if (view.isEventDraggable(seg.event)) {5025_this.segDragMousedown(seg, ev);5026}5027}5028},5029function(name, func) {5030// attach the handler to the container element and only listen for real event elements via bubbling5031_this.el.on(name, '.fc-event-container > *', function(ev) {5032var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents50335034// only call the handlers if there is not a drag/resize in progress5035if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) {5036return func.call(this, seg, ev); // `this` will be the event element5037}5038});5039}5040);5041},504250435044// Updates internal state and triggers handlers for when an event element is moused over5045triggerSegMouseover: function(seg, ev) {5046if (!this.mousedOverSeg) {5047this.mousedOverSeg = seg;5048this.view.trigger('eventMouseover', seg.el[0], seg.event, ev);5049}5050},505150525053// Updates internal state and triggers handlers for when an event element is moused out.5054// Can be given no arguments, in which case it will mouseout the segment that was previously moused over.5055triggerSegMouseout: function(seg, ev) {5056ev = ev || {}; // if given no args, make a mock mouse event50575058if (this.mousedOverSeg) {5059seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment5060this.mousedOverSeg = null;5061this.view.trigger('eventMouseout', seg.el[0], seg.event, ev);5062}5063},506450655066/* Dragging5067------------------------------------------------------------------------------------------------------------------*/506850695070// Called when the user does a mousedown on an event, which might lead to dragging.5071// Generic enough to work with any type of Grid.5072segDragMousedown: function(seg, ev) {5073var _this = this;5074var view = this.view;5075var calendar = view.calendar;5076var el = seg.el;5077var event = seg.event;5078var newStart, newEnd;50795080// A clone of the original element that will move with the mouse5081var mouseFollower = new MouseFollower(seg.el, {5082parentEl: view.el,5083opacity: view.opt('dragOpacity'),5084revertDuration: view.opt('dragRevertDuration'),5085zIndex: 2 // one above the .fc-view5086});50875088// Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents5089// of the view.5090var dragListener = new DragListener(view.coordMap, {5091distance: 5,5092scroll: view.opt('dragScroll'),5093listenStart: function(ev) {5094mouseFollower.hide(); // don't show until we know this is a real drag5095mouseFollower.start(ev);5096},5097dragStart: function(ev) {5098_this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported5099_this.isDraggingSeg = true;5100view.hideEvent(event); // hide all event segments. our mouseFollower will take over5101view.trigger('eventDragStart', el[0], event, ev, {}); // last argument is jqui dummy5102},5103cellOver: function(cell, date) {5104var origDate = seg.cellDate || dragListener.origDate;5105var res = _this.computeDraggedEventDates(seg, origDate, date);5106newStart = res.start;5107newEnd = res.end;51085109if (calendar.isEventAllowedInRange(event, newStart, res.visibleEnd)) { // allowed to drop here?5110if (view.renderDrag(newStart, newEnd, seg)) { // have the view render a visual indication5111mouseFollower.hide(); // if the view is already using a mock event "helper", hide our own5112}5113else {5114mouseFollower.show();5115}5116}5117else {5118// have the helper follow the mouse (no snapping) with a warning-style cursor5119newStart = null; // mark an invalid drop date5120mouseFollower.show();5121disableCursor();5122}5123},5124cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells5125newStart = null;5126view.destroyDrag(); // unrender whatever was done in view.renderDrag5127mouseFollower.show(); // show in case we are moving out of all cells5128enableCursor();5129},5130dragStop: function(ev) {5131var hasChanged = newStart && !newStart.isSame(event.start);51325133// do revert animation if hasn't changed. calls a callback when finished (whether animation or not)5134mouseFollower.stop(!hasChanged, function() {5135_this.isDraggingSeg = false;5136view.destroyDrag();5137view.showEvent(event);5138view.trigger('eventDragStop', el[0], event, ev, {}); // last argument is jqui dummy51395140if (hasChanged) {5141view.eventDrop(el[0], event, newStart, ev); // will rerender all events...5142}5143});51445145enableCursor();5146},5147listenStop: function() {5148mouseFollower.stop(); // put in listenStop in case there was a mousedown but the drag never started5149}5150});51515152dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart5153},515451555156// Given a segment, the dates where a drag began and ended, calculates the Event Object's new start and end dates.5157// Might return a `null` end (even when forceEventDuration is on).5158computeDraggedEventDates: function(seg, dragStartDate, dropDate) {5159var view = this.view;5160var event = seg.event;5161var start = event.start;5162var end = view.calendar.getEventEnd(event);5163var delta;5164var newStart;5165var newEnd;5166var newAllDay;5167var visibleEnd;51685169if (dropDate.hasTime() === dragStartDate.hasTime()) {5170delta = dayishDiff(dropDate, dragStartDate);5171newStart = start.clone().add(delta);5172if (event.end === null) { // do we need to compute an end?5173newEnd = null;5174}5175else {5176newEnd = end.clone().add(delta);5177}5178newAllDay = event.allDay; // keep it the same5179}5180else {5181// if switching from day <-> timed, start should be reset to the dropped date, and the end cleared5182newStart = dropDate;5183newEnd = null; // end should be cleared5184newAllDay = !dropDate.hasTime();5185}51865187// compute what the end date will appear to be5188visibleEnd = newEnd || view.calendar.getDefaultEventEnd(newAllDay, newStart);51895190return { start: newStart, end: newEnd, visibleEnd: visibleEnd };5191},519251935194/* Resizing5195------------------------------------------------------------------------------------------------------------------*/519651975198// Called when the user does a mousedown on an event's resizer, which might lead to resizing.5199// Generic enough to work with any type of Grid.5200segResizeMousedown: function(seg, ev) {5201var _this = this;5202var view = this.view;5203var calendar = view.calendar;5204var el = seg.el;5205var event = seg.event;5206var start = event.start;5207var end = view.calendar.getEventEnd(event);5208var newEnd = null;5209var dragListener;52105211function destroy() { // resets the rendering to show the original event5212_this.destroyResize();5213view.showEvent(event);5214}52155216// Tracks mouse movement over the *grid's* coordinate map5217dragListener = new DragListener(this.coordMap, {5218distance: 5,5219scroll: view.opt('dragScroll'),5220dragStart: function(ev) {5221_this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported5222_this.isResizingSeg = true;5223view.trigger('eventResizeStart', el[0], event, ev, {}); // last argument is jqui dummy5224},5225cellOver: function(cell, date) {5226// compute the new end. don't allow it to go before the event's start5227if (date.isBefore(start)) { // allows comparing ambig to non-ambig5228date = start;5229}5230newEnd = date.clone().add(_this.cellDuration); // make it an exclusive end52315232if (calendar.isEventAllowedInRange(event, start, newEnd)) { // allowed to be resized here?5233if (newEnd.isSame(end)) {5234newEnd = null; // mark an invalid resize5235destroy();5236}5237else {5238_this.renderResize(start, newEnd, seg);5239view.hideEvent(event);5240}5241}5242else {5243newEnd = null; // mark an invalid resize5244destroy();5245disableCursor();5246}5247},5248cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells5249newEnd = null;5250destroy();5251enableCursor();5252},5253dragStop: function(ev) {5254_this.isResizingSeg = false;5255destroy();5256enableCursor();5257view.trigger('eventResizeStop', el[0], event, ev, {}); // last argument is jqui dummy52585259if (newEnd) {5260view.eventResize(el[0], event, newEnd, ev); // will rerender all events...5261}5262}5263});52645265dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart5266},526752685269/* Rendering Utils5270------------------------------------------------------------------------------------------------------------------*/527152725273// Generic utility for generating the HTML classNames for an event segment's element5274getSegClasses: function(seg, isDraggable, isResizable) {5275var event = seg.event;5276var classes = [5277'fc-event',5278seg.isStart ? 'fc-start' : 'fc-not-start',5279seg.isEnd ? 'fc-end' : 'fc-not-end'5280].concat(5281event.className,5282event.source ? event.source.className : []5283);52845285if (isDraggable) {5286classes.push('fc-draggable');5287}5288if (isResizable) {5289classes.push('fc-resizable');5290}52915292return classes;5293},529452955296// Utility for generating a CSS string with all the event skin-related properties5297getEventSkinCss: function(event) {5298var view = this.view;5299var source = event.source || {};5300var eventColor = event.color;5301var sourceColor = source.color;5302var optionColor = view.opt('eventColor');5303var backgroundColor =5304event.backgroundColor ||5305eventColor ||5306source.backgroundColor ||5307sourceColor ||5308view.opt('eventBackgroundColor') ||5309optionColor;5310var borderColor =5311event.borderColor ||5312eventColor ||5313source.borderColor ||5314sourceColor ||5315view.opt('eventBorderColor') ||5316optionColor;5317var textColor =5318event.textColor ||5319source.textColor ||5320view.opt('eventTextColor');5321var statements = [];5322if (backgroundColor) {5323statements.push('background-color:' + backgroundColor);5324}5325if (borderColor) {5326statements.push('border-color:' + borderColor);5327}5328if (textColor) {5329statements.push('color:' + textColor);5330}5331return statements.join(';');5332},533353345335/* Converting events -> ranges -> segs5336------------------------------------------------------------------------------------------------------------------*/533753385339// Converts an array of event objects into an array of event segment objects.5340// A custom `rangeToSegsFunc` may be given for arbitrarily slicing up events.5341eventsToSegs: function(events, rangeToSegsFunc) {5342var eventRanges = this.eventsToRanges(events);5343var segs = [];5344var i;53455346for (i = 0; i < eventRanges.length; i++) {5347segs.push.apply(5348segs,5349this.eventRangeToSegs(eventRanges[i], rangeToSegsFunc)5350);5351}53525353return segs;5354},535553565357// Converts an array of events into an array of "range" objects.5358// A "range" object is a plain object with start/end properties denoting the time it covers. Also an event property.5359// For "normal" events, this will be identical to the event's start/end, but for "inverse-background" events,5360// will create an array of ranges that span the time *not* covered by the given event.5361eventsToRanges: function(events) {5362var _this = this;5363var eventsById = groupEventsById(events);5364var ranges = [];53655366// group by ID so that related inverse-background events can be rendered together5367$.each(eventsById, function(id, eventGroup) {5368if (eventGroup.length) {5369ranges.push.apply(5370ranges,5371isInverseBgEvent(eventGroup[0]) ?5372_this.eventsToInverseRanges(eventGroup) :5373_this.eventsToNormalRanges(eventGroup)5374);5375}5376});53775378return ranges;5379},538053815382// Converts an array of "normal" events (not inverted rendering) into a parallel array of ranges5383eventsToNormalRanges: function(events) {5384var calendar = this.view.calendar;5385var ranges = [];5386var i, event;5387var eventStart, eventEnd;53885389for (i = 0; i < events.length; i++) {5390event = events[i];53915392// make copies and normalize by stripping timezone5393eventStart = event.start.clone().stripZone();5394eventEnd = calendar.getEventEnd(event).stripZone();53955396ranges.push({5397event: event,5398start: eventStart,5399end: eventEnd,5400eventStartMS: +eventStart,5401eventDurationMS: eventEnd - eventStart5402});5403}54045405return ranges;5406},540754085409// Converts an array of events, with inverse-background rendering, into an array of range objects.5410// The range objects will cover all the time NOT covered by the events.5411eventsToInverseRanges: function(events) {5412var view = this.view;5413var viewStart = view.start.clone().stripZone(); // normalize timezone5414var viewEnd = view.end.clone().stripZone(); // normalize timezone5415var normalRanges = this.eventsToNormalRanges(events); // will give us normalized dates we can use w/o copies5416var inverseRanges = [];5417var event0 = events[0]; // assign this to each range's `.event`5418var start = viewStart; // the end of the previous range. the start of the new range5419var i, normalRange;54205421// ranges need to be in order. required for our date-walking algorithm5422normalRanges.sort(compareNormalRanges);54235424for (i = 0; i < normalRanges.length; i++) {5425normalRange = normalRanges[i];54265427// add the span of time before the event (if there is any)5428if (normalRange.start > start) { // compare millisecond time (skip any ambig logic)5429inverseRanges.push({5430event: event0,5431start: start,5432end: normalRange.start5433});5434}54355436start = normalRange.end;5437}54385439// add the span of time after the last event (if there is any)5440if (start < viewEnd) { // compare millisecond time (skip any ambig logic)5441inverseRanges.push({5442event: event0,5443start: start,5444end: viewEnd5445});5446}54475448return inverseRanges;5449},545054515452// Slices the given event range into one or more segment objects.5453// A `rangeToSegsFunc` custom slicing function can be given.5454eventRangeToSegs: function(eventRange, rangeToSegsFunc) {5455var segs;5456var i, seg;54575458if (rangeToSegsFunc) {5459segs = rangeToSegsFunc(eventRange.start, eventRange.end);5460}5461else {5462segs = this.rangeToSegs(eventRange.start, eventRange.end); // defined by the subclass5463}54645465for (i = 0; i < segs.length; i++) {5466seg = segs[i];5467seg.event = eventRange.event;5468seg.eventStartMS = eventRange.eventStartMS;5469seg.eventDurationMS = eventRange.eventDurationMS;5470}54715472return segs;5473}54745475});547654775478/* Utilities5479----------------------------------------------------------------------------------------------------------------------*/548054815482function isBgEvent(event) { // returns true if background OR inverse-background5483var rendering = getEventRendering(event);5484return rendering === 'background' || rendering === 'inverse-background';5485}548654875488function isInverseBgEvent(event) {5489return getEventRendering(event) === 'inverse-background';5490}549154925493function getEventRendering(event) {5494return firstDefined((event.source || {}).rendering, event.rendering);5495}549654975498function groupEventsById(events) {5499var eventsById = {};5500var i, event;55015502for (i = 0; i < events.length; i++) {5503event = events[i];5504(eventsById[event._id] || (eventsById[event._id] = [])).push(event);5505}55065507return eventsById;5508}550955105511// A cmp function for determining which non-inverted "ranges" (see above) happen earlier5512function compareNormalRanges(range1, range2) {5513return range1.eventStartMS - range2.eventStartMS; // earlier ranges go first5514}551555165517// A cmp function for determining which segments should take visual priority5518// DOES NOT WORK ON INVERTED BACKGROUND EVENTS because they have no eventStartMS/eventDurationMS5519function compareSegs(seg1, seg2) {5520return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first5521seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first5522seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1)5523(seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title5524}552555265527;;55285529/* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week.5530----------------------------------------------------------------------------------------------------------------------*/55315532function DayGrid(view) {5533Grid.call(this, view); // call the super-constructor5534}553555365537DayGrid.prototype = createObject(Grid.prototype); // declare the super-class5538$.extend(DayGrid.prototype, {55395540numbersVisible: false, // should render a row for day/week numbers? manually set by the view5541cellDuration: moment.duration({ days: 1 }), // required for Grid.event.js. Each cell is always a single day5542bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid55435544rowEls: null, // set of fake row elements5545dayEls: null, // set of whole-day elements comprising the row's background5546helperEls: null, // set of cell skeleton elements for rendering the mock event "helper"554755485549// Renders the rows and columns into the component's `this.el`, which should already be assigned.5550// isRigid determins whether the individual rows should ignore the contents and be a constant height.5551// Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient.5552render: function(isRigid) {5553var view = this.view;5554var html = '';5555var row;55565557for (row = 0; row < view.rowCnt; row++) {5558html += this.dayRowHtml(row, isRigid);5559}5560this.el.html(html);55615562this.rowEls = this.el.find('.fc-row');5563this.dayEls = this.el.find('.fc-day');55645565// run all the day cells through the dayRender callback5566this.dayEls.each(function(i, node) {5567var date = view.cellToDate(Math.floor(i / view.colCnt), i % view.colCnt);5568view.trigger('dayRender', null, date, $(node));5569});55705571Grid.prototype.render.call(this); // call the super-method5572},557355745575destroy: function() {5576this.destroySegPopover();5577},557855795580// Generates the HTML for a single row. `row` is the row number.5581dayRowHtml: function(row, isRigid) {5582var view = this.view;5583var classes = [ 'fc-row', 'fc-week', view.widgetContentClass ];55845585if (isRigid) {5586classes.push('fc-rigid');5587}55885589return '' +5590'<div class="' + classes.join(' ') + '">' +5591'<div class="fc-bg">' +5592'<table>' +5593this.rowHtml('day', row) + // leverages RowRenderer. calls dayCellHtml()5594'</table>' +5595'</div>' +5596'<div class="fc-content-skeleton">' +5597'<table>' +5598(this.numbersVisible ?5599'<thead>' +5600this.rowHtml('number', row) + // leverages RowRenderer. View will define render method5601'</thead>' :5602''5603) +5604'</table>' +5605'</div>' +5606'</div>';5607},560856095610// Renders the HTML for a whole-day cell. Will eventually end up in the day-row's background.5611// We go through a 'day' row type instead of just doing a 'bg' row type so that the View can do custom rendering5612// specifically for whole-day rows, whereas a 'bg' might also be used for other purposes (TimeGrid bg for example).5613dayCellHtml: function(row, col, date) {5614return this.bgCellHtml(row, col, date);5615},561656175618/* Coordinates & Cells5619------------------------------------------------------------------------------------------------------------------*/562056215622// Populates the empty `rows` and `cols` arrays with coordinates of the cells. For CoordGrid.5623buildCoords: function(rows, cols) {5624var colCnt = this.view.colCnt;5625var e, n, p;56265627this.dayEls.slice(0, colCnt).each(function(i, _e) { // iterate the first row of day elements5628e = $(_e);5629n = e.offset().left;5630if (i) {5631p[1] = n;5632}5633p = [ n ];5634cols[i] = p;5635});5636p[1] = n + e.outerWidth();56375638this.rowEls.each(function(i, _e) {5639e = $(_e);5640n = e.offset().top;5641if (i) {5642p[1] = n;5643}5644p = [ n ];5645rows[i] = p;5646});5647p[1] = n + e.outerHeight() + this.bottomCoordPadding; // hack to extend hit area of last row5648},564956505651// Converts a cell to a date5652getCellDate: function(cell) {5653return this.view.cellToDate(cell); // leverages the View's cell system5654},565556565657// Gets the whole-day element associated with the cell5658getCellDayEl: function(cell) {5659return this.dayEls.eq(cell.row * this.view.colCnt + cell.col);5660},566156625663// Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects5664rangeToSegs: function(start, end) {5665return this.view.rangeToSegments(start, end); // leverages the View's cell system5666},566756685669/* Event Drag Visualization5670------------------------------------------------------------------------------------------------------------------*/567156725673// Renders a visual indication of an event hovering over the given date(s).5674// `end` can be null, as well as `seg`. See View's documentation on renderDrag for more info.5675// A returned value of `true` signals that a mock "helper" event has been rendered.5676renderDrag: function(start, end, seg) {5677var opacity;56785679// always render a highlight underneath5680this.renderHighlight(5681start,5682end || this.view.calendar.getDefaultEventEnd(true, start)5683);56845685// if a segment from the same calendar but another component is being dragged, render a helper event5686if (seg && !seg.el.closest(this.el).length) {56875688this.renderRangeHelper(start, end, seg);56895690opacity = this.view.opt('dragOpacity');5691if (opacity !== undefined) {5692this.helperEls.css('opacity', opacity);5693}56945695return true; // a helper has been rendered5696}5697},569856995700// Unrenders any visual indication of a hovering event5701destroyDrag: function() {5702this.destroyHighlight();5703this.destroyHelper();5704},570557065707/* Event Resize Visualization5708------------------------------------------------------------------------------------------------------------------*/570957105711// Renders a visual indication of an event being resized5712renderResize: function(start, end, seg) {5713this.renderHighlight(start, end);5714this.renderRangeHelper(start, end, seg);5715},571657175718// Unrenders a visual indication of an event being resized5719destroyResize: function() {5720this.destroyHighlight();5721this.destroyHelper();5722},572357245725/* Event Helper5726------------------------------------------------------------------------------------------------------------------*/572757285729// Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null.5730renderHelper: function(event, sourceSeg) {5731var helperNodes = [];5732var segs = this.eventsToSegs([ event ]);5733var rowStructs;57345735segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered5736rowStructs = this.renderSegRows(segs);57375738// inject each new event skeleton into each associated row5739this.rowEls.each(function(row, rowNode) {5740var rowEl = $(rowNode); // the .fc-row5741var skeletonEl = $('<div class="fc-helper-skeleton"><table/></div>'); // will be absolutely positioned5742var skeletonTop;57435744// If there is an original segment, match the top position. Otherwise, put it at the row's top level5745if (sourceSeg && sourceSeg.row === row) {5746skeletonTop = sourceSeg.el.position().top;5747}5748else {5749skeletonTop = rowEl.find('.fc-content-skeleton tbody').position().top;5750}57515752skeletonEl.css('top', skeletonTop)5753.find('table')5754.append(rowStructs[row].tbodyEl);57555756rowEl.append(skeletonEl);5757helperNodes.push(skeletonEl[0]);5758});57595760this.helperEls = $(helperNodes); // array -> jQuery set5761},576257635764// Unrenders any visual indication of a mock helper event5765destroyHelper: function() {5766if (this.helperEls) {5767this.helperEls.remove();5768this.helperEls = null;5769}5770},577157725773/* Fill System (highlight, background events, business hours)5774------------------------------------------------------------------------------------------------------------------*/577557765777fillSegTag: 'td', // override the default tag name577857795780// Renders a set of rectangles over the given segments of days.5781// Only returns segments that successfully rendered.5782renderFill: function(type, segs) {5783var nodes = [];5784var i, seg;5785var skeletonEl;57865787segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs57885789for (i = 0; i < segs.length; i++) {5790seg = segs[i];5791skeletonEl = this.renderFillRow(type, seg);5792this.rowEls.eq(seg.row).append(skeletonEl);5793nodes.push(skeletonEl[0]);5794}57955796this.elsByFill[type] = $(nodes);57975798return segs;5799},580058015802// Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered.5803renderFillRow: function(type, seg) {5804var colCnt = this.view.colCnt;5805var startCol = seg.leftCol;5806var endCol = seg.rightCol + 1;5807var skeletonEl;5808var trEl;58095810skeletonEl = $(5811'<div class="fc-' + type.toLowerCase() + '-skeleton">' +5812'<table><tr/></table>' +5813'</div>'5814);5815trEl = skeletonEl.find('tr');58165817if (startCol > 0) {5818trEl.append('<td colspan="' + startCol + '"/>');5819}58205821trEl.append(5822seg.el.attr('colspan', endCol - startCol)5823);58245825if (endCol < colCnt) {5826trEl.append('<td colspan="' + (colCnt - endCol) + '"/>');5827}58285829this.bookendCells(trEl, type);58305831return skeletonEl;5832}58335834});58355836;;58375838/* Event-rendering methods for the DayGrid class5839----------------------------------------------------------------------------------------------------------------------*/58405841$.extend(DayGrid.prototype, {58425843rowStructs: null, // an array of objects, each holding information about a row's foreground event-rendering584458455846// Unrenders all events currently rendered on the grid5847destroyEvents: function() {5848this.destroySegPopover(); // removes the "more.." events popover5849Grid.prototype.destroyEvents.apply(this, arguments); // calls the super-method5850},585158525853// Retrieves all rendered segment objects currently rendered on the grid5854getSegs: function() {5855return Grid.prototype.getSegs.call(this) // get the segments from the super-method5856.concat(this.popoverSegs || []); // append the segments from the "more..." popover5857},585858595860// Renders the given background event segments onto the grid5861renderBgSegs: function(segs) {58625863// don't render timed background events5864var allDaySegs = $.grep(segs, function(seg) {5865return seg.event.allDay;5866});58675868return Grid.prototype.renderBgSegs.call(this, allDaySegs); // call the super-method5869},587058715872// Renders the given foreground event segments onto the grid5873renderFgSegs: function(segs) {5874var rowStructs;58755876// render an `.el` on each seg5877// returns a subset of the segs. segs that were actually rendered5878segs = this.renderFgSegEls(segs);58795880rowStructs = this.rowStructs = this.renderSegRows(segs);58815882// append to each row's content skeleton5883this.rowEls.each(function(i, rowNode) {5884$(rowNode).find('.fc-content-skeleton > table').append(5885rowStructs[i].tbodyEl5886);5887});58885889return segs; // return only the segs that were actually rendered5890},589158925893// Unrenders all currently rendered foreground event segments5894destroyFgSegs: function() {5895var rowStructs = this.rowStructs || [];5896var rowStruct;58975898while ((rowStruct = rowStructs.pop())) {5899rowStruct.tbodyEl.remove();5900}59015902this.rowStructs = null;5903},590459055906// Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton.5907// Returns an array of rowStruct objects (see the bottom of `renderSegRow`).5908// PRECONDITION: each segment shoud already have a rendered and assigned `.el`5909renderSegRows: function(segs) {5910var rowStructs = [];5911var segRows;5912var row;59135914segRows = this.groupSegRows(segs); // group into nested arrays59155916// iterate each row of segment groupings5917for (row = 0; row < segRows.length; row++) {5918rowStructs.push(5919this.renderSegRow(row, segRows[row])5920);5921}59225923return rowStructs;5924},592559265927// Builds the HTML to be used for the default element for an individual segment5928fgSegHtml: function(seg, disableResizing) {5929var view = this.view;5930var isRTL = view.opt('isRTL');5931var event = seg.event;5932var isDraggable = view.isEventDraggable(event);5933var isResizable = !disableResizing && event.allDay && seg.isEnd && view.isEventResizable(event);5934var classes = this.getSegClasses(seg, isDraggable, isResizable);5935var skinCss = this.getEventSkinCss(event);5936var timeHtml = '';5937var titleHtml;59385939classes.unshift('fc-day-grid-event');59405941// Only display a timed events time if it is the starting segment5942if (!event.allDay && seg.isStart) {5943timeHtml = '<span class="fc-time">' + htmlEscape(view.getEventTimeText(event)) + '</span>';5944}59455946titleHtml =5947'<span class="fc-title">' +5948(htmlEscape(event.title || '') || ' ') + // we always want one line of height5949'</span>';59505951return '<a class="' + classes.join(' ') + '"' +5952(event.url ?5953' href="' + htmlEscape(event.url) + '"' :5954''5955) +5956(skinCss ?5957' style="' + skinCss + '"' :5958''5959) +5960'>' +5961'<div class="fc-content">' +5962(isRTL ?5963titleHtml + ' ' + timeHtml : // put a natural space in between5964timeHtml + ' ' + titleHtml //5965) +5966'</div>' +5967(isResizable ?5968'<div class="fc-resizer"/>' :5969''5970) +5971'</a>';5972},597359745975// Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains5976// the segments. Returns object with a bunch of internal data about how the render was calculated.5977renderSegRow: function(row, rowSegs) {5978var view = this.view;5979var colCnt = view.colCnt;5980var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels5981var levelCnt = Math.max(1, segLevels.length); // ensure at least one level5982var tbody = $('<tbody/>');5983var segMatrix = []; // lookup for which segments are rendered into which level+col cells5984var cellMatrix = []; // lookup for all <td> elements of the level+col matrix5985var loneCellMatrix = []; // lookup for <td> elements that only take up a single column5986var i, levelSegs;5987var col;5988var tr;5989var j, seg;5990var td;59915992// populates empty cells from the current column (`col`) to `endCol`5993function emptyCellsUntil(endCol) {5994while (col < endCol) {5995// try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell5996td = (loneCellMatrix[i - 1] || [])[col];5997if (td) {5998td.attr(5999'rowspan',6000parseInt(td.attr('rowspan') || 1, 10) + 16001);6002}6003else {6004td = $('<td/>');6005tr.append(td);6006}6007cellMatrix[i][col] = td;6008loneCellMatrix[i][col] = td;6009col++;6010}6011}60126013for (i = 0; i < levelCnt; i++) { // iterate through all levels6014levelSegs = segLevels[i];6015col = 0;6016tr = $('<tr/>');60176018segMatrix.push([]);6019cellMatrix.push([]);6020loneCellMatrix.push([]);60216022// levelCnt might be 1 even though there are no actual levels. protect against this.6023// this single empty row is useful for styling.6024if (levelSegs) {6025for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level6026seg = levelSegs[j];60276028emptyCellsUntil(seg.leftCol);60296030// create a container that occupies or more columns. append the event element.6031td = $('<td class="fc-event-container"/>').append(seg.el);6032if (seg.leftCol != seg.rightCol) {6033td.attr('colspan', seg.rightCol - seg.leftCol + 1);6034}6035else { // a single-column segment6036loneCellMatrix[i][col] = td;6037}60386039while (col <= seg.rightCol) {6040cellMatrix[i][col] = td;6041segMatrix[i][col] = seg;6042col++;6043}60446045tr.append(td);6046}6047}60486049emptyCellsUntil(colCnt); // finish off the row6050this.bookendCells(tr, 'eventSkeleton');6051tbody.append(tr);6052}60536054return { // a "rowStruct"6055row: row, // the row number6056tbodyEl: tbody,6057cellMatrix: cellMatrix,6058segMatrix: segMatrix,6059segLevels: segLevels,6060segs: rowSegs6061};6062},606360646065// Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels.6066buildSegLevels: function(segs) {6067var levels = [];6068var i, seg;6069var j;60706071// Give preference to elements with certain criteria, so they have6072// a chance to be closer to the top.6073segs.sort(compareSegs);60746075for (i = 0; i < segs.length; i++) {6076seg = segs[i];60776078// loop through levels, starting with the topmost, until the segment doesn't collide with other segments6079for (j = 0; j < levels.length; j++) {6080if (!isDaySegCollision(seg, levels[j])) {6081break;6082}6083}6084// `j` now holds the desired subrow index6085seg.level = j;60866087// create new level array if needed and append segment6088(levels[j] || (levels[j] = [])).push(seg);6089}60906091// order segments left-to-right. very important if calendar is RTL6092for (j = 0; j < levels.length; j++) {6093levels[j].sort(compareDaySegCols);6094}60956096return levels;6097},609860996100// Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row6101groupSegRows: function(segs) {6102var view = this.view;6103var segRows = [];6104var i;61056106for (i = 0; i < view.rowCnt; i++) {6107segRows.push([]);6108}61096110for (i = 0; i < segs.length; i++) {6111segRows[segs[i].row].push(segs[i]);6112}61136114return segRows;6115}61166117});611861196120// Computes whether two segments' columns collide. They are assumed to be in the same row.6121function isDaySegCollision(seg, otherSegs) {6122var i, otherSeg;61236124for (i = 0; i < otherSegs.length; i++) {6125otherSeg = otherSegs[i];61266127if (6128otherSeg.leftCol <= seg.rightCol &&6129otherSeg.rightCol >= seg.leftCol6130) {6131return true;6132}6133}61346135return false;6136}613761386139// A cmp function for determining the leftmost event6140function compareDaySegCols(a, b) {6141return a.leftCol - b.leftCol;6142}61436144;;61456146/* Methods relate to limiting the number events for a given day on a DayGrid6147----------------------------------------------------------------------------------------------------------------------*/6148// NOTE: all the segs being passed around in here are foreground segs61496150$.extend(DayGrid.prototype, {615161526153segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible6154popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible615561566157destroySegPopover: function() {6158if (this.segPopover) {6159this.segPopover.hide(); // will trigger destruction of `segPopover` and `popoverSegs`6160}6161},616261636164// Limits the number of "levels" (vertically stacking layers of events) for each row of the grid.6165// `levelLimit` can be false (don't limit), a number, or true (should be computed).6166limitRows: function(levelLimit) {6167var rowStructs = this.rowStructs || [];6168var row; // row #6169var rowLevelLimit;61706171for (row = 0; row < rowStructs.length; row++) {6172this.unlimitRow(row);61736174if (!levelLimit) {6175rowLevelLimit = false;6176}6177else if (typeof levelLimit === 'number') {6178rowLevelLimit = levelLimit;6179}6180else {6181rowLevelLimit = this.computeRowLevelLimit(row);6182}61836184if (rowLevelLimit !== false) {6185this.limitRow(row, rowLevelLimit);6186}6187}6188},618961906191// Computes the number of levels a row will accomodate without going outside its bounds.6192// Assumes the row is "rigid" (maintains a constant height regardless of what is inside).6193// `row` is the row number.6194computeRowLevelLimit: function(row) {6195var rowEl = this.rowEls.eq(row); // the containing "fake" row div6196var rowHeight = rowEl.height(); // TODO: cache somehow?6197var trEls = this.rowStructs[row].tbodyEl.children();6198var i, trEl;61996200// Reveal one level <tr> at a time and stop when we find one out of bounds6201for (i = 0; i < trEls.length; i++) {6202trEl = trEls.eq(i).removeClass('fc-limited'); // get and reveal6203if (trEl.position().top + trEl.outerHeight() > rowHeight) {6204return i;6205}6206}62076208return false; // should not limit at all6209},621062116212// Limits the given grid row to the maximum number of levels and injects "more" links if necessary.6213// `row` is the row number.6214// `levelLimit` is a number for the maximum (inclusive) number of levels allowed.6215limitRow: function(row, levelLimit) {6216var _this = this;6217var view = this.view;6218var rowStruct = this.rowStructs[row];6219var moreNodes = []; // array of "more" <a> links and <td> DOM nodes6220var col = 0; // col #6221var cell;6222var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right6223var cellMatrix; // a matrix (by level, then column) of all <td> jQuery elements in the row6224var limitedNodes; // array of temporarily hidden level <tr> and segment <td> DOM nodes6225var i, seg;6226var segsBelow; // array of segment objects below `seg` in the current `col`6227var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies6228var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column)6229var td, rowspan;6230var segMoreNodes; // array of "more" <td> cells that will stand-in for the current seg's cell6231var j;6232var moreTd, moreWrap, moreLink;62336234// Iterates through empty level cells and places "more" links inside if need be6235function emptyCellsUntil(endCol) { // goes from current `col` to `endCol`6236while (col < endCol) {6237cell = { row: row, col: col };6238segsBelow = _this.getCellSegs(cell, levelLimit);6239if (segsBelow.length) {6240td = cellMatrix[levelLimit - 1][col];6241moreLink = _this.renderMoreLink(cell, segsBelow);6242moreWrap = $('<div/>').append(moreLink);6243td.append(moreWrap);6244moreNodes.push(moreWrap[0]);6245}6246col++;6247}6248}62496250if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit?6251levelSegs = rowStruct.segLevels[levelLimit - 1];6252cellMatrix = rowStruct.cellMatrix;62536254limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level <tr> elements past the limit6255.addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array62566257// iterate though segments in the last allowable level6258for (i = 0; i < levelSegs.length; i++) {6259seg = levelSegs[i];6260emptyCellsUntil(seg.leftCol); // process empty cells before the segment62616262// determine *all* segments below `seg` that occupy the same columns6263colSegsBelow = [];6264totalSegsBelow = 0;6265while (col <= seg.rightCol) {6266cell = { row: row, col: col };6267segsBelow = this.getCellSegs(cell, levelLimit);6268colSegsBelow.push(segsBelow);6269totalSegsBelow += segsBelow.length;6270col++;6271}62726273if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links?6274td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell6275rowspan = td.attr('rowspan') || 1;6276segMoreNodes = [];62776278// make a replacement <td> for each column the segment occupies. will be one for each colspan6279for (j = 0; j < colSegsBelow.length; j++) {6280moreTd = $('<td class="fc-more-cell"/>').attr('rowspan', rowspan);6281segsBelow = colSegsBelow[j];6282cell = { row: row, col: seg.leftCol + j };6283moreLink = this.renderMoreLink(cell, [ seg ].concat(segsBelow)); // count seg as hidden too6284moreWrap = $('<div/>').append(moreLink);6285moreTd.append(moreWrap);6286segMoreNodes.push(moreTd[0]);6287moreNodes.push(moreTd[0]);6288}62896290td.addClass('fc-limited').after($(segMoreNodes)); // hide original <td> and inject replacements6291limitedNodes.push(td[0]);6292}6293}62946295emptyCellsUntil(view.colCnt); // finish off the level6296rowStruct.moreEls = $(moreNodes); // for easy undoing later6297rowStruct.limitedEls = $(limitedNodes); // for easy undoing later6298}6299},630063016302// Reveals all levels and removes all "more"-related elements for a grid's row.6303// `row` is a row number.6304unlimitRow: function(row) {6305var rowStruct = this.rowStructs[row];63066307if (rowStruct.moreEls) {6308rowStruct.moreEls.remove();6309rowStruct.moreEls = null;6310}63116312if (rowStruct.limitedEls) {6313rowStruct.limitedEls.removeClass('fc-limited');6314rowStruct.limitedEls = null;6315}6316},631763186319// Renders an <a> element that represents hidden event element for a cell.6320// Responsible for attaching click handler as well.6321renderMoreLink: function(cell, hiddenSegs) {6322var _this = this;6323var view = this.view;63246325return $('<a class="fc-more"/>')6326.text(6327this.getMoreLinkText(hiddenSegs.length)6328)6329.on('click', function(ev) {6330var clickOption = view.opt('eventLimitClick');6331var date = view.cellToDate(cell);6332var moreEl = $(this);6333var dayEl = _this.getCellDayEl(cell);6334var allSegs = _this.getCellSegs(cell);63356336// rescope the segments to be within the cell's date6337var reslicedAllSegs = _this.resliceDaySegs(allSegs, date);6338var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date);63396340if (typeof clickOption === 'function') {6341// the returned value can be an atomic option6342clickOption = view.trigger('eventLimitClick', null, {6343date: date,6344dayEl: dayEl,6345moreEl: moreEl,6346segs: reslicedAllSegs,6347hiddenSegs: reslicedHiddenSegs6348}, ev);6349}63506351if (clickOption === 'popover') {6352_this.showSegPopover(date, cell, moreEl, reslicedAllSegs);6353}6354else if (typeof clickOption === 'string') { // a view name6355view.calendar.zoomTo(date, clickOption);6356}6357});6358},635963606361// Reveals the popover that displays all events within a cell6362showSegPopover: function(date, cell, moreLink, segs) {6363var _this = this;6364var view = this.view;6365var moreWrap = moreLink.parent(); // the <div> wrapper around the <a>6366var topEl; // the element we want to match the top coordinate of6367var options;63686369if (view.rowCnt == 1) {6370topEl = this.view.el; // will cause the popover to cover any sort of header6371}6372else {6373topEl = this.rowEls.eq(cell.row); // will align with top of row6374}63756376options = {6377className: 'fc-more-popover',6378content: this.renderSegPopoverContent(date, segs),6379parentEl: this.el,6380top: topEl.offset().top,6381autoHide: true, // when the user clicks elsewhere, hide the popover6382viewportConstrain: view.opt('popoverViewportConstrain'),6383hide: function() {6384// destroy everything when the popover is hidden6385_this.segPopover.destroy();6386_this.segPopover = null;6387_this.popoverSegs = null;6388}6389};63906391// Determine horizontal coordinate.6392// We use the moreWrap instead of the <td> to avoid border confusion.6393if (view.opt('isRTL')) {6394options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border6395}6396else {6397options.left = moreWrap.offset().left - 1; // -1 to be over cell border6398}63996400this.segPopover = new Popover(options);6401this.segPopover.show();6402},640364046405// Builds the inner DOM contents of the segment popover6406renderSegPopoverContent: function(date, segs) {6407var view = this.view;6408var isTheme = view.opt('theme');6409var title = date.format(view.opt('dayPopoverFormat'));6410var content = $(6411'<div class="fc-header ' + view.widgetHeaderClass + '">' +6412'<span class="fc-close ' +6413(isTheme ? 'ui-icon ui-icon-closethick' : 'fc-icon fc-icon-x') +6414'"></span>' +6415'<span class="fc-title">' +6416htmlEscape(title) +6417'</span>' +6418'<div class="fc-clear"/>' +6419'</div>' +6420'<div class="fc-body ' + view.widgetContentClass + '">' +6421'<div class="fc-event-container"></div>' +6422'</div>'6423);6424var segContainer = content.find('.fc-event-container');6425var i;64266427// render each seg's `el` and only return the visible segs6428segs = this.renderFgSegEls(segs, true); // disableResizing=true6429this.popoverSegs = segs;64306431for (i = 0; i < segs.length; i++) {64326433// because segments in the popover are not part of a grid coordinate system, provide a hint to any6434// grids that want to do drag-n-drop about which cell it came from6435segs[i].cellDate = date;64366437segContainer.append(segs[i].el);6438}64396440return content;6441},644264436444// Given the events within an array of segment objects, reslice them to be in a single day6445resliceDaySegs: function(segs, dayDate) {64466447// build an array of the original events6448var events = $.map(segs, function(seg) {6449return seg.event;6450});64516452var dayStart = dayDate.clone().stripTime();6453var dayEnd = dayStart.clone().add(1, 'days');64546455// slice the events with a custom slicing function6456return this.eventsToSegs(6457events,6458function(rangeStart, rangeEnd) {6459var seg = intersectionToSeg(rangeStart, rangeEnd, dayStart, dayEnd); // if no intersection, undefined6460return seg ? [ seg ] : []; // must return an array of segments6461}6462);6463},646464656466// Generates the text that should be inside a "more" link, given the number of events it represents6467getMoreLinkText: function(num) {6468var view = this.view;6469var opt = view.opt('eventLimitText');64706471if (typeof opt === 'function') {6472return opt(num);6473}6474else {6475return '+' + num + ' ' + opt;6476}6477},647864796480// Returns segments within a given cell.6481// If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs.6482getCellSegs: function(cell, startLevel) {6483var segMatrix = this.rowStructs[cell.row].segMatrix;6484var level = startLevel || 0;6485var segs = [];6486var seg;64876488while (level < segMatrix.length) {6489seg = segMatrix[level][cell.col];6490if (seg) {6491segs.push(seg);6492}6493level++;6494}64956496return segs;6497}64986499});65006501;;65026503/* A component that renders one or more columns of vertical time slots6504----------------------------------------------------------------------------------------------------------------------*/65056506function TimeGrid(view) {6507Grid.call(this, view); // call the super-constructor6508}650965106511TimeGrid.prototype = createObject(Grid.prototype); // define the super-class6512$.extend(TimeGrid.prototype, {65136514slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines6515snapDuration: null, // granularity of time for dragging and selecting65166517minTime: null, // Duration object that denotes the first visible time of any given day6518maxTime: null, // Duration object that denotes the exclusive visible end time of any given day65196520dayEls: null, // cells elements in the day-row background6521slatEls: null, // elements running horizontally across all columns65226523slatTops: null, // an array of top positions, relative to the container. last item holds bottom of last slot65246525helperEl: null, // cell skeleton element for rendering the mock event "helper"65266527businessHourSegs: null,652865296530// Renders the time grid into `this.el`, which should already be assigned.6531// Relies on the view's colCnt. In the future, this component should probably be self-sufficient.6532render: function() {6533this.processOptions();65346535this.el.html(this.renderHtml());65366537this.dayEls = this.el.find('.fc-day');6538this.slatEls = this.el.find('.fc-slats tr');65396540this.computeSlatTops();65416542this.renderBusinessHours();65436544Grid.prototype.render.call(this); // call the super-method6545},654665476548renderBusinessHours: function() {6549var events = this.view.calendar.getBusinessHoursEvents();6550this.businessHourSegs = this.renderFill('businessHours', this.eventsToSegs(events), 'bgevent');6551},655265536554// Renders the basic HTML skeleton for the grid6555renderHtml: function() {6556return '' +6557'<div class="fc-bg">' +6558'<table>' +6559this.rowHtml('slotBg') + // leverages RowRenderer, which will call slotBgCellHtml6560'</table>' +6561'</div>' +6562'<div class="fc-slats">' +6563'<table>' +6564this.slatRowHtml() +6565'</table>' +6566'</div>';6567},656865696570// Renders the HTML for a vertical background cell behind the slots.6571// This method is distinct from 'bg' because we wanted a new `rowType` so the View could customize the rendering.6572slotBgCellHtml: function(row, col, date) {6573return this.bgCellHtml(row, col, date);6574},657565766577// Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL.6578slatRowHtml: function() {6579var view = this.view;6580var calendar = view.calendar;6581var isRTL = view.opt('isRTL');6582var html = '';6583var slotNormal = this.slotDuration.asMinutes() % 15 === 0;6584var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations6585var slotDate; // will be on the view's first day, but we only care about its time6586var minutes;6587var axisHtml;65886589// Calculate the time for each slot6590while (slotTime < this.maxTime) {6591slotDate = view.start.clone().time(slotTime); // will be in UTC but that's good. to avoid DST issues6592minutes = slotDate.minutes();65936594axisHtml =6595'<td class="fc-axis fc-time ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' +6596((!slotNormal || !minutes) ? // if irregular slot duration, or on the hour, then display the time6597'<span>' + // for matchCellWidths6598htmlEscape(calendar.formatDate(slotDate, view.opt('axisFormat'))) +6599'</span>' :6600''6601) +6602'</td>';66036604html +=6605'<tr ' + (!minutes ? '' : 'class="fc-minor"') + '>' +6606(!isRTL ? axisHtml : '') +6607'<td class="' + view.widgetContentClass + '"/>' +6608(isRTL ? axisHtml : '') +6609"</tr>";66106611slotTime.add(this.slotDuration);6612}66136614return html;6615},661666176618// Parses various options into properties of this object6619processOptions: function() {6620var view = this.view;6621var slotDuration = view.opt('slotDuration');6622var snapDuration = view.opt('snapDuration');66236624slotDuration = moment.duration(slotDuration);6625snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration;66266627this.slotDuration = slotDuration;6628this.snapDuration = snapDuration;6629this.cellDuration = snapDuration; // important to assign this for Grid.events.js66306631this.minTime = moment.duration(view.opt('minTime'));6632this.maxTime = moment.duration(view.opt('maxTime'));6633},663466356636// Slices up a date range into a segment for each column6637rangeToSegs: function(rangeStart, rangeEnd) {6638var view = this.view;6639var segs = [];6640var seg;6641var col;6642var cellDate;6643var colStart, colEnd;66446645// normalize6646rangeStart = rangeStart.clone().stripZone();6647rangeEnd = rangeEnd.clone().stripZone();66486649for (col = 0; col < view.colCnt; col++) {6650cellDate = view.cellToDate(0, col); // use the View's cell system for this6651colStart = cellDate.clone().time(this.minTime);6652colEnd = cellDate.clone().time(this.maxTime);6653seg = intersectionToSeg(rangeStart, rangeEnd, colStart, colEnd);6654if (seg) {6655seg.col = col;6656segs.push(seg);6657}6658}66596660return segs;6661},666266636664/* Coordinates6665------------------------------------------------------------------------------------------------------------------*/666666676668// Called when there is a window resize/zoom and we need to recalculate coordinates for the grid6669resize: function() {6670this.computeSlatTops();6671this.updateSegVerticals();6672},667366746675// Populates the given empty `rows` and `cols` arrays with offset positions of the "snap" cells.6676// "Snap" cells are different the slots because they might have finer granularity.6677buildCoords: function(rows, cols) {6678var colCnt = this.view.colCnt;6679var originTop = this.el.offset().top;6680var snapTime = moment.duration(+this.minTime);6681var p = null;6682var e, n;66836684this.dayEls.slice(0, colCnt).each(function(i, _e) {6685e = $(_e);6686n = e.offset().left;6687if (p) {6688p[1] = n;6689}6690p = [ n ];6691cols[i] = p;6692});6693p[1] = n + e.outerWidth();66946695p = null;6696while (snapTime < this.maxTime) {6697n = originTop + this.computeTimeTop(snapTime);6698if (p) {6699p[1] = n;6700}6701p = [ n ];6702rows.push(p);6703snapTime.add(this.snapDuration);6704}6705p[1] = originTop + this.computeTimeTop(snapTime); // the position of the exclusive end6706},670767086709// Gets the datetime for the given slot cell6710getCellDate: function(cell) {6711var view = this.view;6712var calendar = view.calendar;67136714return calendar.rezoneDate( // since we are adding a time, it needs to be in the calendar's timezone6715view.cellToDate(0, cell.col) // View's coord system only accounts for start-of-day for column6716.time(this.minTime + this.snapDuration * cell.row)6717);6718},671967206721// Gets the element that represents the whole-day the cell resides on6722getCellDayEl: function(cell) {6723return this.dayEls.eq(cell.col);6724},672567266727// Computes the top coordinate, relative to the bounds of the grid, of the given date.6728// A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.6729computeDateTop: function(date, startOfDayDate) {6730return this.computeTimeTop(6731moment.duration(6732date.clone().stripZone() - startOfDayDate.clone().stripTime()6733)6734);6735},673667376738// Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).6739computeTimeTop: function(time) {6740var slatCoverage = (time - this.minTime) / this.slotDuration; // floating-point value of # of slots covered6741var slatIndex;6742var slatRemainder;6743var slatTop;6744var slatBottom;67456746// constrain. because minTime/maxTime might be customized6747slatCoverage = Math.max(0, slatCoverage);6748slatCoverage = Math.min(this.slatEls.length, slatCoverage);67496750slatIndex = Math.floor(slatCoverage); // an integer index of the furthest whole slot6751slatRemainder = slatCoverage - slatIndex;6752slatTop = this.slatTops[slatIndex]; // the top position of the furthest whole slot67536754if (slatRemainder) { // time spans part-way into the slot6755slatBottom = this.slatTops[slatIndex + 1];6756return slatTop + (slatBottom - slatTop) * slatRemainder; // part-way between slots6757}6758else {6759return slatTop;6760}6761},676267636764// Queries each `slatEl` for its position relative to the grid's container and stores it in `slatTops`.6765// Includes the the bottom of the last slat as the last item in the array.6766computeSlatTops: function() {6767var tops = [];6768var top;67696770this.slatEls.each(function(i, node) {6771top = $(node).position().top;6772tops.push(top);6773});67746775tops.push(top + this.slatEls.last().outerHeight()); // bottom of the last slat67766777this.slatTops = tops;6778},677967806781/* Event Drag Visualization6782------------------------------------------------------------------------------------------------------------------*/678367846785// Renders a visual indication of an event being dragged over the specified date(s).6786// `end` and `seg` can be null. See View's documentation on renderDrag for more info.6787renderDrag: function(start, end, seg) {6788var opacity;67896790if (seg) { // if there is event information for this drag, render a helper event6791this.renderRangeHelper(start, end, seg);67926793opacity = this.view.opt('dragOpacity');6794if (opacity !== undefined) {6795this.helperEl.css('opacity', opacity);6796}67976798return true; // signal that a helper has been rendered6799}6800else {6801// otherwise, just render a highlight6802this.renderHighlight(6803start,6804end || this.view.calendar.getDefaultEventEnd(false, start)6805);6806}6807},680868096810// Unrenders any visual indication of an event being dragged6811destroyDrag: function() {6812this.destroyHelper();6813this.destroyHighlight();6814},681568166817/* Event Resize Visualization6818------------------------------------------------------------------------------------------------------------------*/681968206821// Renders a visual indication of an event being resized6822renderResize: function(start, end, seg) {6823this.renderRangeHelper(start, end, seg);6824},682568266827// Unrenders any visual indication of an event being resized6828destroyResize: function() {6829this.destroyHelper();6830},683168326833/* Event Helper6834------------------------------------------------------------------------------------------------------------------*/683568366837// Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag)6838renderHelper: function(event, sourceSeg) {6839var segs = this.eventsToSegs([ event ]);6840var tableEl;6841var i, seg;6842var sourceEl;68436844segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered6845tableEl = this.renderSegTable(segs);68466847// Try to make the segment that is in the same row as sourceSeg look the same6848for (i = 0; i < segs.length; i++) {6849seg = segs[i];6850if (sourceSeg && sourceSeg.col === seg.col) {6851sourceEl = sourceSeg.el;6852seg.el.css({6853left: sourceEl.css('left'),6854right: sourceEl.css('right'),6855'margin-left': sourceEl.css('margin-left'),6856'margin-right': sourceEl.css('margin-right')6857});6858}6859}68606861this.helperEl = $('<div class="fc-helper-skeleton"/>')6862.append(tableEl)6863.appendTo(this.el);6864},686568666867// Unrenders any mock helper event6868destroyHelper: function() {6869if (this.helperEl) {6870this.helperEl.remove();6871this.helperEl = null;6872}6873},687468756876/* Selection6877------------------------------------------------------------------------------------------------------------------*/687868796880// Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight.6881renderSelection: function(start, end) {6882if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered6883this.renderRangeHelper(start, end);6884}6885else {6886this.renderHighlight(start, end);6887}6888},688968906891// Unrenders any visual indication of a selection6892destroySelection: function() {6893this.destroyHelper();6894this.destroyHighlight();6895},689668976898/* Fill System (highlight, background events, business hours)6899------------------------------------------------------------------------------------------------------------------*/690069016902// Renders a set of rectangles over the given time segments.6903// Only returns segments that successfully rendered.6904renderFill: function(type, segs, className) {6905var view = this.view;6906var segCols;6907var skeletonEl;6908var trEl;6909var col, colSegs;6910var tdEl;6911var containerEl;6912var dayDate;6913var i, seg;69146915if (segs.length) {69166917segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs6918segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg69196920className = className || type.toLowerCase();6921skeletonEl = $(6922'<div class="fc-' + className + '-skeleton">' +6923'<table><tr/></table>' +6924'</div>'6925);6926trEl = skeletonEl.find('tr');69276928for (col = 0; col < segCols.length; col++) {6929colSegs = segCols[col];6930tdEl = $('<td/>').appendTo(trEl);69316932if (colSegs.length) {6933containerEl = $('<div class="fc-' + className + '-container"/>').appendTo(tdEl);6934dayDate = view.cellToDate(0, col);69356936for (i = 0; i < colSegs.length; i++) {6937seg = colSegs[i];6938containerEl.append(6939seg.el.css({6940top: this.computeDateTop(seg.start, dayDate),6941bottom: -this.computeDateTop(seg.end, dayDate) // the y position of the bottom edge6942})6943);6944}6945}6946}69476948this.bookendCells(trEl, type);69496950this.el.append(skeletonEl);6951this.elsByFill[type] = skeletonEl;6952}69536954return segs;6955}69566957});69586959;;69606961/* Event-rendering methods for the TimeGrid class6962----------------------------------------------------------------------------------------------------------------------*/69636964$.extend(TimeGrid.prototype, {69656966eventSkeletonEl: null, // has cells with event-containers, which contain absolutely positioned event elements696769686969// Renders the given foreground event segments onto the grid6970renderFgSegs: function(segs) {6971segs = this.renderFgSegEls(segs); // returns a subset of the segs. segs that were actually rendered69726973this.el.append(6974this.eventSkeletonEl = $('<div class="fc-content-skeleton"/>')6975.append(this.renderSegTable(segs))6976);69776978return segs; // return only the segs that were actually rendered6979},698069816982// Unrenders all currently rendered foreground event segments6983destroyFgSegs: function(segs) {6984if (this.eventSkeletonEl) {6985this.eventSkeletonEl.remove();6986this.eventSkeletonEl = null;6987}6988},698969906991// Renders and returns the <table> portion of the event-skeleton.6992// Returns an object with properties 'tbodyEl' and 'segs'.6993renderSegTable: function(segs) {6994var tableEl = $('<table><tr/></table>');6995var trEl = tableEl.find('tr');6996var segCols;6997var i, seg;6998var col, colSegs;6999var containerEl;70007001segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg70027003this.computeSegVerticals(segs); // compute and assign top/bottom70047005for (col = 0; col < segCols.length; col++) { // iterate each column grouping7006colSegs = segCols[col];7007placeSlotSegs(colSegs); // compute horizontal coordinates, z-index's, and reorder the array70087009containerEl = $('<div class="fc-event-container"/>');70107011// assign positioning CSS and insert into container7012for (i = 0; i < colSegs.length; i++) {7013seg = colSegs[i];7014seg.el.css(this.generateSegPositionCss(seg));70157016// if the height is short, add a className for alternate styling7017if (seg.bottom - seg.top < 30) {7018seg.el.addClass('fc-short');7019}70207021containerEl.append(seg.el);7022}70237024trEl.append($('<td/>').append(containerEl));7025}70267027this.bookendCells(trEl, 'eventSkeleton');70287029return tableEl;7030},703170327033// Refreshes the CSS top/bottom coordinates for each segment element. Probably after a window resize/zoom.7034// Repositions business hours segs too, so not just for events. Maybe shouldn't be here.7035updateSegVerticals: function() {7036var allSegs = (this.segs || []).concat(this.businessHourSegs || []);7037var i;70387039this.computeSegVerticals(allSegs);70407041for (i = 0; i < allSegs.length; i++) {7042allSegs[i].el.css(7043this.generateSegVerticalCss(allSegs[i])7044);7045}7046},704770487049// For each segment in an array, computes and assigns its top and bottom properties7050computeSegVerticals: function(segs) {7051var i, seg;70527053for (i = 0; i < segs.length; i++) {7054seg = segs[i];7055seg.top = this.computeDateTop(seg.start, seg.start);7056seg.bottom = this.computeDateTop(seg.end, seg.start);7057}7058},705970607061// Renders the HTML for a single event segment's default rendering7062fgSegHtml: function(seg, disableResizing) {7063var view = this.view;7064var event = seg.event;7065var isDraggable = view.isEventDraggable(event);7066var isResizable = !disableResizing && seg.isEnd && view.isEventResizable(event);7067var classes = this.getSegClasses(seg, isDraggable, isResizable);7068var skinCss = this.getEventSkinCss(event);7069var timeText;7070var fullTimeText; // more verbose time text. for the print stylesheet7071var startTimeText; // just the start time text70727073classes.unshift('fc-time-grid-event');70747075if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day...7076// Don't display time text on segments that run entirely through a day.7077// That would appear as midnight-midnight and would look dumb.7078// Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am)7079if (seg.isStart || seg.isEnd) {7080timeText = view.getEventTimeText(seg.start, seg.end);7081fullTimeText = view.getEventTimeText(seg.start, seg.end, 'LT');7082startTimeText = view.getEventTimeText(seg.start, null);7083}7084} else {7085// Display the normal time text for the *event's* times7086timeText = view.getEventTimeText(event);7087fullTimeText = view.getEventTimeText(event, 'LT');7088startTimeText = view.getEventTimeText(event.start, null);7089}70907091return '<a class="' + classes.join(' ') + '"' +7092(event.url ?7093' href="' + htmlEscape(event.url) + '"' :7094''7095) +7096(skinCss ?7097' style="' + skinCss + '"' :7098''7099) +7100'>' +7101'<div class="fc-content">' +7102(timeText ?7103'<div class="fc-time"' +7104' data-start="' + htmlEscape(startTimeText) + '"' +7105' data-full="' + htmlEscape(fullTimeText) + '"' +7106'>' +7107'<span>' + htmlEscape(timeText) + '</span>' +7108'</div>' :7109''7110) +7111(event.title ?7112'<div class="fc-title">' +7113htmlEscape(event.title) +7114'</div>' :7115''7116) +7117'</div>' +7118'<div class="fc-bg"/>' +7119(isResizable ?7120'<div class="fc-resizer"/>' :7121''7122) +7123'</a>';7124},712571267127// Generates an object with CSS properties/values that should be applied to an event segment element.7128// Contains important positioning-related properties that should be applied to any event element, customized or not.7129generateSegPositionCss: function(seg) {7130var view = this.view;7131var isRTL = view.opt('isRTL');7132var shouldOverlap = view.opt('slotEventOverlap');7133var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point7134var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point7135var props = this.generateSegVerticalCss(seg); // get top/bottom first7136var left; // amount of space from left edge, a fraction of the total width7137var right; // amount of space from right edge, a fraction of the total width71387139if (shouldOverlap) {7140// double the width, but don't go beyond the maximum forward coordinate (1.0)7141forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2);7142}71437144if (isRTL) {7145left = 1 - forwardCoord;7146right = backwardCoord;7147}7148else {7149left = backwardCoord;7150right = 1 - forwardCoord;7151}71527153props.zIndex = seg.level + 1; // convert from 0-base to 1-based7154props.left = left * 100 + '%';7155props.right = right * 100 + '%';71567157if (shouldOverlap && seg.forwardPressure) {7158// add padding to the edge so that forward stacked events don't cover the resizer's icon7159props[isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width7160}71617162return props;7163},716471657166// Generates an object with CSS properties for the top/bottom coordinates of a segment element7167generateSegVerticalCss: function(seg) {7168return {7169top: seg.top,7170bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container7171};7172},717371747175// Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col7176groupSegCols: function(segs) {7177var view = this.view;7178var segCols = [];7179var i;71807181for (i = 0; i < view.colCnt; i++) {7182segCols.push([]);7183}71847185for (i = 0; i < segs.length; i++) {7186segCols[segs[i].col].push(segs[i]);7187}71887189return segCols;7190}71917192});719371947195// Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each.7196// Also reorders the given array by date!7197function placeSlotSegs(segs) {7198var levels;7199var level0;7200var i;72017202segs.sort(compareSegs); // order by date7203levels = buildSlotSegLevels(segs);7204computeForwardSlotSegs(levels);72057206if ((level0 = levels[0])) {72077208for (i = 0; i < level0.length; i++) {7209computeSlotSegPressures(level0[i]);7210}72117212for (i = 0; i < level0.length; i++) {7213computeSlotSegCoords(level0[i], 0, 0);7214}7215}7216}721772187219// Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is7220// left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date.7221function buildSlotSegLevels(segs) {7222var levels = [];7223var i, seg;7224var j;72257226for (i=0; i<segs.length; i++) {7227seg = segs[i];72287229// go through all the levels and stop on the first level where there are no collisions7230for (j=0; j<levels.length; j++) {7231if (!computeSlotSegCollisions(seg, levels[j]).length) {7232break;7233}7234}72357236seg.level = j;72377238(levels[j] || (levels[j] = [])).push(seg);7239}72407241return levels;7242}724372447245// For every segment, figure out the other segments that are in subsequent7246// levels that also occupy the same vertical space. Accumulate in seg.forwardSegs7247function computeForwardSlotSegs(levels) {7248var i, level;7249var j, seg;7250var k;72517252for (i=0; i<levels.length; i++) {7253level = levels[i];72547255for (j=0; j<level.length; j++) {7256seg = level[j];72577258seg.forwardSegs = [];7259for (k=i+1; k<levels.length; k++) {7260computeSlotSegCollisions(seg, levels[k], seg.forwardSegs);7261}7262}7263}7264}726572667267// Figure out which path forward (via seg.forwardSegs) results in the longest path until7268// the furthest edge is reached. The number of segments in this path will be seg.forwardPressure7269function computeSlotSegPressures(seg) {7270var forwardSegs = seg.forwardSegs;7271var forwardPressure = 0;7272var i, forwardSeg;72737274if (seg.forwardPressure === undefined) { // not already computed72757276for (i=0; i<forwardSegs.length; i++) {7277forwardSeg = forwardSegs[i];72787279// figure out the child's maximum forward path7280computeSlotSegPressures(forwardSeg);72817282// either use the existing maximum, or use the child's forward pressure7283// plus one (for the forwardSeg itself)7284forwardPressure = Math.max(7285forwardPressure,72861 + forwardSeg.forwardPressure7287);7288}72897290seg.forwardPressure = forwardPressure;7291}7292}729372947295// Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range7296// from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and7297// seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.7298//7299// The segment might be part of a "series", which means consecutive segments with the same pressure7300// who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of7301// segments behind this one in the current series, and `seriesBackwardCoord` is the starting7302// coordinate of the first segment in the series.7303function computeSlotSegCoords(seg, seriesBackwardPressure, seriesBackwardCoord) {7304var forwardSegs = seg.forwardSegs;7305var i;73067307if (seg.forwardCoord === undefined) { // not already computed73087309if (!forwardSegs.length) {73107311// if there are no forward segments, this segment should butt up against the edge7312seg.forwardCoord = 1;7313}7314else {73157316// sort highest pressure first7317forwardSegs.sort(compareForwardSlotSegs);73187319// this segment's forwardCoord will be calculated from the backwardCoord of the7320// highest-pressure forward segment.7321computeSlotSegCoords(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord);7322seg.forwardCoord = forwardSegs[0].backwardCoord;7323}73247325// calculate the backwardCoord from the forwardCoord. consider the series7326seg.backwardCoord = seg.forwardCoord -7327(seg.forwardCoord - seriesBackwardCoord) / // available width for series7328(seriesBackwardPressure + 1); // # of segments in the series73297330// use this segment's coordinates to computed the coordinates of the less-pressurized7331// forward segments7332for (i=0; i<forwardSegs.length; i++) {7333computeSlotSegCoords(forwardSegs[i], 0, seg.forwardCoord);7334}7335}7336}733773387339// Find all the segments in `otherSegs` that vertically collide with `seg`.7340// Append into an optionally-supplied `results` array and return.7341function computeSlotSegCollisions(seg, otherSegs, results) {7342results = results || [];73437344for (var i=0; i<otherSegs.length; i++) {7345if (isSlotSegCollision(seg, otherSegs[i])) {7346results.push(otherSegs[i]);7347}7348}73497350return results;7351}735273537354// Do these segments occupy the same vertical space?7355function isSlotSegCollision(seg1, seg2) {7356return seg1.bottom > seg2.top && seg1.top < seg2.bottom;7357}735873597360// A cmp function for determining which forward segment to rely on more when computing coordinates.7361function compareForwardSlotSegs(seg1, seg2) {7362// put higher-pressure first7363return seg2.forwardPressure - seg1.forwardPressure ||7364// put segments that are closer to initial edge first (and favor ones with no coords yet)7365(seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) ||7366// do normal sorting...7367compareSegs(seg1, seg2);7368}73697370;;73717372/* An abstract class from which other views inherit from7373----------------------------------------------------------------------------------------------------------------------*/7374// Newer methods should be written as prototype methods, not in the monster `View` function at the bottom.73757376View.prototype = {73777378calendar: null, // owner Calendar object7379coordMap: null, // a CoordMap object for converting pixel regions to dates7380el: null, // the view's containing element. set by Calendar73817382// important Moments7383start: null, // the date of the very first cell7384end: null, // the date after the very last cell7385intervalStart: null, // the start of the interval of time the view represents (1st of month for month view)7386intervalEnd: null, // the exclusive end of the interval of time the view represents73877388// used for cell-to-date and date-to-cell calculations7389rowCnt: null, // # of weeks7390colCnt: null, // # of days displayed in a week73917392isSelected: false, // boolean whether cells are user-selected or not73937394// subclasses can optionally use a scroll container7395scrollerEl: null, // the element that will most likely scroll when content is too tall7396scrollTop: null, // cached vertical scroll value73977398// classNames styled by jqui themes7399widgetHeaderClass: null,7400widgetContentClass: null,7401highlightStateClass: null,74027403// document handlers, bound to `this` object7404documentMousedownProxy: null,7405documentDragStartProxy: null,740674077408// Serves as a "constructor" to suppliment the monster `View` constructor below7409init: function() {7410var tm = this.opt('theme') ? 'ui' : 'fc';74117412this.widgetHeaderClass = tm + '-widget-header';7413this.widgetContentClass = tm + '-widget-content';7414this.highlightStateClass = tm + '-state-highlight';74157416// save references to `this`-bound handlers7417this.documentMousedownProxy = $.proxy(this, 'documentMousedown');7418this.documentDragStartProxy = $.proxy(this, 'documentDragStart');7419},742074217422// Renders the view inside an already-defined `this.el`.7423// Subclasses should override this and then call the super method afterwards.7424render: function() {7425this.updateSize();7426this.trigger('viewRender', this, this, this.el);74277428// attach handlers to document. do it here to allow for destroy/rerender7429$(document)7430.on('mousedown', this.documentMousedownProxy)7431.on('dragstart', this.documentDragStartProxy); // jqui drag7432},743374347435// Clears all view rendering, event elements, and unregisters handlers7436destroy: function() {7437this.unselect();7438this.trigger('viewDestroy', this, this, this.el);7439this.destroyEvents();7440this.el.empty(); // removes inner contents but leaves the element intact74417442$(document)7443.off('mousedown', this.documentMousedownProxy)7444.off('dragstart', this.documentDragStartProxy);7445},744674477448// Used to determine what happens when the users clicks next/prev. Given -1 for prev, 1 for next.7449// Should apply the delta to `date` (a Moment) and return it.7450incrementDate: function(date, delta) {7451// subclasses should implement7452},745374547455/* Dimensions7456------------------------------------------------------------------------------------------------------------------*/745774587459// Refreshes anything dependant upon sizing of the container element of the grid7460updateSize: function(isResize) {7461if (isResize) {7462this.recordScroll();7463}7464this.updateHeight();7465this.updateWidth();7466},746774687469// Refreshes the horizontal dimensions of the calendar7470updateWidth: function() {7471// subclasses should implement7472},747374747475// Refreshes the vertical dimensions of the calendar7476updateHeight: function() {7477var calendar = this.calendar; // we poll the calendar for height information74787479this.setHeight(7480calendar.getSuggestedViewHeight(),7481calendar.isHeightAuto()7482);7483},748474857486// Updates the vertical dimensions of the calendar to the specified height.7487// if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height.7488setHeight: function(height, isAuto) {7489// subclasses should implement7490},749174927493// Given the total height of the view, return the number of pixels that should be used for the scroller.7494// Utility for subclasses.7495computeScrollerHeight: function(totalHeight) {7496var both = this.el.add(this.scrollerEl);7497var otherHeight; // cumulative height of everything that is not the scrollerEl in the view (header+borders)74987499// fuckin IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked7500both.css({7501position: 'relative', // cause a reflow, which will force fresh dimension recalculation7502left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll7503});7504otherHeight = this.el.outerHeight() - this.scrollerEl.height(); // grab the dimensions7505both.css({ position: '', left: '' }); // undo hack75067507return totalHeight - otherHeight;7508},750975107511// Called for remembering the current scroll value of the scroller.7512// Should be called before there is a destructive operation (like removing DOM elements) that might inadvertently7513// change the scroll of the container.7514recordScroll: function() {7515if (this.scrollerEl) {7516this.scrollTop = this.scrollerEl.scrollTop();7517}7518},751975207521// Set the scroll value of the scroller to the previously recorded value.7522// Should be called after we know the view's dimensions have been restored following some type of destructive7523// operation (like temporarily removing DOM elements).7524restoreScroll: function() {7525if (this.scrollTop !== null) {7526this.scrollerEl.scrollTop(this.scrollTop);7527}7528},752975307531/* Events7532------------------------------------------------------------------------------------------------------------------*/753375347535// Renders the events onto the view.7536// Should be overriden by subclasses. Subclasses should call the super-method afterwards.7537renderEvents: function(events) {7538this.segEach(function(seg) {7539this.trigger('eventAfterRender', seg.event, seg.event, seg.el);7540});7541this.trigger('eventAfterAllRender');7542},754375447545// Removes event elements from the view.7546// Should be overridden by subclasses. Should call this super-method FIRST, then subclass DOM destruction.7547destroyEvents: function() {7548this.segEach(function(seg) {7549this.trigger('eventDestroy', seg.event, seg.event, seg.el);7550});7551},755275537554// Given an event and the default element used for rendering, returns the element that should actually be used.7555// Basically runs events and elements through the eventRender hook.7556resolveEventEl: function(event, el) {7557var custom = this.trigger('eventRender', event, event, el);75587559if (custom === false) { // means don't render at all7560el = null;7561}7562else if (custom && custom !== true) {7563el = $(custom);7564}75657566return el;7567},756875697570// Hides all rendered event segments linked to the given event7571showEvent: function(event) {7572this.segEach(function(seg) {7573seg.el.css('visibility', '');7574}, event);7575},757675777578// Shows all rendered event segments linked to the given event7579hideEvent: function(event) {7580this.segEach(function(seg) {7581seg.el.css('visibility', 'hidden');7582}, event);7583},758475857586// Iterates through event segments. Goes through all by default.7587// If the optional `event` argument is specified, only iterates through segments linked to that event.7588// The `this` value of the callback function will be the view.7589segEach: function(func, event) {7590var segs = this.getSegs();7591var i;75927593for (i = 0; i < segs.length; i++) {7594if (!event || segs[i].event._id === event._id) {7595func.call(this, segs[i]);7596}7597}7598},759976007601// Retrieves all the rendered segment objects for the view7602getSegs: function() {7603// subclasses must implement7604},760576067607/* Event Drag Visualization7608------------------------------------------------------------------------------------------------------------------*/760976107611// Renders a visual indication of an event hovering over the specified date.7612// `end` is a Moment and might be null.7613// `seg` might be null. if specified, it is the segment object of the event being dragged.7614// otherwise, an external event from outside the calendar is being dragged.7615renderDrag: function(start, end, seg) {7616// subclasses should implement7617},761876197620// Unrenders a visual indication of event hovering7621destroyDrag: function() {7622// subclasses should implement7623},762476257626// Handler for accepting externally dragged events being dropped in the view.7627// Gets called when jqui's 'dragstart' is fired.7628documentDragStart: function(ev, ui) {7629var _this = this;7630var calendar = this.calendar;7631var eventStart = null; // a null value signals an unsuccessful drag7632var eventEnd = null;7633var visibleEnd = null; // will be calculated event when no eventEnd7634var el;7635var accept;7636var meta;7637var eventProps; // if an object, signals an event should be created upon drop7638var dragListener;76397640if (this.opt('droppable')) { // only listen if this setting is on7641el = $(ev.target);76427643// Test that the dragged element passes the dropAccept selector or filter function.7644// FYI, the default is "*" (matches all)7645accept = this.opt('dropAccept');7646if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) {76477648meta = getDraggedElMeta(el); // data for possibly creating an event7649eventProps = meta.eventProps;76507651// listener that tracks mouse movement over date-associated pixel regions7652dragListener = new DragListener(this.coordMap, {7653cellOver: function(cell, cellDate) {7654eventStart = cellDate;7655eventEnd = meta.duration ? eventStart.clone().add(meta.duration) : null;7656visibleEnd = eventEnd || calendar.getDefaultEventEnd(!eventStart.hasTime(), eventStart);76577658// keep the start/end up to date when dragging7659if (eventProps) {7660$.extend(eventProps, { start: eventStart, end: eventEnd });7661}76627663if (calendar.isExternalDragAllowedInRange(eventStart, visibleEnd, eventProps)) {7664_this.renderDrag(eventStart, visibleEnd);7665}7666else {7667eventStart = null; // signal unsuccessful7668disableCursor();7669}7670},7671cellOut: function() {7672eventStart = null;7673_this.destroyDrag();7674enableCursor();7675}7676});76777678// gets called, only once, when jqui drag is finished7679$(document).one('dragstop', function(ev, ui) {7680var renderedEvents;76817682_this.destroyDrag();7683enableCursor();76847685if (eventStart) { // element was dropped on a valid date/time cell76867687// if dropped on an all-day cell, and element's metadata specified a time, set it7688if (meta.startTime && !eventStart.hasTime()) {7689eventStart.time(meta.startTime);7690}76917692// trigger 'drop' regardless of whether element represents an event7693_this.trigger('drop', el[0], eventStart, ev, ui);76947695// create an event from the given properties and the latest dates7696if (eventProps) {7697renderedEvents = calendar.renderEvent(eventProps, meta.stick);7698_this.trigger('eventReceive', null, renderedEvents[0]); // signal an external event landed7699}7700}7701});77027703dragListener.startDrag(ev); // start listening immediately7704}7705}7706},770777087709/* Selection7710------------------------------------------------------------------------------------------------------------------*/771177127713// Selects a date range on the view. `start` and `end` are both Moments.7714// `ev` is the native mouse event that begin the interaction.7715select: function(start, end, ev) {7716this.unselect(ev);7717this.renderSelection(start, end);7718this.reportSelection(start, end, ev);7719},772077217722// Renders a visual indication of the selection7723renderSelection: function(start, end) {7724// subclasses should implement7725},772677277728// Called when a new selection is made. Updates internal state and triggers handlers.7729reportSelection: function(start, end, ev) {7730this.isSelected = true;7731this.trigger('select', null, start, end, ev);7732},773377347735// Undoes a selection. updates in the internal state and triggers handlers.7736// `ev` is the native mouse event that began the interaction.7737unselect: function(ev) {7738if (this.isSelected) {7739this.isSelected = false;7740this.destroySelection();7741this.trigger('unselect', null, ev);7742}7743},774477457746// Unrenders a visual indication of selection7747destroySelection: function() {7748// subclasses should implement7749},775077517752// Handler for unselecting when the user clicks something and the 'unselectAuto' setting is on7753documentMousedown: function(ev) {7754var ignore;77557756// is there a selection, and has the user made a proper left click?7757if (this.isSelected && this.opt('unselectAuto') && isPrimaryMouseButton(ev)) {77587759// only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element7760ignore = this.opt('unselectCancel');7761if (!ignore || !$(ev.target).closest(ignore).length) {7762this.unselect(ev);7763}7764}7765}77667767};776877697770// We are mixing JavaScript OOP design patterns here by putting methods and member variables in the closed scope of the7771// constructor. Going forward, methods should be part of the prototype.7772function View(calendar) {7773var t = this;77747775// exports7776t.calendar = calendar;7777t.opt = opt;7778t.trigger = trigger;7779t.isEventDraggable = isEventDraggable;7780t.isEventResizable = isEventResizable;7781t.eventDrop = eventDrop;7782t.eventResize = eventResize;77837784// imports7785var reportEventChange = calendar.reportEventChange;77867787// locals7788var options = calendar.options;7789var nextDayThreshold = moment.duration(options.nextDayThreshold);779077917792t.init(); // the "constructor" that concerns the prototype methods779377947795function opt(name) {7796var v = options[name];7797if ($.isPlainObject(v) && !isForcedAtomicOption(name)) {7798return smartProperty(v, t.name);7799}7800return v;7801}780278037804function trigger(name, thisObj) {7805return calendar.trigger.apply(7806calendar,7807[name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t])7808);7809}7810781178127813/* Event Editable Boolean Calculations7814------------------------------------------------------------------------------*/781578167817function isEventDraggable(event) {7818var source = event.source || {};78197820return firstDefined(7821event.startEditable,7822source.startEditable,7823opt('eventStartEditable'),7824event.editable,7825source.editable,7826opt('editable')7827);7828}782978307831function isEventResizable(event) {7832var source = event.source || {};78337834return firstDefined(7835event.durationEditable,7836source.durationEditable,7837opt('eventDurationEditable'),7838event.editable,7839source.editable,7840opt('editable')7841);7842}7843784478457846/* Event Elements7847------------------------------------------------------------------------------*/784878497850// Compute the text that should be displayed on an event's element.7851// Based off the settings of the view. Possible signatures:7852// .getEventTimeText(event, formatStr)7853// .getEventTimeText(startMoment, endMoment, formatStr)7854// .getEventTimeText(startMoment, null, formatStr)7855// `timeFormat` is used but the `formatStr` argument can be used to override.7856t.getEventTimeText = function(event, formatStr) {7857var start;7858var end;78597860if (typeof event === 'object' && typeof formatStr === 'object') {7861// first two arguments are actually moments (or null). shift arguments.7862start = event;7863end = formatStr;7864formatStr = arguments[2];7865}7866else {7867// otherwise, an event object was the first argument7868start = event.start;7869end = event.end;7870}78717872formatStr = formatStr || opt('timeFormat');78737874if (end && opt('displayEventEnd')) {7875return calendar.formatRange(start, end, formatStr);7876}7877else {7878return calendar.formatDate(start, formatStr);7879}7880};7881788278837884/* Event Modification Reporting7885---------------------------------------------------------------------------------*/788678877888function eventDrop(el, event, newStart, ev) {7889var mutateResult = calendar.mutateEvent(event, newStart, null);78907891trigger(7892'eventDrop',7893el,7894event,7895mutateResult.dateDelta,7896function() {7897mutateResult.undo();7898reportEventChange();7899},7900ev,7901{} // jqui dummy7902);79037904reportEventChange();7905}790679077908function eventResize(el, event, newEnd, ev) {7909var mutateResult = calendar.mutateEvent(event, null, newEnd);79107911trigger(7912'eventResize',7913el,7914event,7915mutateResult.durationDelta,7916function() {7917mutateResult.undo();7918reportEventChange();7919},7920ev,7921{} // jqui dummy7922);79237924reportEventChange();7925}792679277928// ====================================================================================================7929// Utilities for day "cells"7930// ====================================================================================================7931// The "basic" views are completely made up of day cells.7932// The "agenda" views have day cells at the top "all day" slot.7933// This was the obvious common place to put these utilities, but they should be abstracted out into7934// a more meaningful class (like DayEventRenderer).7935// ====================================================================================================793679377938// For determining how a given "cell" translates into a "date":7939//7940// 1. Convert the "cell" (row and column) into a "cell offset" (the # of the cell, cronologically from the first).7941// Keep in mind that column indices are inverted with isRTL. This is taken into account.7942//7943// 2. Convert the "cell offset" to a "day offset" (the # of days since the first visible day in the view).7944//7945// 3. Convert the "day offset" into a "date" (a Moment).7946//7947// The reverse transformation happens when transforming a date into a cell.794879497950// exports7951t.isHiddenDay = isHiddenDay;7952t.skipHiddenDays = skipHiddenDays;7953t.getCellsPerWeek = getCellsPerWeek;7954t.dateToCell = dateToCell;7955t.dateToDayOffset = dateToDayOffset;7956t.dayOffsetToCellOffset = dayOffsetToCellOffset;7957t.cellOffsetToCell = cellOffsetToCell;7958t.cellToDate = cellToDate;7959t.cellToCellOffset = cellToCellOffset;7960t.cellOffsetToDayOffset = cellOffsetToDayOffset;7961t.dayOffsetToDate = dayOffsetToDate;7962t.rangeToSegments = rangeToSegments;7963t.isMultiDayEvent = isMultiDayEvent;796479657966// internals7967var hiddenDays = opt('hiddenDays') || []; // array of day-of-week indices that are hidden7968var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)7969var cellsPerWeek;7970var dayToCellMap = []; // hash from dayIndex -> cellIndex, for one week7971var cellToDayMap = []; // hash from cellIndex -> dayIndex, for one week7972var isRTL = opt('isRTL');797379747975// initialize important internal variables7976(function() {79777978if (opt('weekends') === false) {7979hiddenDays.push(0, 6); // 0=sunday, 6=saturday7980}79817982// Loop through a hypothetical week and determine which7983// days-of-week are hidden. Record in both hashes (one is the reverse of the other).7984for (var dayIndex=0, cellIndex=0; dayIndex<7; dayIndex++) {7985dayToCellMap[dayIndex] = cellIndex;7986isHiddenDayHash[dayIndex] = $.inArray(dayIndex, hiddenDays) != -1;7987if (!isHiddenDayHash[dayIndex]) {7988cellToDayMap[cellIndex] = dayIndex;7989cellIndex++;7990}7991}79927993cellsPerWeek = cellIndex;7994if (!cellsPerWeek) {7995throw 'invalid hiddenDays'; // all days were hidden? bad.7996}79977998})();799980008001// Is the current day hidden?8002// `day` is a day-of-week index (0-6), or a Moment8003function isHiddenDay(day) {8004if (moment.isMoment(day)) {8005day = day.day();8006}8007return isHiddenDayHash[day];8008}800980108011function getCellsPerWeek() {8012return cellsPerWeek;8013}801480158016// Incrementing the current day until it is no longer a hidden day, returning a copy.8017// If the initial value of `date` is not a hidden day, don't do anything.8018// Pass `isExclusive` as `true` if you are dealing with an end date.8019// `inc` defaults to `1` (increment one day forward each time)8020function skipHiddenDays(date, inc, isExclusive) {8021var out = date.clone();8022inc = inc || 1;8023while (8024isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7]8025) {8026out.add(inc, 'days');8027}8028return out;8029}803080318032//8033// TRANSFORMATIONS: cell -> cell offset -> day offset -> date8034//80358036// cell -> date (combines all transformations)8037// Possible arguments:8038// - row, col8039// - { row:#, col: # }8040function cellToDate() {8041var cellOffset = cellToCellOffset.apply(null, arguments);8042var dayOffset = cellOffsetToDayOffset(cellOffset);8043var date = dayOffsetToDate(dayOffset);8044return date;8045}80468047// cell -> cell offset8048// Possible arguments:8049// - row, col8050// - { row:#, col:# }8051function cellToCellOffset(row, col) {8052var colCnt = t.colCnt;80538054// rtl variables. wish we could pre-populate these. but where?8055var dis = isRTL ? -1 : 1;8056var dit = isRTL ? colCnt - 1 : 0;80578058if (typeof row == 'object') {8059col = row.col;8060row = row.row;8061}8062var cellOffset = row * colCnt + (col * dis + dit); // column, adjusted for RTL (dis & dit)80638064return cellOffset;8065}80668067// cell offset -> day offset8068function cellOffsetToDayOffset(cellOffset) {8069var day0 = t.start.day(); // first date's day of week8070cellOffset += dayToCellMap[day0]; // normlize cellOffset to beginning-of-week8071return Math.floor(cellOffset / cellsPerWeek) * 7 + // # of days from full weeks8072cellToDayMap[ // # of days from partial last week8073(cellOffset % cellsPerWeek + cellsPerWeek) % cellsPerWeek // crazy math to handle negative cellOffsets8074] -8075day0; // adjustment for beginning-of-week normalization8076}80778078// day offset -> date8079function dayOffsetToDate(dayOffset) {8080return t.start.clone().add(dayOffset, 'days');8081}808280838084//8085// TRANSFORMATIONS: date -> day offset -> cell offset -> cell8086//80878088// date -> cell (combines all transformations)8089function dateToCell(date) {8090var dayOffset = dateToDayOffset(date);8091var cellOffset = dayOffsetToCellOffset(dayOffset);8092var cell = cellOffsetToCell(cellOffset);8093return cell;8094}80958096// date -> day offset8097function dateToDayOffset(date) {8098return date.clone().stripTime().diff(t.start, 'days');8099}81008101// day offset -> cell offset8102function dayOffsetToCellOffset(dayOffset) {8103var day0 = t.start.day(); // first date's day of week8104dayOffset += day0; // normalize dayOffset to beginning-of-week8105return Math.floor(dayOffset / 7) * cellsPerWeek + // # of cells from full weeks8106dayToCellMap[ // # of cells from partial last week8107(dayOffset % 7 + 7) % 7 // crazy math to handle negative dayOffsets8108] -8109dayToCellMap[day0]; // adjustment for beginning-of-week normalization8110}81118112// cell offset -> cell (object with row & col keys)8113function cellOffsetToCell(cellOffset) {8114var colCnt = t.colCnt;81158116// rtl variables. wish we could pre-populate these. but where?8117var dis = isRTL ? -1 : 1;8118var dit = isRTL ? colCnt - 1 : 0;81198120var row = Math.floor(cellOffset / colCnt);8121var col = ((cellOffset % colCnt + colCnt) % colCnt) * dis + dit; // column, adjusted for RTL (dis & dit)8122return {8123row: row,8124col: col8125};8126}812781288129//8130// Converts a date range into an array of segment objects.8131// "Segments" are horizontal stretches of time, sliced up by row.8132// A segment object has the following properties:8133// - row8134// - cols8135// - isStart8136// - isEnd8137//8138function rangeToSegments(start, end) {81398140var rowCnt = t.rowCnt;8141var colCnt = t.colCnt;8142var segments = []; // array of segments to return81438144// day offset for given date range8145var dayRange = computeDayRange(start, end); // convert to a whole-day range8146var rangeDayOffsetStart = dateToDayOffset(dayRange.start);8147var rangeDayOffsetEnd = dateToDayOffset(dayRange.end); // an exclusive value81488149// first and last cell offset for the given date range8150// "last" implies inclusivity8151var rangeCellOffsetFirst = dayOffsetToCellOffset(rangeDayOffsetStart);8152var rangeCellOffsetLast = dayOffsetToCellOffset(rangeDayOffsetEnd) - 1;81538154// loop through all the rows in the view8155for (var row=0; row<rowCnt; row++) {81568157// first and last cell offset for the row8158var rowCellOffsetFirst = row * colCnt;8159var rowCellOffsetLast = rowCellOffsetFirst + colCnt - 1;81608161// get the segment's cell offsets by constraining the range's cell offsets to the bounds of the row8162var segmentCellOffsetFirst = Math.max(rangeCellOffsetFirst, rowCellOffsetFirst);8163var segmentCellOffsetLast = Math.min(rangeCellOffsetLast, rowCellOffsetLast);81648165// make sure segment's offsets are valid and in view8166if (segmentCellOffsetFirst <= segmentCellOffsetLast) {81678168// translate to cells8169var segmentCellFirst = cellOffsetToCell(segmentCellOffsetFirst);8170var segmentCellLast = cellOffsetToCell(segmentCellOffsetLast);81718172// view might be RTL, so order by leftmost column8173var cols = [ segmentCellFirst.col, segmentCellLast.col ].sort(compareNumbers);81748175// Determine if segment's first/last cell is the beginning/end of the date range.8176// We need to compare "day offset" because "cell offsets" are often ambiguous and8177// can translate to multiple days, and an edge case reveals itself when we the8178// range's first cell is hidden (we don't want isStart to be true).8179var isStart = cellOffsetToDayOffset(segmentCellOffsetFirst) == rangeDayOffsetStart;8180var isEnd = cellOffsetToDayOffset(segmentCellOffsetLast) + 1 == rangeDayOffsetEnd;8181// +1 for comparing exclusively81828183segments.push({8184row: row,8185leftCol: cols[0],8186rightCol: cols[1],8187isStart: isStart,8188isEnd: isEnd8189});8190}8191}81928193return segments;8194}819581968197// Returns the date range of the full days the given range visually appears to occupy.8198// Returns object with properties `start` (moment) and `end` (moment, exclusive end).8199function computeDayRange(start, end) {8200var startDay = start.clone().stripTime(); // the beginning of the day the range starts8201var endDay;8202var endTimeMS;82038204if (end) {8205endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends8206endTimeMS = +end.time(); // # of milliseconds into `endDay`82078208// If the end time is actually inclusively part of the next day and is equal to or8209// beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.8210// Otherwise, leaving it as inclusive will cause it to exclude `endDay`.8211if (endTimeMS && endTimeMS >= nextDayThreshold) {8212endDay.add(1, 'days');8213}8214}82158216// If no end was specified, or if it is within `startDay` but not past nextDayThreshold,8217// assign the default duration of one day.8218if (!end || endDay <= startDay) {8219endDay = startDay.clone().add(1, 'days');8220}82218222return { start: startDay, end: endDay };8223}822482258226// Does the given event visually appear to occupy more than one day?8227function isMultiDayEvent(event) {8228var range = computeDayRange(event.start, event.end);82298230return range.end.diff(range.start, 'days') > 1;8231}82328233}823482358236/* Utils8237----------------------------------------------------------------------------------------------------------------------*/82388239// Require all HTML5 data-* attributes used by FullCalendar to have this prefix.8240// A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event.8241fc.dataAttrPrefix = '';82428243// Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure8244// to be used for Event Object creation.8245// A defined `.eventProps`, even when empty, indicates that an event should be created.8246function getDraggedElMeta(el) {8247var prefix = fc.dataAttrPrefix;8248var eventProps; // properties for creating the event, not related to date/time8249var startTime; // a Duration8250var duration;8251var stick;82528253if (prefix) { prefix += '-'; }8254eventProps = el.data(prefix + 'event') || null;82558256if (eventProps) {8257if (typeof eventProps === 'object') {8258eventProps = $.extend({}, eventProps); // make a copy8259}8260else { // something like 1 or true. still signal event creation8261eventProps = {};8262}82638264// pluck special-cased date/time properties8265startTime = eventProps.start;8266if (startTime == null) { startTime = eventProps.time; } // accept 'time' as well8267duration = eventProps.duration;8268stick = eventProps.stick;8269delete eventProps.start;8270delete eventProps.time;8271delete eventProps.duration;8272delete eventProps.stick;8273}82748275// fallback to standalone attribute values for each of the date/time properties8276if (startTime == null) { startTime = el.data(prefix + 'start'); }8277if (startTime == null) { startTime = el.data(prefix + 'time'); } // accept 'time' as well8278if (duration == null) { duration = el.data(prefix + 'duration'); }8279if (stick == null) { stick = el.data(prefix + 'stick'); }82808281// massage into correct data types8282startTime = startTime != null ? moment.duration(startTime) : null;8283duration = duration != null ? moment.duration(duration) : null;8284stick = Boolean(stick);82858286return { eventProps: eventProps, startTime: startTime, duration: duration, stick: stick };8287}82888289;;82908291/* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells.8292----------------------------------------------------------------------------------------------------------------------*/8293// It is a manager for a DayGrid subcomponent, which does most of the heavy lifting.8294// It is responsible for managing width/height.82958296function BasicView(calendar) {8297View.call(this, calendar); // call the super-constructor8298this.dayGrid = new DayGrid(this);8299this.coordMap = this.dayGrid.coordMap; // the view's date-to-cell mapping is identical to the subcomponent's8300}830183028303BasicView.prototype = createObject(View.prototype); // define the super-class8304$.extend(BasicView.prototype, {83058306dayGrid: null, // the main subcomponent that does most of the heavy lifting83078308dayNumbersVisible: false, // display day numbers on each day cell?8309weekNumbersVisible: false, // display week numbers along the side?83108311weekNumberWidth: null, // width of all the week-number cells running down the side83128313headRowEl: null, // the fake row element of the day-of-week header831483158316// Renders the view into `this.el`, which should already be assigned.8317// rowCnt, colCnt, and dayNumbersVisible have been calculated by a subclass and passed here.8318render: function(rowCnt, colCnt, dayNumbersVisible) {83198320// needed for cell-to-date and date-to-cell calculations in View8321this.rowCnt = rowCnt;8322this.colCnt = colCnt;83238324this.dayNumbersVisible = dayNumbersVisible;8325this.weekNumbersVisible = this.opt('weekNumbers');8326this.dayGrid.numbersVisible = this.dayNumbersVisible || this.weekNumbersVisible;83278328this.el.addClass('fc-basic-view').html(this.renderHtml());83298330this.headRowEl = this.el.find('thead .fc-row');83318332this.scrollerEl = this.el.find('.fc-day-grid-container');8333this.dayGrid.coordMap.containerEl = this.scrollerEl; // constrain clicks/etc to the dimensions of the scroller83348335this.dayGrid.el = this.el.find('.fc-day-grid');8336this.dayGrid.render(this.hasRigidRows());83378338View.prototype.render.call(this); // call the super-method8339},834083418342// Make subcomponents ready for cleanup8343destroy: function() {8344this.dayGrid.destroy();8345View.prototype.destroy.call(this); // call the super-method8346},834783488349// Builds the HTML skeleton for the view.8350// The day-grid component will render inside of a container defined by this HTML.8351renderHtml: function() {8352return '' +8353'<table>' +8354'<thead>' +8355'<tr>' +8356'<td class="' + this.widgetHeaderClass + '">' +8357this.dayGrid.headHtml() + // render the day-of-week headers8358'</td>' +8359'</tr>' +8360'</thead>' +8361'<tbody>' +8362'<tr>' +8363'<td class="' + this.widgetContentClass + '">' +8364'<div class="fc-day-grid-container">' +8365'<div class="fc-day-grid"/>' +8366'</div>' +8367'</td>' +8368'</tr>' +8369'</tbody>' +8370'</table>';8371},837283738374// Generates the HTML that will go before the day-of week header cells.8375// Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL.8376headIntroHtml: function() {8377if (this.weekNumbersVisible) {8378return '' +8379'<th class="fc-week-number ' + this.widgetHeaderClass + '" ' + this.weekNumberStyleAttr() + '>' +8380'<span>' + // needed for matchCellWidths8381htmlEscape(this.opt('weekNumberTitle')) +8382'</span>' +8383'</th>';8384}8385},838683878388// Generates the HTML that will go before content-skeleton cells that display the day/week numbers.8389// Queried by the DayGrid subcomponent. Ordering depends on isRTL.8390numberIntroHtml: function(row) {8391if (this.weekNumbersVisible) {8392return '' +8393'<td class="fc-week-number" ' + this.weekNumberStyleAttr() + '>' +8394'<span>' + // needed for matchCellWidths8395this.calendar.calculateWeekNumber(this.cellToDate(row, 0)) +8396'</span>' +8397'</td>';8398}8399},840084018402// Generates the HTML that goes before the day bg cells for each day-row.8403// Queried by the DayGrid subcomponent. Ordering depends on isRTL.8404dayIntroHtml: function() {8405if (this.weekNumbersVisible) {8406return '<td class="fc-week-number ' + this.widgetContentClass + '" ' +8407this.weekNumberStyleAttr() + '></td>';8408}8409},841084118412// Generates the HTML that goes before every other type of row generated by DayGrid. Ordering depends on isRTL.8413// Affects helper-skeleton and highlight-skeleton rows.8414introHtml: function() {8415if (this.weekNumbersVisible) {8416return '<td class="fc-week-number" ' + this.weekNumberStyleAttr() + '></td>';8417}8418},841984208421// Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton.8422// The number row will only exist if either day numbers or week numbers are turned on.8423numberCellHtml: function(row, col, date) {8424var classes;84258426if (!this.dayNumbersVisible) { // if there are week numbers but not day numbers8427return '<td/>'; // will create an empty space above events :(8428}84298430classes = this.dayGrid.getDayClasses(date);8431classes.unshift('fc-day-number');84328433return '' +8434'<td class="' + classes.join(' ') + '" data-date="' + date.format() + '">' +8435date.date() +8436'</td>';8437},843884398440// Generates an HTML attribute string for setting the width of the week number column, if it is known8441weekNumberStyleAttr: function() {8442if (this.weekNumberWidth !== null) {8443return 'style="width:' + this.weekNumberWidth + 'px"';8444}8445return '';8446},844784488449// Determines whether each row should have a constant height8450hasRigidRows: function() {8451var eventLimit = this.opt('eventLimit');8452return eventLimit && typeof eventLimit !== 'number';8453},845484558456/* Dimensions8457------------------------------------------------------------------------------------------------------------------*/845884598460// Refreshes the horizontal dimensions of the view8461updateWidth: function() {8462if (this.weekNumbersVisible) {8463// Make sure all week number cells running down the side have the same width.8464// Record the width for cells created later.8465this.weekNumberWidth = matchCellWidths(8466this.el.find('.fc-week-number')8467);8468}8469},847084718472// Adjusts the vertical dimensions of the view to the specified values8473setHeight: function(totalHeight, isAuto) {8474var eventLimit = this.opt('eventLimit');8475var scrollerHeight;84768477// reset all heights to be natural8478unsetScroller(this.scrollerEl);8479uncompensateScroll(this.headRowEl);84808481this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed84828483// is the event limit a constant level number?8484if (eventLimit && typeof eventLimit === 'number') {8485this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after8486}84878488scrollerHeight = this.computeScrollerHeight(totalHeight);8489this.setGridHeight(scrollerHeight, isAuto);84908491// is the event limit dynamically calculated?8492if (eventLimit && typeof eventLimit !== 'number') {8493this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set8494}84958496if (!isAuto && setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars?84978498compensateScroll(this.headRowEl, getScrollbarWidths(this.scrollerEl));84998500// doing the scrollbar compensation might have created text overflow which created more height. redo8501scrollerHeight = this.computeScrollerHeight(totalHeight);8502this.scrollerEl.height(scrollerHeight);85038504this.restoreScroll();8505}8506},850785088509// Sets the height of just the DayGrid component in this view8510setGridHeight: function(height, isAuto) {8511if (isAuto) {8512undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding8513}8514else {8515distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows8516}8517},851885198520/* Events8521------------------------------------------------------------------------------------------------------------------*/852285238524// Renders the given events onto the view and populates the segments array8525renderEvents: function(events) {8526this.dayGrid.renderEvents(events);85278528this.updateHeight(); // must compensate for events that overflow the row85298530View.prototype.renderEvents.call(this, events); // call the super-method8531},853285338534// Retrieves all segment objects that are rendered in the view8535getSegs: function() {8536return this.dayGrid.getSegs();8537},853885398540// Unrenders all event elements and clears internal segment data8541destroyEvents: function() {8542View.prototype.destroyEvents.call(this); // do this before dayGrid's segs have been cleared85438544this.recordScroll(); // removing events will reduce height and mess with the scroll, so record beforehand8545this.dayGrid.destroyEvents();85468547// we DON'T need to call updateHeight() because:8548// A) a renderEvents() call always happens after this, which will eventually call updateHeight()8549// B) in IE8, this causes a flash whenever events are rerendered8550},855185528553/* Event Dragging8554------------------------------------------------------------------------------------------------------------------*/855585568557// Renders a visual indication of an event being dragged over the view.8558// A returned value of `true` signals that a mock "helper" event has been rendered.8559renderDrag: function(start, end, seg) {8560return this.dayGrid.renderDrag(start, end, seg);8561},856285638564// Unrenders the visual indication of an event being dragged over the view8565destroyDrag: function() {8566this.dayGrid.destroyDrag();8567},856885698570/* Selection8571------------------------------------------------------------------------------------------------------------------*/857285738574// Renders a visual indication of a selection8575renderSelection: function(start, end) {8576this.dayGrid.renderSelection(start, end);8577},857885798580// Unrenders a visual indications of a selection8581destroySelection: function() {8582this.dayGrid.destroySelection();8583}85848585});85868587;;85888589/* A month view with day cells running in rows (one-per-week) and columns8590----------------------------------------------------------------------------------------------------------------------*/85918592setDefaults({8593fixedWeekCount: true8594});85958596fcViews.month = MonthView; // register the view85978598function MonthView(calendar) {8599BasicView.call(this, calendar); // call the super-constructor8600}860186028603MonthView.prototype = createObject(BasicView.prototype); // define the super-class8604$.extend(MonthView.prototype, {86058606name: 'month',860786088609incrementDate: function(date, delta) {8610return date.clone().stripTime().add(delta, 'months').startOf('month');8611},861286138614render: function(date) {8615var rowCnt;86168617this.intervalStart = date.clone().stripTime().startOf('month');8618this.intervalEnd = this.intervalStart.clone().add(1, 'months');86198620this.start = this.intervalStart.clone();8621this.start = this.skipHiddenDays(this.start); // move past the first week if no visible days8622this.start.startOf('week');8623this.start = this.skipHiddenDays(this.start); // move past the first invisible days of the week86248625this.end = this.intervalEnd.clone();8626this.end = this.skipHiddenDays(this.end, -1, true); // move in from the last week if no visible days8627this.end.add((7 - this.end.weekday()) % 7, 'days'); // move to end of week if not already8628this.end = this.skipHiddenDays(this.end, -1, true); // move in from the last invisible days of the week86298630rowCnt = Math.ceil( // need to ceil in case there are hidden days8631this.end.diff(this.start, 'weeks', true) // returnfloat=true8632);8633if (this.isFixedWeeks()) {8634this.end.add(6 - rowCnt, 'weeks');8635rowCnt = 6;8636}86378638this.title = this.calendar.formatDate(this.intervalStart, this.opt('titleFormat'));86398640BasicView.prototype.render.call(this, rowCnt, this.getCellsPerWeek(), true); // call the super-method8641},864286438644// Overrides the default BasicView behavior to have special multi-week auto-height logic8645setGridHeight: function(height, isAuto) {86468647isAuto = isAuto || this.opt('weekMode') === 'variable'; // LEGACY: weekMode is deprecated86488649// if auto, make the height of each row the height that it would be if there were 6 weeks8650if (isAuto) {8651height *= this.rowCnt / 6;8652}86538654distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows8655},865686578658isFixedWeeks: function() {8659var weekMode = this.opt('weekMode'); // LEGACY: weekMode is deprecated8660if (weekMode) {8661return weekMode === 'fixed'; // if any other type of weekMode, assume NOT fixed8662}86638664return this.opt('fixedWeekCount');8665}86668667});86688669;;86708671/* A week view with simple day cells running horizontally8672----------------------------------------------------------------------------------------------------------------------*/8673// TODO: a WeekView mixin for calculating dates and titles86748675fcViews.basicWeek = BasicWeekView; // register this view86768677function BasicWeekView(calendar) {8678BasicView.call(this, calendar); // call the super-constructor8679}868086818682BasicWeekView.prototype = createObject(BasicView.prototype); // define the super-class8683$.extend(BasicWeekView.prototype, {86848685name: 'basicWeek',868686878688incrementDate: function(date, delta) {8689return date.clone().stripTime().add(delta, 'weeks').startOf('week');8690},869186928693render: function(date) {86948695this.intervalStart = date.clone().stripTime().startOf('week');8696this.intervalEnd = this.intervalStart.clone().add(1, 'weeks');86978698this.start = this.skipHiddenDays(this.intervalStart);8699this.end = this.skipHiddenDays(this.intervalEnd, -1, true);87008701this.title = this.calendar.formatRange(8702this.start,8703this.end.clone().subtract(1), // make inclusive by subtracting 1 ms8704this.opt('titleFormat'),8705' \u2014 ' // emphasized dash8706);87078708BasicView.prototype.render.call(this, 1, this.getCellsPerWeek(), false); // call the super-method8709}87108711});8712;;87138714/* A view with a single simple day cell8715----------------------------------------------------------------------------------------------------------------------*/87168717fcViews.basicDay = BasicDayView; // register this view87188719function BasicDayView(calendar) {8720BasicView.call(this, calendar); // call the super-constructor8721}872287238724BasicDayView.prototype = createObject(BasicView.prototype); // define the super-class8725$.extend(BasicDayView.prototype, {87268727name: 'basicDay',872887298730incrementDate: function(date, delta) {8731var out = date.clone().stripTime().add(delta, 'days');8732out = this.skipHiddenDays(out, delta < 0 ? -1 : 1);8733return out;8734},873587368737render: function(date) {87388739this.start = this.intervalStart = date.clone().stripTime();8740this.end = this.intervalEnd = this.start.clone().add(1, 'days');87418742this.title = this.calendar.formatDate(this.start, this.opt('titleFormat'));87438744BasicView.prototype.render.call(this, 1, 1, false); // call the super-method8745}87468747});8748;;87498750/* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically.8751----------------------------------------------------------------------------------------------------------------------*/8752// Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on).8753// Responsible for managing width/height.87548755setDefaults({8756allDaySlot: true,8757allDayText: 'all-day',87588759scrollTime: '06:00:00',87608761slotDuration: '00:30:00',87628763axisFormat: generateAgendaAxisFormat,8764timeFormat: {8765agenda: generateAgendaTimeFormat8766},87678768minTime: '00:00:00',8769maxTime: '24:00:00',8770slotEventOverlap: true8771});87728773var AGENDA_ALL_DAY_EVENT_LIMIT = 5;877487758776function generateAgendaAxisFormat(options, langData) {8777return langData.longDateFormat('LT')8778.replace(':mm', '(:mm)')8779.replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs8780.replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand8781}878287838784function generateAgendaTimeFormat(options, langData) {8785return langData.longDateFormat('LT')8786.replace(/\s*a$/i, ''); // remove trailing AM/PM8787}878887898790function AgendaView(calendar) {8791View.call(this, calendar); // call the super-constructor87928793this.timeGrid = new TimeGrid(this);87948795if (this.opt('allDaySlot')) { // should we display the "all-day" area?8796this.dayGrid = new DayGrid(this); // the all-day subcomponent of this view87978798// the coordinate grid will be a combination of both subcomponents' grids8799this.coordMap = new ComboCoordMap([8800this.dayGrid.coordMap,8801this.timeGrid.coordMap8802]);8803}8804else {8805this.coordMap = this.timeGrid.coordMap;8806}8807}880888098810AgendaView.prototype = createObject(View.prototype); // define the super-class8811$.extend(AgendaView.prototype, {88128813timeGrid: null, // the main time-grid subcomponent of this view8814dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null88158816axisWidth: null, // the width of the time axis running down the side88178818noScrollRowEls: null, // set of fake row elements that must compensate when scrollerEl has scrollbars88198820// when the time-grid isn't tall enough to occupy the given height, we render an <hr> underneath8821bottomRuleEl: null,8822bottomRuleHeight: null,882388248825/* Rendering8826------------------------------------------------------------------------------------------------------------------*/882788288829// Renders the view into `this.el`, which has already been assigned.8830// `colCnt` has been calculated by a subclass and passed here.8831render: function(colCnt) {88328833// needed for cell-to-date and date-to-cell calculations in View8834this.rowCnt = 1;8835this.colCnt = colCnt;88368837this.el.addClass('fc-agenda-view').html(this.renderHtml());88388839// the element that wraps the time-grid that will probably scroll8840this.scrollerEl = this.el.find('.fc-time-grid-container');8841this.timeGrid.coordMap.containerEl = this.scrollerEl; // don't accept clicks/etc outside of this88428843this.timeGrid.el = this.el.find('.fc-time-grid');8844this.timeGrid.render();88458846// the <hr> that sometimes displays under the time-grid8847this.bottomRuleEl = $('<hr class="' + this.widgetHeaderClass + '"/>')8848.appendTo(this.timeGrid.el); // inject it into the time-grid88498850if (this.dayGrid) {8851this.dayGrid.el = this.el.find('.fc-day-grid');8852this.dayGrid.render();88538854// have the day-grid extend it's coordinate area over the <hr> dividing the two grids8855this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight();8856}88578858this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller88598860View.prototype.render.call(this); // call the super-method88618862this.resetScroll(); // do this after sizes have been set8863},886488658866// Make subcomponents ready for cleanup8867destroy: function() {8868this.timeGrid.destroy();8869if (this.dayGrid) {8870this.dayGrid.destroy();8871}8872View.prototype.destroy.call(this); // call the super-method8873},887488758876// Builds the HTML skeleton for the view.8877// The day-grid and time-grid components will render inside containers defined by this HTML.8878renderHtml: function() {8879return '' +8880'<table>' +8881'<thead>' +8882'<tr>' +8883'<td class="' + this.widgetHeaderClass + '">' +8884this.timeGrid.headHtml() + // render the day-of-week headers8885'</td>' +8886'</tr>' +8887'</thead>' +8888'<tbody>' +8889'<tr>' +8890'<td class="' + this.widgetContentClass + '">' +8891(this.dayGrid ?8892'<div class="fc-day-grid"/>' +8893'<hr class="' + this.widgetHeaderClass + '"/>' :8894''8895) +8896'<div class="fc-time-grid-container">' +8897'<div class="fc-time-grid"/>' +8898'</div>' +8899'</td>' +8900'</tr>' +8901'</tbody>' +8902'</table>';8903},890489058906// Generates the HTML that will go before the day-of week header cells.8907// Queried by the TimeGrid subcomponent when generating rows. Ordering depends on isRTL.8908headIntroHtml: function() {8909var date;8910var weekNumber;8911var weekTitle;8912var weekText;89138914if (this.opt('weekNumbers')) {8915date = this.cellToDate(0, 0);8916weekNumber = this.calendar.calculateWeekNumber(date);8917weekTitle = this.opt('weekNumberTitle');89188919if (this.opt('isRTL')) {8920weekText = weekNumber + weekTitle;8921}8922else {8923weekText = weekTitle + weekNumber;8924}89258926return '' +8927'<th class="fc-axis fc-week-number ' + this.widgetHeaderClass + '" ' + this.axisStyleAttr() + '>' +8928'<span>' + // needed for matchCellWidths8929htmlEscape(weekText) +8930'</span>' +8931'</th>';8932}8933else {8934return '<th class="fc-axis ' + this.widgetHeaderClass + '" ' + this.axisStyleAttr() + '></th>';8935}8936},893789388939// Generates the HTML that goes before the all-day cells.8940// Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL.8941dayIntroHtml: function() {8942return '' +8943'<td class="fc-axis ' + this.widgetContentClass + '" ' + this.axisStyleAttr() + '>' +8944'<span>' + // needed for matchCellWidths8945(this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'))) +8946'</span>' +8947'</td>';8948},894989508951// Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column.8952slotBgIntroHtml: function() {8953return '<td class="fc-axis ' + this.widgetContentClass + '" ' + this.axisStyleAttr() + '></td>';8954},895589568957// Generates the HTML that goes before all other types of cells.8958// Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.8959// Queried by the TimeGrid and DayGrid subcomponents when generating rows. Ordering depends on isRTL.8960introHtml: function() {8961return '<td class="fc-axis" ' + this.axisStyleAttr() + '></td>';8962},896389648965// Generates an HTML attribute string for setting the width of the axis, if it is known8966axisStyleAttr: function() {8967if (this.axisWidth !== null) {8968return 'style="width:' + this.axisWidth + 'px"';8969}8970return '';8971},897289738974/* Dimensions8975------------------------------------------------------------------------------------------------------------------*/89768977updateSize: function(isResize) {8978if (isResize) {8979this.timeGrid.resize();8980}8981View.prototype.updateSize.call(this, isResize);8982},898389848985// Refreshes the horizontal dimensions of the view8986updateWidth: function() {8987// make all axis cells line up, and record the width so newly created axis cells will have it8988this.axisWidth = matchCellWidths(this.el.find('.fc-axis'));8989},899089918992// Adjusts the vertical dimensions of the view to the specified values8993setHeight: function(totalHeight, isAuto) {8994var eventLimit;8995var scrollerHeight;89968997if (this.bottomRuleHeight === null) {8998// calculate the height of the rule the very first time8999this.bottomRuleHeight = this.bottomRuleEl.outerHeight();9000}9001this.bottomRuleEl.hide(); // .show() will be called later if this <hr> is necessary90029003// reset all dimensions back to the original state9004this.scrollerEl.css('overflow', '');9005unsetScroller(this.scrollerEl);9006uncompensateScroll(this.noScrollRowEls);90079008// limit number of events in the all-day area9009if (this.dayGrid) {9010this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed90119012eventLimit = this.opt('eventLimit');9013if (eventLimit && typeof eventLimit !== 'number') {9014eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number9015}9016if (eventLimit) {9017this.dayGrid.limitRows(eventLimit);9018}9019}90209021if (!isAuto) { // should we force dimensions of the scroll container, or let the contents be natural height?90229023scrollerHeight = this.computeScrollerHeight(totalHeight);9024if (setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars?90259026// make the all-day and header rows lines up9027compensateScroll(this.noScrollRowEls, getScrollbarWidths(this.scrollerEl));90289029// the scrollbar compensation might have changed text flow, which might affect height, so recalculate9030// and reapply the desired height to the scroller.9031scrollerHeight = this.computeScrollerHeight(totalHeight);9032this.scrollerEl.height(scrollerHeight);90339034this.restoreScroll();9035}9036else { // no scrollbars9037// still, force a height and display the bottom rule (marks the end of day)9038this.scrollerEl.height(scrollerHeight).css('overflow', 'hidden'); // in case <hr> goes outside9039this.bottomRuleEl.show();9040}9041}9042},904390449045// Sets the scroll value of the scroller to the intial pre-configured state prior to allowing the user to change it.9046resetScroll: function() {9047var _this = this;9048var scrollTime = moment.duration(this.opt('scrollTime'));9049var top = this.timeGrid.computeTimeTop(scrollTime);90509051// zoom can give weird floating-point values. rather scroll a little bit further9052top = Math.ceil(top);90539054if (top) {9055top++; // to overcome top border that slots beyond the first have. looks better9056}90579058function scroll() {9059_this.scrollerEl.scrollTop(top);9060}90619062scroll();9063setTimeout(scroll, 0); // overrides any previous scroll state made by the browser9064},906590669067/* Events9068------------------------------------------------------------------------------------------------------------------*/906990709071// Renders events onto the view and populates the View's segment array9072renderEvents: function(events) {9073var dayEvents = [];9074var timedEvents = [];9075var daySegs = [];9076var timedSegs;9077var i;90789079// separate the events into all-day and timed9080for (i = 0; i < events.length; i++) {9081if (events[i].allDay) {9082dayEvents.push(events[i]);9083}9084else {9085timedEvents.push(events[i]);9086}9087}90889089// render the events in the subcomponents9090timedSegs = this.timeGrid.renderEvents(timedEvents);9091if (this.dayGrid) {9092daySegs = this.dayGrid.renderEvents(dayEvents);9093}90949095// the all-day area is flexible and might have a lot of events, so shift the height9096this.updateHeight();90979098View.prototype.renderEvents.call(this, events); // call the super-method9099},910091019102// Retrieves all segment objects that are rendered in the view9103getSegs: function() {9104return this.timeGrid.getSegs().concat(9105this.dayGrid ? this.dayGrid.getSegs() : []9106);9107},910891099110// Unrenders all event elements and clears internal segment data9111destroyEvents: function() {9112View.prototype.destroyEvents.call(this); // do this before the grids' segs have been cleared91139114// if destroyEvents is being called as part of an event rerender, renderEvents will be called shortly9115// after, so remember what the scroll value was so we can restore it.9116this.recordScroll();91179118// destroy the events in the subcomponents9119this.timeGrid.destroyEvents();9120if (this.dayGrid) {9121this.dayGrid.destroyEvents();9122}91239124// we DON'T need to call updateHeight() because:9125// A) a renderEvents() call always happens after this, which will eventually call updateHeight()9126// B) in IE8, this causes a flash whenever events are rerendered9127},912891299130/* Event Dragging9131------------------------------------------------------------------------------------------------------------------*/913291339134// Renders a visual indication of an event being dragged over the view.9135// A returned value of `true` signals that a mock "helper" event has been rendered.9136renderDrag: function(start, end, seg) {9137if (start.hasTime()) {9138return this.timeGrid.renderDrag(start, end, seg);9139}9140else if (this.dayGrid) {9141return this.dayGrid.renderDrag(start, end, seg);9142}9143},914491459146// Unrenders a visual indications of an event being dragged over the view9147destroyDrag: function() {9148this.timeGrid.destroyDrag();9149if (this.dayGrid) {9150this.dayGrid.destroyDrag();9151}9152},915391549155/* Selection9156------------------------------------------------------------------------------------------------------------------*/915791589159// Renders a visual indication of a selection9160renderSelection: function(start, end) {9161if (start.hasTime() || end.hasTime()) {9162this.timeGrid.renderSelection(start, end);9163}9164else if (this.dayGrid) {9165this.dayGrid.renderSelection(start, end);9166}9167},916891699170// Unrenders a visual indications of a selection9171destroySelection: function() {9172this.timeGrid.destroySelection();9173if (this.dayGrid) {9174this.dayGrid.destroySelection();9175}9176}91779178});91799180;;91819182/* A week view with an all-day cell area at the top, and a time grid below9183----------------------------------------------------------------------------------------------------------------------*/9184// TODO: a WeekView mixin for calculating dates and titles91859186fcViews.agendaWeek = AgendaWeekView; // register the view91879188function AgendaWeekView(calendar) {9189AgendaView.call(this, calendar); // call the super-constructor9190}919191929193AgendaWeekView.prototype = createObject(AgendaView.prototype); // define the super-class9194$.extend(AgendaWeekView.prototype, {91959196name: 'agendaWeek',919791989199incrementDate: function(date, delta) {9200return date.clone().stripTime().add(delta, 'weeks').startOf('week');9201},920292039204render: function(date) {92059206this.intervalStart = date.clone().stripTime().startOf('week');9207this.intervalEnd = this.intervalStart.clone().add(1, 'weeks');92089209this.start = this.skipHiddenDays(this.intervalStart);9210this.end = this.skipHiddenDays(this.intervalEnd, -1, true);92119212this.title = this.calendar.formatRange(9213this.start,9214this.end.clone().subtract(1), // make inclusive by subtracting 1 ms9215this.opt('titleFormat'),9216' \u2014 ' // emphasized dash9217);92189219AgendaView.prototype.render.call(this, this.getCellsPerWeek()); // call the super-method9220}92219222});92239224;;92259226/* A day view with an all-day cell area at the top, and a time grid below9227----------------------------------------------------------------------------------------------------------------------*/92289229fcViews.agendaDay = AgendaDayView; // register the view92309231function AgendaDayView(calendar) {9232AgendaView.call(this, calendar); // call the super-constructor9233}923492359236AgendaDayView.prototype = createObject(AgendaView.prototype); // define the super-class9237$.extend(AgendaDayView.prototype, {92389239name: 'agendaDay',924092419242incrementDate: function(date, delta) {9243var out = date.clone().stripTime().add(delta, 'days');9244out = this.skipHiddenDays(out, delta < 0 ? -1 : 1);9245return out;9246},924792489249render: function(date) {92509251this.start = this.intervalStart = date.clone().stripTime();9252this.end = this.intervalEnd = this.start.clone().add(1, 'days');92539254this.title = this.calendar.formatDate(this.start, this.opt('titleFormat'));92559256AgendaView.prototype.render.call(this, 1); // call the super-method9257}92589259});92609261;;92629263});92649265