diff --git a/dist/cdnjs/2.1.1/fullcalendar.css b/dist/cdnjs/2.1.1/fullcalendar.css index ab8b6e6..a5b9d33 100644 --- a/dist/cdnjs/2.1.1/fullcalendar.css +++ b/dist/cdnjs/2.1.1/fullcalendar.css @@ -1,933 +1,933 @@ -/*! - * FullCalendar v2.1.1 Stylesheet - * Docs & License: http://arshaw.com/fullcalendar/ - * (c) 2013 Adam Shaw - */ - - -.fc { - direction: ltr; - text-align: left; -} - -.fc-rtl { - text-align: right; -} - -body .fc { /* extra precedence to overcome jqui */ - font-size: 1em; -} - - -/* Colors ---------------------------------------------------------------------------------------------------*/ - -.fc-unthemed th, -.fc-unthemed td, -.fc-unthemed hr, -.fc-unthemed thead, -.fc-unthemed tbody, -.fc-unthemed .fc-row, -.fc-unthemed .fc-popover { - border-color: #ddd; -} - -.fc-unthemed .fc-popover { - background-color: #fff; -} - -.fc-unthemed hr, -.fc-unthemed .fc-popover .fc-header { - background: #eee; -} - -.fc-unthemed .fc-popover .fc-header .fc-close { - color: #666; -} - -.fc-unthemed .fc-today { - background: #fcf8e3; -} - -.fc-highlight { /* when user is selecting cells */ - background: #bce8f1; - opacity: .3; - filter: alpha(opacity=30); /* for IE */ -} - - -/* Icons (inline elements with styled text that mock arrow icons) ---------------------------------------------------------------------------------------------------*/ - -.fc-icon { - display: inline-block; - font-size: 2em; - line-height: .5em; - height: .5em; /* will make the total height 1em */ - font-family: "Courier New", Courier, monospace; -} - -.fc-icon-left-single-arrow:after { - content: "\02039"; - font-weight: bold; -} - -.fc-icon-right-single-arrow:after { - content: "\0203A"; - font-weight: bold; -} - -.fc-icon-left-double-arrow:after { - content: "\000AB"; -} - -.fc-icon-right-double-arrow:after { - content: "\000BB"; -} - -.fc-icon-x:after { - content: "\000D7"; -} - - -/* Buttons (styled ' - ) - .click(function() { - // don't process clicks for disabled buttons - if (!button.hasClass(tm + '-state-disabled')) { - - buttonClick(); - - // after the click action, if the button becomes the "active" tab, or disabled, - // it should never have a hover class, so remove it now. - if ( - button.hasClass(tm + '-state-active') || - button.hasClass(tm + '-state-disabled') - ) { - button.removeClass(tm + '-state-hover'); - } - } - }) - .mousedown(function() { - // the *down* effect (mouse pressed in). - // only on buttons that are not the "active" tab, or disabled - button - .not('.' + tm + '-state-active') - .not('.' + tm + '-state-disabled') - .addClass(tm + '-state-down'); - }) - .mouseup(function() { - // undo the *down* effect - button.removeClass(tm + '-state-down'); - }) - .hover( - function() { - // the *hover* effect. - // only on buttons that are not the "active" tab, or disabled - button - .not('.' + tm + '-state-active') - .not('.' + tm + '-state-disabled') - .addClass(tm + '-state-hover'); - }, - function() { - // undo the *hover* effect - button - .removeClass(tm + '-state-hover') - .removeClass(tm + '-state-down'); // if mouseleave happens before mouseup - } - ); - - groupChildren = groupChildren.add(button); - } - } - }); - - if (isOnlyButtons) { - groupChildren - .first().addClass(tm + '-corner-left').end() - .last().addClass(tm + '-corner-right').end(); - } - - if (groupChildren.length > 1) { - groupEl = $('
'); - if (isOnlyButtons) { - groupEl.addClass('fc-button-group'); - } - groupEl.append(groupChildren); - sectionEl.append(groupEl); - } - else { - sectionEl.append(groupChildren); // 1 or 0 children - } - }); - } - - return sectionEl; - } - - - function updateTitle(text) { - el.find('h2').text(text); - } - - - function activateButton(buttonName) { - el.find('.fc-' + buttonName + '-button') - .addClass(tm + '-state-active'); - } - - - function deactivateButton(buttonName) { - el.find('.fc-' + buttonName + '-button') - .removeClass(tm + '-state-active'); - } - - - function disableButton(buttonName) { - el.find('.fc-' + buttonName + '-button') - .attr('disabled', 'disabled') - .addClass(tm + '-state-disabled'); - } - - - function enableButton(buttonName) { - el.find('.fc-' + buttonName + '-button') - .removeAttr('disabled') - .removeClass(tm + '-state-disabled'); - } - - - function getViewsWithButtons() { - return viewsWithButtons; - } - -} - -;; - -fc.sourceNormalizers = []; -fc.sourceFetchers = []; - -var ajaxDefaults = { - dataType: 'json', - cache: false -}; - -var eventGUID = 1; - - -function EventManager(options) { // assumed to be a calendar - var t = this; - - - // exports - t.isFetchNeeded = isFetchNeeded; - t.fetchEvents = fetchEvents; - t.addEventSource = addEventSource; - t.removeEventSource = removeEventSource; - t.updateEvent = updateEvent; - t.renderEvent = renderEvent; - t.removeEvents = removeEvents; - t.clientEvents = clientEvents; - t.mutateEvent = mutateEvent; - - - // imports - var trigger = t.trigger; - var getView = t.getView; - var reportEvents = t.reportEvents; - var getEventEnd = t.getEventEnd; - - - // locals - var stickySource = { events: [] }; - var sources = [ stickySource ]; - var rangeStart, rangeEnd; - var currentFetchID = 0; - var pendingSourceCnt = 0; - var loadingLevel = 0; - var cache = []; - - - $.each( - (options.events ? [ options.events ] : []).concat(options.eventSources || []), - function(i, sourceInput) { - var source = buildEventSource(sourceInput); - if (source) { - sources.push(source); - } - } - ); - - - - /* Fetching - -----------------------------------------------------------------------------*/ - - - function isFetchNeeded(start, end) { - return !rangeStart || // nothing has been fetched yet? - // or, a part of the new range is outside of the old range? (after normalizing) - start.clone().stripZone() < rangeStart.clone().stripZone() || - end.clone().stripZone() > rangeEnd.clone().stripZone(); - } - - - function fetchEvents(start, end) { - rangeStart = start; - rangeEnd = end; - cache = []; - var fetchID = ++currentFetchID; - var len = sources.length; - pendingSourceCnt = len; - for (var i=0; i cells, find the cell with the largest natural width and set the widths of all the -// cells to be that width. -// PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline -function matchCellWidths(els) { - var maxInnerWidth = 0; - - els.find('> *').each(function(i, innerEl) { - var innerWidth = $(innerEl).outerWidth(); - if (innerWidth > maxInnerWidth) { - maxInnerWidth = innerWidth; - } - }); - - maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance - - els.width(maxInnerWidth); - - return maxInnerWidth; -} - - -// Turns a container element into a scroller if its contents is taller than the allotted height. -// Returns true if the element is now a scroller, false otherwise. -// NOTE: this method is best because it takes weird zooming dimensions into account -function setPotentialScroller(containerEl, height) { - containerEl.height(height).addClass('fc-scroller'); - - // are scrollbars needed? - if (containerEl[0].scrollHeight - 1 > containerEl[0].clientHeight) { // !!! -1 because IE is often off-by-one :( - return true; - } - - unsetScroller(containerEl); // undo - return false; -} - - -// Takes an element that might have been a scroller, and turns it back into a normal element. -function unsetScroller(containerEl) { - containerEl.height('').removeClass('fc-scroller'); -} - - -/* General DOM Utilities -----------------------------------------------------------------------------------------------------------------------*/ - - -// borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51 -function getScrollParent(el) { - var position = el.css('position'), - scrollParent = el.parents().filter(function() { - var parent = $(this); - return (/(auto|scroll)/).test( - parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x') - ); - }).eq(0); - - return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent; -} - - -// Given a container element, return an object with the pixel values of the left/right scrollbars. -// Left scrollbars might occur on RTL browsers (IE maybe?) but I have not tested. -// PREREQUISITE: container element must have a single child with display:block -function getScrollbarWidths(container) { - var containerLeft = container.offset().left; - var containerRight = containerLeft + container.width(); - var inner = container.children(); - var innerLeft = inner.offset().left; - var innerRight = innerLeft + inner.outerWidth(); - - return { - left: innerLeft - containerLeft, - right: containerRight - innerRight - }; -} - - -// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac) -function isPrimaryMouseButton(ev) { - return ev.which == 1 && !ev.ctrlKey; -} - - -/* FullCalendar-specific Misc Utilities -----------------------------------------------------------------------------------------------------------------------*/ - - -// Creates a basic segment with the intersection of the two ranges. Returns undefined if no intersection. -// Expects all dates to be normalized to the same timezone beforehand. -function intersectionToSeg(subjectStart, subjectEnd, intervalStart, intervalEnd) { - var segStart, segEnd; - var isStart, isEnd; - - if (subjectEnd > intervalStart && subjectStart < intervalEnd) { // in bounds at all? - - if (subjectStart >= intervalStart) { - segStart = subjectStart.clone(); - isStart = true; - } - else { - segStart = intervalStart.clone(); - isStart = false; - } - - if (subjectEnd <= intervalEnd) { - segEnd = subjectEnd.clone(); - isEnd = true; - } - else { - segEnd = intervalEnd.clone(); - isEnd = false; - } - - return { - start: segStart, - end: segEnd, - isStart: isStart, - isEnd: isEnd - }; - } -} - - -function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object - obj = obj || {}; - if (obj[name] !== undefined) { - return obj[name]; - } - var parts = name.split(/(?=[A-Z])/), - i = parts.length - 1, res; - for (; i>=0; i--) { - res = obj[parts[i].toLowerCase()]; - if (res !== undefined) { - return res; - } - } - return obj['default']; -} - - -/* Date Utilities -----------------------------------------------------------------------------------------------------------------------*/ - -var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ]; - - -// Diffs the two moments into a Duration where full-days are recorded first, then the remaining time. -// Moments will have their timezones normalized. -function dayishDiff(a, b) { - return moment.duration({ - days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'), - ms: a.time() - b.time() - }); -} - - -function isNativeDate(input) { - return Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date; -} - - -function dateCompare(a, b) { // works with Moments and native Dates - return a - b; -} - - -/* General Utilities -----------------------------------------------------------------------------------------------------------------------*/ - -fc.applyAll = applyAll; // export - - -// Create an object that has the given prototype. Just like Object.create -function createObject(proto) { - var f = function() {}; - f.prototype = proto; - return new f(); -} - - -// Copies specifically-owned (non-protoype) properties of `b` onto `a`. -// FYI, $.extend would copy *all* properties of `b` onto `a`. -function extend(a, b) { - for (var i in b) { - if (b.hasOwnProperty(i)) { - a[i] = b[i]; - } - } -} - - -function applyAll(functions, thisObj, args) { - if ($.isFunction(functions)) { - functions = [ functions ]; - } - if (functions) { - var i; - var ret; - for (i=0; i/g, '>') - .replace(/'/g, ''') - .replace(/"/g, '"') - .replace(/\n/g, '
'); -} - - -function stripHtmlEntities(text) { - return text.replace(/&.*?;/g, ''); -} - - -function capitaliseFirstLetter(str) { - return str.charAt(0).toUpperCase() + str.slice(1); -} - - -// Returns a function, that, as long as it continues to be invoked, will not -// be triggered. The function will be called after it stops being called for -// N milliseconds. -// https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714 -function debounce(func, wait) { - var timeoutId; - var args; - var context; - var timestamp; // of most recent call - var later = function() { - var last = +new Date() - timestamp; - if (last < wait && last > 0) { - timeoutId = setTimeout(later, wait - last); - } - else { - timeoutId = null; - func.apply(context, args); - if (!timeoutId) { - context = args = null; - } - } - }; - - return function() { - context = this; - args = arguments; - timestamp = +new Date(); - if (!timeoutId) { - timeoutId = setTimeout(later, wait); - } - }; -} - -;; - -var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/; -var ambigTimeOrZoneRegex = - /^\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+)?)?)?)?)?$/; - - -// Creating -// ------------------------------------------------------------------------------------------------- - -// Creates a new moment, similar to the vanilla moment(...) constructor, but with -// extra features (ambiguous time, enhanced formatting). When gived an existing moment, -// it will function as a clone (and retain the zone of the moment). Anything else will -// result in a moment in the local zone. -fc.moment = function() { - return makeMoment(arguments); -}; - -// Sames as fc.moment, but forces the resulting moment to be in the UTC timezone. -fc.moment.utc = function() { - var mom = makeMoment(arguments, true); - - // Force it into UTC because makeMoment doesn't guarantee it. - if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone - mom.utc(); - } - - return mom; -}; - -// Same as fc.moment, but when given an ISO8601 string, the timezone offset is preserved. -// ISO8601 strings with no timezone offset will become ambiguously zoned. -fc.moment.parseZone = function() { - return makeMoment(arguments, true, true); -}; - -// Builds an FCMoment from args. When given an existing moment, it clones. When given a native -// Date, or called with no arguments (the current time), the resulting moment will be local. -// Anything else needs to be "parsed" (a string or an array), and will be affected by: -// parseAsUTC - if there is no zone information, should we parse the input in UTC? -// parseZone - if there is zone information, should we force the zone of the moment? -function makeMoment(args, parseAsUTC, parseZone) { - var input = args[0]; - var isSingleString = args.length == 1 && typeof input === 'string'; - var isAmbigTime; - var isAmbigZone; - var ambigMatch; - var output; // an object with fields for the new FCMoment object - - if (moment.isMoment(input)) { - output = moment.apply(null, args); // clone it - - // the ambig properties have not been preserved in the clone, so reassign them - if (input._ambigTime) { - output._ambigTime = true; - } - if (input._ambigZone) { - output._ambigZone = true; - } - } - else if (isNativeDate(input) || input === undefined) { - output = moment.apply(null, args); // will be local - } - else { // "parsing" is required - isAmbigTime = false; - isAmbigZone = false; - - if (isSingleString) { - if (ambigDateOfMonthRegex.test(input)) { - // accept strings like '2014-05', but convert to the first of the month - input += '-01'; - args = [ input ]; // for when we pass it on to moment's constructor - isAmbigTime = true; - isAmbigZone = true; - } - else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) { - isAmbigTime = !ambigMatch[5]; // no time part? - isAmbigZone = true; - } - } - else if ($.isArray(input)) { - // arrays have no timezone information, so assume ambiguous zone - isAmbigZone = true; - } - // otherwise, probably a string with a format - - if (parseAsUTC) { - output = moment.utc.apply(moment, args); - } - else { - output = moment.apply(null, args); - } - - if (isAmbigTime) { - output._ambigTime = true; - output._ambigZone = true; // ambiguous time always means ambiguous zone - } - else if (parseZone) { // let's record the inputted zone somehow - if (isAmbigZone) { - output._ambigZone = true; - } - else if (isSingleString) { - output.zone(input); // if not a valid zone, will assign UTC - } - } - } - - return new FCMoment(output); -} - -// Our subclass of Moment. -// Accepts an object with the internal Moment properties that should be copied over to -// `this` object (most likely another Moment object). The values in this data must not -// be referenced by anything else (two moments sharing a Date object for example). -function FCMoment(internalData) { - extend(this, internalData); -} - -// Chain the prototype to Moment's -FCMoment.prototype = createObject(moment.fn); - -// We need this because Moment's implementation won't create an FCMoment, -// nor will it copy over the ambig flags. -FCMoment.prototype.clone = function() { - return makeMoment([ this ]); -}; - - -// Time-of-day -// ------------------------------------------------------------------------------------------------- - -// GETTER -// Returns a Duration with the hours/minutes/seconds/ms values of the moment. -// If the moment has an ambiguous time, a duration of 00:00 will be returned. -// -// SETTER -// You can supply a Duration, a Moment, or a Duration-like argument. -// When setting the time, and the moment has an ambiguous time, it then becomes unambiguous. -FCMoment.prototype.time = function(time) { - if (time == null) { // getter - return moment.duration({ - hours: this.hours(), - minutes: this.minutes(), - seconds: this.seconds(), - milliseconds: this.milliseconds() - }); - } - else { // setter - - delete this._ambigTime; // mark that the moment now has a time - - if (!moment.isDuration(time) && !moment.isMoment(time)) { - time = moment.duration(time); - } - - // The day value should cause overflow (so 24 hours becomes 00:00:00 of next day). - // Only for Duration times, not Moment times. - var dayHours = 0; - if (moment.isDuration(time)) { - dayHours = Math.floor(time.asDays()) * 24; - } - - // We need to set the individual fields. - // Can't use startOf('day') then add duration. In case of DST at start of day. - return this.hours(dayHours + time.hours()) - .minutes(time.minutes()) - .seconds(time.seconds()) - .milliseconds(time.milliseconds()); - } -}; - -// Converts the moment to UTC, stripping out its time-of-day and timezone offset, -// but preserving its YMD. A moment with a stripped time will display no time -// nor timezone offset when .format() is called. -FCMoment.prototype.stripTime = function() { - var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array - - // set the internal UTC flag - moment.fn.utc.call(this); // call the original method, because we don't want to affect _ambigZone - - this.year(a[0]) // TODO: find a way to do this in one shot - .month(a[1]) - .date(a[2]) - .hours(0) - .minutes(0) - .seconds(0) - .milliseconds(0); - - // Mark the time as ambiguous. This needs to happen after the .utc() call, which calls .zone(), which - // clears all ambig flags. Same concept with the .year/month/date calls in the case of moment-timezone. - this._ambigTime = true; - this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset - - return this; // for chaining -}; - -// Returns if the moment has a non-ambiguous time (boolean) -FCMoment.prototype.hasTime = function() { - return !this._ambigTime; -}; - - -// Timezone -// ------------------------------------------------------------------------------------------------- - -// Converts the moment to UTC, stripping out its timezone offset, but preserving its -// YMD and time-of-day. A moment with a stripped timezone offset will display no -// timezone offset when .format() is called. -FCMoment.prototype.stripZone = function() { - var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array - var wasAmbigTime = this._ambigTime; - - moment.fn.utc.call(this); // set the internal UTC flag - - this.year(a[0]) // TODO: find a way to do this in one shot - .month(a[1]) - .date(a[2]) - .hours(a[3]) - .minutes(a[4]) - .seconds(a[5]) - .milliseconds(a[6]); - - if (wasAmbigTime) { - // the above call to .utc()/.zone() unfortunately clears the ambig flags, so reassign - this._ambigTime = true; - } - - // Mark the zone as ambiguous. This needs to happen after the .utc() call, which calls .zone(), which - // clears all ambig flags. Same concept with the .year/month/date calls in the case of moment-timezone. - this._ambigZone = true; - - return this; // for chaining -}; - -// Returns of the moment has a non-ambiguous timezone offset (boolean) -FCMoment.prototype.hasZone = function() { - return !this._ambigZone; -}; - -// this method implicitly marks a zone -FCMoment.prototype.zone = function(tzo) { - - if (tzo != null) { - // FYI, the delete statements need to be before the .zone() call or else chaos ensues - // for reasons I don't understand. - delete this._ambigTime; - delete this._ambigZone; - } - - return moment.fn.zone.apply(this, arguments); -}; - -// this method implicitly marks a zone -FCMoment.prototype.local = function() { - var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array - var wasAmbigZone = this._ambigZone; - - // will happen anyway via .local()/.zone(), but don't want to rely on internal implementation - delete this._ambigTime; - delete this._ambigZone; - - moment.fn.local.apply(this, arguments); - - if (wasAmbigZone) { - // If the moment was ambiguously zoned, the date fields were stored as UTC. - // We want to preserve these, but in local time. - this.year(a[0]) // TODO: find a way to do this in one shot - .month(a[1]) - .date(a[2]) - .hours(a[3]) - .minutes(a[4]) - .seconds(a[5]) - .milliseconds(a[6]); - } - - return this; // for chaining -}; - -// this method implicitly marks a zone -FCMoment.prototype.utc = function() { - - // will happen anyway via .local()/.zone(), but don't want to rely on internal implementation - delete this._ambigTime; - delete this._ambigZone; - - return moment.fn.utc.apply(this, arguments); -}; - - -// Formatting -// ------------------------------------------------------------------------------------------------- - -FCMoment.prototype.format = function() { - if (arguments[0]) { - return formatDate(this, arguments[0]); // our extended formatting - } - if (this._ambigTime) { - return momentFormat(this, 'YYYY-MM-DD'); - } - if (this._ambigZone) { - return momentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss'); - } - return momentFormat(this); // default moment original formatting -}; - -FCMoment.prototype.toISOString = function() { - if (this._ambigTime) { - return momentFormat(this, 'YYYY-MM-DD'); - } - if (this._ambigZone) { - return momentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss'); - } - return moment.fn.toISOString.apply(this, arguments); -}; - - -// Querying -// ------------------------------------------------------------------------------------------------- - -// Is the moment within the specified range? `end` is exclusive. -FCMoment.prototype.isWithin = function(start, end) { - var a = commonlyAmbiguate([ this, start, end ]); - return a[0] >= a[1] && a[0] < a[2]; -}; - -// When isSame is called with units, timezone ambiguity is normalized before the comparison happens. -// If no units are specified, the two moments must be identically the same, with matching ambig flags. -FCMoment.prototype.isSame = function(input, units) { - var a; - - if (units) { - a = commonlyAmbiguate([ this, input ], true); // normalize timezones but don't erase times - return moment.fn.isSame.call(a[0], a[1], units); - } - else { - input = fc.moment.parseZone(input); // normalize input - return moment.fn.isSame.call(this, input) && - Boolean(this._ambigTime) === Boolean(input._ambigTime) && - Boolean(this._ambigZone) === Boolean(input._ambigZone); - } -}; - -// Make these query methods work with ambiguous moments -$.each([ - 'isBefore', - 'isAfter' -], function(i, methodName) { - FCMoment.prototype[methodName] = function(input, units) { - var a = commonlyAmbiguate([ this, input ]); - return moment.fn[methodName].call(a[0], a[1], units); - }; -}); - - -// Misc Internals -// ------------------------------------------------------------------------------------------------- - -// given an array of moment-like inputs, return a parallel array w/ moments similarly ambiguated. -// for example, of one moment has ambig time, but not others, all moments will have their time stripped. -// set `preserveTime` to `true` to keep times, but only normalize zone ambiguity. -function commonlyAmbiguate(inputs, preserveTime) { - var outputs = []; - var anyAmbigTime = false; - var anyAmbigZone = false; - var i; - - for (i=0; i "MMMM D YYYY" - formatStr = localeData.longDateFormat(formatStr) || formatStr; - // BTW, this is not important for `formatDate` because it is impossible to put custom tokens - // or non-zero areas in Moment's localized format strings. - - separator = separator || ' - '; - - return formatRangeWithChunks( - date1, - date2, - getFormatStringChunks(formatStr), - separator, - isRTL - ); -} -fc.formatRange = formatRange; // expose - - -function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) { - var chunkStr; // the rendering of the chunk - var leftI; - var leftStr = ''; - var rightI; - var rightStr = ''; - var middleI; - var middleStr1 = ''; - var middleStr2 = ''; - var middleStr = ''; - - // Start at the leftmost side of the formatting string and continue until you hit a token - // that is not the same between dates. - for (leftI=0; leftIleftI; rightI--) { - chunkStr = formatSimilarChunk(date1, date2, chunks[rightI]); - if (chunkStr === false) { - break; - } - rightStr = chunkStr + rightStr; - } - - // The area in the middle is different for both of the dates. - // Collect them distinctly so we can jam them together later. - for (middleI=leftI; middleI<=rightI; middleI++) { - middleStr1 += formatDateWithChunk(date1, chunks[middleI]); - middleStr2 += formatDateWithChunk(date2, chunks[middleI]); - } - - if (middleStr1 || middleStr2) { - if (isRTL) { - middleStr = middleStr2 + separator + middleStr1; - } - else { - middleStr = middleStr1 + separator + middleStr2; - } - } - - return leftStr + middleStr + rightStr; -} - - -var similarUnitMap = { - Y: 'year', - M: 'month', - D: 'day', // day of month - d: 'day', // day of week - // prevents a separator between anything time-related... - A: 'second', // AM/PM - a: 'second', // am/pm - T: 'second', // A/P - t: 'second', // a/p - H: 'second', // hour (24) - h: 'second', // hour (12) - m: 'second', // minute - s: 'second' // second -}; -// TODO: week maybe? - - -// Given a formatting chunk, and given that both dates are similar in the regard the -// formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`. -function formatSimilarChunk(date1, date2, chunk) { - var token; - var unit; - - if (typeof chunk === 'string') { // a literal string - return chunk; - } - else if ((token = chunk.token)) { - unit = similarUnitMap[token.charAt(0)]; - // are the dates the same for this unit of measurement? - if (unit && date1.isSame(date2, unit)) { - return momentFormat(date1, token); // would be the same if we used `date2` - // BTW, don't support custom tokens - } - } - - return false; // the chunk is NOT the same for the two dates - // BTW, don't support splitting on non-zero areas -} - - -// Chunking Utils -// ------------------------------------------------------------------------------------------------- - - -var formatStringChunkCache = {}; - - -function getFormatStringChunks(formatStr) { - if (formatStr in formatStringChunkCache) { - return formatStringChunkCache[formatStr]; - } - return (formatStringChunkCache[formatStr] = chunkFormatString(formatStr)); -} - - -// Break the formatting string into an array of chunks -function chunkFormatString(formatStr) { - var chunks = []; - var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LT|(\w)\4*o?)|([^\w\[\(]+)/g; // TODO: more descrimination - var match; - - while ((match = chunker.exec(formatStr))) { - if (match[1]) { // a literal string inside [ ... ] - chunks.push(match[1]); - } - else if (match[2]) { // non-zero formatting inside ( ... ) - chunks.push({ maybe: chunkFormatString(match[2]) }); - } - else if (match[3]) { // a formatting token - chunks.push({ token: match[3] }); - } - else if (match[5]) { // an unenclosed literal string - chunks.push(match[5]); - } - } - - return chunks; -} - -;; - -/* A rectangular panel that is absolutely positioned over other content ------------------------------------------------------------------------------------------------------------------------- -Options: - - className (string) - - content (HTML string or jQuery element set) - - parentEl - - top - - left - - right (the x coord of where the right edge should be. not a "CSS" right) - - autoHide (boolean) - - show (callback) - - hide (callback) -*/ - -function Popover(options) { - this.options = options || {}; -} - - -Popover.prototype = { - - isHidden: true, - options: null, - el: null, // the container element for the popover. generated by this object - documentMousedownProxy: null, // document mousedown handler bound to `this` - margin: 10, // the space required between the popover and the edges of the scroll container - - - // Shows the popover on the specified position. Renders it if not already - show: function() { - if (this.isHidden) { - if (!this.el) { - this.render(); - } - this.el.show(); - this.position(); - this.isHidden = false; - this.trigger('show'); - } - }, - - - // Hides the popover, through CSS, but does not remove it from the DOM - hide: function() { - if (!this.isHidden) { - this.el.hide(); - this.isHidden = true; - this.trigger('hide'); - } - }, - - - // Creates `this.el` and renders content inside of it - render: function() { - var _this = this; - var options = this.options; - - this.el = $('
') - .addClass(options.className || '') - .css({ - // position initially to the top left to avoid creating scrollbars - top: 0, - left: 0 - }) - .append(options.content) - .appendTo(options.parentEl); - - // when a click happens on anything inside with a 'fc-close' className, hide the popover - this.el.on('click', '.fc-close', function() { - _this.hide(); - }); - - if (options.autoHide) { - $(document).on('mousedown', this.documentMousedownProxy = $.proxy(this, 'documentMousedown')); - } - }, - - - // Triggered when the user clicks *anywhere* in the document, for the autoHide feature - documentMousedown: function(ev) { - // only hide the popover if the click happened outside the popover - if (this.el && !$(ev.target).closest(this.el).length) { - this.hide(); - } - }, - - - // Hides and unregisters any handlers - destroy: function() { - this.hide(); - - if (this.el) { - this.el.remove(); - this.el = null; - } - - $(document).off('mousedown', this.documentMousedownProxy); - }, - - - // Positions the popover optimally, using the top/left/right options - position: function() { - var options = this.options; - var origin = this.el.offsetParent().offset(); - var width = this.el.outerWidth(); - var height = this.el.outerHeight(); - var windowEl = $(window); - var viewportEl = getScrollParent(this.el); - var viewportTop; - var viewportLeft; - var viewportOffset; - var top; // the "position" (not "offset") values for the popover - var left; // - - // compute top and left - top = options.top || 0; - if (options.left !== undefined) { - left = options.left; - } - else if (options.right !== undefined) { - left = options.right - width; // derive the left value from the right value - } - else { - left = 0; - } - - if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result - viewportEl = windowEl; - viewportTop = 0; // the window is always at the top left - viewportLeft = 0; // (and .offset() won't work if called here) - } - else { - viewportOffset = viewportEl.offset(); - viewportTop = viewportOffset.top; - viewportLeft = viewportOffset.left; - } - - // if the window is scrolled, it causes the visible area to be further down - viewportTop += windowEl.scrollTop(); - viewportLeft += windowEl.scrollLeft(); - - // constrain to the view port. if constrained by two edges, give precedence to top/left - if (options.viewportConstrain !== false) { - top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin); - top = Math.max(top, viewportTop + this.margin); - left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin); - left = Math.max(left, viewportLeft + this.margin); - } - - this.el.css({ - top: top - origin.top, - left: left - origin.left - }); - }, - - - // Triggers a callback. Calls a function in the option hash of the same name. - // Arguments beyond the first `name` are forwarded on. - // TODO: better code reuse for this. Repeat code - trigger: function(name) { - if (this.options[name]) { - this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); - } - } - -}; - -;; - -/* A "coordinate map" converts pixel coordinates into an associated cell, which has an associated date ------------------------------------------------------------------------------------------------------------------------- -Common interface: - - CoordMap.prototype = { - build: function() {}, - getCell: function(x, y) {} - }; - -*/ - -/* Coordinate map for a grid component -----------------------------------------------------------------------------------------------------------------------*/ - -function GridCoordMap(grid) { - this.grid = grid; -} - - -GridCoordMap.prototype = { - - grid: null, // reference to the Grid - rows: null, // the top-to-bottom y coordinates. including the bottom of the last item - cols: null, // the left-to-right x coordinates. including the right of the last item - - containerEl: null, // container element that all coordinates are constrained to. optionally assigned - minX: null, - maxX: null, // exclusive - minY: null, - maxY: null, // exclusive - - - // Queries the grid for the coordinates of all the cells - build: function() { - this.grid.buildCoords( - this.rows = [], - this.cols = [] - ); - this.computeBounds(); - }, - - - // Given a coordinate of the document, gets the associated cell. If no cell is underneath, returns null - getCell: function(x, y) { - var cell = null; - var rows = this.rows; - var cols = this.cols; - var r = -1; - var c = -1; - var i; - - if (this.inBounds(x, y)) { - - for (i = 0; i < rows.length; i++) { - if (y >= rows[i][0] && y < rows[i][1]) { - r = i; - break; - } - } - - for (i = 0; i < cols.length; i++) { - if (x >= cols[i][0] && x < cols[i][1]) { - c = i; - break; - } - } - - if (r >= 0 && c >= 0) { - cell = { row: r, col: c }; - cell.grid = this.grid; - cell.date = this.grid.getCellDate(cell); - } - } - - return cell; - }, - - - // If there is a containerEl, compute the bounds into min/max values - computeBounds: function() { - var containerOffset; - - if (this.containerEl) { - containerOffset = this.containerEl.offset(); - this.minX = containerOffset.left; - this.maxX = containerOffset.left + this.containerEl.outerWidth(); - this.minY = containerOffset.top; - this.maxY = containerOffset.top + this.containerEl.outerHeight(); - } - }, - - - // Determines if the given coordinates are in bounds. If no `containerEl`, always true - inBounds: function(x, y) { - if (this.containerEl) { - return x >= this.minX && x < this.maxX && y >= this.minY && y < this.maxY; - } - return true; - } - -}; - - -/* Coordinate map that is a combination of multiple other coordinate maps -----------------------------------------------------------------------------------------------------------------------*/ - -function ComboCoordMap(coordMaps) { - this.coordMaps = coordMaps; -} - - -ComboCoordMap.prototype = { - - coordMaps: null, // an array of CoordMaps - - - // Builds all coordMaps - build: function() { - var coordMaps = this.coordMaps; - var i; - - for (i = 0; i < coordMaps.length; i++) { - coordMaps[i].build(); - } - }, - - - // Queries all coordMaps for the cell underneath the given coordinates, returning the first result - getCell: function(x, y) { - var coordMaps = this.coordMaps; - var cell = null; - var i; - - for (i = 0; i < coordMaps.length && !cell; i++) { - cell = coordMaps[i].getCell(x, y); - } - - return cell; - } - -}; -;; - -/* Tracks mouse movements over a CoordMap and raises events about which cell the mouse is over. -----------------------------------------------------------------------------------------------------------------------*/ -// TODO: implement scrolling - -function DragListener(coordMap, options) { - this.coordMap = coordMap; - this.options = options || {}; -} - - -DragListener.prototype = { - - coordMap: null, - options: null, - - isListening: false, - isDragging: false, - - // the cell/date the mouse was over when listening started - origCell: null, - origDate: null, - - // the cell/date the mouse is over - cell: null, - date: null, - - // coordinates of the initial mousedown - mouseX0: null, - mouseY0: null, - - // handler attached to the document, bound to the DragListener's `this` - mousemoveProxy: null, - mouseupProxy: null, - - scrollEl: null, - scrollBounds: null, // { top, bottom, left, right } - scrollTopVel: null, // pixels per second - scrollLeftVel: null, // pixels per second - scrollIntervalId: null, // ID of setTimeout for scrolling animation loop - scrollHandlerProxy: null, // this-scoped function for handling when scrollEl is scrolled - - scrollSensitivity: 30, // pixels from edge for scrolling to start - scrollSpeed: 200, // pixels per second, at maximum speed - scrollIntervalMs: 50, // millisecond wait between scroll increment - - - // Call this when the user does a mousedown. Will probably lead to startListening - mousedown: function(ev) { - if (isPrimaryMouseButton(ev)) { - - ev.preventDefault(); // prevents native selection in most browsers - - this.startListening(ev); - - // start the drag immediately if there is no minimum distance for a drag start - if (!this.options.distance) { - this.startDrag(ev); - } - } - }, - - - // Call this to start tracking mouse movements - startListening: function(ev) { - var scrollParent; - var cell; - - if (!this.isListening) { - - // grab scroll container and attach handler - if (ev && this.options.scroll) { - scrollParent = getScrollParent($(ev.target)); - if (!scrollParent.is(window) && !scrollParent.is(document)) { - this.scrollEl = scrollParent; - - // scope to `this`, and use `debounce` to make sure rapid calls don't happen - this.scrollHandlerProxy = debounce($.proxy(this, 'scrollHandler'), 100); - this.scrollEl.on('scroll', this.scrollHandlerProxy); - } - } - - this.computeCoords(); // relies on `scrollEl` - - // get info on the initial cell, date, and coordinates - if (ev) { - cell = this.getCell(ev); - this.origCell = cell; - this.origDate = cell ? cell.date : null; - - this.mouseX0 = ev.pageX; - this.mouseY0 = ev.pageY; - } - - $(document) - .on('mousemove', this.mousemoveProxy = $.proxy(this, 'mousemove')) - .on('mouseup', this.mouseupProxy = $.proxy(this, 'mouseup')) - .on('selectstart', this.preventDefault); // prevents native selection in IE<=8 - - this.isListening = true; - this.trigger('listenStart', ev); - } - }, - - - // Recomputes the drag-critical positions of elements - computeCoords: function() { - this.coordMap.build(); - this.computeScrollBounds(); - }, - - - // Called when the user moves the mouse - mousemove: function(ev) { - var minDistance; - var distanceSq; // current distance from mouseX0/mouseY0, squared - - if (!this.isDragging) { // if not already dragging... - // then start the drag if the minimum distance criteria is met - minDistance = this.options.distance || 1; - distanceSq = Math.pow(ev.pageX - this.mouseX0, 2) + Math.pow(ev.pageY - this.mouseY0, 2); - if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem - this.startDrag(ev); - } - } - - if (this.isDragging) { - this.drag(ev); // report a drag, even if this mousemove initiated the drag - } - }, - - - // Call this to initiate a legitimate drag. - // This function is called internally from this class, but can also be called explicitly from outside - startDrag: function(ev) { - var cell; - - if (!this.isListening) { // startDrag must have manually initiated - this.startListening(); - } - - if (!this.isDragging) { - this.isDragging = true; - this.trigger('dragStart', ev); - - // report the initial cell the mouse is over - cell = this.getCell(ev); - if (cell) { - this.cellOver(cell, true); - } - } - }, - - - // Called while the mouse is being moved and when we know a legitimate drag is taking place - drag: function(ev) { - var cell; - - if (this.isDragging) { - cell = this.getCell(ev); - - if (!isCellsEqual(cell, this.cell)) { // a different cell than before? - if (this.cell) { - this.cellOut(); - } - if (cell) { - this.cellOver(cell); - } - } - - this.dragScroll(ev); // will possibly cause scrolling - } - }, - - - // Called when a the mouse has just moved over a new cell - cellOver: function(cell) { - this.cell = cell; - this.date = cell.date; - this.trigger('cellOver', cell, cell.date); - }, - - - // Called when the mouse has just moved out of a cell - cellOut: function() { - if (this.cell) { - this.trigger('cellOut', this.cell); - this.cell = null; - this.date = null; - } - }, - - - // Called when the user does a mouseup - mouseup: function(ev) { - this.stopDrag(ev); - this.stopListening(ev); - }, - - - // Called when the drag is over. Will not cause listening to stop however. - // A concluding 'cellOut' event will NOT be triggered. - stopDrag: function(ev) { - if (this.isDragging) { - this.stopScrolling(); - this.trigger('dragStop', ev); - this.isDragging = false; - } - }, - - - // Call this to stop listening to the user's mouse events - stopListening: function(ev) { - if (this.isListening) { - - // remove the scroll handler if there is a scrollEl - if (this.scrollEl) { - this.scrollEl.off('scroll', this.scrollHandlerProxy); - this.scrollHandlerProxy = null; - } - - $(document) - .off('mousemove', this.mousemoveProxy) - .off('mouseup', this.mouseupProxy) - .off('selectstart', this.preventDefault); - - this.mousemoveProxy = null; - this.mouseupProxy = null; - - this.isListening = false; - this.trigger('listenStop', ev); - - this.origCell = this.cell = null; - this.origDate = this.date = null; - } - }, - - - // Gets the cell underneath the coordinates for the given mouse event - getCell: function(ev) { - return this.coordMap.getCell(ev.pageX, ev.pageY); - }, - - - // Triggers a callback. Calls a function in the option hash of the same name. - // Arguments beyond the first `name` are forwarded on. - trigger: function(name) { - if (this.options[name]) { - this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); - } - }, - - - // Stops a given mouse event from doing it's native browser action. In our case, text selection. - preventDefault: function(ev) { - ev.preventDefault(); - }, - - - /* Scrolling - ------------------------------------------------------------------------------------------------------------------*/ - - - // Computes and stores the bounding rectangle of scrollEl - computeScrollBounds: function() { - var el = this.scrollEl; - var offset; - - if (el) { - offset = el.offset(); - this.scrollBounds = { - top: offset.top, - left: offset.left, - bottom: offset.top + el.outerHeight(), - right: offset.left + el.outerWidth() - }; - } - }, - - - // Called when the dragging is in progress and scrolling should be updated - dragScroll: function(ev) { - var sensitivity = this.scrollSensitivity; - var bounds = this.scrollBounds; - var topCloseness, bottomCloseness; - var leftCloseness, rightCloseness; - var topVel = 0; - var leftVel = 0; - - if (bounds) { // only scroll if scrollEl exists - - // compute closeness to edges. valid range is from 0.0 - 1.0 - topCloseness = (sensitivity - (ev.pageY - bounds.top)) / sensitivity; - bottomCloseness = (sensitivity - (bounds.bottom - ev.pageY)) / sensitivity; - leftCloseness = (sensitivity - (ev.pageX - bounds.left)) / sensitivity; - rightCloseness = (sensitivity - (bounds.right - ev.pageX)) / sensitivity; - - // translate vertical closeness into velocity. - // mouse must be completely in bounds for velocity to happen. - if (topCloseness >= 0 && topCloseness <= 1) { - topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up - } - else if (bottomCloseness >= 0 && bottomCloseness <= 1) { - topVel = bottomCloseness * this.scrollSpeed; - } - - // translate horizontal closeness into velocity - if (leftCloseness >= 0 && leftCloseness <= 1) { - leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left - } - else if (rightCloseness >= 0 && rightCloseness <= 1) { - leftVel = rightCloseness * this.scrollSpeed; - } - } - - this.setScrollVel(topVel, leftVel); - }, - - - // Sets the speed-of-scrolling for the scrollEl - setScrollVel: function(topVel, leftVel) { - - this.scrollTopVel = topVel; - this.scrollLeftVel = leftVel; - - this.constrainScrollVel(); // massages into realistic values - - // if there is non-zero velocity, and an animation loop hasn't already started, then START - if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) { - this.scrollIntervalId = setInterval( - $.proxy(this, 'scrollIntervalFunc'), // scope to `this` - this.scrollIntervalMs - ); - } - }, - - - // Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way - constrainScrollVel: function() { - var el = this.scrollEl; - - if (this.scrollTopVel < 0) { // scrolling up? - if (el.scrollTop() <= 0) { // already scrolled all the way up? - this.scrollTopVel = 0; - } - } - else if (this.scrollTopVel > 0) { // scrolling down? - if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) { // already scrolled all the way down? - this.scrollTopVel = 0; - } - } - - if (this.scrollLeftVel < 0) { // scrolling left? - if (el.scrollLeft() <= 0) { // already scrolled all the left? - this.scrollLeftVel = 0; - } - } - else if (this.scrollLeftVel > 0) { // scrolling right? - if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) { // already scrolled all the way right? - this.scrollLeftVel = 0; - } - } - }, - - - // This function gets called during every iteration of the scrolling animation loop - scrollIntervalFunc: function() { - var el = this.scrollEl; - var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by - - // change the value of scrollEl's scroll - if (this.scrollTopVel) { - el.scrollTop(el.scrollTop() + this.scrollTopVel * frac); - } - if (this.scrollLeftVel) { - el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac); - } - - this.constrainScrollVel(); // since the scroll values changed, recompute the velocities - - // if scrolled all the way, which causes the vels to be zero, stop the animation loop - if (!this.scrollTopVel && !this.scrollLeftVel) { - this.stopScrolling(); - } - }, - - - // Kills any existing scrolling animation loop - stopScrolling: function() { - if (this.scrollIntervalId) { - clearInterval(this.scrollIntervalId); - this.scrollIntervalId = null; - - // when all done with scrolling, recompute positions since they probably changed - this.computeCoords(); - } - }, - - - // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce) - scrollHandler: function() { - // recompute all coordinates, but *only* if this is *not* part of our scrolling animation - if (!this.scrollIntervalId) { - this.computeCoords(); - } - } - -}; - - -// Returns `true` if the cells are identically equal. `false` otherwise. -// They must have the same row, col, and be from the same grid. -// Two null values will be considered equal, as two "out of the grid" states are the same. -function isCellsEqual(cell1, cell2) { - - if (!cell1 && !cell2) { - return true; - } - - if (cell1 && cell2) { - return cell1.grid === cell2.grid && - cell1.row === cell2.row && - cell1.col === cell2.col; - } - - return false; -} - -;; - -/* Creates a clone of an element and lets it track the mouse as it moves -----------------------------------------------------------------------------------------------------------------------*/ - -function MouseFollower(sourceEl, options) { - this.options = options = options || {}; - this.sourceEl = sourceEl; - this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent -} - - -MouseFollower.prototype = { - - options: null, - - sourceEl: null, // the element that will be cloned and made to look like it is dragging - el: null, // the clone of `sourceEl` that will track the mouse - parentEl: null, // the element that `el` (the clone) will be attached to - - // the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl - top0: null, - left0: null, - - // the initial position of the mouse - mouseY0: null, - mouseX0: null, - - // the number of pixels the mouse has moved from its initial position - topDelta: null, - leftDelta: null, - - mousemoveProxy: null, // document mousemove handler, bound to the MouseFollower's `this` - - isFollowing: false, - isHidden: false, - isAnimating: false, // doing the revert animation? - - - // Causes the element to start following the mouse - start: function(ev) { - if (!this.isFollowing) { - this.isFollowing = true; - - this.mouseY0 = ev.pageY; - this.mouseX0 = ev.pageX; - this.topDelta = 0; - this.leftDelta = 0; - - if (!this.isHidden) { - this.updatePosition(); - } - - $(document).on('mousemove', this.mousemoveProxy = $.proxy(this, 'mousemove')); - } - }, - - - // Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position. - // `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately. - stop: function(shouldRevert, callback) { - var _this = this; - var revertDuration = this.options.revertDuration; - - function complete() { - this.isAnimating = false; - _this.destroyEl(); - - this.top0 = this.left0 = null; // reset state for future updatePosition calls - - if (callback) { - callback(); - } - } - - if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time - this.isFollowing = false; - - $(document).off('mousemove', this.mousemoveProxy); - - if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation? - this.isAnimating = true; - this.el.animate({ - top: this.top0, - left: this.left0 - }, { - duration: revertDuration, - complete: complete - }); - } - else { - complete(); - } - } - }, - - - // Gets the tracking element. Create it if necessary - getEl: function() { - var el = this.el; - - if (!el) { - this.sourceEl.width(); // hack to force IE8 to compute correct bounding box - el = this.el = this.sourceEl.clone() - .css({ - position: 'absolute', - visibility: '', // in case original element was hidden (commonly through hideEvents()) - display: this.isHidden ? 'none' : '', // for when initially hidden - margin: 0, - right: 'auto', // erase and set width instead - bottom: 'auto', // erase and set height instead - width: this.sourceEl.width(), // explicit height in case there was a 'right' value - height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value - opacity: this.options.opacity || '', - zIndex: this.options.zIndex - }) - .appendTo(this.parentEl); - } - - return el; - }, - - - // Removes the tracking element if it has already been created - destroyEl: function() { - if (this.el) { - this.el.remove(); - this.el = null; - } - }, - - - // Update the CSS position of the tracking element - updatePosition: function() { - var sourceOffset; - var origin; - - this.getEl(); // ensure this.el - - // make sure origin info was computed - if (this.top0 === null) { - this.sourceEl.width(); // hack to force IE8 to compute correct bounding box - sourceOffset = this.sourceEl.offset(); - origin = this.el.offsetParent().offset(); - this.top0 = sourceOffset.top - origin.top; - this.left0 = sourceOffset.left - origin.left; - } - - this.el.css({ - top: this.top0 + this.topDelta, - left: this.left0 + this.leftDelta - }); - }, - - - // Gets called when the user moves the mouse - mousemove: function(ev) { - this.topDelta = ev.pageY - this.mouseY0; - this.leftDelta = ev.pageX - this.mouseX0; - - if (!this.isHidden) { - this.updatePosition(); - } - }, - - - // Temporarily makes the tracking element invisible. Can be called before following starts - hide: function() { - if (!this.isHidden) { - this.isHidden = true; - if (this.el) { - this.el.hide(); - } - } - }, - - - // Show the tracking element after it has been temporarily hidden - show: function() { - if (this.isHidden) { - this.isHidden = false; - this.updatePosition(); - this.getEl().show(); - } - } - -}; - -;; - -/* A utility class for rendering rows. -----------------------------------------------------------------------------------------------------------------------*/ -// It leverages methods of the subclass and the View to determine custom rendering behavior for each row "type" -// (such as highlight rows, day rows, helper rows, etc). - -function RowRenderer(view) { - this.view = view; -} - - -RowRenderer.prototype = { - - view: null, // a View object - cellHtml: '', // plain default HTML used for a cell when no other is available - - - // Renders the HTML for a row, leveraging custom cell-HTML-renderers based on the `rowType`. - // Also applies the "intro" and "outro" cells, which are specified by the subclass and views. - // `row` is an optional row number. - rowHtml: function(rowType, row) { - var view = this.view; - var renderCell = this.getHtmlRenderer('cell', rowType); - var cellHtml = ''; - var col; - var date; - - row = row || 0; - - for (col = 0; col < view.colCnt; col++) { - date = view.cellToDate(row, col); - cellHtml += renderCell(row, col, date); - } - - cellHtml = this.bookendCells(cellHtml, rowType, row); // apply intro and outro - - return '' + cellHtml + ''; - }, - - - // Applies the "intro" and "outro" HTML to the given cells. - // Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro. - // `cells` can be an HTML string of 's or a jQuery element - // `row` is an optional row number. - bookendCells: function(cells, rowType, row) { - var view = this.view; - var intro = this.getHtmlRenderer('intro', rowType)(row || 0); - var outro = this.getHtmlRenderer('outro', rowType)(row || 0); - var isRTL = view.opt('isRTL'); - var prependHtml = isRTL ? outro : intro; - var appendHtml = isRTL ? intro : outro; - - if (typeof cells === 'string') { - return prependHtml + cells + appendHtml; - } - else { // a jQuery element - return cells.prepend(prependHtml).append(appendHtml); - } - }, - - - // Returns an HTML-rendering function given a specific `rendererName` (like cell, intro, or outro) and a specific - // `rowType` (like day, eventSkeleton, helperSkeleton), which is optional. - // If a renderer for the specific rowType doesn't exist, it will fall back to a generic renderer. - // We will query the View object first for any custom rendering functions, then the methods of the subclass. - getHtmlRenderer: function(rendererName, rowType) { - var view = this.view; - var generalName; // like "cellHtml" - var specificName; // like "dayCellHtml". based on rowType - var provider; // either the View or the RowRenderer subclass, whichever provided the method - var renderer; - - generalName = rendererName + 'Html'; - if (rowType) { - specificName = rowType + capitaliseFirstLetter(rendererName) + 'Html'; - } - - if (specificName && (renderer = view[specificName])) { - provider = view; - } - else if (specificName && (renderer = this[specificName])) { - provider = this; - } - else if ((renderer = view[generalName])) { - provider = view; - } - else if ((renderer = this[generalName])) { - provider = this; - } - - if (typeof renderer === 'function') { - return function(row) { - return renderer.apply(provider, arguments) || ''; // use correct `this` and always return a string - }; - } - - // the rendered can be a plain string as well. if not specified, always an empty string. - return function() { - return renderer || ''; - }; - } - -}; - -;; - -/* An abstract class comprised of a "grid" of cells that each represent a specific datetime -----------------------------------------------------------------------------------------------------------------------*/ - -function Grid(view) { - RowRenderer.call(this, view); // call the super-constructor - this.coordMap = new GridCoordMap(this); -} - - -Grid.prototype = createObject(RowRenderer.prototype); // declare the super-class -$.extend(Grid.prototype, { - - el: null, // the containing element - coordMap: null, // a GridCoordMap that converts pixel values to datetimes - cellDuration: null, // a cell's duration. subclasses must assign this ASAP - - - // Renders the grid into the `el` element. - // Subclasses should override and call this super-method when done. - render: function() { - this.bindHandlers(); - }, - - - // Called when the grid's resources need to be cleaned up - destroy: function() { - // subclasses can implement - }, - - - /* Coordinates & Cells - ------------------------------------------------------------------------------------------------------------------*/ - - - // Populates the given empty arrays with the y and x coordinates of the cells - buildCoords: function(rows, cols) { - // subclasses must implement - }, - - - // Given a cell object, returns the date for that cell - getCellDate: function(cell) { - // subclasses must implement - }, - - - // Given a cell object, returns the element that represents the cell's whole-day - getCellDayEl: function(cell) { - // subclasses must implement - }, - - - // Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects - rangeToSegs: function(start, end) { - // subclasses must implement - }, - - - /* Handlers - ------------------------------------------------------------------------------------------------------------------*/ - - - // Attach handlers to `this.el`, using bubbling to listen to all ancestors. - // We don't need to undo any of this in a "destroy" method, because the view will simply remove `this.el` from the - // DOM and jQuery will be smart enough to garbage collect the handlers. - bindHandlers: function() { - var _this = this; - - this.el.on('mousedown', function(ev) { - if ( - !$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link - !$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one) - ) { - _this.dayMousedown(ev); - } - }); - - this.bindSegHandlers(); // attach event-element-related handlers. in Grid.events.js - }, - - - // Process a mousedown on an element that represents a day. For day clicking and selecting. - dayMousedown: function(ev) { - var _this = this; - var view = this.view; - var isSelectable = view.opt('selectable'); - var dates = null; // the inclusive dates of the selection. will be null if no selection - var start; // the inclusive start of the selection - var end; // the *exclusive* end of the selection - var dayEl; - - // this listener tracks a mousedown on a day element, and a subsequent drag. - // if the drag ends on the same day, it is a 'dayClick'. - // if 'selectable' is enabled, this listener also detects selections. - var dragListener = new DragListener(this.coordMap, { - //distance: 5, // needs more work if we want dayClick to fire correctly - scroll: view.opt('dragScroll'), - dragStart: function() { - view.unselect(); // since we could be rendering a new selection, we want to clear any old one - }, - cellOver: function(cell, date) { - if (dragListener.origDate) { // click needs to have started on a cell - - dayEl = _this.getCellDayEl(cell); - - dates = [ date, dragListener.origDate ].sort(dateCompare); - start = dates[0]; - end = dates[1].clone().add(_this.cellDuration); - - if (isSelectable) { - _this.renderSelection(start, end); - } - } - }, - cellOut: function(cell, date) { - dates = null; - _this.destroySelection(); - }, - listenStop: function(ev) { - if (dates) { // started and ended on a cell? - if (dates[0].isSame(dates[1])) { - view.trigger('dayClick', dayEl[0], start, ev); - } - if (isSelectable) { - // the selection will already have been rendered. just report it - view.reportSelection(start, end, ev); - } - } - } - }); - - dragListener.mousedown(ev); // start listening, which will eventually initiate a dragStart - }, - - - /* Event Dragging - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of a event being dragged over the given date(s). - // `end` can be null, as well as `seg`. See View's documentation on renderDrag for more info. - // A returned value of `true` signals that a mock "helper" event has been rendered. - renderDrag: function(start, end, seg) { - // subclasses must implement - }, - - - // Unrenders a visual indication of an event being dragged - destroyDrag: function() { - // subclasses must implement - }, - - - /* Event Resizing - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event being resized. - // `start` and `end` are the updated dates of the event. `seg` is the original segment object involved in the drag. - renderResize: function(start, end, seg) { - // subclasses must implement - }, - - - // Unrenders a visual indication of an event being resized. - destroyResize: function() { - // subclasses must implement - }, - - - /* Event Helper - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a mock event over the given date(s). - // `end` can be null, in which case the mock event that is rendered will have a null end time. - // `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging. - renderRangeHelper: function(start, end, sourceSeg) { - var view = this.view; - var fakeEvent; - - // compute the end time if forced to do so (this is what EventManager does) - if (!end && view.opt('forceEventDuration')) { - end = view.calendar.getDefaultEventEnd(!start.hasTime(), start); - } - - fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible - fakeEvent.start = start; - fakeEvent.end = end; - fakeEvent.allDay = !(start.hasTime() || (end && end.hasTime())); // freshly compute allDay - - // this extra className will be useful for differentiating real events from mock events in CSS - fakeEvent.className = (fakeEvent.className || []).concat('fc-helper'); - - // if something external is being dragged in, don't render a resizer - if (!sourceSeg) { - fakeEvent.editable = false; - } - - this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering - }, - - - // Renders a mock event - renderHelper: function(event, sourceSeg) { - // subclasses must implement - }, - - - // Unrenders a mock event - destroyHelper: function() { - // subclasses must implement - }, - - - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses. - renderSelection: function(start, end) { - this.renderHighlight(start, end); - }, - - - // Unrenders any visual indications of a selection. Will unrender a highlight by default. - destroySelection: function() { - this.destroyHighlight(); - }, - - - /* Highlight - ------------------------------------------------------------------------------------------------------------------*/ - - - // Puts visual emphasis on a certain date range - renderHighlight: function(start, end) { - // subclasses should implement - }, - - - // Removes visual emphasis on a date range - destroyHighlight: function() { - // subclasses should implement - }, - - - - /* Generic rendering utilities for subclasses - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a day-of-week header row - headHtml: function() { - return '' + - '
' + - '' + - '' + - this.rowHtml('head') + // leverages RowRenderer - '' + - '
' + - '
'; - }, - - - // Used by the `headHtml` method, via RowRenderer, for rendering the HTML of a day-of-week header cell - headCellHtml: function(row, col, date) { - var view = this.view; - var calendar = view.calendar; - var colFormat = view.opt('columnFormat'); - - return '' + - '' + - htmlEscape(calendar.formatDate(date, colFormat)) + - ''; - }, - - - // Renders the HTML for a single-day background cell - bgCellHtml: function(row, col, date) { - var view = this.view; - var classes = this.getDayClasses(date); - - classes.unshift('fc-day', view.widgetContentClass); - - return ''; - }, - - - // Computes HTML classNames for a single-day cell - getDayClasses: function(date) { - var view = this.view; - var today = view.calendar.getNow().stripTime(); - var classes = [ 'fc-' + dayIDs[date.day()] ]; - - if ( - view.name === 'month' && - date.month() != view.intervalStart.month() - ) { - classes.push('fc-other-month'); - } - - if (date.isSame(today, 'day')) { - classes.push( - 'fc-today', - view.highlightStateClass - ); - } - else if (date < today) { - classes.push('fc-past'); - } - else { - classes.push('fc-future'); - } - - return classes; - } - -}); - -;; - -/* Event-rendering and event-interaction methods for the abstract Grid class -----------------------------------------------------------------------------------------------------------------------*/ - -$.extend(Grid.prototype, { - - mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing - isDraggingSeg: false, // is a segment being dragged? boolean - isResizingSeg: false, // is a segment being resized? boolean - - - // Renders the given events onto the grid - renderEvents: function(events) { - // subclasses must implement - }, - - - // Retrieves all rendered segment objects in this grid - getSegs: function() { - // subclasses must implement - }, - - - // Unrenders all events. Subclasses should implement, calling this super-method first. - destroyEvents: function() { - this.triggerSegMouseout(); // trigger an eventMouseout if user's mouse is over an event - }, - - - // Renders a `el` property for each seg, and only returns segments that successfully rendered - renderSegs: function(segs, disableResizing) { - var view = this.view; - var html = ''; - var renderedSegs = []; - var i; - - // build a large concatenation of event segment HTML - for (i = 0; i < segs.length; i++) { - html += this.renderSegHtml(segs[i], disableResizing); - } - - // Grab individual elements from the combined HTML string. Use each as the default rendering. - // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false. - $(html).each(function(i, node) { - var seg = segs[i]; - var el = view.resolveEventEl(seg.event, $(node)); - if (el) { - el.data('fc-seg', seg); // used by handlers - seg.el = el; - renderedSegs.push(seg); - } - }); - - return renderedSegs; - }, - - - // Generates the HTML for the default rendering of a segment - renderSegHtml: function(seg, disableResizing) { - // subclasses must implement - }, - - - // Converts an array of event objects into an array of segment objects - eventsToSegs: function(events, intervalStart, intervalEnd) { - var _this = this; - - return $.map(events, function(event) { - return _this.eventToSegs(event, intervalStart, intervalEnd); // $.map flattens all returned arrays together - }); - }, - - - // Slices a single event into an array of event segments. - // When `intervalStart` and `intervalEnd` are specified, intersect the events with that interval. - // Otherwise, let the subclass decide how it wants to slice the segments over the grid. - eventToSegs: function(event, intervalStart, intervalEnd) { - var eventStart = event.start.clone().stripZone(); // normalize - var eventEnd = this.view.calendar.getEventEnd(event).stripZone(); // compute (if necessary) and normalize - var segs; - var i, seg; - - if (intervalStart && intervalEnd) { - seg = intersectionToSeg(eventStart, eventEnd, intervalStart, intervalEnd); - segs = seg ? [ seg ] : []; - } - else { - segs = this.rangeToSegs(eventStart, eventEnd); // defined by the subclass - } - - // assign extra event-related properties to the segment objects - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - seg.event = event; - seg.eventStartMS = +eventStart; - seg.eventDurationMS = eventEnd - eventStart; - } - - return segs; - }, - - - /* Handlers - ------------------------------------------------------------------------------------------------------------------*/ - - - // Attaches event-element-related handlers to the container element and leverage bubbling - bindSegHandlers: function() { - var _this = this; - var view = this.view; - - $.each( - { - mouseenter: function(seg, ev) { - _this.triggerSegMouseover(seg, ev); - }, - mouseleave: function(seg, ev) { - _this.triggerSegMouseout(seg, ev); - }, - click: function(seg, ev) { - return view.trigger('eventClick', this, seg.event, ev); // can return `false` to cancel - }, - mousedown: function(seg, ev) { - if ($(ev.target).is('.fc-resizer') && view.isEventResizable(seg.event)) { - _this.segResizeMousedown(seg, ev); - } - else if (view.isEventDraggable(seg.event)) { - _this.segDragMousedown(seg, ev); - } - } - }, - function(name, func) { - // attach the handler to the container element and only listen for real event elements via bubbling - _this.el.on(name, '.fc-event-container > *', function(ev) { - var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents - - // only call the handlers if there is not a drag/resize in progress - if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) { - return func.call(this, seg, ev); // `this` will be the event element - } - }); - } - ); - }, - - - // Updates internal state and triggers handlers for when an event element is moused over - triggerSegMouseover: function(seg, ev) { - if (!this.mousedOverSeg) { - this.mousedOverSeg = seg; - this.view.trigger('eventMouseover', seg.el[0], seg.event, ev); - } - }, - - - // Updates internal state and triggers handlers for when an event element is moused out. - // Can be given no arguments, in which case it will mouseout the segment that was previously moused over. - triggerSegMouseout: function(seg, ev) { - ev = ev || {}; // if given no args, make a mock mouse event - - if (this.mousedOverSeg) { - seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment - this.mousedOverSeg = null; - this.view.trigger('eventMouseout', seg.el[0], seg.event, ev); - } - }, - - - /* Dragging - ------------------------------------------------------------------------------------------------------------------*/ - - - // Called when the user does a mousedown on an event, which might lead to dragging. - // Generic enough to work with any type of Grid. - segDragMousedown: function(seg, ev) { - var _this = this; - var view = this.view; - var el = seg.el; - var event = seg.event; - var newStart, newEnd; - - // A clone of the original element that will move with the mouse - var mouseFollower = new MouseFollower(seg.el, { - parentEl: view.el, - opacity: view.opt('dragOpacity'), - revertDuration: view.opt('dragRevertDuration'), - zIndex: 2 // one above the .fc-view - }); - - // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents - // of the view. - var dragListener = new DragListener(view.coordMap, { - distance: 5, - scroll: view.opt('dragScroll'), - listenStart: function(ev) { - mouseFollower.hide(); // don't show until we know this is a real drag - mouseFollower.start(ev); - }, - dragStart: function(ev) { - _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported - _this.isDraggingSeg = true; - view.hideEvent(event); // hide all event segments. our mouseFollower will take over - view.trigger('eventDragStart', el[0], event, ev, {}); // last argument is jqui dummy - }, - cellOver: function(cell, date) { - var origDate = seg.cellDate || dragListener.origDate; - var res = _this.computeDraggedEventDates(seg, origDate, date); - newStart = res.start; - newEnd = res.end; - - if (view.renderDrag(newStart, newEnd, seg)) { // have the view render a visual indication - mouseFollower.hide(); // if the view is already using a mock event "helper", hide our own - } - else { - mouseFollower.show(); - } - }, - cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells - newStart = null; - view.destroyDrag(); // unrender whatever was done in view.renderDrag - mouseFollower.show(); // show in case we are moving out of all cells - }, - dragStop: function(ev) { - var hasChanged = newStart && !newStart.isSame(event.start); - - // do revert animation if hasn't changed. calls a callback when finished (whether animation or not) - mouseFollower.stop(!hasChanged, function() { - _this.isDraggingSeg = false; - view.destroyDrag(); - view.showEvent(event); - view.trigger('eventDragStop', el[0], event, ev, {}); // last argument is jqui dummy - - if (hasChanged) { - view.eventDrop(el[0], event, newStart, ev); // will rerender all events... - } - }); - }, - listenStop: function() { - mouseFollower.stop(); // put in listenStop in case there was a mousedown but the drag never started - } - }); - - dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart - }, - - - // Given a segment, the dates where a drag began and ended, calculates the Event Object's new start and end dates - computeDraggedEventDates: function(seg, dragStartDate, dropDate) { - var view = this.view; - var event = seg.event; - var start = event.start; - var end = view.calendar.getEventEnd(event); - var delta; - var newStart; - var newEnd; - - if (dropDate.hasTime() === dragStartDate.hasTime()) { - delta = dayishDiff(dropDate, dragStartDate); - newStart = start.clone().add(delta); - if (event.end === null) { // do we need to compute an end? - newEnd = null; - } - else { - newEnd = end.clone().add(delta); - } - } - else { - // if switching from day <-> timed, start should be reset to the dropped date, and the end cleared - newStart = dropDate; - newEnd = null; // end should be cleared - } - - return { start: newStart, end: newEnd }; - }, - - - /* Resizing - ------------------------------------------------------------------------------------------------------------------*/ - - - // Called when the user does a mousedown on an event's resizer, which might lead to resizing. - // Generic enough to work with any type of Grid. - segResizeMousedown: function(seg, ev) { - var _this = this; - var view = this.view; - var el = seg.el; - var event = seg.event; - var start = event.start; - var end = view.calendar.getEventEnd(event); - var newEnd = null; - var dragListener; - - function destroy() { // resets the rendering - _this.destroyResize(); - view.showEvent(event); - } - - // Tracks mouse movement over the *grid's* coordinate map - dragListener = new DragListener(this.coordMap, { - distance: 5, - scroll: view.opt('dragScroll'), - dragStart: function(ev) { - _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported - _this.isResizingSeg = true; - view.trigger('eventResizeStart', el[0], event, ev, {}); // last argument is jqui dummy - }, - cellOver: function(cell, date) { - // compute the new end. don't allow it to go before the event's start - if (date.isBefore(start)) { // allows comparing ambig to non-ambig - date = start; - } - newEnd = date.clone().add(_this.cellDuration); // make it an exclusive end - - if (newEnd.isSame(end)) { - newEnd = null; - destroy(); - } - else { - _this.renderResize(start, newEnd, seg); - view.hideEvent(event); - } - }, - cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells - newEnd = null; - destroy(); - }, - dragStop: function(ev) { - _this.isResizingSeg = false; - destroy(); - view.trigger('eventResizeStop', el[0], event, ev, {}); // last argument is jqui dummy - - if (newEnd) { - view.eventResize(el[0], event, newEnd, ev); // will rerender all events... - } - } - }); - - dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart - }, - - - /* Rendering Utils - ------------------------------------------------------------------------------------------------------------------*/ - - - // Generic utility for generating the HTML classNames for an event segment's element - getSegClasses: function(seg, isDraggable, isResizable) { - var event = seg.event; - var classes = [ - 'fc-event', - seg.isStart ? 'fc-start' : 'fc-not-start', - seg.isEnd ? 'fc-end' : 'fc-not-end' - ].concat( - event.className, - event.source ? event.source.className : [] - ); - - if (isDraggable) { - classes.push('fc-draggable'); - } - if (isResizable) { - classes.push('fc-resizable'); - } - - return classes; - }, - - - // Utility for generating a CSS string with all the event skin-related properties - getEventSkinCss: function(event) { - var view = this.view; - var source = event.source || {}; - var eventColor = event.color; - var sourceColor = source.color; - var optionColor = view.opt('eventColor'); - var backgroundColor = - event.backgroundColor || - eventColor || - source.backgroundColor || - sourceColor || - view.opt('eventBackgroundColor') || - optionColor; - var borderColor = - event.borderColor || - eventColor || - source.borderColor || - sourceColor || - view.opt('eventBorderColor') || - optionColor; - var textColor = - event.textColor || - source.textColor || - view.opt('eventTextColor'); - var statements = []; - if (backgroundColor) { - statements.push('background-color:' + backgroundColor); - } - if (borderColor) { - statements.push('border-color:' + borderColor); - } - if (textColor) { - statements.push('color:' + textColor); - } - return statements.join(';'); - } - -}); - - -/* Event Segment Utilities -----------------------------------------------------------------------------------------------------------------------*/ - - -// A cmp function for determining which segments should take visual priority -function compareSegs(seg1, seg2) { - return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first - seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first - seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1) - (seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title -} - - -;; - -/* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week. -----------------------------------------------------------------------------------------------------------------------*/ - -function DayGrid(view) { - Grid.call(this, view); // call the super-constructor -} - - -DayGrid.prototype = createObject(Grid.prototype); // declare the super-class -$.extend(DayGrid.prototype, { - - numbersVisible: false, // should render a row for day/week numbers? manually set by the view - cellDuration: moment.duration({ days: 1 }), // required for Grid.event.js. Each cell is always a single day - bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid - - rowEls: null, // set of fake row elements - dayEls: null, // set of whole-day elements comprising the row's background - helperEls: null, // set of cell skeleton elements for rendering the mock event "helper" - highlightEls: null, // set of cell skeleton elements for rendering the highlight - - - // Renders the rows and columns into the component's `this.el`, which should already be assigned. - // isRigid determins whether the individual rows should ignore the contents and be a constant height. - // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient. - render: function(isRigid) { - var view = this.view; - var html = ''; - var row; - - for (row = 0; row < view.rowCnt; row++) { - html += this.dayRowHtml(row, isRigid); - } - this.el.html(html); - - this.rowEls = this.el.find('.fc-row'); - this.dayEls = this.el.find('.fc-day'); - - // run all the day cells through the dayRender callback - this.dayEls.each(function(i, node) { - var date = view.cellToDate(Math.floor(i / view.colCnt), i % view.colCnt); - view.trigger('dayRender', null, date, $(node)); - }); - - Grid.prototype.render.call(this); // call the super-method - }, - - - destroy: function() { - this.destroySegPopover(); - }, - - - // Generates the HTML for a single row. `row` is the row number. - dayRowHtml: function(row, isRigid) { - var view = this.view; - var classes = [ 'fc-row', 'fc-week', view.widgetContentClass ]; - - if (isRigid) { - classes.push('fc-rigid'); - } - - return '' + - '
' + - '
' + - '' + - this.rowHtml('day', row) + // leverages RowRenderer. calls dayCellHtml() - '
' + - '
' + - '
' + - '' + - (this.numbersVisible ? - '' + - this.rowHtml('number', row) + // leverages RowRenderer. View will define render method - '' : - '' - ) + - '
' + - '
' + - '
'; - }, - - - // Renders the HTML for a whole-day cell. Will eventually end up in the day-row's background. - // We go through a 'day' row type instead of just doing a 'bg' row type so that the View can do custom rendering - // specifically for whole-day rows, whereas a 'bg' might also be used for other purposes (TimeGrid bg for example). - dayCellHtml: function(row, col, date) { - return this.bgCellHtml(row, col, date); - }, - - - /* Coordinates & Cells - ------------------------------------------------------------------------------------------------------------------*/ - - - // Populates the empty `rows` and `cols` arrays with coordinates of the cells. For CoordGrid. - buildCoords: function(rows, cols) { - var colCnt = this.view.colCnt; - var e, n, p; - - this.dayEls.slice(0, colCnt).each(function(i, _e) { // iterate the first row of day elements - e = $(_e); - n = e.offset().left; - if (i) { - p[1] = n; - } - p = [ n ]; - cols[i] = p; - }); - p[1] = n + e.outerWidth(); - - this.rowEls.each(function(i, _e) { - e = $(_e); - n = e.offset().top; - if (i) { - p[1] = n; - } - p = [ n ]; - rows[i] = p; - }); - p[1] = n + e.outerHeight() + this.bottomCoordPadding; // hack to extend hit area of last row - }, - - - // Converts a cell to a date - getCellDate: function(cell) { - return this.view.cellToDate(cell); // leverages the View's cell system - }, - - - // Gets the whole-day element associated with the cell - getCellDayEl: function(cell) { - return this.dayEls.eq(cell.row * this.view.colCnt + cell.col); - }, - - - // Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects - rangeToSegs: function(start, end) { - return this.view.rangeToSegments(start, end); // leverages the View's cell system - }, - - - /* Event Drag Visualization - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event hovering over the given date(s). - // `end` can be null, as well as `seg`. See View's documentation on renderDrag for more info. - // A returned value of `true` signals that a mock "helper" event has been rendered. - renderDrag: function(start, end, seg) { - var opacity; - - // always render a highlight underneath - this.renderHighlight( - start, - end || this.view.calendar.getDefaultEventEnd(true, start) - ); - - // if a segment from the same calendar but another component is being dragged, render a helper event - if (seg && !seg.el.closest(this.el).length) { - - this.renderRangeHelper(start, end, seg); - - opacity = this.view.opt('dragOpacity'); - if (opacity !== undefined) { - this.helperEls.css('opacity', opacity); - } - - return true; // a helper has been rendered - } - }, - - - // Unrenders any visual indication of a hovering event - destroyDrag: function() { - this.destroyHighlight(); - this.destroyHelper(); - }, - - - /* Event Resize Visualization - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event being resized - renderResize: function(start, end, seg) { - this.renderHighlight(start, end); - this.renderRangeHelper(start, end, seg); - }, - - - // Unrenders a visual indication of an event being resized - destroyResize: function() { - this.destroyHighlight(); - this.destroyHelper(); - }, - - - /* Event Helper - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null. - renderHelper: function(event, sourceSeg) { - var helperNodes = []; - var rowStructs = this.renderEventRows([ event ]); - - // inject each new event skeleton into each associated row - this.rowEls.each(function(row, rowNode) { - var rowEl = $(rowNode); // the .fc-row - var skeletonEl = $('
'); // will be absolutely positioned - var skeletonTop; - - // If there is an original segment, match the top position. Otherwise, put it at the row's top level - if (sourceSeg && sourceSeg.row === row) { - skeletonTop = sourceSeg.el.position().top; - } - else { - skeletonTop = rowEl.find('.fc-content-skeleton tbody').position().top; - } - - skeletonEl.css('top', skeletonTop) - .find('table') - .append(rowStructs[row].tbodyEl); - - rowEl.append(skeletonEl); - helperNodes.push(skeletonEl[0]); - }); - - this.helperEls = $(helperNodes); // array -> jQuery set - }, - - - // Unrenders any visual indication of a mock helper event - destroyHelper: function() { - if (this.helperEls) { - this.helperEls.remove(); - this.helperEls = null; - } - }, - - - /* Highlighting - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders an emphasis on the given date range. `start` is an inclusive, `end` is exclusive. - renderHighlight: function(start, end) { - var segs = this.rangeToSegs(start, end); - var highlightNodes = []; - var i, seg; - var el; - - // build an event skeleton for each row that needs it - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - el = $( - this.highlightSkeletonHtml(seg.leftCol, seg.rightCol + 1) // make end exclusive - ); - el.appendTo(this.rowEls[seg.row]); - highlightNodes.push(el[0]); - } - - this.highlightEls = $(highlightNodes); // array -> jQuery set - }, - - - // Unrenders any visual emphasis on a date range - destroyHighlight: function() { - if (this.highlightEls) { - this.highlightEls.remove(); - this.highlightEls = null; - } - }, - - - // Generates the HTML used to build a single-row "highlight skeleton", a table that frames highlight cells - highlightSkeletonHtml: function(startCol, endCol) { - var colCnt = this.view.colCnt; - var cellHtml = ''; - - if (startCol > 0) { - cellHtml += ' elements that should be appended to each row's content skeleton. - // Returns an array of rowStruct objects (see the bottom of `renderEventRow`). - renderEventRows: function(events) { - var segs = this.eventsToSegs(events); - var rowStructs = []; - var segRows; - var row; - - segs = this.renderSegs(segs); // returns a new array with only visible segments - segRows = this.groupSegRows(segs); // group into nested arrays - - // iterate each row of segment groupings - for (row = 0; row < segRows.length; row++) { - rowStructs.push( - this.renderEventRow(row, segRows[row]) - ); - } - - return rowStructs; - }, - - - // Builds the HTML to be used for the default element for an individual segment - renderSegHtml: function(seg, disableResizing) { - var view = this.view; - var isRTL = view.opt('isRTL'); - var event = seg.event; - var isDraggable = view.isEventDraggable(event); - var isResizable = !disableResizing && event.allDay && seg.isEnd && view.isEventResizable(event); - var classes = this.getSegClasses(seg, isDraggable, isResizable); - var skinCss = this.getEventSkinCss(event); - var timeHtml = ''; - var titleHtml; - - classes.unshift('fc-day-grid-event'); - - // Only display a timed events time if it is the starting segment - if (!event.allDay && seg.isStart) { - timeHtml = '' + htmlEscape(view.getEventTimeText(event)) + ''; - } - - titleHtml = - '' + - (htmlEscape(event.title || '') || ' ') + // we always want one line of height - ''; - - return '' + - '
' + - (isRTL ? - titleHtml + ' ' + timeHtml : // put a natural space in between - timeHtml + ' ' + titleHtml // - ) + - '
' + - (isResizable ? - '
' : - '' - ) + - ''; - }, - - - // Given a row # and an array of segments all in the same row, render a
element, a skeleton that contains - // the segments. Returns object with a bunch of internal data about how the render was calculated. - renderEventRow: function(row, rowSegs) { - var view = this.view; - var colCnt = view.colCnt; - var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels - var levelCnt = Math.max(1, segLevels.length); // ensure at least one level - var tbody = $(''); - var segMatrix = []; // lookup for which segments are rendered into which level+col cells - var cellMatrix = []; // lookup for all '); - - segMatrix.push([]); - cellMatrix.push([]); - loneCellMatrix.push([]); - - // levelCnt might be 1 even though there are no actual levels. protect against this. - // this single empty row is useful for styling. - if (levelSegs) { - for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level - seg = levelSegs[j]; - - emptyCellsUntil(seg.leftCol); - - // create a container that occupies or more columns. append the event element. - td = $(' at a time and stop when we find one out of bounds - for (i = 0; i < trEls.length; i++) { - trEl = trEls.eq(i).removeClass('fc-limited'); // get and reveal - if (trEl.position().top + trEl.outerHeight() > rowHeight) { - return i; - } - } - - return false; // should not limit at all - }, - - - // Limits the given grid row to the maximum number of levels and injects "more" links if necessary. - // `row` is the row number. - // `levelLimit` is a number for the maximum (inclusive) number of levels allowed. - limitRow: function(row, levelLimit) { - var _this = this; - var view = this.view; - var rowStruct = this.rowStructs[row]; - var moreNodes = []; // array of "more" links and and segment elements past the limit - .addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array - - // iterate though segments in the last allowable level - for (i = 0; i < levelSegs.length; i++) { - seg = levelSegs[i]; - emptyCellsUntil(seg.leftCol); // process empty cells before the segment - - // determine *all* segments below `seg` that occupy the same columns - colSegsBelow = []; - totalSegsBelow = 0; - while (col <= seg.rightCol) { - cell = { row: row, col: col }; - segsBelow = this.getCellSegs(cell, levelLimit); - colSegsBelow.push(segsBelow); - totalSegsBelow += segsBelow.length; - col++; - } - - if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links? - td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell - rowspan = td.attr('rowspan') || 1; - segMoreNodes = []; - - // make a replacement '; - - html += - '' + - (!isRTL ? axisHtml : '') + - '"; - - slotTime.add(this.slotDuration); - } - - return html; - }, - - - // Parses various options into properties of this object - processOptions: function() { - var view = this.view; - var slotDuration = view.opt('slotDuration'); - var snapDuration = view.opt('snapDuration'); - - slotDuration = moment.duration(slotDuration); - snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration; - - this.slotDuration = slotDuration; - this.snapDuration = snapDuration; - this.cellDuration = snapDuration; // important to assign this for Grid.events.js - - this.minTime = moment.duration(view.opt('minTime')); - this.maxTime = moment.duration(view.opt('maxTime')); - }, - - - // Slices up a date range into a segment for each column - rangeToSegs: function(rangeStart, rangeEnd) { - var view = this.view; - var segs = []; - var seg; - var col; - var cellDate; - var colStart, colEnd; - - // normalize - rangeStart = rangeStart.clone().stripZone(); - rangeEnd = rangeEnd.clone().stripZone(); - - for (col = 0; col < view.colCnt; col++) { - cellDate = view.cellToDate(0, col); // use the View's cell system for this - colStart = cellDate.clone().time(this.minTime); - colEnd = cellDate.clone().time(this.maxTime); - seg = intersectionToSeg(rangeStart, rangeEnd, colStart, colEnd); - if (seg) { - seg.col = col; - segs.push(seg); - } - } - - return segs; - }, - - - /* Coordinates - ------------------------------------------------------------------------------------------------------------------*/ - - - // Called when there is a window resize/zoom and we need to recalculate coordinates for the grid - resize: function() { - this.computeSlatTops(); - this.updateSegVerticals(); - }, - - - // Populates the given empty `rows` and `cols` arrays with offset positions of the "snap" cells. - // "Snap" cells are different the slots because they might have finer granularity. - buildCoords: function(rows, cols) { - var colCnt = this.view.colCnt; - var originTop = this.el.offset().top; - var snapTime = moment.duration(+this.minTime); - var p = null; - var e, n; - - this.dayEls.slice(0, colCnt).each(function(i, _e) { - e = $(_e); - n = e.offset().left; - if (p) { - p[1] = n; - } - p = [ n ]; - cols[i] = p; - }); - p[1] = n + e.outerWidth(); - - p = null; - while (snapTime < this.maxTime) { - n = originTop + this.computeTimeTop(snapTime); - if (p) { - p[1] = n; - } - p = [ n ]; - rows.push(p); - snapTime.add(this.snapDuration); - } - p[1] = originTop + this.computeTimeTop(snapTime); // the position of the exclusive end - }, - - - // Gets the datetime for the given slot cell - getCellDate: function(cell) { - var view = this.view; - var calendar = view.calendar; - - return calendar.rezoneDate( // since we are adding a time, it needs to be in the calendar's timezone - view.cellToDate(0, cell.col) // View's coord system only accounts for start-of-day for column - .time(this.minTime + this.snapDuration * cell.row) - ); - }, - - - // Gets the element that represents the whole-day the cell resides on - getCellDayEl: function(cell) { - return this.dayEls.eq(cell.col); - }, - - - // Computes the top coordinate, relative to the bounds of the grid, of the given date. - // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight. - computeDateTop: function(date, startOfDayDate) { - return this.computeTimeTop( - moment.duration( - date.clone().stripZone() - startOfDayDate.clone().stripTime() - ) - ); - }, - - - // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration). - computeTimeTop: function(time) { - var slatCoverage = (time - this.minTime) / this.slotDuration; // floating-point value of # of slots covered - var slatIndex; - var slatRemainder; - var slatTop; - var slatBottom; - - // constrain. because minTime/maxTime might be customized - slatCoverage = Math.max(0, slatCoverage); - slatCoverage = Math.min(this.slatEls.length, slatCoverage); - - slatIndex = Math.floor(slatCoverage); // an integer index of the furthest whole slot - slatRemainder = slatCoverage - slatIndex; - slatTop = this.slatTops[slatIndex]; // the top position of the furthest whole slot - - if (slatRemainder) { // time spans part-way into the slot - slatBottom = this.slatTops[slatIndex + 1]; - return slatTop + (slatBottom - slatTop) * slatRemainder; // part-way between slots - } - else { - return slatTop; - } - }, - - - // Queries each `slatEl` for its position relative to the grid's container and stores it in `slatTops`. - // Includes the the bottom of the last slat as the last item in the array. - computeSlatTops: function() { - var tops = []; - var top; - - this.slatEls.each(function(i, node) { - top = $(node).position().top; - tops.push(top); - }); - - tops.push(top + this.slatEls.last().outerHeight()); // bottom of the last slat - - this.slatTops = tops; - }, - - - /* Event Drag Visualization - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event being dragged over the specified date(s). - // `end` and `seg` can be null. See View's documentation on renderDrag for more info. - renderDrag: function(start, end, seg) { - var opacity; - - if (seg) { // if there is event information for this drag, render a helper event - this.renderRangeHelper(start, end, seg); - - opacity = this.view.opt('dragOpacity'); - if (opacity !== undefined) { - this.helperEl.css('opacity', opacity); - } - - return true; // signal that a helper has been rendered - } - else { - // otherwise, just render a highlight - this.renderHighlight( - start, - end || this.view.calendar.getDefaultEventEnd(false, start) - ); - } - }, - - - // Unrenders any visual indication of an event being dragged - destroyDrag: function() { - this.destroyHelper(); - this.destroyHighlight(); - }, - - - /* Event Resize Visualization - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event being resized - renderResize: function(start, end, seg) { - this.renderRangeHelper(start, end, seg); - }, - - - // Unrenders any visual indication of an event being resized - destroyResize: function() { - this.destroyHelper(); - }, - - - /* Event Helper - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag) - renderHelper: function(event, sourceSeg) { - var res = this.renderEventTable([ event ]); - var tableEl = res.tableEl; - var segs = res.segs; - var i, seg; - var sourceEl; - - // Try to make the segment that is in the same row as sourceSeg look the same - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - if (sourceSeg && sourceSeg.col === seg.col) { - sourceEl = sourceSeg.el; - seg.el.css({ - left: sourceEl.css('left'), - right: sourceEl.css('right'), - 'margin-left': sourceEl.css('margin-left'), - 'margin-right': sourceEl.css('margin-right') - }); - } - } - - this.helperEl = $('
') - .append(tableEl) - .appendTo(this.el); - }, - - - // Unrenders any mock helper event - destroyHelper: function() { - if (this.helperEl) { - this.helperEl.remove(); - this.helperEl = null; - } - }, - - - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight. - renderSelection: function(start, end) { - if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered - this.renderRangeHelper(start, end); - } - else { - this.renderHighlight(start, end); - } - }, - - - // Unrenders any visual indication of a selection - destroySelection: function() { - this.destroyHelper(); - this.destroyHighlight(); - }, - - - /* Highlight - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders an emphasis on the given date range. `start` is inclusive. `end` is exclusive. - renderHighlight: function(start, end) { - this.highlightEl = $( - this.highlightSkeletonHtml(start, end) - ).appendTo(this.el); - }, - - - // Unrenders the emphasis on a date range - destroyHighlight: function() { - if (this.highlightEl) { - this.highlightEl.remove(); - this.highlightEl = null; - } - }, - - - // Generates HTML for a table element with containers in each column, responsible for absolutely positioning the - // highlight elements to cover the highlighted slots. - highlightSkeletonHtml: function(start, end) { - var view = this.view; - var segs = this.rangeToSegs(start, end); - var cellHtml = ''; - var col = 0; - var i, seg; - var dayDate; - var top, bottom; - - for (i = 0; i < segs.length; i++) { // loop through the segments. one per column - seg = segs[i]; - - // need empty cells beforehand? - if (col < seg.col) { - cellHtml += '
'; - - col++; - } - - // need empty cells after the last segment? - if (col < view.colCnt) { - cellHtml += ''; - } - }, - - - // Generates the HTML that will go before content-skeleton cells that display the day/week numbers. - // Queried by the DayGrid subcomponent. Ordering depends on isRTL. - numberIntroHtml: function(row) { - if (this.weekNumbersVisible) { - return '' + - ''; - } - }, - - - // Generates the HTML that goes before the day bg cells for each day-row. - // Queried by the DayGrid subcomponent. Ordering depends on isRTL. - dayIntroHtml: function() { - if (this.weekNumbersVisible) { - return ''; - } - }, - - - // Generates the HTML that goes before every other type of row generated by DayGrid. Ordering depends on isRTL. - // Affects helper-skeleton and highlight-skeleton rows. - introHtml: function() { - if (this.weekNumbersVisible) { - return ''; - } - }, - - - // Generates the HTML for the '; - }, - - - // Generates an HTML attribute string for setting the width of the week number column, if it is known - weekNumberStyleAttr: function() { - if (this.weekNumberWidth !== null) { - return 'style="width:' + this.weekNumberWidth + 'px"'; - } - return ''; - }, - - - // Determines whether each row should have a constant height - hasRigidRows: function() { - var eventLimit = this.opt('eventLimit'); - return eventLimit && typeof eventLimit !== 'number'; - }, - - - /* Dimensions - ------------------------------------------------------------------------------------------------------------------*/ - - - // Refreshes the horizontal dimensions of the view - updateWidth: function() { - if (this.weekNumbersVisible) { - // Make sure all week number cells running down the side have the same width. - // Record the width for cells created later. - this.weekNumberWidth = matchCellWidths( - this.el.find('.fc-week-number') - ); - } - }, - - - // Adjusts the vertical dimensions of the view to the specified values - setHeight: function(totalHeight, isAuto) { - var eventLimit = this.opt('eventLimit'); - var scrollerHeight; - - // reset all heights to be natural - unsetScroller(this.scrollerEl); - uncompensateScroll(this.headRowEl); - - this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed - - // is the event limit a constant level number? - if (eventLimit && typeof eventLimit === 'number') { - this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after - } - - scrollerHeight = this.computeScrollerHeight(totalHeight); - this.setGridHeight(scrollerHeight, isAuto); - - // is the event limit dynamically calculated? - if (eventLimit && typeof eventLimit !== 'number') { - this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set - } - - if (!isAuto && setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars? - - compensateScroll(this.headRowEl, getScrollbarWidths(this.scrollerEl)); - - // doing the scrollbar compensation might have created text overflow which created more height. redo - scrollerHeight = this.computeScrollerHeight(totalHeight); - this.scrollerEl.height(scrollerHeight); - - this.restoreScroll(); - } - }, - - - // Sets the height of just the DayGrid component in this view - setGridHeight: function(height, isAuto) { - if (isAuto) { - undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding - } - else { - distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows - } - }, - - - /* Events - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders the given events onto the view and populates the segments array - renderEvents: function(events) { - this.dayGrid.renderEvents(events); - - this.updateHeight(); // must compensate for events that overflow the row - - View.prototype.renderEvents.call(this, events); // call the super-method - }, - - - // Retrieves all segment objects that are rendered in the view - getSegs: function() { - return this.dayGrid.getSegs(); - }, - - - // Unrenders all event elements and clears internal segment data - destroyEvents: function() { - View.prototype.destroyEvents.call(this); // do this before dayGrid's segs have been cleared - - this.recordScroll(); // removing events will reduce height and mess with the scroll, so record beforehand - this.dayGrid.destroyEvents(); - - // we DON'T need to call updateHeight() because: - // A) a renderEvents() call always happens after this, which will eventually call updateHeight() - // B) in IE8, this causes a flash whenever events are rerendered - }, - - - /* Event Dragging - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event being dragged over the view. - // A returned value of `true` signals that a mock "helper" event has been rendered. - renderDrag: function(start, end, seg) { - return this.dayGrid.renderDrag(start, end, seg); - }, - - - // Unrenders the visual indication of an event being dragged over the view - destroyDrag: function() { - this.dayGrid.destroyDrag(); - }, - - - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of a selection - renderSelection: function(start, end) { - this.dayGrid.renderSelection(start, end); - }, - - - // Unrenders a visual indications of a selection - destroySelection: function() { - this.dayGrid.destroySelection(); - } - -}); - -;; - -/* A month view with day cells running in rows (one-per-week) and columns -----------------------------------------------------------------------------------------------------------------------*/ - -setDefaults({ - fixedWeekCount: true -}); - -fcViews.month = MonthView; // register the view - -function MonthView(calendar) { - BasicView.call(this, calendar); // call the super-constructor -} - - -MonthView.prototype = createObject(BasicView.prototype); // define the super-class -$.extend(MonthView.prototype, { - - name: 'month', - - - incrementDate: function(date, delta) { - return date.clone().stripTime().add(delta, 'months').startOf('month'); - }, - - - render: function(date) { - var rowCnt; - - this.intervalStart = date.clone().stripTime().startOf('month'); - this.intervalEnd = this.intervalStart.clone().add(1, 'months'); - - this.start = this.intervalStart.clone(); - this.start = this.skipHiddenDays(this.start); // move past the first week if no visible days - this.start.startOf('week'); - this.start = this.skipHiddenDays(this.start); // move past the first invisible days of the week - - this.end = this.intervalEnd.clone(); - this.end = this.skipHiddenDays(this.end, -1, true); // move in from the last week if no visible days - this.end.add((7 - this.end.weekday()) % 7, 'days'); // move to end of week if not already - this.end = this.skipHiddenDays(this.end, -1, true); // move in from the last invisible days of the week - - rowCnt = Math.ceil( // need to ceil in case there are hidden days - this.end.diff(this.start, 'weeks', true) // returnfloat=true - ); - if (this.isFixedWeeks()) { - this.end.add(6 - rowCnt, 'weeks'); - rowCnt = 6; - } - - this.title = this.calendar.formatDate(this.intervalStart, this.opt('titleFormat')); - - BasicView.prototype.render.call(this, rowCnt, this.getCellsPerWeek(), true); // call the super-method - }, - - - // Overrides the default BasicView behavior to have special multi-week auto-height logic - setGridHeight: function(height, isAuto) { - - isAuto = isAuto || this.opt('weekMode') === 'variable'; // LEGACY: weekMode is deprecated - - // if auto, make the height of each row the height that it would be if there were 6 weeks - if (isAuto) { - height *= this.rowCnt / 6; - } - - distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows - }, - - - isFixedWeeks: function() { - var weekMode = this.opt('weekMode'); // LEGACY: weekMode is deprecated - if (weekMode) { - return weekMode === 'fixed'; // if any other type of weekMode, assume NOT fixed - } - - return this.opt('fixedWeekCount'); - } - -}); - -;; - -/* A week view with simple day cells running horizontally -----------------------------------------------------------------------------------------------------------------------*/ -// TODO: a WeekView mixin for calculating dates and titles - -fcViews.basicWeek = BasicWeekView; // register this view - -function BasicWeekView(calendar) { - BasicView.call(this, calendar); // call the super-constructor -} - - -BasicWeekView.prototype = createObject(BasicView.prototype); // define the super-class -$.extend(BasicWeekView.prototype, { - - name: 'basicWeek', - - - incrementDate: function(date, delta) { - return date.clone().stripTime().add(delta, 'weeks').startOf('week'); - }, - - - render: function(date) { - - this.intervalStart = date.clone().stripTime().startOf('week'); - this.intervalEnd = this.intervalStart.clone().add(1, 'weeks'); - - this.start = this.skipHiddenDays(this.intervalStart); - this.end = this.skipHiddenDays(this.intervalEnd, -1, true); - - this.title = this.calendar.formatRange( - this.start, - this.end.clone().subtract(1), // make inclusive by subtracting 1 ms - this.opt('titleFormat'), - ' \u2014 ' // emphasized dash - ); - - BasicView.prototype.render.call(this, 1, this.getCellsPerWeek(), false); // call the super-method - } - -}); -;; - -/* A view with a single simple day cell -----------------------------------------------------------------------------------------------------------------------*/ - -fcViews.basicDay = BasicDayView; // register this view - -function BasicDayView(calendar) { - BasicView.call(this, calendar); // call the super-constructor -} - - -BasicDayView.prototype = createObject(BasicView.prototype); // define the super-class -$.extend(BasicDayView.prototype, { - - name: 'basicDay', - - - incrementDate: function(date, delta) { - var out = date.clone().stripTime().add(delta, 'days'); - out = this.skipHiddenDays(out, delta < 0 ? -1 : 1); - return out; - }, - - - render: function(date) { - - this.start = this.intervalStart = date.clone().stripTime(); - this.end = this.intervalEnd = this.start.clone().add(1, 'days'); - - this.title = this.calendar.formatDate(this.start, this.opt('titleFormat')); - - BasicView.prototype.render.call(this, 1, 1, false); // call the super-method - } - -}); -;; - -/* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically. -----------------------------------------------------------------------------------------------------------------------*/ -// Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on). -// Responsible for managing width/height. - -setDefaults({ - allDaySlot: true, - allDayText: 'all-day', - - scrollTime: '06:00:00', - - slotDuration: '00:30:00', - - axisFormat: generateAgendaAxisFormat, - timeFormat: { - agenda: generateAgendaTimeFormat - }, - - minTime: '00:00:00', - maxTime: '24:00:00', - slotEventOverlap: true -}); - -var AGENDA_ALL_DAY_EVENT_LIMIT = 5; - - -function generateAgendaAxisFormat(options, langData) { - return langData.longDateFormat('LT') - .replace(':mm', '(:mm)') - .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs - .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand -} - - -function generateAgendaTimeFormat(options, langData) { - return langData.longDateFormat('LT') - .replace(/\s*a$/i, ''); // remove trailing AM/PM -} - - -function AgendaView(calendar) { - View.call(this, calendar); // call the super-constructor - - this.timeGrid = new TimeGrid(this); - - if (this.opt('allDaySlot')) { // should we display the "all-day" area? - this.dayGrid = new DayGrid(this); // the all-day subcomponent of this view - - // the coordinate grid will be a combination of both subcomponents' grids - this.coordMap = new ComboCoordMap([ - this.dayGrid.coordMap, - this.timeGrid.coordMap - ]); - } - else { - this.coordMap = this.timeGrid.coordMap; - } -} - - -AgendaView.prototype = createObject(View.prototype); // define the super-class -$.extend(AgendaView.prototype, { - - timeGrid: null, // the main time-grid subcomponent of this view - dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null - - axisWidth: null, // the width of the time axis running down the side - - noScrollRowEls: null, // set of fake row elements that must compensate when scrollerEl has scrollbars - - // when the time-grid isn't tall enough to occupy the given height, we render an
underneath - bottomRuleEl: null, - bottomRuleHeight: null, - - - /* Rendering - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders the view into `this.el`, which has already been assigned. - // `colCnt` has been calculated by a subclass and passed here. - render: function(colCnt) { - - // needed for cell-to-date and date-to-cell calculations in View - this.rowCnt = 1; - this.colCnt = colCnt; - - this.el.addClass('fc-agenda-view').html(this.renderHtml()); - - // the element that wraps the time-grid that will probably scroll - this.scrollerEl = this.el.find('.fc-time-grid-container'); - this.timeGrid.coordMap.containerEl = this.scrollerEl; // don't accept clicks/etc outside of this - - this.timeGrid.el = this.el.find('.fc-time-grid'); - this.timeGrid.render(); - - // the
that sometimes displays under the time-grid - this.bottomRuleEl = $('
') - .appendTo(this.timeGrid.el); // inject it into the time-grid - - if (this.dayGrid) { - this.dayGrid.el = this.el.find('.fc-day-grid'); - this.dayGrid.render(); - - // have the day-grid extend it's coordinate area over the
dividing the two grids - this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight(); - } - - this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller - - View.prototype.render.call(this); // call the super-method - - this.resetScroll(); // do this after sizes have been set - }, - - - // Make subcomponents ready for cleanup - destroy: function() { - this.timeGrid.destroy(); - if (this.dayGrid) { - this.dayGrid.destroy(); - } - View.prototype.destroy.call(this); // call the super-method - }, - - - // Builds the HTML skeleton for the view. - // The day-grid and time-grid components will render inside containers defined by this HTML. - renderHtml: function() { - return '' + - '
'; - } - if (endCol > startCol) { - cellHtml += ''; - } - if (colCnt > endCol) { - cellHtml += ''; - } - - cellHtml = this.bookendCells(cellHtml, 'highlight'); - - return '' + - '
' + - '' + - '' + - cellHtml + - '' + - '
' + - '
'; - } - -}); - -;; - -/* Event-rendering methods for the DayGrid class -----------------------------------------------------------------------------------------------------------------------*/ - -$.extend(DayGrid.prototype, { - - segs: null, - rowStructs: null, // an array of objects, each holding information about a row's event-rendering - - - // Render the given events onto the Grid and return the rendered segments - renderEvents: function(events) { - var rowStructs = this.rowStructs = this.renderEventRows(events); - var segs = []; - - // append to each row's content skeleton - this.rowEls.each(function(i, rowNode) { - $(rowNode).find('.fc-content-skeleton > table').append( - rowStructs[i].tbodyEl - ); - segs.push.apply(segs, rowStructs[i].segs); - }); - - this.segs = segs; - }, - - - // Retrieves all segment objects that have been rendered - getSegs: function() { - return (this.segs || []).concat( - this.popoverSegs || [] // segs rendered in the "more" events popover - ); - }, - - - // Removes all rendered event elements - destroyEvents: function() { - var rowStructs; - var rowStruct; - - Grid.prototype.destroyEvents.call(this); // call the super-method - - rowStructs = this.rowStructs || []; - while ((rowStruct = rowStructs.pop())) { - rowStruct.tbodyEl.remove(); - } - - this.segs = null; - this.destroySegPopover(); // removes the "more.." events popover - }, - - - // Uses the given events array to generate
elements of the level+col matrix - var loneCellMatrix = []; // lookup for elements that only take up a single column - var i, levelSegs; - var col; - var tr; - var j, seg; - var td; - - // populates empty cells from the current column (`col`) to `endCol` - function emptyCellsUntil(endCol) { - while (col < endCol) { - // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell - td = (loneCellMatrix[i - 1] || [])[col]; - if (td) { - td.attr( - 'rowspan', - parseInt(td.attr('rowspan') || 1, 10) + 1 - ); - } - else { - td = $(''); - tr.append(td); - } - cellMatrix[i][col] = td; - loneCellMatrix[i][col] = td; - col++; - } - } - - for (i = 0; i < levelCnt; i++) { // iterate through all levels - levelSegs = segLevels[i]; - col = 0; - tr = $('
').append(seg.el); - if (seg.leftCol != seg.rightCol) { - td.attr('colspan', seg.rightCol - seg.leftCol + 1); - } - else { // a single-column segment - loneCellMatrix[i][col] = td; - } - - while (col <= seg.rightCol) { - cellMatrix[i][col] = td; - segMatrix[i][col] = seg; - col++; - } - - tr.append(td); - } - } - - emptyCellsUntil(colCnt); // finish off the row - this.bookendCells(tr, 'eventSkeleton'); - tbody.append(tr); - } - - return { // a "rowStruct" - row: row, // the row number - tbodyEl: tbody, - cellMatrix: cellMatrix, - segMatrix: segMatrix, - segLevels: segLevels, - segs: rowSegs - }; - }, - - - // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels. - buildSegLevels: function(segs) { - var levels = []; - var i, seg; - var j; - - // Give preference to elements with certain criteria, so they have - // a chance to be closer to the top. - segs.sort(compareSegs); - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - - // loop through levels, starting with the topmost, until the segment doesn't collide with other segments - for (j = 0; j < levels.length; j++) { - if (!isDaySegCollision(seg, levels[j])) { - break; - } - } - // `j` now holds the desired subrow index - seg.level = j; - - // create new level array if needed and append segment - (levels[j] || (levels[j] = [])).push(seg); - } - - // order segments left-to-right. very important if calendar is RTL - for (j = 0; j < levels.length; j++) { - levels[j].sort(compareDaySegCols); - } - - return levels; - }, - - - // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row - groupSegRows: function(segs) { - var view = this.view; - var segRows = []; - var i; - - for (i = 0; i < view.rowCnt; i++) { - segRows.push([]); - } - - for (i = 0; i < segs.length; i++) { - segRows[segs[i].row].push(segs[i]); - } - - return segRows; - } - -}); - - -// Computes whether two segments' columns collide. They are assumed to be in the same row. -function isDaySegCollision(seg, otherSegs) { - var i, otherSeg; - - for (i = 0; i < otherSegs.length; i++) { - otherSeg = otherSegs[i]; - - if ( - otherSeg.leftCol <= seg.rightCol && - otherSeg.rightCol >= seg.leftCol - ) { - return true; - } - } - - return false; -} - - -// A cmp function for determining the leftmost event -function compareDaySegCols(a, b) { - return a.leftCol - b.leftCol; -} - -;; - -/* Methods relate to limiting the number events for a given day on a DayGrid -----------------------------------------------------------------------------------------------------------------------*/ - -$.extend(DayGrid.prototype, { - - - segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible - popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible - - - destroySegPopover: function() { - if (this.segPopover) { - this.segPopover.hide(); // will trigger destruction of `segPopover` and `popoverSegs` - } - }, - - - // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid. - // `levelLimit` can be false (don't limit), a number, or true (should be computed). - limitRows: function(levelLimit) { - var rowStructs = this.rowStructs || []; - var row; // row # - var rowLevelLimit; - - for (row = 0; row < rowStructs.length; row++) { - this.unlimitRow(row); - - if (!levelLimit) { - rowLevelLimit = false; - } - else if (typeof levelLimit === 'number') { - rowLevelLimit = levelLimit; - } - else { - rowLevelLimit = this.computeRowLevelLimit(row); - } - - if (rowLevelLimit !== false) { - this.limitRow(row, rowLevelLimit); - } - } - }, - - - // Computes the number of levels a row will accomodate without going outside its bounds. - // Assumes the row is "rigid" (maintains a constant height regardless of what is inside). - // `row` is the row number. - computeRowLevelLimit: function(row) { - var rowEl = this.rowEls.eq(row); // the containing "fake" row div - var rowHeight = rowEl.height(); // TODO: cache somehow? - var trEls = this.rowStructs[row].tbodyEl.children(); - var i, trEl; - - // Reveal one level
DOM nodes - var col = 0; // col # - var cell; - var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right - var cellMatrix; // a matrix (by level, then column) of all jQuery elements in the row - var limitedNodes; // array of temporarily hidden level
DOM nodes - var i, seg; - var segsBelow; // array of segment objects below `seg` in the current `col` - var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies - var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column) - var td, rowspan; - var segMoreNodes; // array of "more" cells that will stand-in for the current seg's cell - var j; - var moreTd, moreWrap, moreLink; - - // Iterates through empty level cells and places "more" links inside if need be - function emptyCellsUntil(endCol) { // goes from current `col` to `endCol` - while (col < endCol) { - cell = { row: row, col: col }; - segsBelow = _this.getCellSegs(cell, levelLimit); - if (segsBelow.length) { - td = cellMatrix[levelLimit - 1][col]; - moreLink = _this.renderMoreLink(cell, segsBelow); - moreWrap = $('
').append(moreLink); - td.append(moreWrap); - moreNodes.push(moreWrap[0]); - } - col++; - } - } - - if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit? - levelSegs = rowStruct.segLevels[levelLimit - 1]; - cellMatrix = rowStruct.cellMatrix; - - limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level
for each column the segment occupies. will be one for each colspan - for (j = 0; j < colSegsBelow.length; j++) { - moreTd = $('').attr('rowspan', rowspan); - segsBelow = colSegsBelow[j]; - cell = { row: row, col: seg.leftCol + j }; - moreLink = this.renderMoreLink(cell, [ seg ].concat(segsBelow)); // count seg as hidden too - moreWrap = $('
').append(moreLink); - moreTd.append(moreWrap); - segMoreNodes.push(moreTd[0]); - moreNodes.push(moreTd[0]); - } - - td.addClass('fc-limited').after($(segMoreNodes)); // hide original
and inject replacements - limitedNodes.push(td[0]); - } - } - - emptyCellsUntil(view.colCnt); // finish off the level - rowStruct.moreEls = $(moreNodes); // for easy undoing later - rowStruct.limitedEls = $(limitedNodes); // for easy undoing later - } - }, - - - // Reveals all levels and removes all "more"-related elements for a grid's row. - // `row` is a row number. - unlimitRow: function(row) { - var rowStruct = this.rowStructs[row]; - - if (rowStruct.moreEls) { - rowStruct.moreEls.remove(); - rowStruct.moreEls = null; - } - - if (rowStruct.limitedEls) { - rowStruct.limitedEls.removeClass('fc-limited'); - rowStruct.limitedEls = null; - } - }, - - - // Renders an element that represents hidden event element for a cell. - // Responsible for attaching click handler as well. - renderMoreLink: function(cell, hiddenSegs) { - var _this = this; - var view = this.view; - - return $('') - .text( - this.getMoreLinkText(hiddenSegs.length) - ) - .on('click', function(ev) { - var clickOption = view.opt('eventLimitClick'); - var date = view.cellToDate(cell); - var moreEl = $(this); - var dayEl = _this.getCellDayEl(cell); - var allSegs = _this.getCellSegs(cell); - - // rescope the segments to be within the cell's date - var reslicedAllSegs = _this.resliceDaySegs(allSegs, date); - var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date); - - if (typeof clickOption === 'function') { - // the returned value can be an atomic option - clickOption = view.trigger('eventLimitClick', null, { - date: date, - dayEl: dayEl, - moreEl: moreEl, - segs: reslicedAllSegs, - hiddenSegs: reslicedHiddenSegs - }, ev); - } - - if (clickOption === 'popover') { - _this.showSegPopover(date, cell, moreEl, reslicedAllSegs); - } - else if (typeof clickOption === 'string') { // a view name - view.calendar.zoomTo(date, clickOption); - } - }); - }, - - - // Reveals the popover that displays all events within a cell - showSegPopover: function(date, cell, moreLink, segs) { - var _this = this; - var view = this.view; - var moreWrap = moreLink.parent(); // the
wrapper around the - var topEl; // the element we want to match the top coordinate of - var options; - - if (view.rowCnt == 1) { - topEl = this.view.el; // will cause the popover to cover any sort of header - } - else { - topEl = this.rowEls.eq(cell.row); // will align with top of row - } - - options = { - className: 'fc-more-popover', - content: this.renderSegPopoverContent(date, segs), - parentEl: this.el, - top: topEl.offset().top, - autoHide: true, // when the user clicks elsewhere, hide the popover - viewportConstrain: view.opt('popoverViewportConstrain'), - hide: function() { - // destroy everything when the popover is hidden - _this.segPopover.destroy(); - _this.segPopover = null; - _this.popoverSegs = null; - } - }; - - // Determine horizontal coordinate. - // We use the moreWrap instead of the
to avoid border confusion. - if (view.opt('isRTL')) { - options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border - } - else { - options.left = moreWrap.offset().left - 1; // -1 to be over cell border - } - - this.segPopover = new Popover(options); - this.segPopover.show(); - }, - - - // Builds the inner DOM contents of the segment popover - renderSegPopoverContent: function(date, segs) { - var view = this.view; - var isTheme = view.opt('theme'); - var title = date.format(view.opt('dayPopoverFormat')); - var content = $( - '
' + - '' + - '' + - htmlEscape(title) + - '' + - '
' + - '
' + - '
' + - '
' + - '
' - ); - var segContainer = content.find('.fc-event-container'); - var i; - - // render each seg's `el` and only return the visible segs - segs = this.renderSegs(segs, true); // disableResizing=true - this.popoverSegs = segs; - - for (i = 0; i < segs.length; i++) { - - // because segments in the popover are not part of a grid coordinate system, provide a hint to any - // grids that want to do drag-n-drop about which cell it came from - segs[i].cellDate = date; - - segContainer.append(segs[i].el); - } - - return content; - }, - - - // Given the events within an array of segment objects, reslice them to be in a single day - resliceDaySegs: function(segs, dayDate) { - var events = $.map(segs, function(seg) { - return seg.event; - }); - var dayStart = dayDate.clone().stripTime(); - var dayEnd = dayStart.clone().add(1, 'days'); - - return this.eventsToSegs(events, dayStart, dayEnd); - }, - - - // Generates the text that should be inside a "more" link, given the number of events it represents - getMoreLinkText: function(num) { - var view = this.view; - var opt = view.opt('eventLimitText'); - - if (typeof opt === 'function') { - return opt(num); - } - else { - return '+' + num + ' ' + opt; - } - }, - - - // Returns segments within a given cell. - // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs. - getCellSegs: function(cell, startLevel) { - var segMatrix = this.rowStructs[cell.row].segMatrix; - var level = startLevel || 0; - var segs = []; - var seg; - - while (level < segMatrix.length) { - seg = segMatrix[level][cell.col]; - if (seg) { - segs.push(seg); - } - level++; - } - - return segs; - } - -}); - -;; - -/* A component that renders one or more columns of vertical time slots -----------------------------------------------------------------------------------------------------------------------*/ - -function TimeGrid(view) { - Grid.call(this, view); // call the super-constructor -} - - -TimeGrid.prototype = createObject(Grid.prototype); // define the super-class -$.extend(TimeGrid.prototype, { - - slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines - snapDuration: null, // granularity of time for dragging and selecting - - minTime: null, // Duration object that denotes the first visible time of any given day - maxTime: null, // Duration object that denotes the exclusive visible end time of any given day - - dayEls: null, // cells elements in the day-row background - slatEls: null, // elements running horizontally across all columns - - slatTops: null, // an array of top positions, relative to the container. last item holds bottom of last slot - - highlightEl: null, // cell skeleton element for rendering the highlight - helperEl: null, // cell skeleton element for rendering the mock event "helper" - - - // Renders the time grid into `this.el`, which should already be assigned. - // Relies on the view's colCnt. In the future, this component should probably be self-sufficient. - render: function() { - this.processOptions(); - - this.el.html(this.renderHtml()); - - this.dayEls = this.el.find('.fc-day'); - this.slatEls = this.el.find('.fc-slats tr'); - - this.computeSlatTops(); - - Grid.prototype.render.call(this); // call the super-method - }, - - - // Renders the basic HTML skeleton for the grid - renderHtml: function() { - return '' + - '
' + - '' + - this.rowHtml('slotBg') + // leverages RowRenderer, which will call slotBgCellHtml - '
' + - '
' + - '
' + - '' + - this.slatRowHtml() + - '
' + - '
'; - }, - - - // Renders the HTML for a vertical background cell behind the slots. - // This method is distinct from 'bg' because we wanted a new `rowType` so the View could customize the rendering. - slotBgCellHtml: function(row, col, date) { - return this.bgCellHtml(row, col, date); - }, - - - // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL. - slatRowHtml: function() { - var view = this.view; - var calendar = view.calendar; - var isRTL = view.opt('isRTL'); - var html = ''; - var slotNormal = this.slotDuration.asMinutes() % 15 === 0; - var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations - var slotDate; // will be on the view's first day, but we only care about its time - var minutes; - var axisHtml; - - // Calculate the time for each slot - while (slotTime < this.maxTime) { - slotDate = view.start.clone().time(slotTime); // will be in UTC but that's good. to avoid DST issues - minutes = slotDate.minutes(); - - axisHtml = - '
' + - ((!slotNormal || !minutes) ? // if irregular slot duration, or on the hour, then display the time - '' + // for matchCellWidths - htmlEscape(calendar.formatDate(slotDate, view.opt('axisFormat'))) + - '' : - '' - ) + - '
' + - (isRTL ? axisHtml : '') + - "
'; - col = seg.col; - } - - // compute vertical position - dayDate = view.cellToDate(0, col); - top = this.computeDateTop(seg.start, dayDate); - bottom = this.computeDateTop(seg.end, dayDate); // the y position of the bottom edge - - // generate the cell HTML. bottom becomes negative because it needs to be a CSS value relative to the - // bottom edge of the zero-height container. - cellHtml += - '' + - '
' + - '
' + - '
' + - '
'; - } - - cellHtml = this.bookendCells(cellHtml, 'highlight'); - - return '' + - '
' + - '' + - '' + - cellHtml + - '' + - '
' + - '
'; - } - -}); - -;; - -/* Event-rendering methods for the TimeGrid class -----------------------------------------------------------------------------------------------------------------------*/ - -$.extend(TimeGrid.prototype, { - - segs: null, // segment objects rendered in the component. null of events haven't been rendered yet - eventSkeletonEl: null, // has cells with event-containers, which contain absolutely positioned event elements - - - // Renders the events onto the grid and returns an array of segments that have been rendered - renderEvents: function(events) { - var res = this.renderEventTable(events); - - this.eventSkeletonEl = $('
').append(res.tableEl); - this.el.append(this.eventSkeletonEl); - - this.segs = res.segs; - }, - - - // Retrieves rendered segment objects - getSegs: function() { - return this.segs || []; - }, - - - // Removes all event segment elements from the view - destroyEvents: function() { - Grid.prototype.destroyEvents.call(this); // call the super-method - - if (this.eventSkeletonEl) { - this.eventSkeletonEl.remove(); - this.eventSkeletonEl = null; - } - - this.segs = null; - }, - - - // Renders and returns the portion of the event-skeleton. - // Returns an object with properties 'tbodyEl' and 'segs'. - renderEventTable: function(events) { - var tableEl = $('
'); - var trEl = tableEl.find('tr'); - var segs = this.eventsToSegs(events); - var segCols; - var i, seg; - var col, colSegs; - var containerEl; - - segs = this.renderSegs(segs); // returns only the visible segs - segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg - - this.computeSegVerticals(segs); // compute and assign top/bottom - - for (col = 0; col < segCols.length; col++) { // iterate each column grouping - colSegs = segCols[col]; - placeSlotSegs(colSegs); // compute horizontal coordinates, z-index's, and reorder the array - - containerEl = $('
'); - - // assign positioning CSS and insert into container - for (i = 0; i < colSegs.length; i++) { - seg = colSegs[i]; - seg.el.css(this.generateSegPositionCss(seg)); - - // if the height is short, add a className for alternate styling - if (seg.bottom - seg.top < 30) { - seg.el.addClass('fc-short'); - } - - containerEl.append(seg.el); - } - - trEl.append($('
').append(containerEl)); - } - - this.bookendCells(trEl, 'eventSkeleton'); - - return { - tableEl: tableEl, - segs: segs - }; - }, - - - // Refreshes the CSS top/bottom coordinates for each segment element. Probably after a window resize/zoom. - updateSegVerticals: function() { - var segs = this.segs; - var i; - - if (segs) { - this.computeSegVerticals(segs); - - for (i = 0; i < segs.length; i++) { - segs[i].el.css( - this.generateSegVerticalCss(segs[i]) - ); - } - } - }, - - - // For each segment in an array, computes and assigns its top and bottom properties - computeSegVerticals: function(segs) { - var i, seg; - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - seg.top = this.computeDateTop(seg.start, seg.start); - seg.bottom = this.computeDateTop(seg.end, seg.start); - } - }, - - - // Renders the HTML for a single event segment's default rendering - renderSegHtml: function(seg, disableResizing) { - var view = this.view; - var event = seg.event; - var isDraggable = view.isEventDraggable(event); - var isResizable = !disableResizing && seg.isEnd && view.isEventResizable(event); - var classes = this.getSegClasses(seg, isDraggable, isResizable); - var skinCss = this.getEventSkinCss(event); - var timeText; - var fullTimeText; // more verbose time text. for the print stylesheet - var startTimeText; // just the start time text - - classes.unshift('fc-time-grid-event'); - - if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day... - // Don't display time text on segments that run entirely through a day. - // That would appear as midnight-midnight and would look dumb. - // Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am) - if (seg.isStart || seg.isEnd) { - timeText = view.getEventTimeText(seg.start, seg.end); - fullTimeText = view.getEventTimeText(seg.start, seg.end, 'LT'); - startTimeText = view.getEventTimeText(seg.start, null); - } - } else { - // Display the normal time text for the *event's* times - timeText = view.getEventTimeText(event); - fullTimeText = view.getEventTimeText(event, 'LT'); - startTimeText = view.getEventTimeText(event.start, null); - } - - return '' + - '
' + - (timeText ? - '
' + - '' + htmlEscape(timeText) + '' + - '
' : - '' - ) + - (event.title ? - '
' + - htmlEscape(event.title) + - '
' : - '' - ) + - '
' + - '
' + - (isResizable ? - '
' : - '' - ) + - ''; - }, - - - // Generates an object with CSS properties/values that should be applied to an event segment element. - // Contains important positioning-related properties that should be applied to any event element, customized or not. - generateSegPositionCss: function(seg) { - var view = this.view; - var isRTL = view.opt('isRTL'); - var shouldOverlap = view.opt('slotEventOverlap'); - var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point - var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point - var props = this.generateSegVerticalCss(seg); // get top/bottom first - var left; // amount of space from left edge, a fraction of the total width - var right; // amount of space from right edge, a fraction of the total width - - if (shouldOverlap) { - // double the width, but don't go beyond the maximum forward coordinate (1.0) - forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2); - } - - if (isRTL) { - left = 1 - forwardCoord; - right = backwardCoord; - } - else { - left = backwardCoord; - right = 1 - forwardCoord; - } - - props.zIndex = seg.level + 1; // convert from 0-base to 1-based - props.left = left * 100 + '%'; - props.right = right * 100 + '%'; - - if (shouldOverlap && seg.forwardPressure) { - // add padding to the edge so that forward stacked events don't cover the resizer's icon - props[isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width - } - - return props; - }, - - - // Generates an object with CSS properties for the top/bottom coordinates of a segment element - generateSegVerticalCss: function(seg) { - return { - top: seg.top, - bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container - }; - }, - - - // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col - groupSegCols: function(segs) { - var view = this.view; - var segCols = []; - var i; - - for (i = 0; i < view.colCnt; i++) { - segCols.push([]); - } - - for (i = 0; i < segs.length; i++) { - segCols[segs[i].col].push(segs[i]); - } - - return segCols; - } - -}); - - -// Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each. -// Also reorders the given array by date! -function placeSlotSegs(segs) { - var levels; - var level0; - var i; - - segs.sort(compareSegs); // order by date - levels = buildSlotSegLevels(segs); - computeForwardSlotSegs(levels); - - if ((level0 = levels[0])) { - - for (i = 0; i < level0.length; i++) { - computeSlotSegPressures(level0[i]); - } - - for (i = 0; i < level0.length; i++) { - computeSlotSegCoords(level0[i], 0, 0); - } - } -} - - -// Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is -// left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date. -function buildSlotSegLevels(segs) { - var levels = []; - var i, seg; - var j; - - for (i=0; i seg2.top && seg1.top < seg2.bottom; -} - - -// A cmp function for determining which forward segment to rely on more when computing coordinates. -function compareForwardSlotSegs(seg1, seg2) { - // put higher-pressure first - return seg2.forwardPressure - seg1.forwardPressure || - // put segments that are closer to initial edge first (and favor ones with no coords yet) - (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) || - // do normal sorting... - compareSegs(seg1, seg2); -} - -;; - -/* An abstract class from which other views inherit from -----------------------------------------------------------------------------------------------------------------------*/ -// Newer methods should be written as prototype methods, not in the monster `View` function at the bottom. - -View.prototype = { - - calendar: null, // owner Calendar object - coordMap: null, // a CoordMap object for converting pixel regions to dates - el: null, // the view's containing element. set by Calendar - - // important Moments - start: null, // the date of the very first cell - end: null, // the date after the very last cell - intervalStart: null, // the start of the interval of time the view represents (1st of month for month view) - intervalEnd: null, // the exclusive end of the interval of time the view represents - - // used for cell-to-date and date-to-cell calculations - rowCnt: null, // # of weeks - colCnt: null, // # of days displayed in a week - - isSelected: false, // boolean whether cells are user-selected or not - - // subclasses can optionally use a scroll container - scrollerEl: null, // the element that will most likely scroll when content is too tall - scrollTop: null, // cached vertical scroll value - - // classNames styled by jqui themes - widgetHeaderClass: null, - widgetContentClass: null, - highlightStateClass: null, - - // document handlers, bound to `this` object - documentMousedownProxy: null, - documentDragStartProxy: null, - - - // Serves as a "constructor" to suppliment the monster `View` constructor below - init: function() { - var tm = this.opt('theme') ? 'ui' : 'fc'; - - this.widgetHeaderClass = tm + '-widget-header'; - this.widgetContentClass = tm + '-widget-content'; - this.highlightStateClass = tm + '-state-highlight'; - - // save references to `this`-bound handlers - this.documentMousedownProxy = $.proxy(this, 'documentMousedown'); - this.documentDragStartProxy = $.proxy(this, 'documentDragStart'); - }, - - - // Renders the view inside an already-defined `this.el`. - // Subclasses should override this and then call the super method afterwards. - render: function() { - this.updateSize(); - this.trigger('viewRender', this, this, this.el); - - // attach handlers to document. do it here to allow for destroy/rerender - $(document) - .on('mousedown', this.documentMousedownProxy) - .on('dragstart', this.documentDragStartProxy); // jqui drag - }, - - - // Clears all view rendering, event elements, and unregisters handlers - destroy: function() { - this.unselect(); - this.trigger('viewDestroy', this, this, this.el); - this.destroyEvents(); - this.el.empty(); // removes inner contents but leaves the element intact - - $(document) - .off('mousedown', this.documentMousedownProxy) - .off('dragstart', this.documentDragStartProxy); - }, - - - // Used to determine what happens when the users clicks next/prev. Given -1 for prev, 1 for next. - // Should apply the delta to `date` (a Moment) and return it. - incrementDate: function(date, delta) { - // subclasses should implement - }, - - - /* Dimensions - ------------------------------------------------------------------------------------------------------------------*/ - - - // Refreshes anything dependant upon sizing of the container element of the grid - updateSize: function(isResize) { - if (isResize) { - this.recordScroll(); - } - this.updateHeight(); - this.updateWidth(); - }, - - - // Refreshes the horizontal dimensions of the calendar - updateWidth: function() { - // subclasses should implement - }, - - - // Refreshes the vertical dimensions of the calendar - updateHeight: function() { - var calendar = this.calendar; // we poll the calendar for height information - - this.setHeight( - calendar.getSuggestedViewHeight(), - calendar.isHeightAuto() - ); - }, - - - // Updates the vertical dimensions of the calendar to the specified height. - // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height. - setHeight: function(height, isAuto) { - // subclasses should implement - }, - - - // Given the total height of the view, return the number of pixels that should be used for the scroller. - // Utility for subclasses. - computeScrollerHeight: function(totalHeight) { - var both = this.el.add(this.scrollerEl); - var otherHeight; // cumulative height of everything that is not the scrollerEl in the view (header+borders) - - // fuckin IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked - both.css({ - position: 'relative', // cause a reflow, which will force fresh dimension recalculation - left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll - }); - otherHeight = this.el.outerHeight() - this.scrollerEl.height(); // grab the dimensions - both.css({ position: '', left: '' }); // undo hack - - return totalHeight - otherHeight; - }, - - - // Called for remembering the current scroll value of the scroller. - // Should be called before there is a destructive operation (like removing DOM elements) that might inadvertently - // change the scroll of the container. - recordScroll: function() { - if (this.scrollerEl) { - this.scrollTop = this.scrollerEl.scrollTop(); - } - }, - - - // Set the scroll value of the scroller to the previously recorded value. - // Should be called after we know the view's dimensions have been restored following some type of destructive - // operation (like temporarily removing DOM elements). - restoreScroll: function() { - if (this.scrollTop !== null) { - this.scrollerEl.scrollTop(this.scrollTop); - } - }, - - - /* Events - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders the events onto the view. - // Should be overriden by subclasses. Subclasses should call the super-method afterwards. - renderEvents: function(events) { - this.segEach(function(seg) { - this.trigger('eventAfterRender', seg.event, seg.event, seg.el); - }); - this.trigger('eventAfterAllRender'); - }, - - - // Removes event elements from the view. - // Should be overridden by subclasses. Should call this super-method FIRST, then subclass DOM destruction. - destroyEvents: function() { - this.segEach(function(seg) { - this.trigger('eventDestroy', seg.event, seg.event, seg.el); - }); - }, - - - // Given an event and the default element used for rendering, returns the element that should actually be used. - // Basically runs events and elements through the eventRender hook. - resolveEventEl: function(event, el) { - var custom = this.trigger('eventRender', event, event, el); - - if (custom === false) { // means don't render at all - el = null; - } - else if (custom && custom !== true) { - el = $(custom); - } - - return el; - }, - - - // Hides all rendered event segments linked to the given event - showEvent: function(event) { - this.segEach(function(seg) { - seg.el.css('visibility', ''); - }, event); - }, - - - // Shows all rendered event segments linked to the given event - hideEvent: function(event) { - this.segEach(function(seg) { - seg.el.css('visibility', 'hidden'); - }, event); - }, - - - // Iterates through event segments. Goes through all by default. - // If the optional `event` argument is specified, only iterates through segments linked to that event. - // The `this` value of the callback function will be the view. - segEach: function(func, event) { - var segs = this.getSegs(); - var i; - - for (i = 0; i < segs.length; i++) { - if (!event || segs[i].event._id === event._id) { - func.call(this, segs[i]); - } - } - }, - - - // Retrieves all the rendered segment objects for the view - getSegs: function() { - // subclasses must implement - }, - - - /* Event Drag Visualization - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event hovering over the specified date. - // `end` is a Moment and might be null. - // `seg` might be null. if specified, it is the segment object of the event being dragged. - // otherwise, an external event from outside the calendar is being dragged. - renderDrag: function(start, end, seg) { - // subclasses should implement - }, - - - // Unrenders a visual indication of event hovering - destroyDrag: function() { - // subclasses should implement - }, - - - // Handler for accepting externally dragged events being dropped in the view. - // Gets called when jqui's 'dragstart' is fired. - documentDragStart: function(ev, ui) { - var _this = this; - var dropDate = null; - var dragListener; - - if (this.opt('droppable')) { // only listen if this setting is on - - // listener that tracks mouse movement over date-associated pixel regions - dragListener = new DragListener(this.coordMap, { - cellOver: function(cell, date) { - dropDate = date; - _this.renderDrag(date); - }, - cellOut: function() { - dropDate = null; - _this.destroyDrag(); - } - }); - - // gets called, only once, when jqui drag is finished - $(document).one('dragstop', function(ev, ui) { - _this.destroyDrag(); - if (dropDate) { - _this.trigger('drop', ev.target, dropDate, ev, ui); - } - }); - - dragListener.startDrag(ev); // start listening immediately - } - }, - - - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - // Selects a date range on the view. `start` and `end` are both Moments. - // `ev` is the native mouse event that begin the interaction. - select: function(start, end, ev) { - this.unselect(ev); - this.renderSelection(start, end); - this.reportSelection(start, end, ev); - }, - - - // Renders a visual indication of the selection - renderSelection: function(start, end) { - // subclasses should implement - }, - - - // Called when a new selection is made. Updates internal state and triggers handlers. - reportSelection: function(start, end, ev) { - this.isSelected = true; - this.trigger('select', null, start, end, ev); - }, - - - // Undoes a selection. updates in the internal state and triggers handlers. - // `ev` is the native mouse event that began the interaction. - unselect: function(ev) { - if (this.isSelected) { - this.isSelected = false; - this.destroySelection(); - this.trigger('unselect', null, ev); - } - }, - - - // Unrenders a visual indication of selection - destroySelection: function() { - // subclasses should implement - }, - - - // Handler for unselecting when the user clicks something and the 'unselectAuto' setting is on - documentMousedown: function(ev) { - var ignore; - - // is there a selection, and has the user made a proper left click? - if (this.isSelected && this.opt('unselectAuto') && isPrimaryMouseButton(ev)) { - - // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element - ignore = this.opt('unselectCancel'); - if (!ignore || !$(ev.target).closest(ignore).length) { - this.unselect(ev); - } - } - } - -}; - - -// We are mixing JavaScript OOP design patterns here by putting methods and member variables in the closed scope of the -// constructor. Going forward, methods should be part of the prototype. -function View(calendar) { - var t = this; - - // exports - t.calendar = calendar; - t.opt = opt; - t.trigger = trigger; - t.isEventDraggable = isEventDraggable; - t.isEventResizable = isEventResizable; - t.eventDrop = eventDrop; - t.eventResize = eventResize; - - // imports - var reportEventChange = calendar.reportEventChange; - - // locals - var options = calendar.options; - var nextDayThreshold = moment.duration(options.nextDayThreshold); - - - t.init(); // the "constructor" that concerns the prototype methods - - - function opt(name) { - var v = options[name]; - if ($.isPlainObject(v) && !isForcedAtomicOption(name)) { - return smartProperty(v, t.name); - } - return v; - } - - - function trigger(name, thisObj) { - return calendar.trigger.apply( - calendar, - [name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t]) - ); - } - - - - /* Event Editable Boolean Calculations - ------------------------------------------------------------------------------*/ - - - function isEventDraggable(event) { - var source = event.source || {}; - - return firstDefined( - event.startEditable, - source.startEditable, - opt('eventStartEditable'), - event.editable, - source.editable, - opt('editable') - ); - } - - - function isEventResizable(event) { - var source = event.source || {}; - - return firstDefined( - event.durationEditable, - source.durationEditable, - opt('eventDurationEditable'), - event.editable, - source.editable, - opt('editable') - ); - } - - - - /* Event Elements - ------------------------------------------------------------------------------*/ - - - // Compute the text that should be displayed on an event's element. - // Based off the settings of the view. Possible signatures: - // .getEventTimeText(event, formatStr) - // .getEventTimeText(startMoment, endMoment, formatStr) - // .getEventTimeText(startMoment, null, formatStr) - // `timeFormat` is used but the `formatStr` argument can be used to override. - t.getEventTimeText = function(event, formatStr) { - var start; - var end; - - if (typeof event === 'object' && typeof formatStr === 'object') { - // first two arguments are actually moments (or null). shift arguments. - start = event; - end = formatStr; - formatStr = arguments[2]; - } - else { - // otherwise, an event object was the first argument - start = event.start; - end = event.end; - } - - formatStr = formatStr || opt('timeFormat'); - - if (end && opt('displayEventEnd')) { - return calendar.formatRange(start, end, formatStr); - } - else { - return calendar.formatDate(start, formatStr); - } - }; - - - - /* Event Modification Reporting - ---------------------------------------------------------------------------------*/ - - - function eventDrop(el, event, newStart, ev) { - var mutateResult = calendar.mutateEvent(event, newStart, null); - - trigger( - 'eventDrop', - el, - event, - mutateResult.dateDelta, - function() { - mutateResult.undo(); - reportEventChange(); - }, - ev, - {} // jqui dummy - ); - - reportEventChange(); - } - - - function eventResize(el, event, newEnd, ev) { - var mutateResult = calendar.mutateEvent(event, null, newEnd); - - trigger( - 'eventResize', - el, - event, - mutateResult.durationDelta, - function() { - mutateResult.undo(); - reportEventChange(); - }, - ev, - {} // jqui dummy - ); - - reportEventChange(); - } - - - // ==================================================================================================== - // Utilities for day "cells" - // ==================================================================================================== - // The "basic" views are completely made up of day cells. - // The "agenda" views have day cells at the top "all day" slot. - // This was the obvious common place to put these utilities, but they should be abstracted out into - // a more meaningful class (like DayEventRenderer). - // ==================================================================================================== - - - // For determining how a given "cell" translates into a "date": - // - // 1. Convert the "cell" (row and column) into a "cell offset" (the # of the cell, cronologically from the first). - // Keep in mind that column indices are inverted with isRTL. This is taken into account. - // - // 2. Convert the "cell offset" to a "day offset" (the # of days since the first visible day in the view). - // - // 3. Convert the "day offset" into a "date" (a Moment). - // - // The reverse transformation happens when transforming a date into a cell. - - - // exports - t.isHiddenDay = isHiddenDay; - t.skipHiddenDays = skipHiddenDays; - t.getCellsPerWeek = getCellsPerWeek; - t.dateToCell = dateToCell; - t.dateToDayOffset = dateToDayOffset; - t.dayOffsetToCellOffset = dayOffsetToCellOffset; - t.cellOffsetToCell = cellOffsetToCell; - t.cellToDate = cellToDate; - t.cellToCellOffset = cellToCellOffset; - t.cellOffsetToDayOffset = cellOffsetToDayOffset; - t.dayOffsetToDate = dayOffsetToDate; - t.rangeToSegments = rangeToSegments; - t.isMultiDayEvent = isMultiDayEvent; - - - // internals - var hiddenDays = opt('hiddenDays') || []; // array of day-of-week indices that are hidden - var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool) - var cellsPerWeek; - var dayToCellMap = []; // hash from dayIndex -> cellIndex, for one week - var cellToDayMap = []; // hash from cellIndex -> dayIndex, for one week - var isRTL = opt('isRTL'); - - - // initialize important internal variables - (function() { - - if (opt('weekends') === false) { - hiddenDays.push(0, 6); // 0=sunday, 6=saturday - } - - // Loop through a hypothetical week and determine which - // days-of-week are hidden. Record in both hashes (one is the reverse of the other). - for (var dayIndex=0, cellIndex=0; dayIndex<7; dayIndex++) { - dayToCellMap[dayIndex] = cellIndex; - isHiddenDayHash[dayIndex] = $.inArray(dayIndex, hiddenDays) != -1; - if (!isHiddenDayHash[dayIndex]) { - cellToDayMap[cellIndex] = dayIndex; - cellIndex++; - } - } - - cellsPerWeek = cellIndex; - if (!cellsPerWeek) { - throw 'invalid hiddenDays'; // all days were hidden? bad. - } - - })(); - - - // Is the current day hidden? - // `day` is a day-of-week index (0-6), or a Moment - function isHiddenDay(day) { - if (moment.isMoment(day)) { - day = day.day(); - } - return isHiddenDayHash[day]; - } - - - function getCellsPerWeek() { - return cellsPerWeek; - } - - - // Incrementing the current day until it is no longer a hidden day, returning a copy. - // If the initial value of `date` is not a hidden day, don't do anything. - // Pass `isExclusive` as `true` if you are dealing with an end date. - // `inc` defaults to `1` (increment one day forward each time) - function skipHiddenDays(date, inc, isExclusive) { - var out = date.clone(); - inc = inc || 1; - while ( - isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7] - ) { - out.add(inc, 'days'); - } - return out; - } - - - // - // TRANSFORMATIONS: cell -> cell offset -> day offset -> date - // - - // cell -> date (combines all transformations) - // Possible arguments: - // - row, col - // - { row:#, col: # } - function cellToDate() { - var cellOffset = cellToCellOffset.apply(null, arguments); - var dayOffset = cellOffsetToDayOffset(cellOffset); - var date = dayOffsetToDate(dayOffset); - return date; - } - - // cell -> cell offset - // Possible arguments: - // - row, col - // - { row:#, col:# } - function cellToCellOffset(row, col) { - var colCnt = t.colCnt; - - // rtl variables. wish we could pre-populate these. but where? - var dis = isRTL ? -1 : 1; - var dit = isRTL ? colCnt - 1 : 0; - - if (typeof row == 'object') { - col = row.col; - row = row.row; - } - var cellOffset = row * colCnt + (col * dis + dit); // column, adjusted for RTL (dis & dit) - - return cellOffset; - } - - // cell offset -> day offset - function cellOffsetToDayOffset(cellOffset) { - var day0 = t.start.day(); // first date's day of week - cellOffset += dayToCellMap[day0]; // normlize cellOffset to beginning-of-week - return Math.floor(cellOffset / cellsPerWeek) * 7 + // # of days from full weeks - cellToDayMap[ // # of days from partial last week - (cellOffset % cellsPerWeek + cellsPerWeek) % cellsPerWeek // crazy math to handle negative cellOffsets - ] - - day0; // adjustment for beginning-of-week normalization - } - - // day offset -> date - function dayOffsetToDate(dayOffset) { - return t.start.clone().add(dayOffset, 'days'); - } - - - // - // TRANSFORMATIONS: date -> day offset -> cell offset -> cell - // - - // date -> cell (combines all transformations) - function dateToCell(date) { - var dayOffset = dateToDayOffset(date); - var cellOffset = dayOffsetToCellOffset(dayOffset); - var cell = cellOffsetToCell(cellOffset); - return cell; - } - - // date -> day offset - function dateToDayOffset(date) { - return date.clone().stripTime().diff(t.start, 'days'); - } - - // day offset -> cell offset - function dayOffsetToCellOffset(dayOffset) { - var day0 = t.start.day(); // first date's day of week - dayOffset += day0; // normalize dayOffset to beginning-of-week - return Math.floor(dayOffset / 7) * cellsPerWeek + // # of cells from full weeks - dayToCellMap[ // # of cells from partial last week - (dayOffset % 7 + 7) % 7 // crazy math to handle negative dayOffsets - ] - - dayToCellMap[day0]; // adjustment for beginning-of-week normalization - } - - // cell offset -> cell (object with row & col keys) - function cellOffsetToCell(cellOffset) { - var colCnt = t.colCnt; - - // rtl variables. wish we could pre-populate these. but where? - var dis = isRTL ? -1 : 1; - var dit = isRTL ? colCnt - 1 : 0; - - var row = Math.floor(cellOffset / colCnt); - var col = ((cellOffset % colCnt + colCnt) % colCnt) * dis + dit; // column, adjusted for RTL (dis & dit) - return { - row: row, - col: col - }; - } - - - // - // Converts a date range into an array of segment objects. - // "Segments" are horizontal stretches of time, sliced up by row. - // A segment object has the following properties: - // - row - // - cols - // - isStart - // - isEnd - // - function rangeToSegments(start, end) { - - var rowCnt = t.rowCnt; - var colCnt = t.colCnt; - var segments = []; // array of segments to return - - // day offset for given date range - var dayRange = computeDayRange(start, end); // convert to a whole-day range - var rangeDayOffsetStart = dateToDayOffset(dayRange.start); - var rangeDayOffsetEnd = dateToDayOffset(dayRange.end); // an exclusive value - - // first and last cell offset for the given date range - // "last" implies inclusivity - var rangeCellOffsetFirst = dayOffsetToCellOffset(rangeDayOffsetStart); - var rangeCellOffsetLast = dayOffsetToCellOffset(rangeDayOffsetEnd) - 1; - - // loop through all the rows in the view - for (var row=0; row
' + - '' + // needed for matchCellWidths - htmlEscape(this.opt('weekNumberTitle')) + - '' + - '' + - '' + // needed for matchCellWidths - this.calendar.calculateWeekNumber(this.cellToDate(row, 0)) + - '' + - 's of the "number" row in the DayGrid's content skeleton. - // The number row will only exist if either day numbers or week numbers are turned on. - numberCellHtml: function(row, col, date) { - var classes; - - if (!this.dayNumbersVisible) { // if there are week numbers but not day numbers - return ''; // will create an empty space above events :( - } - - classes = this.dayGrid.getDayClasses(date); - classes.unshift('fc-day-number'); - - return '' + - '' + - date.date() + - '
' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '
' + - this.timeGrid.headHtml() + // render the day-of-week headers - '
' + - (this.dayGrid ? - '
' + - '
' : - '' - ) + - '
' + - '
' + - '
' + - '
'; - }, - - - // Generates the HTML that will go before the day-of week header cells. - // Queried by the TimeGrid subcomponent when generating rows. Ordering depends on isRTL. - headIntroHtml: function() { - var date; - var weekNumber; - var weekTitle; - var weekText; - - if (this.opt('weekNumbers')) { - date = this.cellToDate(0, 0); - weekNumber = this.calendar.calculateWeekNumber(date); - weekTitle = this.opt('weekNumberTitle'); - - if (this.opt('isRTL')) { - weekText = weekNumber + weekTitle; - } - else { - weekText = weekTitle + weekNumber; - } - - return '' + - '' + - '' + // needed for matchCellWidths - htmlEscape(weekText) + - '' + - ''; - } - else { - return ''; - } - }, - - - // Generates the HTML that goes before the all-day cells. - // Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL. - dayIntroHtml: function() { - return '' + - '' + - '' + // needed for matchCellWidths - (this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'))) + - '' + - ''; - }, - - - // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column. - slotBgIntroHtml: function() { - return ''; - }, - - - // Generates the HTML that goes before all other types of cells. - // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid. - // Queried by the TimeGrid and DayGrid subcomponents when generating rows. Ordering depends on isRTL. - introHtml: function() { - return ''; - }, - - - // Generates an HTML attribute string for setting the width of the axis, if it is known - axisStyleAttr: function() { - if (this.axisWidth !== null) { - return 'style="width:' + this.axisWidth + 'px"'; - } - return ''; - }, - - - /* Dimensions - ------------------------------------------------------------------------------------------------------------------*/ - - updateSize: function(isResize) { - if (isResize) { - this.timeGrid.resize(); - } - View.prototype.updateSize.call(this, isResize); - }, - - - // Refreshes the horizontal dimensions of the view - updateWidth: function() { - // make all axis cells line up, and record the width so newly created axis cells will have it - this.axisWidth = matchCellWidths(this.el.find('.fc-axis')); - }, - - - // Adjusts the vertical dimensions of the view to the specified values - setHeight: function(totalHeight, isAuto) { - var eventLimit; - var scrollerHeight; - - if (this.bottomRuleHeight === null) { - // calculate the height of the rule the very first time - this.bottomRuleHeight = this.bottomRuleEl.outerHeight(); - } - this.bottomRuleEl.hide(); // .show() will be called later if this
is necessary - - // reset all dimensions back to the original state - this.scrollerEl.css('overflow', ''); - unsetScroller(this.scrollerEl); - uncompensateScroll(this.noScrollRowEls); - - // limit number of events in the all-day area - if (this.dayGrid) { - this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed - - eventLimit = this.opt('eventLimit'); - if (eventLimit && typeof eventLimit !== 'number') { - eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number - } - if (eventLimit) { - this.dayGrid.limitRows(eventLimit); - } - } - - if (!isAuto) { // should we force dimensions of the scroll container, or let the contents be natural height? - - scrollerHeight = this.computeScrollerHeight(totalHeight); - if (setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars? - - // make the all-day and header rows lines up - compensateScroll(this.noScrollRowEls, getScrollbarWidths(this.scrollerEl)); - - // the scrollbar compensation might have changed text flow, which might affect height, so recalculate - // and reapply the desired height to the scroller. - scrollerHeight = this.computeScrollerHeight(totalHeight); - this.scrollerEl.height(scrollerHeight); - - this.restoreScroll(); - } - else { // no scrollbars - // still, force a height and display the bottom rule (marks the end of day) - this.scrollerEl.height(scrollerHeight).css('overflow', 'hidden'); // in case
goes outside - this.bottomRuleEl.show(); - } - } - }, - - - // Sets the scroll value of the scroller to the intial pre-configured state prior to allowing the user to change it. - resetScroll: function() { - var _this = this; - var scrollTime = moment.duration(this.opt('scrollTime')); - var top = this.timeGrid.computeTimeTop(scrollTime); - - // zoom can give weird floating-point values. rather scroll a little bit further - top = Math.ceil(top); - - if (top) { - top++; // to overcome top border that slots beyond the first have. looks better - } - - function scroll() { - _this.scrollerEl.scrollTop(top); - } - - scroll(); - setTimeout(scroll, 0); // overrides any previous scroll state made by the browser - }, - - - /* Events - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders events onto the view and populates the View's segment array - renderEvents: function(events) { - var dayEvents = []; - var timedEvents = []; - var daySegs = []; - var timedSegs; - var i; - - // separate the events into all-day and timed - for (i = 0; i < events.length; i++) { - if (events[i].allDay) { - dayEvents.push(events[i]); - } - else { - timedEvents.push(events[i]); - } - } - - // render the events in the subcomponents - timedSegs = this.timeGrid.renderEvents(timedEvents); - if (this.dayGrid) { - daySegs = this.dayGrid.renderEvents(dayEvents); - } - - // the all-day area is flexible and might have a lot of events, so shift the height - this.updateHeight(); - - View.prototype.renderEvents.call(this, events); // call the super-method - }, - - - // Retrieves all segment objects that are rendered in the view - getSegs: function() { - return this.timeGrid.getSegs().concat( - this.dayGrid ? this.dayGrid.getSegs() : [] - ); - }, - - - // Unrenders all event elements and clears internal segment data - destroyEvents: function() { - View.prototype.destroyEvents.call(this); // do this before the grids' segs have been cleared - - // if destroyEvents is being called as part of an event rerender, renderEvents will be called shortly - // after, so remember what the scroll value was so we can restore it. - this.recordScroll(); - - // destroy the events in the subcomponents - this.timeGrid.destroyEvents(); - if (this.dayGrid) { - this.dayGrid.destroyEvents(); - } - - // we DON'T need to call updateHeight() because: - // A) a renderEvents() call always happens after this, which will eventually call updateHeight() - // B) in IE8, this causes a flash whenever events are rerendered - }, - - - /* Event Dragging - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event being dragged over the view. - // A returned value of `true` signals that a mock "helper" event has been rendered. - renderDrag: function(start, end, seg) { - if (start.hasTime()) { - return this.timeGrid.renderDrag(start, end, seg); - } - else if (this.dayGrid) { - return this.dayGrid.renderDrag(start, end, seg); - } - }, - - - // Unrenders a visual indications of an event being dragged over the view - destroyDrag: function() { - this.timeGrid.destroyDrag(); - if (this.dayGrid) { - this.dayGrid.destroyDrag(); - } - }, - - - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of a selection - renderSelection: function(start, end) { - if (start.hasTime() || end.hasTime()) { - this.timeGrid.renderSelection(start, end); - } - else if (this.dayGrid) { - this.dayGrid.renderSelection(start, end); - } - }, - - - // Unrenders a visual indications of a selection - destroySelection: function() { - this.timeGrid.destroySelection(); - if (this.dayGrid) { - this.dayGrid.destroySelection(); - } - } - -}); - -;; - -/* A week view with an all-day cell area at the top, and a time grid below -----------------------------------------------------------------------------------------------------------------------*/ -// TODO: a WeekView mixin for calculating dates and titles - -fcViews.agendaWeek = AgendaWeekView; // register the view - -function AgendaWeekView(calendar) { - AgendaView.call(this, calendar); // call the super-constructor -} - - -AgendaWeekView.prototype = createObject(AgendaView.prototype); // define the super-class -$.extend(AgendaWeekView.prototype, { - - name: 'agendaWeek', - - - incrementDate: function(date, delta) { - return date.clone().stripTime().add(delta, 'weeks').startOf('week'); - }, - - - render: function(date) { - - this.intervalStart = date.clone().stripTime().startOf('week'); - this.intervalEnd = this.intervalStart.clone().add(1, 'weeks'); - - this.start = this.skipHiddenDays(this.intervalStart); - this.end = this.skipHiddenDays(this.intervalEnd, -1, true); - - this.title = this.calendar.formatRange( - this.start, - this.end.clone().subtract(1), // make inclusive by subtracting 1 ms - this.opt('titleFormat'), - ' \u2014 ' // emphasized dash - ); - - AgendaView.prototype.render.call(this, this.getCellsPerWeek()); // call the super-method - } - -}); - -;; - -/* A day view with an all-day cell area at the top, and a time grid below -----------------------------------------------------------------------------------------------------------------------*/ - -fcViews.agendaDay = AgendaDayView; // register the view - -function AgendaDayView(calendar) { - AgendaView.call(this, calendar); // call the super-constructor -} - - -AgendaDayView.prototype = createObject(AgendaView.prototype); // define the super-class -$.extend(AgendaDayView.prototype, { - - name: 'agendaDay', - - - incrementDate: function(date, delta) { - var out = date.clone().stripTime().add(delta, 'days'); - out = this.skipHiddenDays(out, delta < 0 ? -1 : 1); - return out; - }, - - - render: function(date) { - - this.start = this.intervalStart = date.clone().stripTime(); - this.end = this.intervalEnd = this.start.clone().add(1, 'days'); - - this.title = this.calendar.formatDate(this.start, this.opt('titleFormat')); - - AgendaView.prototype.render.call(this, 1); // call the super-method - } - -}); - -;; - +/*! + * FullCalendar v2.1.1 + * Docs & License: http://arshaw.com/fullcalendar/ + * (c) 2013 Adam Shaw + */ + +(function(factory) { + if (typeof define === 'function' && define.amd) { + define([ 'jquery', 'moment' ], factory); + } + else { + factory(jQuery, moment); + } +})(function($, moment) { + +;; + +var defaults = { + + lang: 'en', + + defaultTimedEventDuration: '02:00:00', + defaultAllDayEventDuration: { days: 1 }, + forceEventDuration: false, + nextDayThreshold: '09:00:00', // 9am + + // display + defaultView: 'month', + aspectRatio: 1.35, + header: { + left: 'title', + center: '', + right: 'today prev,next' + }, + weekends: true, + weekNumbers: false, + + weekNumberTitle: 'W', + weekNumberCalculation: 'local', + + //editable: false, + + // event ajax + lazyFetching: true, + startParam: 'start', + endParam: 'end', + timezoneParam: 'timezone', + + timezone: false, + + //allDayDefault: undefined, + + // time formats + titleFormat: { + month: 'MMMM YYYY', // like "September 1986". each language will override this + week: 'll', // like "Sep 4 1986" + day: 'LL' // like "September 4 1986" + }, + columnFormat: { + month: 'ddd', // like "Sat" + week: generateWeekColumnFormat, + day: 'dddd' // like "Saturday" + }, + timeFormat: { // for event elements + 'default': generateShortTimeFormat + }, + + displayEventEnd: { + month: false, + basicWeek: false, + 'default': true + }, + + // locale + isRTL: false, + defaultButtonText: { + prev: "prev", + next: "next", + prevYear: "prev year", + nextYear: "next year", + today: 'today', + month: 'month', + week: 'week', + day: 'day' + }, + + buttonIcons: { + prev: 'left-single-arrow', + next: 'right-single-arrow', + prevYear: 'left-double-arrow', + nextYear: 'right-double-arrow' + }, + + // jquery-ui theming + theme: false, + themeButtonIcons: { + prev: 'circle-triangle-w', + next: 'circle-triangle-e', + prevYear: 'seek-prev', + nextYear: 'seek-next' + }, + + dragOpacity: .75, + dragRevertDuration: 500, + dragScroll: true, + + //selectable: false, + unselectAuto: true, + + dropAccept: '*', + + eventLimit: false, + eventLimitText: 'more', + eventLimitClick: 'popover', + dayPopoverFormat: 'LL', + + handleWindowResize: true, + windowResizeDelay: 200 // milliseconds before a rerender happens + +}; + + +function generateShortTimeFormat(options, langData) { + return langData.longDateFormat('LT') + .replace(':mm', '(:mm)') + .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs + .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand +} + + +function generateWeekColumnFormat(options, langData) { + var format = langData.longDateFormat('L'); // for the format like "MM/DD/YYYY" + format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, ''); // strip the year off the edge, as well as other misc non-whitespace chars + if (options.isRTL) { + format += ' ddd'; // for RTL, add day-of-week to end + } + else { + format = 'ddd ' + format; // for LTR, add day-of-week to beginning + } + return format; +} + + +var langOptionHash = { + en: { + columnFormat: { + week: 'ddd M/D' // override for english. different from the generated default, which is MM/DD + }, + dayPopoverFormat: 'dddd, MMMM D' + } +}; + + +// right-to-left defaults +var rtlDefaults = { + header: { + left: 'next,prev today', + center: '', + right: 'title' + }, + buttonIcons: { + prev: 'right-single-arrow', + next: 'left-single-arrow', + prevYear: 'right-double-arrow', + nextYear: 'left-double-arrow' + }, + themeButtonIcons: { + prev: 'circle-triangle-e', + next: 'circle-triangle-w', + nextYear: 'seek-prev', + prevYear: 'seek-next' + } +}; + +;; + +var fc = $.fullCalendar = { version: "2.1.1" }; +var fcViews = fc.views = {}; + + +$.fn.fullCalendar = function(options) { + var args = Array.prototype.slice.call(arguments, 1); // for a possible method call + var res = this; // what this function will return (this jQuery object by default) + + this.each(function(i, _element) { // loop each DOM element involved + var element = $(_element); + var calendar = element.data('fullCalendar'); // get the existing calendar object (if any) + var singleRes; // the returned value of this single method call + + // a method call + if (typeof options === 'string') { + if (calendar && $.isFunction(calendar[options])) { + singleRes = calendar[options].apply(calendar, args); + if (!i) { + res = singleRes; // record the first method call result + } + if (options === 'destroy') { // for the destroy method, must remove Calendar object data + element.removeData('fullCalendar'); + } + } + } + // a new calendar initialization + else if (!calendar) { // don't initialize twice + calendar = new Calendar(element, options); + element.data('fullCalendar', calendar); + calendar.render(); + } + }); + + return res; +}; + + +// function for adding/overriding defaults +function setDefaults(d) { + mergeOptions(defaults, d); +} + + +// Recursively combines option hash-objects. +// Better than `$.extend(true, ...)` because arrays are not traversed/copied. +// +// called like: +// mergeOptions(target, obj1, obj2, ...) +// +function mergeOptions(target) { + + function mergeIntoTarget(name, value) { + if ($.isPlainObject(value) && $.isPlainObject(target[name]) && !isForcedAtomicOption(name)) { + // merge into a new object to avoid destruction + target[name] = mergeOptions({}, target[name], value); // combine. `value` object takes precedence + } + else if (value !== undefined) { // only use values that are set and not undefined + target[name] = value; + } + } + + for (var i=1; i").prependTo(element); + + header = new Header(t, options); + headerElement = header.render(); + if (headerElement) { + element.prepend(headerElement); + } + + changeView(options.defaultView); + + if (options.handleWindowResize) { + windowResizeProxy = debounce(windowResize, options.windowResizeDelay); // prevents rapid calls + $(window).resize(windowResizeProxy); + } + } + + + function destroy() { + + if (currentView) { + currentView.destroy(); + } + + header.destroy(); + content.remove(); + element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget'); + + $(window).unbind('resize', windowResizeProxy); + } + + + function elementVisible() { + return element.is(':visible'); + } + + + + // View Rendering + // ----------------------------------------------------------------------------------- + + + function changeView(viewName) { + renderView(0, viewName); + } + + + // Renders a view because of a date change, view-type change, or for the first time + function renderView(delta, viewName) { + ignoreWindowResize++; + + // if viewName is changing, destroy the old view + if (currentView && viewName && currentView.name !== viewName) { + header.deactivateButton(currentView.name); + freezeContentHeight(); // prevent a scroll jump when view element is removed + if (currentView.start) { // rendered before? + currentView.destroy(); + } + currentView.el.remove(); + currentView = null; + } + + // if viewName changed, or the view was never created, create a fresh view + if (!currentView && viewName) { + currentView = new fcViews[viewName](t); + currentView.el = $("
").appendTo(content); + header.activateButton(viewName); + } + + if (currentView) { + + // let the view determine what the delta means + if (delta) { + date = currentView.incrementDate(date, delta); + } + + // render or rerender the view + if ( + !currentView.start || // never rendered before + delta || // explicit date window change + !date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change + ) { + if (elementVisible()) { + + freezeContentHeight(); + if (currentView.start) { // rendered before? + currentView.destroy(); + } + currentView.render(date); + unfreezeContentHeight(); + + // need to do this after View::render, so dates are calculated + updateTitle(); + updateTodayButton(); + + getAndRenderEvents(); + } + } + } + + unfreezeContentHeight(); // undo any lone freezeContentHeight calls + ignoreWindowResize--; + } + + + + // Resizing + // ----------------------------------------------------------------------------------- + + + t.getSuggestedViewHeight = function() { + if (suggestedViewHeight === undefined) { + calcSize(); + } + return suggestedViewHeight; + }; + + + t.isHeightAuto = function() { + return options.contentHeight === 'auto' || options.height === 'auto'; + }; + + + function updateSize(shouldRecalc) { + if (elementVisible()) { + + if (shouldRecalc) { + _calcSize(); + } + + ignoreWindowResize++; + currentView.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto() + ignoreWindowResize--; + + return true; // signal success + } + } + + + function calcSize() { + if (elementVisible()) { + _calcSize(); + } + } + + + function _calcSize() { // assumes elementVisible + if (typeof options.contentHeight === 'number') { // exists and not 'auto' + suggestedViewHeight = options.contentHeight; + } + else if (typeof options.height === 'number') { // exists and not 'auto' + suggestedViewHeight = options.height - (headerElement ? headerElement.outerHeight(true) : 0); + } + else { + suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5)); + } + } + + + function windowResize(ev) { + if ( + !ignoreWindowResize && + ev.target === window && // so we don't process jqui "resize" events that have bubbled up + currentView.start // view has already been rendered + ) { + if (updateSize(true)) { + currentView.trigger('windowResize', _element); + } + } + } + + + + /* Event Fetching/Rendering + -----------------------------------------------------------------------------*/ + // TODO: going forward, most of this stuff should be directly handled by the view + + + function refetchEvents() { // can be called as an API method + destroyEvents(); // so that events are cleared before user starts waiting for AJAX + fetchAndRenderEvents(); + } + + + function renderEvents() { // destroys old events if previously rendered + if (elementVisible()) { + freezeContentHeight(); + currentView.destroyEvents(); // no performance cost if never rendered + currentView.renderEvents(events); + unfreezeContentHeight(); + } + } + + + function destroyEvents() { + freezeContentHeight(); + currentView.destroyEvents(); + unfreezeContentHeight(); + } + + + function getAndRenderEvents() { + if (!options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) { + fetchAndRenderEvents(); + } + else { + renderEvents(); + } + } + + + function fetchAndRenderEvents() { + fetchEvents(currentView.start, currentView.end); + // ... will call reportEvents + // ... which will call renderEvents + } + + + // called when event data arrives + function reportEvents(_events) { + events = _events; + renderEvents(); + } + + + // called when a single event's data has been changed + function reportEventChange() { + renderEvents(); + } + + + + /* Header Updating + -----------------------------------------------------------------------------*/ + + + function updateTitle() { + header.updateTitle(currentView.title); + } + + + function updateTodayButton() { + var now = t.getNow(); + if (now.isWithin(currentView.intervalStart, currentView.intervalEnd)) { + header.disableButton('today'); + } + else { + header.enableButton('today'); + } + } + + + + /* Selection + -----------------------------------------------------------------------------*/ + + + function select(start, end) { + + start = t.moment(start); + if (end) { + end = t.moment(end); + } + else if (start.hasTime()) { + end = start.clone().add(t.defaultTimedEventDuration); + } + else { + end = start.clone().add(t.defaultAllDayEventDuration); + } + + currentView.select(start, end); + } + + + function unselect() { // safe to be called before renderView + if (currentView) { + currentView.unselect(); + } + } + + + + /* Date + -----------------------------------------------------------------------------*/ + + + function prev() { + renderView(-1); + } + + + function next() { + renderView(1); + } + + + function prevYear() { + date.add(-1, 'years'); + renderView(); + } + + + function nextYear() { + date.add(1, 'years'); + renderView(); + } + + + function today() { + date = t.getNow(); + renderView(); + } + + + function gotoDate(dateInput) { + date = t.moment(dateInput); + renderView(); + } + + + function incrementDate(delta) { + date.add(moment.duration(delta)); + renderView(); + } + + + // Forces navigation to a view for the given date. + // `viewName` can be a specific view name or a generic one like "week" or "day". + function zoomTo(newDate, viewName) { + var viewStr; + var match; + + if (!viewName || fcViews[viewName] === undefined) { // a general view name, or "auto" + viewName = viewName || 'day'; + viewStr = header.getViewsWithButtons().join(' '); // space-separated string of all the views in the header + + // try to match a general view name, like "week", against a specific one, like "agendaWeek" + match = viewStr.match(new RegExp('\\w+' + capitaliseFirstLetter(viewName))); + + // fall back to the day view being used in the header + if (!match) { + match = viewStr.match(/\w+Day/); + } + + viewName = match ? match[0] : 'agendaDay'; // fall back to agendaDay + } + + date = newDate; + changeView(viewName); + } + + + function getDate() { + return date.clone(); + } + + + + /* Height "Freezing" + -----------------------------------------------------------------------------*/ + + + function freezeContentHeight() { + content.css({ + width: '100%', + height: content.height(), + overflow: 'hidden' + }); + } + + + function unfreezeContentHeight() { + content.css({ + width: '', + height: '', + overflow: '' + }); + } + + + + /* Misc + -----------------------------------------------------------------------------*/ + + + function getCalendar() { + return t; + } + + + function getView() { + return currentView; + } + + + function option(name, value) { + if (value === undefined) { + return options[name]; + } + if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') { + options[name] = value; + updateSize(true); // true = allow recalculation of height + } + } + + + function trigger(name, thisObj) { + if (options[name]) { + return options[name].apply( + thisObj || _element, + Array.prototype.slice.call(arguments, 2) + ); + } + } + +} + +;; + +/* Top toolbar area with buttons and title +----------------------------------------------------------------------------------------------------------------------*/ +// TODO: rename all header-related things to "toolbar" + +function Header(calendar, options) { + var t = this; + + // exports + t.render = render; + t.destroy = destroy; + t.updateTitle = updateTitle; + t.activateButton = activateButton; + t.deactivateButton = deactivateButton; + t.disableButton = disableButton; + t.enableButton = enableButton; + t.getViewsWithButtons = getViewsWithButtons; + + // locals + var el = $(); + var viewsWithButtons = []; + var tm; + + + function render() { + var sections = options.header; + + tm = options.theme ? 'ui' : 'fc'; + + if (sections) { + el = $("
") + .append(renderSection('left')) + .append(renderSection('right')) + .append(renderSection('center')) + .append('
'); + + return el; + } + } + + + function destroy() { + el.remove(); + } + + + function renderSection(position) { + var sectionEl = $('
'); + var buttonStr = options.header[position]; + + if (buttonStr) { + $.each(buttonStr.split(' '), function(i) { + var groupChildren = $(); + var isOnlyButtons = true; + var groupEl; + + $.each(this.split(','), function(j, buttonName) { + var buttonClick; + var themeIcon; + var normalIcon; + var defaultText; + var customText; + var innerHtml; + var classes; + var button; + + if (buttonName == 'title') { + groupChildren = groupChildren.add($('

 

')); // we always want it to take up height + isOnlyButtons = false; + } + else { + if (calendar[buttonName]) { // a calendar method + buttonClick = function() { + calendar[buttonName](); + }; + } + else if (fcViews[buttonName]) { // a view name + buttonClick = function() { + calendar.changeView(buttonName); + }; + viewsWithButtons.push(buttonName); + } + if (buttonClick) { + + // smartProperty allows different text per view button (ex: "Agenda Week" vs "Basic Week") + themeIcon = smartProperty(options.themeButtonIcons, buttonName); + normalIcon = smartProperty(options.buttonIcons, buttonName); + defaultText = smartProperty(options.defaultButtonText, buttonName); + customText = smartProperty(options.buttonText, buttonName); + + if (customText) { + innerHtml = htmlEscape(customText); + } + else if (themeIcon && options.theme) { + innerHtml = ""; + } + else if (normalIcon && !options.theme) { + innerHtml = ""; + } + else { + innerHtml = htmlEscape(defaultText || buttonName); + } + + classes = [ + 'fc-' + buttonName + '-button', + tm + '-button', + tm + '-state-default' + ]; + + button = $( // type="button" so that it doesn't submit a form + '' + ) + .click(function() { + // don't process clicks for disabled buttons + if (!button.hasClass(tm + '-state-disabled')) { + + buttonClick(); + + // after the click action, if the button becomes the "active" tab, or disabled, + // it should never have a hover class, so remove it now. + if ( + button.hasClass(tm + '-state-active') || + button.hasClass(tm + '-state-disabled') + ) { + button.removeClass(tm + '-state-hover'); + } + } + }) + .mousedown(function() { + // the *down* effect (mouse pressed in). + // only on buttons that are not the "active" tab, or disabled + button + .not('.' + tm + '-state-active') + .not('.' + tm + '-state-disabled') + .addClass(tm + '-state-down'); + }) + .mouseup(function() { + // undo the *down* effect + button.removeClass(tm + '-state-down'); + }) + .hover( + function() { + // the *hover* effect. + // only on buttons that are not the "active" tab, or disabled + button + .not('.' + tm + '-state-active') + .not('.' + tm + '-state-disabled') + .addClass(tm + '-state-hover'); + }, + function() { + // undo the *hover* effect + button + .removeClass(tm + '-state-hover') + .removeClass(tm + '-state-down'); // if mouseleave happens before mouseup + } + ); + + groupChildren = groupChildren.add(button); + } + } + }); + + if (isOnlyButtons) { + groupChildren + .first().addClass(tm + '-corner-left').end() + .last().addClass(tm + '-corner-right').end(); + } + + if (groupChildren.length > 1) { + groupEl = $('
'); + if (isOnlyButtons) { + groupEl.addClass('fc-button-group'); + } + groupEl.append(groupChildren); + sectionEl.append(groupEl); + } + else { + sectionEl.append(groupChildren); // 1 or 0 children + } + }); + } + + return sectionEl; + } + + + function updateTitle(text) { + el.find('h2').text(text); + } + + + function activateButton(buttonName) { + el.find('.fc-' + buttonName + '-button') + .addClass(tm + '-state-active'); + } + + + function deactivateButton(buttonName) { + el.find('.fc-' + buttonName + '-button') + .removeClass(tm + '-state-active'); + } + + + function disableButton(buttonName) { + el.find('.fc-' + buttonName + '-button') + .attr('disabled', 'disabled') + .addClass(tm + '-state-disabled'); + } + + + function enableButton(buttonName) { + el.find('.fc-' + buttonName + '-button') + .removeAttr('disabled') + .removeClass(tm + '-state-disabled'); + } + + + function getViewsWithButtons() { + return viewsWithButtons; + } + +} + +;; + +fc.sourceNormalizers = []; +fc.sourceFetchers = []; + +var ajaxDefaults = { + dataType: 'json', + cache: false +}; + +var eventGUID = 1; + + +function EventManager(options) { // assumed to be a calendar + var t = this; + + + // exports + t.isFetchNeeded = isFetchNeeded; + t.fetchEvents = fetchEvents; + t.addEventSource = addEventSource; + t.removeEventSource = removeEventSource; + t.updateEvent = updateEvent; + t.renderEvent = renderEvent; + t.removeEvents = removeEvents; + t.clientEvents = clientEvents; + t.mutateEvent = mutateEvent; + + + // imports + var trigger = t.trigger; + var getView = t.getView; + var reportEvents = t.reportEvents; + var getEventEnd = t.getEventEnd; + + + // locals + var stickySource = { events: [] }; + var sources = [ stickySource ]; + var rangeStart, rangeEnd; + var currentFetchID = 0; + var pendingSourceCnt = 0; + var loadingLevel = 0; + var cache = []; + + + $.each( + (options.events ? [ options.events ] : []).concat(options.eventSources || []), + function(i, sourceInput) { + var source = buildEventSource(sourceInput); + if (source) { + sources.push(source); + } + } + ); + + + + /* Fetching + -----------------------------------------------------------------------------*/ + + + function isFetchNeeded(start, end) { + return !rangeStart || // nothing has been fetched yet? + // or, a part of the new range is outside of the old range? (after normalizing) + start.clone().stripZone() < rangeStart.clone().stripZone() || + end.clone().stripZone() > rangeEnd.clone().stripZone(); + } + + + function fetchEvents(start, end) { + rangeStart = start; + rangeEnd = end; + cache = []; + var fetchID = ++currentFetchID; + var len = sources.length; + pendingSourceCnt = len; + for (var i=0; i cells, find the cell with the largest natural width and set the widths of all the +// cells to be that width. +// PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline +function matchCellWidths(els) { + var maxInnerWidth = 0; + + els.find('> *').each(function(i, innerEl) { + var innerWidth = $(innerEl).outerWidth(); + if (innerWidth > maxInnerWidth) { + maxInnerWidth = innerWidth; + } + }); + + maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance + + els.width(maxInnerWidth); + + return maxInnerWidth; +} + + +// Turns a container element into a scroller if its contents is taller than the allotted height. +// Returns true if the element is now a scroller, false otherwise. +// NOTE: this method is best because it takes weird zooming dimensions into account +function setPotentialScroller(containerEl, height) { + containerEl.height(height).addClass('fc-scroller'); + + // are scrollbars needed? + if (containerEl[0].scrollHeight - 1 > containerEl[0].clientHeight) { // !!! -1 because IE is often off-by-one :( + return true; + } + + unsetScroller(containerEl); // undo + return false; +} + + +// Takes an element that might have been a scroller, and turns it back into a normal element. +function unsetScroller(containerEl) { + containerEl.height('').removeClass('fc-scroller'); +} + + +/* General DOM Utilities +----------------------------------------------------------------------------------------------------------------------*/ + + +// borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51 +function getScrollParent(el) { + var position = el.css('position'), + scrollParent = el.parents().filter(function() { + var parent = $(this); + return (/(auto|scroll)/).test( + parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x') + ); + }).eq(0); + + return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent; +} + + +// Given a container element, return an object with the pixel values of the left/right scrollbars. +// Left scrollbars might occur on RTL browsers (IE maybe?) but I have not tested. +// PREREQUISITE: container element must have a single child with display:block +function getScrollbarWidths(container) { + var containerLeft = container.offset().left; + var containerRight = containerLeft + container.width(); + var inner = container.children(); + var innerLeft = inner.offset().left; + var innerRight = innerLeft + inner.outerWidth(); + + return { + left: innerLeft - containerLeft, + right: containerRight - innerRight + }; +} + + +// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac) +function isPrimaryMouseButton(ev) { + return ev.which == 1 && !ev.ctrlKey; +} + + +/* FullCalendar-specific Misc Utilities +----------------------------------------------------------------------------------------------------------------------*/ + + +// Creates a basic segment with the intersection of the two ranges. Returns undefined if no intersection. +// Expects all dates to be normalized to the same timezone beforehand. +function intersectionToSeg(subjectStart, subjectEnd, intervalStart, intervalEnd) { + var segStart, segEnd; + var isStart, isEnd; + + if (subjectEnd > intervalStart && subjectStart < intervalEnd) { // in bounds at all? + + if (subjectStart >= intervalStart) { + segStart = subjectStart.clone(); + isStart = true; + } + else { + segStart = intervalStart.clone(); + isStart = false; + } + + if (subjectEnd <= intervalEnd) { + segEnd = subjectEnd.clone(); + isEnd = true; + } + else { + segEnd = intervalEnd.clone(); + isEnd = false; + } + + return { + start: segStart, + end: segEnd, + isStart: isStart, + isEnd: isEnd + }; + } +} + + +function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object + obj = obj || {}; + if (obj[name] !== undefined) { + return obj[name]; + } + var parts = name.split(/(?=[A-Z])/), + i = parts.length - 1, res; + for (; i>=0; i--) { + res = obj[parts[i].toLowerCase()]; + if (res !== undefined) { + return res; + } + } + return obj['default']; +} + + +/* Date Utilities +----------------------------------------------------------------------------------------------------------------------*/ + +var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ]; + + +// Diffs the two moments into a Duration where full-days are recorded first, then the remaining time. +// Moments will have their timezones normalized. +function dayishDiff(a, b) { + return moment.duration({ + days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'), + ms: a.time() - b.time() + }); +} + + +function isNativeDate(input) { + return Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date; +} + + +function dateCompare(a, b) { // works with Moments and native Dates + return a - b; +} + + +/* General Utilities +----------------------------------------------------------------------------------------------------------------------*/ + +fc.applyAll = applyAll; // export + + +// Create an object that has the given prototype. Just like Object.create +function createObject(proto) { + var f = function() {}; + f.prototype = proto; + return new f(); +} + + +// Copies specifically-owned (non-protoype) properties of `b` onto `a`. +// FYI, $.extend would copy *all* properties of `b` onto `a`. +function extend(a, b) { + for (var i in b) { + if (b.hasOwnProperty(i)) { + a[i] = b[i]; + } + } +} + + +function applyAll(functions, thisObj, args) { + if ($.isFunction(functions)) { + functions = [ functions ]; + } + if (functions) { + var i; + var ret; + for (i=0; i/g, '>') + .replace(/'/g, ''') + .replace(/"/g, '"') + .replace(/\n/g, '
'); +} + + +function stripHtmlEntities(text) { + return text.replace(/&.*?;/g, ''); +} + + +function capitaliseFirstLetter(str) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + + +// Returns a function, that, as long as it continues to be invoked, will not +// be triggered. The function will be called after it stops being called for +// N milliseconds. +// https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714 +function debounce(func, wait) { + var timeoutId; + var args; + var context; + var timestamp; // of most recent call + var later = function() { + var last = +new Date() - timestamp; + if (last < wait && last > 0) { + timeoutId = setTimeout(later, wait - last); + } + else { + timeoutId = null; + func.apply(context, args); + if (!timeoutId) { + context = args = null; + } + } + }; + + return function() { + context = this; + args = arguments; + timestamp = +new Date(); + if (!timeoutId) { + timeoutId = setTimeout(later, wait); + } + }; +} + +;; + +var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/; +var ambigTimeOrZoneRegex = + /^\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+)?)?)?)?)?$/; + + +// Creating +// ------------------------------------------------------------------------------------------------- + +// Creates a new moment, similar to the vanilla moment(...) constructor, but with +// extra features (ambiguous time, enhanced formatting). When gived an existing moment, +// it will function as a clone (and retain the zone of the moment). Anything else will +// result in a moment in the local zone. +fc.moment = function() { + return makeMoment(arguments); +}; + +// Sames as fc.moment, but forces the resulting moment to be in the UTC timezone. +fc.moment.utc = function() { + var mom = makeMoment(arguments, true); + + // Force it into UTC because makeMoment doesn't guarantee it. + if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone + mom.utc(); + } + + return mom; +}; + +// Same as fc.moment, but when given an ISO8601 string, the timezone offset is preserved. +// ISO8601 strings with no timezone offset will become ambiguously zoned. +fc.moment.parseZone = function() { + return makeMoment(arguments, true, true); +}; + +// Builds an FCMoment from args. When given an existing moment, it clones. When given a native +// Date, or called with no arguments (the current time), the resulting moment will be local. +// Anything else needs to be "parsed" (a string or an array), and will be affected by: +// parseAsUTC - if there is no zone information, should we parse the input in UTC? +// parseZone - if there is zone information, should we force the zone of the moment? +function makeMoment(args, parseAsUTC, parseZone) { + var input = args[0]; + var isSingleString = args.length == 1 && typeof input === 'string'; + var isAmbigTime; + var isAmbigZone; + var ambigMatch; + var output; // an object with fields for the new FCMoment object + + if (moment.isMoment(input)) { + output = moment.apply(null, args); // clone it + + // the ambig properties have not been preserved in the clone, so reassign them + if (input._ambigTime) { + output._ambigTime = true; + } + if (input._ambigZone) { + output._ambigZone = true; + } + } + else if (isNativeDate(input) || input === undefined) { + output = moment.apply(null, args); // will be local + } + else { // "parsing" is required + isAmbigTime = false; + isAmbigZone = false; + + if (isSingleString) { + if (ambigDateOfMonthRegex.test(input)) { + // accept strings like '2014-05', but convert to the first of the month + input += '-01'; + args = [ input ]; // for when we pass it on to moment's constructor + isAmbigTime = true; + isAmbigZone = true; + } + else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) { + isAmbigTime = !ambigMatch[5]; // no time part? + isAmbigZone = true; + } + } + else if ($.isArray(input)) { + // arrays have no timezone information, so assume ambiguous zone + isAmbigZone = true; + } + // otherwise, probably a string with a format + + if (parseAsUTC) { + output = moment.utc.apply(moment, args); + } + else { + output = moment.apply(null, args); + } + + if (isAmbigTime) { + output._ambigTime = true; + output._ambigZone = true; // ambiguous time always means ambiguous zone + } + else if (parseZone) { // let's record the inputted zone somehow + if (isAmbigZone) { + output._ambigZone = true; + } + else if (isSingleString) { + output.zone(input); // if not a valid zone, will assign UTC + } + } + } + + return new FCMoment(output); +} + +// Our subclass of Moment. +// Accepts an object with the internal Moment properties that should be copied over to +// `this` object (most likely another Moment object). The values in this data must not +// be referenced by anything else (two moments sharing a Date object for example). +function FCMoment(internalData) { + extend(this, internalData); +} + +// Chain the prototype to Moment's +FCMoment.prototype = createObject(moment.fn); + +// We need this because Moment's implementation won't create an FCMoment, +// nor will it copy over the ambig flags. +FCMoment.prototype.clone = function() { + return makeMoment([ this ]); +}; + + +// Time-of-day +// ------------------------------------------------------------------------------------------------- + +// GETTER +// Returns a Duration with the hours/minutes/seconds/ms values of the moment. +// If the moment has an ambiguous time, a duration of 00:00 will be returned. +// +// SETTER +// You can supply a Duration, a Moment, or a Duration-like argument. +// When setting the time, and the moment has an ambiguous time, it then becomes unambiguous. +FCMoment.prototype.time = function(time) { + if (time == null) { // getter + return moment.duration({ + hours: this.hours(), + minutes: this.minutes(), + seconds: this.seconds(), + milliseconds: this.milliseconds() + }); + } + else { // setter + + delete this._ambigTime; // mark that the moment now has a time + + if (!moment.isDuration(time) && !moment.isMoment(time)) { + time = moment.duration(time); + } + + // The day value should cause overflow (so 24 hours becomes 00:00:00 of next day). + // Only for Duration times, not Moment times. + var dayHours = 0; + if (moment.isDuration(time)) { + dayHours = Math.floor(time.asDays()) * 24; + } + + // We need to set the individual fields. + // Can't use startOf('day') then add duration. In case of DST at start of day. + return this.hours(dayHours + time.hours()) + .minutes(time.minutes()) + .seconds(time.seconds()) + .milliseconds(time.milliseconds()); + } +}; + +// Converts the moment to UTC, stripping out its time-of-day and timezone offset, +// but preserving its YMD. A moment with a stripped time will display no time +// nor timezone offset when .format() is called. +FCMoment.prototype.stripTime = function() { + var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array + + // set the internal UTC flag + moment.fn.utc.call(this); // call the original method, because we don't want to affect _ambigZone + + this.year(a[0]) // TODO: find a way to do this in one shot + .month(a[1]) + .date(a[2]) + .hours(0) + .minutes(0) + .seconds(0) + .milliseconds(0); + + // Mark the time as ambiguous. This needs to happen after the .utc() call, which calls .zone(), which + // clears all ambig flags. Same concept with the .year/month/date calls in the case of moment-timezone. + this._ambigTime = true; + this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset + + return this; // for chaining +}; + +// Returns if the moment has a non-ambiguous time (boolean) +FCMoment.prototype.hasTime = function() { + return !this._ambigTime; +}; + + +// Timezone +// ------------------------------------------------------------------------------------------------- + +// Converts the moment to UTC, stripping out its timezone offset, but preserving its +// YMD and time-of-day. A moment with a stripped timezone offset will display no +// timezone offset when .format() is called. +FCMoment.prototype.stripZone = function() { + var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array + var wasAmbigTime = this._ambigTime; + + moment.fn.utc.call(this); // set the internal UTC flag + + this.year(a[0]) // TODO: find a way to do this in one shot + .month(a[1]) + .date(a[2]) + .hours(a[3]) + .minutes(a[4]) + .seconds(a[5]) + .milliseconds(a[6]); + + if (wasAmbigTime) { + // the above call to .utc()/.zone() unfortunately clears the ambig flags, so reassign + this._ambigTime = true; + } + + // Mark the zone as ambiguous. This needs to happen after the .utc() call, which calls .zone(), which + // clears all ambig flags. Same concept with the .year/month/date calls in the case of moment-timezone. + this._ambigZone = true; + + return this; // for chaining +}; + +// Returns of the moment has a non-ambiguous timezone offset (boolean) +FCMoment.prototype.hasZone = function() { + return !this._ambigZone; +}; + +// this method implicitly marks a zone +FCMoment.prototype.zone = function(tzo) { + + if (tzo != null) { + // FYI, the delete statements need to be before the .zone() call or else chaos ensues + // for reasons I don't understand. + delete this._ambigTime; + delete this._ambigZone; + } + + return moment.fn.zone.apply(this, arguments); +}; + +// this method implicitly marks a zone +FCMoment.prototype.local = function() { + var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array + var wasAmbigZone = this._ambigZone; + + // will happen anyway via .local()/.zone(), but don't want to rely on internal implementation + delete this._ambigTime; + delete this._ambigZone; + + moment.fn.local.apply(this, arguments); + + if (wasAmbigZone) { + // If the moment was ambiguously zoned, the date fields were stored as UTC. + // We want to preserve these, but in local time. + this.year(a[0]) // TODO: find a way to do this in one shot + .month(a[1]) + .date(a[2]) + .hours(a[3]) + .minutes(a[4]) + .seconds(a[5]) + .milliseconds(a[6]); + } + + return this; // for chaining +}; + +// this method implicitly marks a zone +FCMoment.prototype.utc = function() { + + // will happen anyway via .local()/.zone(), but don't want to rely on internal implementation + delete this._ambigTime; + delete this._ambigZone; + + return moment.fn.utc.apply(this, arguments); +}; + + +// Formatting +// ------------------------------------------------------------------------------------------------- + +FCMoment.prototype.format = function() { + if (arguments[0]) { + return formatDate(this, arguments[0]); // our extended formatting + } + if (this._ambigTime) { + return momentFormat(this, 'YYYY-MM-DD'); + } + if (this._ambigZone) { + return momentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss'); + } + return momentFormat(this); // default moment original formatting +}; + +FCMoment.prototype.toISOString = function() { + if (this._ambigTime) { + return momentFormat(this, 'YYYY-MM-DD'); + } + if (this._ambigZone) { + return momentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss'); + } + return moment.fn.toISOString.apply(this, arguments); +}; + + +// Querying +// ------------------------------------------------------------------------------------------------- + +// Is the moment within the specified range? `end` is exclusive. +FCMoment.prototype.isWithin = function(start, end) { + var a = commonlyAmbiguate([ this, start, end ]); + return a[0] >= a[1] && a[0] < a[2]; +}; + +// When isSame is called with units, timezone ambiguity is normalized before the comparison happens. +// If no units are specified, the two moments must be identically the same, with matching ambig flags. +FCMoment.prototype.isSame = function(input, units) { + var a; + + if (units) { + a = commonlyAmbiguate([ this, input ], true); // normalize timezones but don't erase times + return moment.fn.isSame.call(a[0], a[1], units); + } + else { + input = fc.moment.parseZone(input); // normalize input + return moment.fn.isSame.call(this, input) && + Boolean(this._ambigTime) === Boolean(input._ambigTime) && + Boolean(this._ambigZone) === Boolean(input._ambigZone); + } +}; + +// Make these query methods work with ambiguous moments +$.each([ + 'isBefore', + 'isAfter' +], function(i, methodName) { + FCMoment.prototype[methodName] = function(input, units) { + var a = commonlyAmbiguate([ this, input ]); + return moment.fn[methodName].call(a[0], a[1], units); + }; +}); + + +// Misc Internals +// ------------------------------------------------------------------------------------------------- + +// given an array of moment-like inputs, return a parallel array w/ moments similarly ambiguated. +// for example, of one moment has ambig time, but not others, all moments will have their time stripped. +// set `preserveTime` to `true` to keep times, but only normalize zone ambiguity. +function commonlyAmbiguate(inputs, preserveTime) { + var outputs = []; + var anyAmbigTime = false; + var anyAmbigZone = false; + var i; + + for (i=0; i "MMMM D YYYY" + formatStr = localeData.longDateFormat(formatStr) || formatStr; + // BTW, this is not important for `formatDate` because it is impossible to put custom tokens + // or non-zero areas in Moment's localized format strings. + + separator = separator || ' - '; + + return formatRangeWithChunks( + date1, + date2, + getFormatStringChunks(formatStr), + separator, + isRTL + ); +} +fc.formatRange = formatRange; // expose + + +function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) { + var chunkStr; // the rendering of the chunk + var leftI; + var leftStr = ''; + var rightI; + var rightStr = ''; + var middleI; + var middleStr1 = ''; + var middleStr2 = ''; + var middleStr = ''; + + // Start at the leftmost side of the formatting string and continue until you hit a token + // that is not the same between dates. + for (leftI=0; leftIleftI; rightI--) { + chunkStr = formatSimilarChunk(date1, date2, chunks[rightI]); + if (chunkStr === false) { + break; + } + rightStr = chunkStr + rightStr; + } + + // The area in the middle is different for both of the dates. + // Collect them distinctly so we can jam them together later. + for (middleI=leftI; middleI<=rightI; middleI++) { + middleStr1 += formatDateWithChunk(date1, chunks[middleI]); + middleStr2 += formatDateWithChunk(date2, chunks[middleI]); + } + + if (middleStr1 || middleStr2) { + if (isRTL) { + middleStr = middleStr2 + separator + middleStr1; + } + else { + middleStr = middleStr1 + separator + middleStr2; + } + } + + return leftStr + middleStr + rightStr; +} + + +var similarUnitMap = { + Y: 'year', + M: 'month', + D: 'day', // day of month + d: 'day', // day of week + // prevents a separator between anything time-related... + A: 'second', // AM/PM + a: 'second', // am/pm + T: 'second', // A/P + t: 'second', // a/p + H: 'second', // hour (24) + h: 'second', // hour (12) + m: 'second', // minute + s: 'second' // second +}; +// TODO: week maybe? + + +// Given a formatting chunk, and given that both dates are similar in the regard the +// formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`. +function formatSimilarChunk(date1, date2, chunk) { + var token; + var unit; + + if (typeof chunk === 'string') { // a literal string + return chunk; + } + else if ((token = chunk.token)) { + unit = similarUnitMap[token.charAt(0)]; + // are the dates the same for this unit of measurement? + if (unit && date1.isSame(date2, unit)) { + return momentFormat(date1, token); // would be the same if we used `date2` + // BTW, don't support custom tokens + } + } + + return false; // the chunk is NOT the same for the two dates + // BTW, don't support splitting on non-zero areas +} + + +// Chunking Utils +// ------------------------------------------------------------------------------------------------- + + +var formatStringChunkCache = {}; + + +function getFormatStringChunks(formatStr) { + if (formatStr in formatStringChunkCache) { + return formatStringChunkCache[formatStr]; + } + return (formatStringChunkCache[formatStr] = chunkFormatString(formatStr)); +} + + +// Break the formatting string into an array of chunks +function chunkFormatString(formatStr) { + var chunks = []; + var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LT|(\w)\4*o?)|([^\w\[\(]+)/g; // TODO: more descrimination + var match; + + while ((match = chunker.exec(formatStr))) { + if (match[1]) { // a literal string inside [ ... ] + chunks.push(match[1]); + } + else if (match[2]) { // non-zero formatting inside ( ... ) + chunks.push({ maybe: chunkFormatString(match[2]) }); + } + else if (match[3]) { // a formatting token + chunks.push({ token: match[3] }); + } + else if (match[5]) { // an unenclosed literal string + chunks.push(match[5]); + } + } + + return chunks; +} + +;; + +/* A rectangular panel that is absolutely positioned over other content +------------------------------------------------------------------------------------------------------------------------ +Options: + - className (string) + - content (HTML string or jQuery element set) + - parentEl + - top + - left + - right (the x coord of where the right edge should be. not a "CSS" right) + - autoHide (boolean) + - show (callback) + - hide (callback) +*/ + +function Popover(options) { + this.options = options || {}; +} + + +Popover.prototype = { + + isHidden: true, + options: null, + el: null, // the container element for the popover. generated by this object + documentMousedownProxy: null, // document mousedown handler bound to `this` + margin: 10, // the space required between the popover and the edges of the scroll container + + + // Shows the popover on the specified position. Renders it if not already + show: function() { + if (this.isHidden) { + if (!this.el) { + this.render(); + } + this.el.show(); + this.position(); + this.isHidden = false; + this.trigger('show'); + } + }, + + + // Hides the popover, through CSS, but does not remove it from the DOM + hide: function() { + if (!this.isHidden) { + this.el.hide(); + this.isHidden = true; + this.trigger('hide'); + } + }, + + + // Creates `this.el` and renders content inside of it + render: function() { + var _this = this; + var options = this.options; + + this.el = $('
') + .addClass(options.className || '') + .css({ + // position initially to the top left to avoid creating scrollbars + top: 0, + left: 0 + }) + .append(options.content) + .appendTo(options.parentEl); + + // when a click happens on anything inside with a 'fc-close' className, hide the popover + this.el.on('click', '.fc-close', function() { + _this.hide(); + }); + + if (options.autoHide) { + $(document).on('mousedown', this.documentMousedownProxy = $.proxy(this, 'documentMousedown')); + } + }, + + + // Triggered when the user clicks *anywhere* in the document, for the autoHide feature + documentMousedown: function(ev) { + // only hide the popover if the click happened outside the popover + if (this.el && !$(ev.target).closest(this.el).length) { + this.hide(); + } + }, + + + // Hides and unregisters any handlers + destroy: function() { + this.hide(); + + if (this.el) { + this.el.remove(); + this.el = null; + } + + $(document).off('mousedown', this.documentMousedownProxy); + }, + + + // Positions the popover optimally, using the top/left/right options + position: function() { + var options = this.options; + var origin = this.el.offsetParent().offset(); + var width = this.el.outerWidth(); + var height = this.el.outerHeight(); + var windowEl = $(window); + var viewportEl = getScrollParent(this.el); + var viewportTop; + var viewportLeft; + var viewportOffset; + var top; // the "position" (not "offset") values for the popover + var left; // + + // compute top and left + top = options.top || 0; + if (options.left !== undefined) { + left = options.left; + } + else if (options.right !== undefined) { + left = options.right - width; // derive the left value from the right value + } + else { + left = 0; + } + + if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result + viewportEl = windowEl; + viewportTop = 0; // the window is always at the top left + viewportLeft = 0; // (and .offset() won't work if called here) + } + else { + viewportOffset = viewportEl.offset(); + viewportTop = viewportOffset.top; + viewportLeft = viewportOffset.left; + } + + // if the window is scrolled, it causes the visible area to be further down + viewportTop += windowEl.scrollTop(); + viewportLeft += windowEl.scrollLeft(); + + // constrain to the view port. if constrained by two edges, give precedence to top/left + if (options.viewportConstrain !== false) { + top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin); + top = Math.max(top, viewportTop + this.margin); + left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin); + left = Math.max(left, viewportLeft + this.margin); + } + + this.el.css({ + top: top - origin.top, + left: left - origin.left + }); + }, + + + // Triggers a callback. Calls a function in the option hash of the same name. + // Arguments beyond the first `name` are forwarded on. + // TODO: better code reuse for this. Repeat code + trigger: function(name) { + if (this.options[name]) { + this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); + } + } + +}; + +;; + +/* A "coordinate map" converts pixel coordinates into an associated cell, which has an associated date +------------------------------------------------------------------------------------------------------------------------ +Common interface: + + CoordMap.prototype = { + build: function() {}, + getCell: function(x, y) {} + }; + +*/ + +/* Coordinate map for a grid component +----------------------------------------------------------------------------------------------------------------------*/ + +function GridCoordMap(grid) { + this.grid = grid; +} + + +GridCoordMap.prototype = { + + grid: null, // reference to the Grid + rows: null, // the top-to-bottom y coordinates. including the bottom of the last item + cols: null, // the left-to-right x coordinates. including the right of the last item + + containerEl: null, // container element that all coordinates are constrained to. optionally assigned + minX: null, + maxX: null, // exclusive + minY: null, + maxY: null, // exclusive + + + // Queries the grid for the coordinates of all the cells + build: function() { + this.grid.buildCoords( + this.rows = [], + this.cols = [] + ); + this.computeBounds(); + }, + + + // Given a coordinate of the document, gets the associated cell. If no cell is underneath, returns null + getCell: function(x, y) { + var cell = null; + var rows = this.rows; + var cols = this.cols; + var r = -1; + var c = -1; + var i; + + if (this.inBounds(x, y)) { + + for (i = 0; i < rows.length; i++) { + if (y >= rows[i][0] && y < rows[i][1]) { + r = i; + break; + } + } + + for (i = 0; i < cols.length; i++) { + if (x >= cols[i][0] && x < cols[i][1]) { + c = i; + break; + } + } + + if (r >= 0 && c >= 0) { + cell = { row: r, col: c }; + cell.grid = this.grid; + cell.date = this.grid.getCellDate(cell); + } + } + + return cell; + }, + + + // If there is a containerEl, compute the bounds into min/max values + computeBounds: function() { + var containerOffset; + + if (this.containerEl) { + containerOffset = this.containerEl.offset(); + this.minX = containerOffset.left; + this.maxX = containerOffset.left + this.containerEl.outerWidth(); + this.minY = containerOffset.top; + this.maxY = containerOffset.top + this.containerEl.outerHeight(); + } + }, + + + // Determines if the given coordinates are in bounds. If no `containerEl`, always true + inBounds: function(x, y) { + if (this.containerEl) { + return x >= this.minX && x < this.maxX && y >= this.minY && y < this.maxY; + } + return true; + } + +}; + + +/* Coordinate map that is a combination of multiple other coordinate maps +----------------------------------------------------------------------------------------------------------------------*/ + +function ComboCoordMap(coordMaps) { + this.coordMaps = coordMaps; +} + + +ComboCoordMap.prototype = { + + coordMaps: null, // an array of CoordMaps + + + // Builds all coordMaps + build: function() { + var coordMaps = this.coordMaps; + var i; + + for (i = 0; i < coordMaps.length; i++) { + coordMaps[i].build(); + } + }, + + + // Queries all coordMaps for the cell underneath the given coordinates, returning the first result + getCell: function(x, y) { + var coordMaps = this.coordMaps; + var cell = null; + var i; + + for (i = 0; i < coordMaps.length && !cell; i++) { + cell = coordMaps[i].getCell(x, y); + } + + return cell; + } + +}; +;; + +/* Tracks mouse movements over a CoordMap and raises events about which cell the mouse is over. +----------------------------------------------------------------------------------------------------------------------*/ +// TODO: implement scrolling + +function DragListener(coordMap, options) { + this.coordMap = coordMap; + this.options = options || {}; +} + + +DragListener.prototype = { + + coordMap: null, + options: null, + + isListening: false, + isDragging: false, + + // the cell/date the mouse was over when listening started + origCell: null, + origDate: null, + + // the cell/date the mouse is over + cell: null, + date: null, + + // coordinates of the initial mousedown + mouseX0: null, + mouseY0: null, + + // handler attached to the document, bound to the DragListener's `this` + mousemoveProxy: null, + mouseupProxy: null, + + scrollEl: null, + scrollBounds: null, // { top, bottom, left, right } + scrollTopVel: null, // pixels per second + scrollLeftVel: null, // pixels per second + scrollIntervalId: null, // ID of setTimeout for scrolling animation loop + scrollHandlerProxy: null, // this-scoped function for handling when scrollEl is scrolled + + scrollSensitivity: 30, // pixels from edge for scrolling to start + scrollSpeed: 200, // pixels per second, at maximum speed + scrollIntervalMs: 50, // millisecond wait between scroll increment + + + // Call this when the user does a mousedown. Will probably lead to startListening + mousedown: function(ev) { + if (isPrimaryMouseButton(ev)) { + + ev.preventDefault(); // prevents native selection in most browsers + + this.startListening(ev); + + // start the drag immediately if there is no minimum distance for a drag start + if (!this.options.distance) { + this.startDrag(ev); + } + } + }, + + + // Call this to start tracking mouse movements + startListening: function(ev) { + var scrollParent; + var cell; + + if (!this.isListening) { + + // grab scroll container and attach handler + if (ev && this.options.scroll) { + scrollParent = getScrollParent($(ev.target)); + if (!scrollParent.is(window) && !scrollParent.is(document)) { + this.scrollEl = scrollParent; + + // scope to `this`, and use `debounce` to make sure rapid calls don't happen + this.scrollHandlerProxy = debounce($.proxy(this, 'scrollHandler'), 100); + this.scrollEl.on('scroll', this.scrollHandlerProxy); + } + } + + this.computeCoords(); // relies on `scrollEl` + + // get info on the initial cell, date, and coordinates + if (ev) { + cell = this.getCell(ev); + this.origCell = cell; + this.origDate = cell ? cell.date : null; + + this.mouseX0 = ev.pageX; + this.mouseY0 = ev.pageY; + } + + $(document) + .on('mousemove', this.mousemoveProxy = $.proxy(this, 'mousemove')) + .on('mouseup', this.mouseupProxy = $.proxy(this, 'mouseup')) + .on('selectstart', this.preventDefault); // prevents native selection in IE<=8 + + this.isListening = true; + this.trigger('listenStart', ev); + } + }, + + + // Recomputes the drag-critical positions of elements + computeCoords: function() { + this.coordMap.build(); + this.computeScrollBounds(); + }, + + + // Called when the user moves the mouse + mousemove: function(ev) { + var minDistance; + var distanceSq; // current distance from mouseX0/mouseY0, squared + + if (!this.isDragging) { // if not already dragging... + // then start the drag if the minimum distance criteria is met + minDistance = this.options.distance || 1; + distanceSq = Math.pow(ev.pageX - this.mouseX0, 2) + Math.pow(ev.pageY - this.mouseY0, 2); + if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem + this.startDrag(ev); + } + } + + if (this.isDragging) { + this.drag(ev); // report a drag, even if this mousemove initiated the drag + } + }, + + + // Call this to initiate a legitimate drag. + // This function is called internally from this class, but can also be called explicitly from outside + startDrag: function(ev) { + var cell; + + if (!this.isListening) { // startDrag must have manually initiated + this.startListening(); + } + + if (!this.isDragging) { + this.isDragging = true; + this.trigger('dragStart', ev); + + // report the initial cell the mouse is over + cell = this.getCell(ev); + if (cell) { + this.cellOver(cell, true); + } + } + }, + + + // Called while the mouse is being moved and when we know a legitimate drag is taking place + drag: function(ev) { + var cell; + + if (this.isDragging) { + cell = this.getCell(ev); + + if (!isCellsEqual(cell, this.cell)) { // a different cell than before? + if (this.cell) { + this.cellOut(); + } + if (cell) { + this.cellOver(cell); + } + } + + this.dragScroll(ev); // will possibly cause scrolling + } + }, + + + // Called when a the mouse has just moved over a new cell + cellOver: function(cell) { + this.cell = cell; + this.date = cell.date; + this.trigger('cellOver', cell, cell.date); + }, + + + // Called when the mouse has just moved out of a cell + cellOut: function() { + if (this.cell) { + this.trigger('cellOut', this.cell); + this.cell = null; + this.date = null; + } + }, + + + // Called when the user does a mouseup + mouseup: function(ev) { + this.stopDrag(ev); + this.stopListening(ev); + }, + + + // Called when the drag is over. Will not cause listening to stop however. + // A concluding 'cellOut' event will NOT be triggered. + stopDrag: function(ev) { + if (this.isDragging) { + this.stopScrolling(); + this.trigger('dragStop', ev); + this.isDragging = false; + } + }, + + + // Call this to stop listening to the user's mouse events + stopListening: function(ev) { + if (this.isListening) { + + // remove the scroll handler if there is a scrollEl + if (this.scrollEl) { + this.scrollEl.off('scroll', this.scrollHandlerProxy); + this.scrollHandlerProxy = null; + } + + $(document) + .off('mousemove', this.mousemoveProxy) + .off('mouseup', this.mouseupProxy) + .off('selectstart', this.preventDefault); + + this.mousemoveProxy = null; + this.mouseupProxy = null; + + this.isListening = false; + this.trigger('listenStop', ev); + + this.origCell = this.cell = null; + this.origDate = this.date = null; + } + }, + + + // Gets the cell underneath the coordinates for the given mouse event + getCell: function(ev) { + return this.coordMap.getCell(ev.pageX, ev.pageY); + }, + + + // Triggers a callback. Calls a function in the option hash of the same name. + // Arguments beyond the first `name` are forwarded on. + trigger: function(name) { + if (this.options[name]) { + this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); + } + }, + + + // Stops a given mouse event from doing it's native browser action. In our case, text selection. + preventDefault: function(ev) { + ev.preventDefault(); + }, + + + /* Scrolling + ------------------------------------------------------------------------------------------------------------------*/ + + + // Computes and stores the bounding rectangle of scrollEl + computeScrollBounds: function() { + var el = this.scrollEl; + var offset; + + if (el) { + offset = el.offset(); + this.scrollBounds = { + top: offset.top, + left: offset.left, + bottom: offset.top + el.outerHeight(), + right: offset.left + el.outerWidth() + }; + } + }, + + + // Called when the dragging is in progress and scrolling should be updated + dragScroll: function(ev) { + var sensitivity = this.scrollSensitivity; + var bounds = this.scrollBounds; + var topCloseness, bottomCloseness; + var leftCloseness, rightCloseness; + var topVel = 0; + var leftVel = 0; + + if (bounds) { // only scroll if scrollEl exists + + // compute closeness to edges. valid range is from 0.0 - 1.0 + topCloseness = (sensitivity - (ev.pageY - bounds.top)) / sensitivity; + bottomCloseness = (sensitivity - (bounds.bottom - ev.pageY)) / sensitivity; + leftCloseness = (sensitivity - (ev.pageX - bounds.left)) / sensitivity; + rightCloseness = (sensitivity - (bounds.right - ev.pageX)) / sensitivity; + + // translate vertical closeness into velocity. + // mouse must be completely in bounds for velocity to happen. + if (topCloseness >= 0 && topCloseness <= 1) { + topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up + } + else if (bottomCloseness >= 0 && bottomCloseness <= 1) { + topVel = bottomCloseness * this.scrollSpeed; + } + + // translate horizontal closeness into velocity + if (leftCloseness >= 0 && leftCloseness <= 1) { + leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left + } + else if (rightCloseness >= 0 && rightCloseness <= 1) { + leftVel = rightCloseness * this.scrollSpeed; + } + } + + this.setScrollVel(topVel, leftVel); + }, + + + // Sets the speed-of-scrolling for the scrollEl + setScrollVel: function(topVel, leftVel) { + + this.scrollTopVel = topVel; + this.scrollLeftVel = leftVel; + + this.constrainScrollVel(); // massages into realistic values + + // if there is non-zero velocity, and an animation loop hasn't already started, then START + if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) { + this.scrollIntervalId = setInterval( + $.proxy(this, 'scrollIntervalFunc'), // scope to `this` + this.scrollIntervalMs + ); + } + }, + + + // Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way + constrainScrollVel: function() { + var el = this.scrollEl; + + if (this.scrollTopVel < 0) { // scrolling up? + if (el.scrollTop() <= 0) { // already scrolled all the way up? + this.scrollTopVel = 0; + } + } + else if (this.scrollTopVel > 0) { // scrolling down? + if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) { // already scrolled all the way down? + this.scrollTopVel = 0; + } + } + + if (this.scrollLeftVel < 0) { // scrolling left? + if (el.scrollLeft() <= 0) { // already scrolled all the left? + this.scrollLeftVel = 0; + } + } + else if (this.scrollLeftVel > 0) { // scrolling right? + if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) { // already scrolled all the way right? + this.scrollLeftVel = 0; + } + } + }, + + + // This function gets called during every iteration of the scrolling animation loop + scrollIntervalFunc: function() { + var el = this.scrollEl; + var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by + + // change the value of scrollEl's scroll + if (this.scrollTopVel) { + el.scrollTop(el.scrollTop() + this.scrollTopVel * frac); + } + if (this.scrollLeftVel) { + el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac); + } + + this.constrainScrollVel(); // since the scroll values changed, recompute the velocities + + // if scrolled all the way, which causes the vels to be zero, stop the animation loop + if (!this.scrollTopVel && !this.scrollLeftVel) { + this.stopScrolling(); + } + }, + + + // Kills any existing scrolling animation loop + stopScrolling: function() { + if (this.scrollIntervalId) { + clearInterval(this.scrollIntervalId); + this.scrollIntervalId = null; + + // when all done with scrolling, recompute positions since they probably changed + this.computeCoords(); + } + }, + + + // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce) + scrollHandler: function() { + // recompute all coordinates, but *only* if this is *not* part of our scrolling animation + if (!this.scrollIntervalId) { + this.computeCoords(); + } + } + +}; + + +// Returns `true` if the cells are identically equal. `false` otherwise. +// They must have the same row, col, and be from the same grid. +// Two null values will be considered equal, as two "out of the grid" states are the same. +function isCellsEqual(cell1, cell2) { + + if (!cell1 && !cell2) { + return true; + } + + if (cell1 && cell2) { + return cell1.grid === cell2.grid && + cell1.row === cell2.row && + cell1.col === cell2.col; + } + + return false; +} + +;; + +/* Creates a clone of an element and lets it track the mouse as it moves +----------------------------------------------------------------------------------------------------------------------*/ + +function MouseFollower(sourceEl, options) { + this.options = options = options || {}; + this.sourceEl = sourceEl; + this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent +} + + +MouseFollower.prototype = { + + options: null, + + sourceEl: null, // the element that will be cloned and made to look like it is dragging + el: null, // the clone of `sourceEl` that will track the mouse + parentEl: null, // the element that `el` (the clone) will be attached to + + // the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl + top0: null, + left0: null, + + // the initial position of the mouse + mouseY0: null, + mouseX0: null, + + // the number of pixels the mouse has moved from its initial position + topDelta: null, + leftDelta: null, + + mousemoveProxy: null, // document mousemove handler, bound to the MouseFollower's `this` + + isFollowing: false, + isHidden: false, + isAnimating: false, // doing the revert animation? + + + // Causes the element to start following the mouse + start: function(ev) { + if (!this.isFollowing) { + this.isFollowing = true; + + this.mouseY0 = ev.pageY; + this.mouseX0 = ev.pageX; + this.topDelta = 0; + this.leftDelta = 0; + + if (!this.isHidden) { + this.updatePosition(); + } + + $(document).on('mousemove', this.mousemoveProxy = $.proxy(this, 'mousemove')); + } + }, + + + // Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position. + // `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately. + stop: function(shouldRevert, callback) { + var _this = this; + var revertDuration = this.options.revertDuration; + + function complete() { + this.isAnimating = false; + _this.destroyEl(); + + this.top0 = this.left0 = null; // reset state for future updatePosition calls + + if (callback) { + callback(); + } + } + + if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time + this.isFollowing = false; + + $(document).off('mousemove', this.mousemoveProxy); + + if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation? + this.isAnimating = true; + this.el.animate({ + top: this.top0, + left: this.left0 + }, { + duration: revertDuration, + complete: complete + }); + } + else { + complete(); + } + } + }, + + + // Gets the tracking element. Create it if necessary + getEl: function() { + var el = this.el; + + if (!el) { + this.sourceEl.width(); // hack to force IE8 to compute correct bounding box + el = this.el = this.sourceEl.clone() + .css({ + position: 'absolute', + visibility: '', // in case original element was hidden (commonly through hideEvents()) + display: this.isHidden ? 'none' : '', // for when initially hidden + margin: 0, + right: 'auto', // erase and set width instead + bottom: 'auto', // erase and set height instead + width: this.sourceEl.width(), // explicit height in case there was a 'right' value + height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value + opacity: this.options.opacity || '', + zIndex: this.options.zIndex + }) + .appendTo(this.parentEl); + } + + return el; + }, + + + // Removes the tracking element if it has already been created + destroyEl: function() { + if (this.el) { + this.el.remove(); + this.el = null; + } + }, + + + // Update the CSS position of the tracking element + updatePosition: function() { + var sourceOffset; + var origin; + + this.getEl(); // ensure this.el + + // make sure origin info was computed + if (this.top0 === null) { + this.sourceEl.width(); // hack to force IE8 to compute correct bounding box + sourceOffset = this.sourceEl.offset(); + origin = this.el.offsetParent().offset(); + this.top0 = sourceOffset.top - origin.top; + this.left0 = sourceOffset.left - origin.left; + } + + this.el.css({ + top: this.top0 + this.topDelta, + left: this.left0 + this.leftDelta + }); + }, + + + // Gets called when the user moves the mouse + mousemove: function(ev) { + this.topDelta = ev.pageY - this.mouseY0; + this.leftDelta = ev.pageX - this.mouseX0; + + if (!this.isHidden) { + this.updatePosition(); + } + }, + + + // Temporarily makes the tracking element invisible. Can be called before following starts + hide: function() { + if (!this.isHidden) { + this.isHidden = true; + if (this.el) { + this.el.hide(); + } + } + }, + + + // Show the tracking element after it has been temporarily hidden + show: function() { + if (this.isHidden) { + this.isHidden = false; + this.updatePosition(); + this.getEl().show(); + } + } + +}; + +;; + +/* A utility class for rendering rows. +----------------------------------------------------------------------------------------------------------------------*/ +// It leverages methods of the subclass and the View to determine custom rendering behavior for each row "type" +// (such as highlight rows, day rows, helper rows, etc). + +function RowRenderer(view) { + this.view = view; +} + + +RowRenderer.prototype = { + + view: null, // a View object + cellHtml: '', // plain default HTML used for a cell when no other is available + + + // Renders the HTML for a row, leveraging custom cell-HTML-renderers based on the `rowType`. + // Also applies the "intro" and "outro" cells, which are specified by the subclass and views. + // `row` is an optional row number. + rowHtml: function(rowType, row) { + var view = this.view; + var renderCell = this.getHtmlRenderer('cell', rowType); + var cellHtml = ''; + var col; + var date; + + row = row || 0; + + for (col = 0; col < view.colCnt; col++) { + date = view.cellToDate(row, col); + cellHtml += renderCell(row, col, date); + } + + cellHtml = this.bookendCells(cellHtml, rowType, row); // apply intro and outro + + return '' + cellHtml + ''; + }, + + + // Applies the "intro" and "outro" HTML to the given cells. + // Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro. + // `cells` can be an HTML string of 's or a jQuery element + // `row` is an optional row number. + bookendCells: function(cells, rowType, row) { + var view = this.view; + var intro = this.getHtmlRenderer('intro', rowType)(row || 0); + var outro = this.getHtmlRenderer('outro', rowType)(row || 0); + var isRTL = view.opt('isRTL'); + var prependHtml = isRTL ? outro : intro; + var appendHtml = isRTL ? intro : outro; + + if (typeof cells === 'string') { + return prependHtml + cells + appendHtml; + } + else { // a jQuery element + return cells.prepend(prependHtml).append(appendHtml); + } + }, + + + // Returns an HTML-rendering function given a specific `rendererName` (like cell, intro, or outro) and a specific + // `rowType` (like day, eventSkeleton, helperSkeleton), which is optional. + // If a renderer for the specific rowType doesn't exist, it will fall back to a generic renderer. + // We will query the View object first for any custom rendering functions, then the methods of the subclass. + getHtmlRenderer: function(rendererName, rowType) { + var view = this.view; + var generalName; // like "cellHtml" + var specificName; // like "dayCellHtml". based on rowType + var provider; // either the View or the RowRenderer subclass, whichever provided the method + var renderer; + + generalName = rendererName + 'Html'; + if (rowType) { + specificName = rowType + capitaliseFirstLetter(rendererName) + 'Html'; + } + + if (specificName && (renderer = view[specificName])) { + provider = view; + } + else if (specificName && (renderer = this[specificName])) { + provider = this; + } + else if ((renderer = view[generalName])) { + provider = view; + } + else if ((renderer = this[generalName])) { + provider = this; + } + + if (typeof renderer === 'function') { + return function(row) { + return renderer.apply(provider, arguments) || ''; // use correct `this` and always return a string + }; + } + + // the rendered can be a plain string as well. if not specified, always an empty string. + return function() { + return renderer || ''; + }; + } + +}; + +;; + +/* An abstract class comprised of a "grid" of cells that each represent a specific datetime +----------------------------------------------------------------------------------------------------------------------*/ + +function Grid(view) { + RowRenderer.call(this, view); // call the super-constructor + this.coordMap = new GridCoordMap(this); +} + + +Grid.prototype = createObject(RowRenderer.prototype); // declare the super-class +$.extend(Grid.prototype, { + + el: null, // the containing element + coordMap: null, // a GridCoordMap that converts pixel values to datetimes + cellDuration: null, // a cell's duration. subclasses must assign this ASAP + + + // Renders the grid into the `el` element. + // Subclasses should override and call this super-method when done. + render: function() { + this.bindHandlers(); + }, + + + // Called when the grid's resources need to be cleaned up + destroy: function() { + // subclasses can implement + }, + + + /* Coordinates & Cells + ------------------------------------------------------------------------------------------------------------------*/ + + + // Populates the given empty arrays with the y and x coordinates of the cells + buildCoords: function(rows, cols) { + // subclasses must implement + }, + + + // Given a cell object, returns the date for that cell + getCellDate: function(cell) { + // subclasses must implement + }, + + + // Given a cell object, returns the element that represents the cell's whole-day + getCellDayEl: function(cell) { + // subclasses must implement + }, + + + // Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects + rangeToSegs: function(start, end) { + // subclasses must implement + }, + + + /* Handlers + ------------------------------------------------------------------------------------------------------------------*/ + + + // Attach handlers to `this.el`, using bubbling to listen to all ancestors. + // We don't need to undo any of this in a "destroy" method, because the view will simply remove `this.el` from the + // DOM and jQuery will be smart enough to garbage collect the handlers. + bindHandlers: function() { + var _this = this; + + this.el.on('mousedown', function(ev) { + if ( + !$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link + !$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one) + ) { + _this.dayMousedown(ev); + } + }); + + this.bindSegHandlers(); // attach event-element-related handlers. in Grid.events.js + }, + + + // Process a mousedown on an element that represents a day. For day clicking and selecting. + dayMousedown: function(ev) { + var _this = this; + var view = this.view; + var isSelectable = view.opt('selectable'); + var dates = null; // the inclusive dates of the selection. will be null if no selection + var start; // the inclusive start of the selection + var end; // the *exclusive* end of the selection + var dayEl; + + // this listener tracks a mousedown on a day element, and a subsequent drag. + // if the drag ends on the same day, it is a 'dayClick'. + // if 'selectable' is enabled, this listener also detects selections. + var dragListener = new DragListener(this.coordMap, { + //distance: 5, // needs more work if we want dayClick to fire correctly + scroll: view.opt('dragScroll'), + dragStart: function() { + view.unselect(); // since we could be rendering a new selection, we want to clear any old one + }, + cellOver: function(cell, date) { + if (dragListener.origDate) { // click needs to have started on a cell + + dayEl = _this.getCellDayEl(cell); + + dates = [ date, dragListener.origDate ].sort(dateCompare); + start = dates[0]; + end = dates[1].clone().add(_this.cellDuration); + + if (isSelectable) { + _this.renderSelection(start, end); + } + } + }, + cellOut: function(cell, date) { + dates = null; + _this.destroySelection(); + }, + listenStop: function(ev) { + if (dates) { // started and ended on a cell? + if (dates[0].isSame(dates[1])) { + view.trigger('dayClick', dayEl[0], start, ev); + } + if (isSelectable) { + // the selection will already have been rendered. just report it + view.reportSelection(start, end, ev); + } + } + } + }); + + dragListener.mousedown(ev); // start listening, which will eventually initiate a dragStart + }, + + + /* Event Dragging + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of a event being dragged over the given date(s). + // `end` can be null, as well as `seg`. See View's documentation on renderDrag for more info. + // A returned value of `true` signals that a mock "helper" event has been rendered. + renderDrag: function(start, end, seg) { + // subclasses must implement + }, + + + // Unrenders a visual indication of an event being dragged + destroyDrag: function() { + // subclasses must implement + }, + + + /* Event Resizing + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of an event being resized. + // `start` and `end` are the updated dates of the event. `seg` is the original segment object involved in the drag. + renderResize: function(start, end, seg) { + // subclasses must implement + }, + + + // Unrenders a visual indication of an event being resized. + destroyResize: function() { + // subclasses must implement + }, + + + /* Event Helper + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a mock event over the given date(s). + // `end` can be null, in which case the mock event that is rendered will have a null end time. + // `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging. + renderRangeHelper: function(start, end, sourceSeg) { + var view = this.view; + var fakeEvent; + + // compute the end time if forced to do so (this is what EventManager does) + if (!end && view.opt('forceEventDuration')) { + end = view.calendar.getDefaultEventEnd(!start.hasTime(), start); + } + + fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible + fakeEvent.start = start; + fakeEvent.end = end; + fakeEvent.allDay = !(start.hasTime() || (end && end.hasTime())); // freshly compute allDay + + // this extra className will be useful for differentiating real events from mock events in CSS + fakeEvent.className = (fakeEvent.className || []).concat('fc-helper'); + + // if something external is being dragged in, don't render a resizer + if (!sourceSeg) { + fakeEvent.editable = false; + } + + this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering + }, + + + // Renders a mock event + renderHelper: function(event, sourceSeg) { + // subclasses must implement + }, + + + // Unrenders a mock event + destroyHelper: function() { + // subclasses must implement + }, + + + /* Selection + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses. + renderSelection: function(start, end) { + this.renderHighlight(start, end); + }, + + + // Unrenders any visual indications of a selection. Will unrender a highlight by default. + destroySelection: function() { + this.destroyHighlight(); + }, + + + /* Highlight + ------------------------------------------------------------------------------------------------------------------*/ + + + // Puts visual emphasis on a certain date range + renderHighlight: function(start, end) { + // subclasses should implement + }, + + + // Removes visual emphasis on a date range + destroyHighlight: function() { + // subclasses should implement + }, + + + + /* Generic rendering utilities for subclasses + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a day-of-week header row + headHtml: function() { + return '' + + '
' + + '' + + '' + + this.rowHtml('head') + // leverages RowRenderer + '' + + '
' + + '
'; + }, + + + // Used by the `headHtml` method, via RowRenderer, for rendering the HTML of a day-of-week header cell + headCellHtml: function(row, col, date) { + var view = this.view; + var calendar = view.calendar; + var colFormat = view.opt('columnFormat'); + + return '' + + '' + + htmlEscape(calendar.formatDate(date, colFormat)) + + ''; + }, + + + // Renders the HTML for a single-day background cell + bgCellHtml: function(row, col, date) { + var view = this.view; + var classes = this.getDayClasses(date); + + classes.unshift('fc-day', view.widgetContentClass); + + return ''; + }, + + + // Computes HTML classNames for a single-day cell + getDayClasses: function(date) { + var view = this.view; + var today = view.calendar.getNow().stripTime(); + var classes = [ 'fc-' + dayIDs[date.day()] ]; + + if ( + view.name === 'month' && + date.month() != view.intervalStart.month() + ) { + classes.push('fc-other-month'); + } + + if (date.isSame(today, 'day')) { + classes.push( + 'fc-today', + view.highlightStateClass + ); + } + else if (date < today) { + classes.push('fc-past'); + } + else { + classes.push('fc-future'); + } + + return classes; + } + +}); + +;; + +/* Event-rendering and event-interaction methods for the abstract Grid class +----------------------------------------------------------------------------------------------------------------------*/ + +$.extend(Grid.prototype, { + + mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing + isDraggingSeg: false, // is a segment being dragged? boolean + isResizingSeg: false, // is a segment being resized? boolean + + + // Renders the given events onto the grid + renderEvents: function(events) { + // subclasses must implement + }, + + + // Retrieves all rendered segment objects in this grid + getSegs: function() { + // subclasses must implement + }, + + + // Unrenders all events. Subclasses should implement, calling this super-method first. + destroyEvents: function() { + this.triggerSegMouseout(); // trigger an eventMouseout if user's mouse is over an event + }, + + + // Renders a `el` property for each seg, and only returns segments that successfully rendered + renderSegs: function(segs, disableResizing) { + var view = this.view; + var html = ''; + var renderedSegs = []; + var i; + + // build a large concatenation of event segment HTML + for (i = 0; i < segs.length; i++) { + html += this.renderSegHtml(segs[i], disableResizing); + } + + // Grab individual elements from the combined HTML string. Use each as the default rendering. + // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false. + $(html).each(function(i, node) { + var seg = segs[i]; + var el = view.resolveEventEl(seg.event, $(node)); + if (el) { + el.data('fc-seg', seg); // used by handlers + seg.el = el; + renderedSegs.push(seg); + } + }); + + return renderedSegs; + }, + + + // Generates the HTML for the default rendering of a segment + renderSegHtml: function(seg, disableResizing) { + // subclasses must implement + }, + + + // Converts an array of event objects into an array of segment objects + eventsToSegs: function(events, intervalStart, intervalEnd) { + var _this = this; + + return $.map(events, function(event) { + return _this.eventToSegs(event, intervalStart, intervalEnd); // $.map flattens all returned arrays together + }); + }, + + + // Slices a single event into an array of event segments. + // When `intervalStart` and `intervalEnd` are specified, intersect the events with that interval. + // Otherwise, let the subclass decide how it wants to slice the segments over the grid. + eventToSegs: function(event, intervalStart, intervalEnd) { + var eventStart = event.start.clone().stripZone(); // normalize + var eventEnd = this.view.calendar.getEventEnd(event).stripZone(); // compute (if necessary) and normalize + var segs; + var i, seg; + + if (intervalStart && intervalEnd) { + seg = intersectionToSeg(eventStart, eventEnd, intervalStart, intervalEnd); + segs = seg ? [ seg ] : []; + } + else { + segs = this.rangeToSegs(eventStart, eventEnd); // defined by the subclass + } + + // assign extra event-related properties to the segment objects + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + seg.event = event; + seg.eventStartMS = +eventStart; + seg.eventDurationMS = eventEnd - eventStart; + } + + return segs; + }, + + + /* Handlers + ------------------------------------------------------------------------------------------------------------------*/ + + + // Attaches event-element-related handlers to the container element and leverage bubbling + bindSegHandlers: function() { + var _this = this; + var view = this.view; + + $.each( + { + mouseenter: function(seg, ev) { + _this.triggerSegMouseover(seg, ev); + }, + mouseleave: function(seg, ev) { + _this.triggerSegMouseout(seg, ev); + }, + click: function(seg, ev) { + return view.trigger('eventClick', this, seg.event, ev); // can return `false` to cancel + }, + mousedown: function(seg, ev) { + if ($(ev.target).is('.fc-resizer') && view.isEventResizable(seg.event)) { + _this.segResizeMousedown(seg, ev); + } + else if (view.isEventDraggable(seg.event)) { + _this.segDragMousedown(seg, ev); + } + } + }, + function(name, func) { + // attach the handler to the container element and only listen for real event elements via bubbling + _this.el.on(name, '.fc-event-container > *', function(ev) { + var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents + + // only call the handlers if there is not a drag/resize in progress + if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) { + return func.call(this, seg, ev); // `this` will be the event element + } + }); + } + ); + }, + + + // Updates internal state and triggers handlers for when an event element is moused over + triggerSegMouseover: function(seg, ev) { + if (!this.mousedOverSeg) { + this.mousedOverSeg = seg; + this.view.trigger('eventMouseover', seg.el[0], seg.event, ev); + } + }, + + + // Updates internal state and triggers handlers for when an event element is moused out. + // Can be given no arguments, in which case it will mouseout the segment that was previously moused over. + triggerSegMouseout: function(seg, ev) { + ev = ev || {}; // if given no args, make a mock mouse event + + if (this.mousedOverSeg) { + seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment + this.mousedOverSeg = null; + this.view.trigger('eventMouseout', seg.el[0], seg.event, ev); + } + }, + + + /* Dragging + ------------------------------------------------------------------------------------------------------------------*/ + + + // Called when the user does a mousedown on an event, which might lead to dragging. + // Generic enough to work with any type of Grid. + segDragMousedown: function(seg, ev) { + var _this = this; + var view = this.view; + var el = seg.el; + var event = seg.event; + var newStart, newEnd; + + // A clone of the original element that will move with the mouse + var mouseFollower = new MouseFollower(seg.el, { + parentEl: view.el, + opacity: view.opt('dragOpacity'), + revertDuration: view.opt('dragRevertDuration'), + zIndex: 2 // one above the .fc-view + }); + + // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents + // of the view. + var dragListener = new DragListener(view.coordMap, { + distance: 5, + scroll: view.opt('dragScroll'), + listenStart: function(ev) { + mouseFollower.hide(); // don't show until we know this is a real drag + mouseFollower.start(ev); + }, + dragStart: function(ev) { + _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported + _this.isDraggingSeg = true; + view.hideEvent(event); // hide all event segments. our mouseFollower will take over + view.trigger('eventDragStart', el[0], event, ev, {}); // last argument is jqui dummy + }, + cellOver: function(cell, date) { + var origDate = seg.cellDate || dragListener.origDate; + var res = _this.computeDraggedEventDates(seg, origDate, date); + newStart = res.start; + newEnd = res.end; + + if (view.renderDrag(newStart, newEnd, seg)) { // have the view render a visual indication + mouseFollower.hide(); // if the view is already using a mock event "helper", hide our own + } + else { + mouseFollower.show(); + } + }, + cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells + newStart = null; + view.destroyDrag(); // unrender whatever was done in view.renderDrag + mouseFollower.show(); // show in case we are moving out of all cells + }, + dragStop: function(ev) { + var hasChanged = newStart && !newStart.isSame(event.start); + + // do revert animation if hasn't changed. calls a callback when finished (whether animation or not) + mouseFollower.stop(!hasChanged, function() { + _this.isDraggingSeg = false; + view.destroyDrag(); + view.showEvent(event); + view.trigger('eventDragStop', el[0], event, ev, {}); // last argument is jqui dummy + + if (hasChanged) { + view.eventDrop(el[0], event, newStart, ev); // will rerender all events... + } + }); + }, + listenStop: function() { + mouseFollower.stop(); // put in listenStop in case there was a mousedown but the drag never started + } + }); + + dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart + }, + + + // Given a segment, the dates where a drag began and ended, calculates the Event Object's new start and end dates + computeDraggedEventDates: function(seg, dragStartDate, dropDate) { + var view = this.view; + var event = seg.event; + var start = event.start; + var end = view.calendar.getEventEnd(event); + var delta; + var newStart; + var newEnd; + + if (dropDate.hasTime() === dragStartDate.hasTime()) { + delta = dayishDiff(dropDate, dragStartDate); + newStart = start.clone().add(delta); + if (event.end === null) { // do we need to compute an end? + newEnd = null; + } + else { + newEnd = end.clone().add(delta); + } + } + else { + // if switching from day <-> timed, start should be reset to the dropped date, and the end cleared + newStart = dropDate; + newEnd = null; // end should be cleared + } + + return { start: newStart, end: newEnd }; + }, + + + /* Resizing + ------------------------------------------------------------------------------------------------------------------*/ + + + // Called when the user does a mousedown on an event's resizer, which might lead to resizing. + // Generic enough to work with any type of Grid. + segResizeMousedown: function(seg, ev) { + var _this = this; + var view = this.view; + var el = seg.el; + var event = seg.event; + var start = event.start; + var end = view.calendar.getEventEnd(event); + var newEnd = null; + var dragListener; + + function destroy() { // resets the rendering + _this.destroyResize(); + view.showEvent(event); + } + + // Tracks mouse movement over the *grid's* coordinate map + dragListener = new DragListener(this.coordMap, { + distance: 5, + scroll: view.opt('dragScroll'), + dragStart: function(ev) { + _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported + _this.isResizingSeg = true; + view.trigger('eventResizeStart', el[0], event, ev, {}); // last argument is jqui dummy + }, + cellOver: function(cell, date) { + // compute the new end. don't allow it to go before the event's start + if (date.isBefore(start)) { // allows comparing ambig to non-ambig + date = start; + } + newEnd = date.clone().add(_this.cellDuration); // make it an exclusive end + + if (newEnd.isSame(end)) { + newEnd = null; + destroy(); + } + else { + _this.renderResize(start, newEnd, seg); + view.hideEvent(event); + } + }, + cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells + newEnd = null; + destroy(); + }, + dragStop: function(ev) { + _this.isResizingSeg = false; + destroy(); + view.trigger('eventResizeStop', el[0], event, ev, {}); // last argument is jqui dummy + + if (newEnd) { + view.eventResize(el[0], event, newEnd, ev); // will rerender all events... + } + } + }); + + dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart + }, + + + /* Rendering Utils + ------------------------------------------------------------------------------------------------------------------*/ + + + // Generic utility for generating the HTML classNames for an event segment's element + getSegClasses: function(seg, isDraggable, isResizable) { + var event = seg.event; + var classes = [ + 'fc-event', + seg.isStart ? 'fc-start' : 'fc-not-start', + seg.isEnd ? 'fc-end' : 'fc-not-end' + ].concat( + event.className, + event.source ? event.source.className : [] + ); + + if (isDraggable) { + classes.push('fc-draggable'); + } + if (isResizable) { + classes.push('fc-resizable'); + } + + return classes; + }, + + + // Utility for generating a CSS string with all the event skin-related properties + getEventSkinCss: function(event) { + var view = this.view; + var source = event.source || {}; + var eventColor = event.color; + var sourceColor = source.color; + var optionColor = view.opt('eventColor'); + var backgroundColor = + event.backgroundColor || + eventColor || + source.backgroundColor || + sourceColor || + view.opt('eventBackgroundColor') || + optionColor; + var borderColor = + event.borderColor || + eventColor || + source.borderColor || + sourceColor || + view.opt('eventBorderColor') || + optionColor; + var textColor = + event.textColor || + source.textColor || + view.opt('eventTextColor'); + var statements = []; + if (backgroundColor) { + statements.push('background-color:' + backgroundColor); + } + if (borderColor) { + statements.push('border-color:' + borderColor); + } + if (textColor) { + statements.push('color:' + textColor); + } + return statements.join(';'); + } + +}); + + +/* Event Segment Utilities +----------------------------------------------------------------------------------------------------------------------*/ + + +// A cmp function for determining which segments should take visual priority +function compareSegs(seg1, seg2) { + return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first + seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first + seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1) + (seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title +} + + +;; + +/* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week. +----------------------------------------------------------------------------------------------------------------------*/ + +function DayGrid(view) { + Grid.call(this, view); // call the super-constructor +} + + +DayGrid.prototype = createObject(Grid.prototype); // declare the super-class +$.extend(DayGrid.prototype, { + + numbersVisible: false, // should render a row for day/week numbers? manually set by the view + cellDuration: moment.duration({ days: 1 }), // required for Grid.event.js. Each cell is always a single day + bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid + + rowEls: null, // set of fake row elements + dayEls: null, // set of whole-day elements comprising the row's background + helperEls: null, // set of cell skeleton elements for rendering the mock event "helper" + highlightEls: null, // set of cell skeleton elements for rendering the highlight + + + // Renders the rows and columns into the component's `this.el`, which should already be assigned. + // isRigid determins whether the individual rows should ignore the contents and be a constant height. + // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient. + render: function(isRigid) { + var view = this.view; + var html = ''; + var row; + + for (row = 0; row < view.rowCnt; row++) { + html += this.dayRowHtml(row, isRigid); + } + this.el.html(html); + + this.rowEls = this.el.find('.fc-row'); + this.dayEls = this.el.find('.fc-day'); + + // run all the day cells through the dayRender callback + this.dayEls.each(function(i, node) { + var date = view.cellToDate(Math.floor(i / view.colCnt), i % view.colCnt); + view.trigger('dayRender', null, date, $(node)); + }); + + Grid.prototype.render.call(this); // call the super-method + }, + + + destroy: function() { + this.destroySegPopover(); + }, + + + // Generates the HTML for a single row. `row` is the row number. + dayRowHtml: function(row, isRigid) { + var view = this.view; + var classes = [ 'fc-row', 'fc-week', view.widgetContentClass ]; + + if (isRigid) { + classes.push('fc-rigid'); + } + + return '' + + '
' + + '
' + + '' + + this.rowHtml('day', row) + // leverages RowRenderer. calls dayCellHtml() + '
' + + '
' + + '
' + + '' + + (this.numbersVisible ? + '' + + this.rowHtml('number', row) + // leverages RowRenderer. View will define render method + '' : + '' + ) + + '
' + + '
' + + '
'; + }, + + + // Renders the HTML for a whole-day cell. Will eventually end up in the day-row's background. + // We go through a 'day' row type instead of just doing a 'bg' row type so that the View can do custom rendering + // specifically for whole-day rows, whereas a 'bg' might also be used for other purposes (TimeGrid bg for example). + dayCellHtml: function(row, col, date) { + return this.bgCellHtml(row, col, date); + }, + + + /* Coordinates & Cells + ------------------------------------------------------------------------------------------------------------------*/ + + + // Populates the empty `rows` and `cols` arrays with coordinates of the cells. For CoordGrid. + buildCoords: function(rows, cols) { + var colCnt = this.view.colCnt; + var e, n, p; + + this.dayEls.slice(0, colCnt).each(function(i, _e) { // iterate the first row of day elements + e = $(_e); + n = e.offset().left; + if (i) { + p[1] = n; + } + p = [ n ]; + cols[i] = p; + }); + p[1] = n + e.outerWidth(); + + this.rowEls.each(function(i, _e) { + e = $(_e); + n = e.offset().top; + if (i) { + p[1] = n; + } + p = [ n ]; + rows[i] = p; + }); + p[1] = n + e.outerHeight() + this.bottomCoordPadding; // hack to extend hit area of last row + }, + + + // Converts a cell to a date + getCellDate: function(cell) { + return this.view.cellToDate(cell); // leverages the View's cell system + }, + + + // Gets the whole-day element associated with the cell + getCellDayEl: function(cell) { + return this.dayEls.eq(cell.row * this.view.colCnt + cell.col); + }, + + + // Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects + rangeToSegs: function(start, end) { + return this.view.rangeToSegments(start, end); // leverages the View's cell system + }, + + + /* Event Drag Visualization + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of an event hovering over the given date(s). + // `end` can be null, as well as `seg`. See View's documentation on renderDrag for more info. + // A returned value of `true` signals that a mock "helper" event has been rendered. + renderDrag: function(start, end, seg) { + var opacity; + + // always render a highlight underneath + this.renderHighlight( + start, + end || this.view.calendar.getDefaultEventEnd(true, start) + ); + + // if a segment from the same calendar but another component is being dragged, render a helper event + if (seg && !seg.el.closest(this.el).length) { + + this.renderRangeHelper(start, end, seg); + + opacity = this.view.opt('dragOpacity'); + if (opacity !== undefined) { + this.helperEls.css('opacity', opacity); + } + + return true; // a helper has been rendered + } + }, + + + // Unrenders any visual indication of a hovering event + destroyDrag: function() { + this.destroyHighlight(); + this.destroyHelper(); + }, + + + /* Event Resize Visualization + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of an event being resized + renderResize: function(start, end, seg) { + this.renderHighlight(start, end); + this.renderRangeHelper(start, end, seg); + }, + + + // Unrenders a visual indication of an event being resized + destroyResize: function() { + this.destroyHighlight(); + this.destroyHelper(); + }, + + + /* Event Helper + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null. + renderHelper: function(event, sourceSeg) { + var helperNodes = []; + var rowStructs = this.renderEventRows([ event ]); + + // inject each new event skeleton into each associated row + this.rowEls.each(function(row, rowNode) { + var rowEl = $(rowNode); // the .fc-row + var skeletonEl = $('
'); // will be absolutely positioned + var skeletonTop; + + // If there is an original segment, match the top position. Otherwise, put it at the row's top level + if (sourceSeg && sourceSeg.row === row) { + skeletonTop = sourceSeg.el.position().top; + } + else { + skeletonTop = rowEl.find('.fc-content-skeleton tbody').position().top; + } + + skeletonEl.css('top', skeletonTop) + .find('table') + .append(rowStructs[row].tbodyEl); + + rowEl.append(skeletonEl); + helperNodes.push(skeletonEl[0]); + }); + + this.helperEls = $(helperNodes); // array -> jQuery set + }, + + + // Unrenders any visual indication of a mock helper event + destroyHelper: function() { + if (this.helperEls) { + this.helperEls.remove(); + this.helperEls = null; + } + }, + + + /* Highlighting + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders an emphasis on the given date range. `start` is an inclusive, `end` is exclusive. + renderHighlight: function(start, end) { + var segs = this.rangeToSegs(start, end); + var highlightNodes = []; + var i, seg; + var el; + + // build an event skeleton for each row that needs it + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + el = $( + this.highlightSkeletonHtml(seg.leftCol, seg.rightCol + 1) // make end exclusive + ); + el.appendTo(this.rowEls[seg.row]); + highlightNodes.push(el[0]); + } + + this.highlightEls = $(highlightNodes); // array -> jQuery set + }, + + + // Unrenders any visual emphasis on a date range + destroyHighlight: function() { + if (this.highlightEls) { + this.highlightEls.remove(); + this.highlightEls = null; + } + }, + + + // Generates the HTML used to build a single-row "highlight skeleton", a table that frames highlight cells + highlightSkeletonHtml: function(startCol, endCol) { + var colCnt = this.view.colCnt; + var cellHtml = ''; + + if (startCol > 0) { + cellHtml += ' elements that should be appended to each row's content skeleton. + // Returns an array of rowStruct objects (see the bottom of `renderEventRow`). + renderEventRows: function(events) { + var segs = this.eventsToSegs(events); + var rowStructs = []; + var segRows; + var row; + + segs = this.renderSegs(segs); // returns a new array with only visible segments + segRows = this.groupSegRows(segs); // group into nested arrays + + // iterate each row of segment groupings + for (row = 0; row < segRows.length; row++) { + rowStructs.push( + this.renderEventRow(row, segRows[row]) + ); + } + + return rowStructs; + }, + + + // Builds the HTML to be used for the default element for an individual segment + renderSegHtml: function(seg, disableResizing) { + var view = this.view; + var isRTL = view.opt('isRTL'); + var event = seg.event; + var isDraggable = view.isEventDraggable(event); + var isResizable = !disableResizing && event.allDay && seg.isEnd && view.isEventResizable(event); + var classes = this.getSegClasses(seg, isDraggable, isResizable); + var skinCss = this.getEventSkinCss(event); + var timeHtml = ''; + var titleHtml; + + classes.unshift('fc-day-grid-event'); + + // Only display a timed events time if it is the starting segment + if (!event.allDay && seg.isStart) { + timeHtml = '' + htmlEscape(view.getEventTimeText(event)) + ''; + } + + titleHtml = + '' + + (htmlEscape(event.title || '') || ' ') + // we always want one line of height + ''; + + return '' + + '
' + + (isRTL ? + titleHtml + ' ' + timeHtml : // put a natural space in between + timeHtml + ' ' + titleHtml // + ) + + '
' + + (isResizable ? + '
' : + '' + ) + + ''; + }, + + + // Given a row # and an array of segments all in the same row, render a
element, a skeleton that contains + // the segments. Returns object with a bunch of internal data about how the render was calculated. + renderEventRow: function(row, rowSegs) { + var view = this.view; + var colCnt = view.colCnt; + var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels + var levelCnt = Math.max(1, segLevels.length); // ensure at least one level + var tbody = $(''); + var segMatrix = []; // lookup for which segments are rendered into which level+col cells + var cellMatrix = []; // lookup for all '); + + segMatrix.push([]); + cellMatrix.push([]); + loneCellMatrix.push([]); + + // levelCnt might be 1 even though there are no actual levels. protect against this. + // this single empty row is useful for styling. + if (levelSegs) { + for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level + seg = levelSegs[j]; + + emptyCellsUntil(seg.leftCol); + + // create a container that occupies or more columns. append the event element. + td = $(' at a time and stop when we find one out of bounds + for (i = 0; i < trEls.length; i++) { + trEl = trEls.eq(i).removeClass('fc-limited'); // get and reveal + if (trEl.position().top + trEl.outerHeight() > rowHeight) { + return i; + } + } + + return false; // should not limit at all + }, + + + // Limits the given grid row to the maximum number of levels and injects "more" links if necessary. + // `row` is the row number. + // `levelLimit` is a number for the maximum (inclusive) number of levels allowed. + limitRow: function(row, levelLimit) { + var _this = this; + var view = this.view; + var rowStruct = this.rowStructs[row]; + var moreNodes = []; // array of "more" links and and segment elements past the limit + .addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array + + // iterate though segments in the last allowable level + for (i = 0; i < levelSegs.length; i++) { + seg = levelSegs[i]; + emptyCellsUntil(seg.leftCol); // process empty cells before the segment + + // determine *all* segments below `seg` that occupy the same columns + colSegsBelow = []; + totalSegsBelow = 0; + while (col <= seg.rightCol) { + cell = { row: row, col: col }; + segsBelow = this.getCellSegs(cell, levelLimit); + colSegsBelow.push(segsBelow); + totalSegsBelow += segsBelow.length; + col++; + } + + if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links? + td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell + rowspan = td.attr('rowspan') || 1; + segMoreNodes = []; + + // make a replacement '; + + html += + '' + + (!isRTL ? axisHtml : '') + + '"; + + slotTime.add(this.slotDuration); + } + + return html; + }, + + + // Parses various options into properties of this object + processOptions: function() { + var view = this.view; + var slotDuration = view.opt('slotDuration'); + var snapDuration = view.opt('snapDuration'); + + slotDuration = moment.duration(slotDuration); + snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration; + + this.slotDuration = slotDuration; + this.snapDuration = snapDuration; + this.cellDuration = snapDuration; // important to assign this for Grid.events.js + + this.minTime = moment.duration(view.opt('minTime')); + this.maxTime = moment.duration(view.opt('maxTime')); + }, + + + // Slices up a date range into a segment for each column + rangeToSegs: function(rangeStart, rangeEnd) { + var view = this.view; + var segs = []; + var seg; + var col; + var cellDate; + var colStart, colEnd; + + // normalize + rangeStart = rangeStart.clone().stripZone(); + rangeEnd = rangeEnd.clone().stripZone(); + + for (col = 0; col < view.colCnt; col++) { + cellDate = view.cellToDate(0, col); // use the View's cell system for this + colStart = cellDate.clone().time(this.minTime); + colEnd = cellDate.clone().time(this.maxTime); + seg = intersectionToSeg(rangeStart, rangeEnd, colStart, colEnd); + if (seg) { + seg.col = col; + segs.push(seg); + } + } + + return segs; + }, + + + /* Coordinates + ------------------------------------------------------------------------------------------------------------------*/ + + + // Called when there is a window resize/zoom and we need to recalculate coordinates for the grid + resize: function() { + this.computeSlatTops(); + this.updateSegVerticals(); + }, + + + // Populates the given empty `rows` and `cols` arrays with offset positions of the "snap" cells. + // "Snap" cells are different the slots because they might have finer granularity. + buildCoords: function(rows, cols) { + var colCnt = this.view.colCnt; + var originTop = this.el.offset().top; + var snapTime = moment.duration(+this.minTime); + var p = null; + var e, n; + + this.dayEls.slice(0, colCnt).each(function(i, _e) { + e = $(_e); + n = e.offset().left; + if (p) { + p[1] = n; + } + p = [ n ]; + cols[i] = p; + }); + p[1] = n + e.outerWidth(); + + p = null; + while (snapTime < this.maxTime) { + n = originTop + this.computeTimeTop(snapTime); + if (p) { + p[1] = n; + } + p = [ n ]; + rows.push(p); + snapTime.add(this.snapDuration); + } + p[1] = originTop + this.computeTimeTop(snapTime); // the position of the exclusive end + }, + + + // Gets the datetime for the given slot cell + getCellDate: function(cell) { + var view = this.view; + var calendar = view.calendar; + + return calendar.rezoneDate( // since we are adding a time, it needs to be in the calendar's timezone + view.cellToDate(0, cell.col) // View's coord system only accounts for start-of-day for column + .time(this.minTime + this.snapDuration * cell.row) + ); + }, + + + // Gets the element that represents the whole-day the cell resides on + getCellDayEl: function(cell) { + return this.dayEls.eq(cell.col); + }, + + + // Computes the top coordinate, relative to the bounds of the grid, of the given date. + // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight. + computeDateTop: function(date, startOfDayDate) { + return this.computeTimeTop( + moment.duration( + date.clone().stripZone() - startOfDayDate.clone().stripTime() + ) + ); + }, + + + // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration). + computeTimeTop: function(time) { + var slatCoverage = (time - this.minTime) / this.slotDuration; // floating-point value of # of slots covered + var slatIndex; + var slatRemainder; + var slatTop; + var slatBottom; + + // constrain. because minTime/maxTime might be customized + slatCoverage = Math.max(0, slatCoverage); + slatCoverage = Math.min(this.slatEls.length, slatCoverage); + + slatIndex = Math.floor(slatCoverage); // an integer index of the furthest whole slot + slatRemainder = slatCoverage - slatIndex; + slatTop = this.slatTops[slatIndex]; // the top position of the furthest whole slot + + if (slatRemainder) { // time spans part-way into the slot + slatBottom = this.slatTops[slatIndex + 1]; + return slatTop + (slatBottom - slatTop) * slatRemainder; // part-way between slots + } + else { + return slatTop; + } + }, + + + // Queries each `slatEl` for its position relative to the grid's container and stores it in `slatTops`. + // Includes the the bottom of the last slat as the last item in the array. + computeSlatTops: function() { + var tops = []; + var top; + + this.slatEls.each(function(i, node) { + top = $(node).position().top; + tops.push(top); + }); + + tops.push(top + this.slatEls.last().outerHeight()); // bottom of the last slat + + this.slatTops = tops; + }, + + + /* Event Drag Visualization + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of an event being dragged over the specified date(s). + // `end` and `seg` can be null. See View's documentation on renderDrag for more info. + renderDrag: function(start, end, seg) { + var opacity; + + if (seg) { // if there is event information for this drag, render a helper event + this.renderRangeHelper(start, end, seg); + + opacity = this.view.opt('dragOpacity'); + if (opacity !== undefined) { + this.helperEl.css('opacity', opacity); + } + + return true; // signal that a helper has been rendered + } + else { + // otherwise, just render a highlight + this.renderHighlight( + start, + end || this.view.calendar.getDefaultEventEnd(false, start) + ); + } + }, + + + // Unrenders any visual indication of an event being dragged + destroyDrag: function() { + this.destroyHelper(); + this.destroyHighlight(); + }, + + + /* Event Resize Visualization + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of an event being resized + renderResize: function(start, end, seg) { + this.renderRangeHelper(start, end, seg); + }, + + + // Unrenders any visual indication of an event being resized + destroyResize: function() { + this.destroyHelper(); + }, + + + /* Event Helper + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag) + renderHelper: function(event, sourceSeg) { + var res = this.renderEventTable([ event ]); + var tableEl = res.tableEl; + var segs = res.segs; + var i, seg; + var sourceEl; + + // Try to make the segment that is in the same row as sourceSeg look the same + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + if (sourceSeg && sourceSeg.col === seg.col) { + sourceEl = sourceSeg.el; + seg.el.css({ + left: sourceEl.css('left'), + right: sourceEl.css('right'), + 'margin-left': sourceEl.css('margin-left'), + 'margin-right': sourceEl.css('margin-right') + }); + } + } + + this.helperEl = $('
') + .append(tableEl) + .appendTo(this.el); + }, + + + // Unrenders any mock helper event + destroyHelper: function() { + if (this.helperEl) { + this.helperEl.remove(); + this.helperEl = null; + } + }, + + + /* Selection + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight. + renderSelection: function(start, end) { + if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered + this.renderRangeHelper(start, end); + } + else { + this.renderHighlight(start, end); + } + }, + + + // Unrenders any visual indication of a selection + destroySelection: function() { + this.destroyHelper(); + this.destroyHighlight(); + }, + + + /* Highlight + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders an emphasis on the given date range. `start` is inclusive. `end` is exclusive. + renderHighlight: function(start, end) { + this.highlightEl = $( + this.highlightSkeletonHtml(start, end) + ).appendTo(this.el); + }, + + + // Unrenders the emphasis on a date range + destroyHighlight: function() { + if (this.highlightEl) { + this.highlightEl.remove(); + this.highlightEl = null; + } + }, + + + // Generates HTML for a table element with containers in each column, responsible for absolutely positioning the + // highlight elements to cover the highlighted slots. + highlightSkeletonHtml: function(start, end) { + var view = this.view; + var segs = this.rangeToSegs(start, end); + var cellHtml = ''; + var col = 0; + var i, seg; + var dayDate; + var top, bottom; + + for (i = 0; i < segs.length; i++) { // loop through the segments. one per column + seg = segs[i]; + + // need empty cells beforehand? + if (col < seg.col) { + cellHtml += '
'; + + col++; + } + + // need empty cells after the last segment? + if (col < view.colCnt) { + cellHtml += ''; + } + }, + + + // Generates the HTML that will go before content-skeleton cells that display the day/week numbers. + // Queried by the DayGrid subcomponent. Ordering depends on isRTL. + numberIntroHtml: function(row) { + if (this.weekNumbersVisible) { + return '' + + ''; + } + }, + + + // Generates the HTML that goes before the day bg cells for each day-row. + // Queried by the DayGrid subcomponent. Ordering depends on isRTL. + dayIntroHtml: function() { + if (this.weekNumbersVisible) { + return ''; + } + }, + + + // Generates the HTML that goes before every other type of row generated by DayGrid. Ordering depends on isRTL. + // Affects helper-skeleton and highlight-skeleton rows. + introHtml: function() { + if (this.weekNumbersVisible) { + return ''; + } + }, + + + // Generates the HTML for the '; + }, + + + // Generates an HTML attribute string for setting the width of the week number column, if it is known + weekNumberStyleAttr: function() { + if (this.weekNumberWidth !== null) { + return 'style="width:' + this.weekNumberWidth + 'px"'; + } + return ''; + }, + + + // Determines whether each row should have a constant height + hasRigidRows: function() { + var eventLimit = this.opt('eventLimit'); + return eventLimit && typeof eventLimit !== 'number'; + }, + + + /* Dimensions + ------------------------------------------------------------------------------------------------------------------*/ + + + // Refreshes the horizontal dimensions of the view + updateWidth: function() { + if (this.weekNumbersVisible) { + // Make sure all week number cells running down the side have the same width. + // Record the width for cells created later. + this.weekNumberWidth = matchCellWidths( + this.el.find('.fc-week-number') + ); + } + }, + + + // Adjusts the vertical dimensions of the view to the specified values + setHeight: function(totalHeight, isAuto) { + var eventLimit = this.opt('eventLimit'); + var scrollerHeight; + + // reset all heights to be natural + unsetScroller(this.scrollerEl); + uncompensateScroll(this.headRowEl); + + this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed + + // is the event limit a constant level number? + if (eventLimit && typeof eventLimit === 'number') { + this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after + } + + scrollerHeight = this.computeScrollerHeight(totalHeight); + this.setGridHeight(scrollerHeight, isAuto); + + // is the event limit dynamically calculated? + if (eventLimit && typeof eventLimit !== 'number') { + this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set + } + + if (!isAuto && setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars? + + compensateScroll(this.headRowEl, getScrollbarWidths(this.scrollerEl)); + + // doing the scrollbar compensation might have created text overflow which created more height. redo + scrollerHeight = this.computeScrollerHeight(totalHeight); + this.scrollerEl.height(scrollerHeight); + + this.restoreScroll(); + } + }, + + + // Sets the height of just the DayGrid component in this view + setGridHeight: function(height, isAuto) { + if (isAuto) { + undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding + } + else { + distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows + } + }, + + + /* Events + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders the given events onto the view and populates the segments array + renderEvents: function(events) { + this.dayGrid.renderEvents(events); + + this.updateHeight(); // must compensate for events that overflow the row + + View.prototype.renderEvents.call(this, events); // call the super-method + }, + + + // Retrieves all segment objects that are rendered in the view + getSegs: function() { + return this.dayGrid.getSegs(); + }, + + + // Unrenders all event elements and clears internal segment data + destroyEvents: function() { + View.prototype.destroyEvents.call(this); // do this before dayGrid's segs have been cleared + + this.recordScroll(); // removing events will reduce height and mess with the scroll, so record beforehand + this.dayGrid.destroyEvents(); + + // we DON'T need to call updateHeight() because: + // A) a renderEvents() call always happens after this, which will eventually call updateHeight() + // B) in IE8, this causes a flash whenever events are rerendered + }, + + + /* Event Dragging + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of an event being dragged over the view. + // A returned value of `true` signals that a mock "helper" event has been rendered. + renderDrag: function(start, end, seg) { + return this.dayGrid.renderDrag(start, end, seg); + }, + + + // Unrenders the visual indication of an event being dragged over the view + destroyDrag: function() { + this.dayGrid.destroyDrag(); + }, + + + /* Selection + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of a selection + renderSelection: function(start, end) { + this.dayGrid.renderSelection(start, end); + }, + + + // Unrenders a visual indications of a selection + destroySelection: function() { + this.dayGrid.destroySelection(); + } + +}); + +;; + +/* A month view with day cells running in rows (one-per-week) and columns +----------------------------------------------------------------------------------------------------------------------*/ + +setDefaults({ + fixedWeekCount: true +}); + +fcViews.month = MonthView; // register the view + +function MonthView(calendar) { + BasicView.call(this, calendar); // call the super-constructor +} + + +MonthView.prototype = createObject(BasicView.prototype); // define the super-class +$.extend(MonthView.prototype, { + + name: 'month', + + + incrementDate: function(date, delta) { + return date.clone().stripTime().add(delta, 'months').startOf('month'); + }, + + + render: function(date) { + var rowCnt; + + this.intervalStart = date.clone().stripTime().startOf('month'); + this.intervalEnd = this.intervalStart.clone().add(1, 'months'); + + this.start = this.intervalStart.clone(); + this.start = this.skipHiddenDays(this.start); // move past the first week if no visible days + this.start.startOf('week'); + this.start = this.skipHiddenDays(this.start); // move past the first invisible days of the week + + this.end = this.intervalEnd.clone(); + this.end = this.skipHiddenDays(this.end, -1, true); // move in from the last week if no visible days + this.end.add((7 - this.end.weekday()) % 7, 'days'); // move to end of week if not already + this.end = this.skipHiddenDays(this.end, -1, true); // move in from the last invisible days of the week + + rowCnt = Math.ceil( // need to ceil in case there are hidden days + this.end.diff(this.start, 'weeks', true) // returnfloat=true + ); + if (this.isFixedWeeks()) { + this.end.add(6 - rowCnt, 'weeks'); + rowCnt = 6; + } + + this.title = this.calendar.formatDate(this.intervalStart, this.opt('titleFormat')); + + BasicView.prototype.render.call(this, rowCnt, this.getCellsPerWeek(), true); // call the super-method + }, + + + // Overrides the default BasicView behavior to have special multi-week auto-height logic + setGridHeight: function(height, isAuto) { + + isAuto = isAuto || this.opt('weekMode') === 'variable'; // LEGACY: weekMode is deprecated + + // if auto, make the height of each row the height that it would be if there were 6 weeks + if (isAuto) { + height *= this.rowCnt / 6; + } + + distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows + }, + + + isFixedWeeks: function() { + var weekMode = this.opt('weekMode'); // LEGACY: weekMode is deprecated + if (weekMode) { + return weekMode === 'fixed'; // if any other type of weekMode, assume NOT fixed + } + + return this.opt('fixedWeekCount'); + } + +}); + +;; + +/* A week view with simple day cells running horizontally +----------------------------------------------------------------------------------------------------------------------*/ +// TODO: a WeekView mixin for calculating dates and titles + +fcViews.basicWeek = BasicWeekView; // register this view + +function BasicWeekView(calendar) { + BasicView.call(this, calendar); // call the super-constructor +} + + +BasicWeekView.prototype = createObject(BasicView.prototype); // define the super-class +$.extend(BasicWeekView.prototype, { + + name: 'basicWeek', + + + incrementDate: function(date, delta) { + return date.clone().stripTime().add(delta, 'weeks').startOf('week'); + }, + + + render: function(date) { + + this.intervalStart = date.clone().stripTime().startOf('week'); + this.intervalEnd = this.intervalStart.clone().add(1, 'weeks'); + + this.start = this.skipHiddenDays(this.intervalStart); + this.end = this.skipHiddenDays(this.intervalEnd, -1, true); + + this.title = this.calendar.formatRange( + this.start, + this.end.clone().subtract(1), // make inclusive by subtracting 1 ms + this.opt('titleFormat'), + ' \u2014 ' // emphasized dash + ); + + BasicView.prototype.render.call(this, 1, this.getCellsPerWeek(), false); // call the super-method + } + +}); +;; + +/* A view with a single simple day cell +----------------------------------------------------------------------------------------------------------------------*/ + +fcViews.basicDay = BasicDayView; // register this view + +function BasicDayView(calendar) { + BasicView.call(this, calendar); // call the super-constructor +} + + +BasicDayView.prototype = createObject(BasicView.prototype); // define the super-class +$.extend(BasicDayView.prototype, { + + name: 'basicDay', + + + incrementDate: function(date, delta) { + var out = date.clone().stripTime().add(delta, 'days'); + out = this.skipHiddenDays(out, delta < 0 ? -1 : 1); + return out; + }, + + + render: function(date) { + + this.start = this.intervalStart = date.clone().stripTime(); + this.end = this.intervalEnd = this.start.clone().add(1, 'days'); + + this.title = this.calendar.formatDate(this.start, this.opt('titleFormat')); + + BasicView.prototype.render.call(this, 1, 1, false); // call the super-method + } + +}); +;; + +/* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically. +----------------------------------------------------------------------------------------------------------------------*/ +// Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on). +// Responsible for managing width/height. + +setDefaults({ + allDaySlot: true, + allDayText: 'all-day', + + scrollTime: '06:00:00', + + slotDuration: '00:30:00', + + axisFormat: generateAgendaAxisFormat, + timeFormat: { + agenda: generateAgendaTimeFormat + }, + + minTime: '00:00:00', + maxTime: '24:00:00', + slotEventOverlap: true +}); + +var AGENDA_ALL_DAY_EVENT_LIMIT = 5; + + +function generateAgendaAxisFormat(options, langData) { + return langData.longDateFormat('LT') + .replace(':mm', '(:mm)') + .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs + .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand +} + + +function generateAgendaTimeFormat(options, langData) { + return langData.longDateFormat('LT') + .replace(/\s*a$/i, ''); // remove trailing AM/PM +} + + +function AgendaView(calendar) { + View.call(this, calendar); // call the super-constructor + + this.timeGrid = new TimeGrid(this); + + if (this.opt('allDaySlot')) { // should we display the "all-day" area? + this.dayGrid = new DayGrid(this); // the all-day subcomponent of this view + + // the coordinate grid will be a combination of both subcomponents' grids + this.coordMap = new ComboCoordMap([ + this.dayGrid.coordMap, + this.timeGrid.coordMap + ]); + } + else { + this.coordMap = this.timeGrid.coordMap; + } +} + + +AgendaView.prototype = createObject(View.prototype); // define the super-class +$.extend(AgendaView.prototype, { + + timeGrid: null, // the main time-grid subcomponent of this view + dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null + + axisWidth: null, // the width of the time axis running down the side + + noScrollRowEls: null, // set of fake row elements that must compensate when scrollerEl has scrollbars + + // when the time-grid isn't tall enough to occupy the given height, we render an
underneath + bottomRuleEl: null, + bottomRuleHeight: null, + + + /* Rendering + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders the view into `this.el`, which has already been assigned. + // `colCnt` has been calculated by a subclass and passed here. + render: function(colCnt) { + + // needed for cell-to-date and date-to-cell calculations in View + this.rowCnt = 1; + this.colCnt = colCnt; + + this.el.addClass('fc-agenda-view').html(this.renderHtml()); + + // the element that wraps the time-grid that will probably scroll + this.scrollerEl = this.el.find('.fc-time-grid-container'); + this.timeGrid.coordMap.containerEl = this.scrollerEl; // don't accept clicks/etc outside of this + + this.timeGrid.el = this.el.find('.fc-time-grid'); + this.timeGrid.render(); + + // the
that sometimes displays under the time-grid + this.bottomRuleEl = $('
') + .appendTo(this.timeGrid.el); // inject it into the time-grid + + if (this.dayGrid) { + this.dayGrid.el = this.el.find('.fc-day-grid'); + this.dayGrid.render(); + + // have the day-grid extend it's coordinate area over the
dividing the two grids + this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight(); + } + + this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller + + View.prototype.render.call(this); // call the super-method + + this.resetScroll(); // do this after sizes have been set + }, + + + // Make subcomponents ready for cleanup + destroy: function() { + this.timeGrid.destroy(); + if (this.dayGrid) { + this.dayGrid.destroy(); + } + View.prototype.destroy.call(this); // call the super-method + }, + + + // Builds the HTML skeleton for the view. + // The day-grid and time-grid components will render inside containers defined by this HTML. + renderHtml: function() { + return '' + + '
'; + } + if (endCol > startCol) { + cellHtml += ''; + } + if (colCnt > endCol) { + cellHtml += ''; + } + + cellHtml = this.bookendCells(cellHtml, 'highlight'); + + return '' + + '
' + + '' + + '' + + cellHtml + + '' + + '
' + + '
'; + } + +}); + +;; + +/* Event-rendering methods for the DayGrid class +----------------------------------------------------------------------------------------------------------------------*/ + +$.extend(DayGrid.prototype, { + + segs: null, + rowStructs: null, // an array of objects, each holding information about a row's event-rendering + + + // Render the given events onto the Grid and return the rendered segments + renderEvents: function(events) { + var rowStructs = this.rowStructs = this.renderEventRows(events); + var segs = []; + + // append to each row's content skeleton + this.rowEls.each(function(i, rowNode) { + $(rowNode).find('.fc-content-skeleton > table').append( + rowStructs[i].tbodyEl + ); + segs.push.apply(segs, rowStructs[i].segs); + }); + + this.segs = segs; + }, + + + // Retrieves all segment objects that have been rendered + getSegs: function() { + return (this.segs || []).concat( + this.popoverSegs || [] // segs rendered in the "more" events popover + ); + }, + + + // Removes all rendered event elements + destroyEvents: function() { + var rowStructs; + var rowStruct; + + Grid.prototype.destroyEvents.call(this); // call the super-method + + rowStructs = this.rowStructs || []; + while ((rowStruct = rowStructs.pop())) { + rowStruct.tbodyEl.remove(); + } + + this.segs = null; + this.destroySegPopover(); // removes the "more.." events popover + }, + + + // Uses the given events array to generate
elements of the level+col matrix + var loneCellMatrix = []; // lookup for elements that only take up a single column + var i, levelSegs; + var col; + var tr; + var j, seg; + var td; + + // populates empty cells from the current column (`col`) to `endCol` + function emptyCellsUntil(endCol) { + while (col < endCol) { + // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell + td = (loneCellMatrix[i - 1] || [])[col]; + if (td) { + td.attr( + 'rowspan', + parseInt(td.attr('rowspan') || 1, 10) + 1 + ); + } + else { + td = $(''); + tr.append(td); + } + cellMatrix[i][col] = td; + loneCellMatrix[i][col] = td; + col++; + } + } + + for (i = 0; i < levelCnt; i++) { // iterate through all levels + levelSegs = segLevels[i]; + col = 0; + tr = $('
').append(seg.el); + if (seg.leftCol != seg.rightCol) { + td.attr('colspan', seg.rightCol - seg.leftCol + 1); + } + else { // a single-column segment + loneCellMatrix[i][col] = td; + } + + while (col <= seg.rightCol) { + cellMatrix[i][col] = td; + segMatrix[i][col] = seg; + col++; + } + + tr.append(td); + } + } + + emptyCellsUntil(colCnt); // finish off the row + this.bookendCells(tr, 'eventSkeleton'); + tbody.append(tr); + } + + return { // a "rowStruct" + row: row, // the row number + tbodyEl: tbody, + cellMatrix: cellMatrix, + segMatrix: segMatrix, + segLevels: segLevels, + segs: rowSegs + }; + }, + + + // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels. + buildSegLevels: function(segs) { + var levels = []; + var i, seg; + var j; + + // Give preference to elements with certain criteria, so they have + // a chance to be closer to the top. + segs.sort(compareSegs); + + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + + // loop through levels, starting with the topmost, until the segment doesn't collide with other segments + for (j = 0; j < levels.length; j++) { + if (!isDaySegCollision(seg, levels[j])) { + break; + } + } + // `j` now holds the desired subrow index + seg.level = j; + + // create new level array if needed and append segment + (levels[j] || (levels[j] = [])).push(seg); + } + + // order segments left-to-right. very important if calendar is RTL + for (j = 0; j < levels.length; j++) { + levels[j].sort(compareDaySegCols); + } + + return levels; + }, + + + // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row + groupSegRows: function(segs) { + var view = this.view; + var segRows = []; + var i; + + for (i = 0; i < view.rowCnt; i++) { + segRows.push([]); + } + + for (i = 0; i < segs.length; i++) { + segRows[segs[i].row].push(segs[i]); + } + + return segRows; + } + +}); + + +// Computes whether two segments' columns collide. They are assumed to be in the same row. +function isDaySegCollision(seg, otherSegs) { + var i, otherSeg; + + for (i = 0; i < otherSegs.length; i++) { + otherSeg = otherSegs[i]; + + if ( + otherSeg.leftCol <= seg.rightCol && + otherSeg.rightCol >= seg.leftCol + ) { + return true; + } + } + + return false; +} + + +// A cmp function for determining the leftmost event +function compareDaySegCols(a, b) { + return a.leftCol - b.leftCol; +} + +;; + +/* Methods relate to limiting the number events for a given day on a DayGrid +----------------------------------------------------------------------------------------------------------------------*/ + +$.extend(DayGrid.prototype, { + + + segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible + popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible + + + destroySegPopover: function() { + if (this.segPopover) { + this.segPopover.hide(); // will trigger destruction of `segPopover` and `popoverSegs` + } + }, + + + // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid. + // `levelLimit` can be false (don't limit), a number, or true (should be computed). + limitRows: function(levelLimit) { + var rowStructs = this.rowStructs || []; + var row; // row # + var rowLevelLimit; + + for (row = 0; row < rowStructs.length; row++) { + this.unlimitRow(row); + + if (!levelLimit) { + rowLevelLimit = false; + } + else if (typeof levelLimit === 'number') { + rowLevelLimit = levelLimit; + } + else { + rowLevelLimit = this.computeRowLevelLimit(row); + } + + if (rowLevelLimit !== false) { + this.limitRow(row, rowLevelLimit); + } + } + }, + + + // Computes the number of levels a row will accomodate without going outside its bounds. + // Assumes the row is "rigid" (maintains a constant height regardless of what is inside). + // `row` is the row number. + computeRowLevelLimit: function(row) { + var rowEl = this.rowEls.eq(row); // the containing "fake" row div + var rowHeight = rowEl.height(); // TODO: cache somehow? + var trEls = this.rowStructs[row].tbodyEl.children(); + var i, trEl; + + // Reveal one level
DOM nodes + var col = 0; // col # + var cell; + var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right + var cellMatrix; // a matrix (by level, then column) of all jQuery elements in the row + var limitedNodes; // array of temporarily hidden level
DOM nodes + var i, seg; + var segsBelow; // array of segment objects below `seg` in the current `col` + var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies + var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column) + var td, rowspan; + var segMoreNodes; // array of "more" cells that will stand-in for the current seg's cell + var j; + var moreTd, moreWrap, moreLink; + + // Iterates through empty level cells and places "more" links inside if need be + function emptyCellsUntil(endCol) { // goes from current `col` to `endCol` + while (col < endCol) { + cell = { row: row, col: col }; + segsBelow = _this.getCellSegs(cell, levelLimit); + if (segsBelow.length) { + td = cellMatrix[levelLimit - 1][col]; + moreLink = _this.renderMoreLink(cell, segsBelow); + moreWrap = $('
').append(moreLink); + td.append(moreWrap); + moreNodes.push(moreWrap[0]); + } + col++; + } + } + + if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit? + levelSegs = rowStruct.segLevels[levelLimit - 1]; + cellMatrix = rowStruct.cellMatrix; + + limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level
for each column the segment occupies. will be one for each colspan + for (j = 0; j < colSegsBelow.length; j++) { + moreTd = $('').attr('rowspan', rowspan); + segsBelow = colSegsBelow[j]; + cell = { row: row, col: seg.leftCol + j }; + moreLink = this.renderMoreLink(cell, [ seg ].concat(segsBelow)); // count seg as hidden too + moreWrap = $('
').append(moreLink); + moreTd.append(moreWrap); + segMoreNodes.push(moreTd[0]); + moreNodes.push(moreTd[0]); + } + + td.addClass('fc-limited').after($(segMoreNodes)); // hide original
and inject replacements + limitedNodes.push(td[0]); + } + } + + emptyCellsUntil(view.colCnt); // finish off the level + rowStruct.moreEls = $(moreNodes); // for easy undoing later + rowStruct.limitedEls = $(limitedNodes); // for easy undoing later + } + }, + + + // Reveals all levels and removes all "more"-related elements for a grid's row. + // `row` is a row number. + unlimitRow: function(row) { + var rowStruct = this.rowStructs[row]; + + if (rowStruct.moreEls) { + rowStruct.moreEls.remove(); + rowStruct.moreEls = null; + } + + if (rowStruct.limitedEls) { + rowStruct.limitedEls.removeClass('fc-limited'); + rowStruct.limitedEls = null; + } + }, + + + // Renders an element that represents hidden event element for a cell. + // Responsible for attaching click handler as well. + renderMoreLink: function(cell, hiddenSegs) { + var _this = this; + var view = this.view; + + return $('') + .text( + this.getMoreLinkText(hiddenSegs.length) + ) + .on('click', function(ev) { + var clickOption = view.opt('eventLimitClick'); + var date = view.cellToDate(cell); + var moreEl = $(this); + var dayEl = _this.getCellDayEl(cell); + var allSegs = _this.getCellSegs(cell); + + // rescope the segments to be within the cell's date + var reslicedAllSegs = _this.resliceDaySegs(allSegs, date); + var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date); + + if (typeof clickOption === 'function') { + // the returned value can be an atomic option + clickOption = view.trigger('eventLimitClick', null, { + date: date, + dayEl: dayEl, + moreEl: moreEl, + segs: reslicedAllSegs, + hiddenSegs: reslicedHiddenSegs + }, ev); + } + + if (clickOption === 'popover') { + _this.showSegPopover(date, cell, moreEl, reslicedAllSegs); + } + else if (typeof clickOption === 'string') { // a view name + view.calendar.zoomTo(date, clickOption); + } + }); + }, + + + // Reveals the popover that displays all events within a cell + showSegPopover: function(date, cell, moreLink, segs) { + var _this = this; + var view = this.view; + var moreWrap = moreLink.parent(); // the
wrapper around the + var topEl; // the element we want to match the top coordinate of + var options; + + if (view.rowCnt == 1) { + topEl = this.view.el; // will cause the popover to cover any sort of header + } + else { + topEl = this.rowEls.eq(cell.row); // will align with top of row + } + + options = { + className: 'fc-more-popover', + content: this.renderSegPopoverContent(date, segs), + parentEl: this.el, + top: topEl.offset().top, + autoHide: true, // when the user clicks elsewhere, hide the popover + viewportConstrain: view.opt('popoverViewportConstrain'), + hide: function() { + // destroy everything when the popover is hidden + _this.segPopover.destroy(); + _this.segPopover = null; + _this.popoverSegs = null; + } + }; + + // Determine horizontal coordinate. + // We use the moreWrap instead of the
to avoid border confusion. + if (view.opt('isRTL')) { + options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border + } + else { + options.left = moreWrap.offset().left - 1; // -1 to be over cell border + } + + this.segPopover = new Popover(options); + this.segPopover.show(); + }, + + + // Builds the inner DOM contents of the segment popover + renderSegPopoverContent: function(date, segs) { + var view = this.view; + var isTheme = view.opt('theme'); + var title = date.format(view.opt('dayPopoverFormat')); + var content = $( + '
' + + '' + + '' + + htmlEscape(title) + + '' + + '
' + + '
' + + '
' + + '
' + + '
' + ); + var segContainer = content.find('.fc-event-container'); + var i; + + // render each seg's `el` and only return the visible segs + segs = this.renderSegs(segs, true); // disableResizing=true + this.popoverSegs = segs; + + for (i = 0; i < segs.length; i++) { + + // because segments in the popover are not part of a grid coordinate system, provide a hint to any + // grids that want to do drag-n-drop about which cell it came from + segs[i].cellDate = date; + + segContainer.append(segs[i].el); + } + + return content; + }, + + + // Given the events within an array of segment objects, reslice them to be in a single day + resliceDaySegs: function(segs, dayDate) { + var events = $.map(segs, function(seg) { + return seg.event; + }); + var dayStart = dayDate.clone().stripTime(); + var dayEnd = dayStart.clone().add(1, 'days'); + + return this.eventsToSegs(events, dayStart, dayEnd); + }, + + + // Generates the text that should be inside a "more" link, given the number of events it represents + getMoreLinkText: function(num) { + var view = this.view; + var opt = view.opt('eventLimitText'); + + if (typeof opt === 'function') { + return opt(num); + } + else { + return '+' + num + ' ' + opt; + } + }, + + + // Returns segments within a given cell. + // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs. + getCellSegs: function(cell, startLevel) { + var segMatrix = this.rowStructs[cell.row].segMatrix; + var level = startLevel || 0; + var segs = []; + var seg; + + while (level < segMatrix.length) { + seg = segMatrix[level][cell.col]; + if (seg) { + segs.push(seg); + } + level++; + } + + return segs; + } + +}); + +;; + +/* A component that renders one or more columns of vertical time slots +----------------------------------------------------------------------------------------------------------------------*/ + +function TimeGrid(view) { + Grid.call(this, view); // call the super-constructor +} + + +TimeGrid.prototype = createObject(Grid.prototype); // define the super-class +$.extend(TimeGrid.prototype, { + + slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines + snapDuration: null, // granularity of time for dragging and selecting + + minTime: null, // Duration object that denotes the first visible time of any given day + maxTime: null, // Duration object that denotes the exclusive visible end time of any given day + + dayEls: null, // cells elements in the day-row background + slatEls: null, // elements running horizontally across all columns + + slatTops: null, // an array of top positions, relative to the container. last item holds bottom of last slot + + highlightEl: null, // cell skeleton element for rendering the highlight + helperEl: null, // cell skeleton element for rendering the mock event "helper" + + + // Renders the time grid into `this.el`, which should already be assigned. + // Relies on the view's colCnt. In the future, this component should probably be self-sufficient. + render: function() { + this.processOptions(); + + this.el.html(this.renderHtml()); + + this.dayEls = this.el.find('.fc-day'); + this.slatEls = this.el.find('.fc-slats tr'); + + this.computeSlatTops(); + + Grid.prototype.render.call(this); // call the super-method + }, + + + // Renders the basic HTML skeleton for the grid + renderHtml: function() { + return '' + + '
' + + '' + + this.rowHtml('slotBg') + // leverages RowRenderer, which will call slotBgCellHtml + '
' + + '
' + + '
' + + '' + + this.slatRowHtml() + + '
' + + '
'; + }, + + + // Renders the HTML for a vertical background cell behind the slots. + // This method is distinct from 'bg' because we wanted a new `rowType` so the View could customize the rendering. + slotBgCellHtml: function(row, col, date) { + return this.bgCellHtml(row, col, date); + }, + + + // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL. + slatRowHtml: function() { + var view = this.view; + var calendar = view.calendar; + var isRTL = view.opt('isRTL'); + var html = ''; + var slotNormal = this.slotDuration.asMinutes() % 15 === 0; + var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations + var slotDate; // will be on the view's first day, but we only care about its time + var minutes; + var axisHtml; + + // Calculate the time for each slot + while (slotTime < this.maxTime) { + slotDate = view.start.clone().time(slotTime); // will be in UTC but that's good. to avoid DST issues + minutes = slotDate.minutes(); + + axisHtml = + '
' + + ((!slotNormal || !minutes) ? // if irregular slot duration, or on the hour, then display the time + '' + // for matchCellWidths + htmlEscape(calendar.formatDate(slotDate, view.opt('axisFormat'))) + + '' : + '' + ) + + '
' + + (isRTL ? axisHtml : '') + + "
'; + col = seg.col; + } + + // compute vertical position + dayDate = view.cellToDate(0, col); + top = this.computeDateTop(seg.start, dayDate); + bottom = this.computeDateTop(seg.end, dayDate); // the y position of the bottom edge + + // generate the cell HTML. bottom becomes negative because it needs to be a CSS value relative to the + // bottom edge of the zero-height container. + cellHtml += + '' + + '
' + + '
' + + '
' + + '
'; + } + + cellHtml = this.bookendCells(cellHtml, 'highlight'); + + return '' + + '
' + + '' + + '' + + cellHtml + + '' + + '
' + + '
'; + } + +}); + +;; + +/* Event-rendering methods for the TimeGrid class +----------------------------------------------------------------------------------------------------------------------*/ + +$.extend(TimeGrid.prototype, { + + segs: null, // segment objects rendered in the component. null of events haven't been rendered yet + eventSkeletonEl: null, // has cells with event-containers, which contain absolutely positioned event elements + + + // Renders the events onto the grid and returns an array of segments that have been rendered + renderEvents: function(events) { + var res = this.renderEventTable(events); + + this.eventSkeletonEl = $('
').append(res.tableEl); + this.el.append(this.eventSkeletonEl); + + this.segs = res.segs; + }, + + + // Retrieves rendered segment objects + getSegs: function() { + return this.segs || []; + }, + + + // Removes all event segment elements from the view + destroyEvents: function() { + Grid.prototype.destroyEvents.call(this); // call the super-method + + if (this.eventSkeletonEl) { + this.eventSkeletonEl.remove(); + this.eventSkeletonEl = null; + } + + this.segs = null; + }, + + + // Renders and returns the portion of the event-skeleton. + // Returns an object with properties 'tbodyEl' and 'segs'. + renderEventTable: function(events) { + var tableEl = $('
'); + var trEl = tableEl.find('tr'); + var segs = this.eventsToSegs(events); + var segCols; + var i, seg; + var col, colSegs; + var containerEl; + + segs = this.renderSegs(segs); // returns only the visible segs + segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg + + this.computeSegVerticals(segs); // compute and assign top/bottom + + for (col = 0; col < segCols.length; col++) { // iterate each column grouping + colSegs = segCols[col]; + placeSlotSegs(colSegs); // compute horizontal coordinates, z-index's, and reorder the array + + containerEl = $('
'); + + // assign positioning CSS and insert into container + for (i = 0; i < colSegs.length; i++) { + seg = colSegs[i]; + seg.el.css(this.generateSegPositionCss(seg)); + + // if the height is short, add a className for alternate styling + if (seg.bottom - seg.top < 30) { + seg.el.addClass('fc-short'); + } + + containerEl.append(seg.el); + } + + trEl.append($('
').append(containerEl)); + } + + this.bookendCells(trEl, 'eventSkeleton'); + + return { + tableEl: tableEl, + segs: segs + }; + }, + + + // Refreshes the CSS top/bottom coordinates for each segment element. Probably after a window resize/zoom. + updateSegVerticals: function() { + var segs = this.segs; + var i; + + if (segs) { + this.computeSegVerticals(segs); + + for (i = 0; i < segs.length; i++) { + segs[i].el.css( + this.generateSegVerticalCss(segs[i]) + ); + } + } + }, + + + // For each segment in an array, computes and assigns its top and bottom properties + computeSegVerticals: function(segs) { + var i, seg; + + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + seg.top = this.computeDateTop(seg.start, seg.start); + seg.bottom = this.computeDateTop(seg.end, seg.start); + } + }, + + + // Renders the HTML for a single event segment's default rendering + renderSegHtml: function(seg, disableResizing) { + var view = this.view; + var event = seg.event; + var isDraggable = view.isEventDraggable(event); + var isResizable = !disableResizing && seg.isEnd && view.isEventResizable(event); + var classes = this.getSegClasses(seg, isDraggable, isResizable); + var skinCss = this.getEventSkinCss(event); + var timeText; + var fullTimeText; // more verbose time text. for the print stylesheet + var startTimeText; // just the start time text + + classes.unshift('fc-time-grid-event'); + + if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day... + // Don't display time text on segments that run entirely through a day. + // That would appear as midnight-midnight and would look dumb. + // Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am) + if (seg.isStart || seg.isEnd) { + timeText = view.getEventTimeText(seg.start, seg.end); + fullTimeText = view.getEventTimeText(seg.start, seg.end, 'LT'); + startTimeText = view.getEventTimeText(seg.start, null); + } + } else { + // Display the normal time text for the *event's* times + timeText = view.getEventTimeText(event); + fullTimeText = view.getEventTimeText(event, 'LT'); + startTimeText = view.getEventTimeText(event.start, null); + } + + return '' + + '
' + + (timeText ? + '
' + + '' + htmlEscape(timeText) + '' + + '
' : + '' + ) + + (event.title ? + '
' + + htmlEscape(event.title) + + '
' : + '' + ) + + '
' + + '
' + + (isResizable ? + '
' : + '' + ) + + ''; + }, + + + // Generates an object with CSS properties/values that should be applied to an event segment element. + // Contains important positioning-related properties that should be applied to any event element, customized or not. + generateSegPositionCss: function(seg) { + var view = this.view; + var isRTL = view.opt('isRTL'); + var shouldOverlap = view.opt('slotEventOverlap'); + var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point + var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point + var props = this.generateSegVerticalCss(seg); // get top/bottom first + var left; // amount of space from left edge, a fraction of the total width + var right; // amount of space from right edge, a fraction of the total width + + if (shouldOverlap) { + // double the width, but don't go beyond the maximum forward coordinate (1.0) + forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2); + } + + if (isRTL) { + left = 1 - forwardCoord; + right = backwardCoord; + } + else { + left = backwardCoord; + right = 1 - forwardCoord; + } + + props.zIndex = seg.level + 1; // convert from 0-base to 1-based + props.left = left * 100 + '%'; + props.right = right * 100 + '%'; + + if (shouldOverlap && seg.forwardPressure) { + // add padding to the edge so that forward stacked events don't cover the resizer's icon + props[isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width + } + + return props; + }, + + + // Generates an object with CSS properties for the top/bottom coordinates of a segment element + generateSegVerticalCss: function(seg) { + return { + top: seg.top, + bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container + }; + }, + + + // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col + groupSegCols: function(segs) { + var view = this.view; + var segCols = []; + var i; + + for (i = 0; i < view.colCnt; i++) { + segCols.push([]); + } + + for (i = 0; i < segs.length; i++) { + segCols[segs[i].col].push(segs[i]); + } + + return segCols; + } + +}); + + +// Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each. +// Also reorders the given array by date! +function placeSlotSegs(segs) { + var levels; + var level0; + var i; + + segs.sort(compareSegs); // order by date + levels = buildSlotSegLevels(segs); + computeForwardSlotSegs(levels); + + if ((level0 = levels[0])) { + + for (i = 0; i < level0.length; i++) { + computeSlotSegPressures(level0[i]); + } + + for (i = 0; i < level0.length; i++) { + computeSlotSegCoords(level0[i], 0, 0); + } + } +} + + +// Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is +// left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date. +function buildSlotSegLevels(segs) { + var levels = []; + var i, seg; + var j; + + for (i=0; i seg2.top && seg1.top < seg2.bottom; +} + + +// A cmp function for determining which forward segment to rely on more when computing coordinates. +function compareForwardSlotSegs(seg1, seg2) { + // put higher-pressure first + return seg2.forwardPressure - seg1.forwardPressure || + // put segments that are closer to initial edge first (and favor ones with no coords yet) + (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) || + // do normal sorting... + compareSegs(seg1, seg2); +} + +;; + +/* An abstract class from which other views inherit from +----------------------------------------------------------------------------------------------------------------------*/ +// Newer methods should be written as prototype methods, not in the monster `View` function at the bottom. + +View.prototype = { + + calendar: null, // owner Calendar object + coordMap: null, // a CoordMap object for converting pixel regions to dates + el: null, // the view's containing element. set by Calendar + + // important Moments + start: null, // the date of the very first cell + end: null, // the date after the very last cell + intervalStart: null, // the start of the interval of time the view represents (1st of month for month view) + intervalEnd: null, // the exclusive end of the interval of time the view represents + + // used for cell-to-date and date-to-cell calculations + rowCnt: null, // # of weeks + colCnt: null, // # of days displayed in a week + + isSelected: false, // boolean whether cells are user-selected or not + + // subclasses can optionally use a scroll container + scrollerEl: null, // the element that will most likely scroll when content is too tall + scrollTop: null, // cached vertical scroll value + + // classNames styled by jqui themes + widgetHeaderClass: null, + widgetContentClass: null, + highlightStateClass: null, + + // document handlers, bound to `this` object + documentMousedownProxy: null, + documentDragStartProxy: null, + + + // Serves as a "constructor" to suppliment the monster `View` constructor below + init: function() { + var tm = this.opt('theme') ? 'ui' : 'fc'; + + this.widgetHeaderClass = tm + '-widget-header'; + this.widgetContentClass = tm + '-widget-content'; + this.highlightStateClass = tm + '-state-highlight'; + + // save references to `this`-bound handlers + this.documentMousedownProxy = $.proxy(this, 'documentMousedown'); + this.documentDragStartProxy = $.proxy(this, 'documentDragStart'); + }, + + + // Renders the view inside an already-defined `this.el`. + // Subclasses should override this and then call the super method afterwards. + render: function() { + this.updateSize(); + this.trigger('viewRender', this, this, this.el); + + // attach handlers to document. do it here to allow for destroy/rerender + $(document) + .on('mousedown', this.documentMousedownProxy) + .on('dragstart', this.documentDragStartProxy); // jqui drag + }, + + + // Clears all view rendering, event elements, and unregisters handlers + destroy: function() { + this.unselect(); + this.trigger('viewDestroy', this, this, this.el); + this.destroyEvents(); + this.el.empty(); // removes inner contents but leaves the element intact + + $(document) + .off('mousedown', this.documentMousedownProxy) + .off('dragstart', this.documentDragStartProxy); + }, + + + // Used to determine what happens when the users clicks next/prev. Given -1 for prev, 1 for next. + // Should apply the delta to `date` (a Moment) and return it. + incrementDate: function(date, delta) { + // subclasses should implement + }, + + + /* Dimensions + ------------------------------------------------------------------------------------------------------------------*/ + + + // Refreshes anything dependant upon sizing of the container element of the grid + updateSize: function(isResize) { + if (isResize) { + this.recordScroll(); + } + this.updateHeight(); + this.updateWidth(); + }, + + + // Refreshes the horizontal dimensions of the calendar + updateWidth: function() { + // subclasses should implement + }, + + + // Refreshes the vertical dimensions of the calendar + updateHeight: function() { + var calendar = this.calendar; // we poll the calendar for height information + + this.setHeight( + calendar.getSuggestedViewHeight(), + calendar.isHeightAuto() + ); + }, + + + // Updates the vertical dimensions of the calendar to the specified height. + // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height. + setHeight: function(height, isAuto) { + // subclasses should implement + }, + + + // Given the total height of the view, return the number of pixels that should be used for the scroller. + // Utility for subclasses. + computeScrollerHeight: function(totalHeight) { + var both = this.el.add(this.scrollerEl); + var otherHeight; // cumulative height of everything that is not the scrollerEl in the view (header+borders) + + // fuckin IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked + both.css({ + position: 'relative', // cause a reflow, which will force fresh dimension recalculation + left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll + }); + otherHeight = this.el.outerHeight() - this.scrollerEl.height(); // grab the dimensions + both.css({ position: '', left: '' }); // undo hack + + return totalHeight - otherHeight; + }, + + + // Called for remembering the current scroll value of the scroller. + // Should be called before there is a destructive operation (like removing DOM elements) that might inadvertently + // change the scroll of the container. + recordScroll: function() { + if (this.scrollerEl) { + this.scrollTop = this.scrollerEl.scrollTop(); + } + }, + + + // Set the scroll value of the scroller to the previously recorded value. + // Should be called after we know the view's dimensions have been restored following some type of destructive + // operation (like temporarily removing DOM elements). + restoreScroll: function() { + if (this.scrollTop !== null) { + this.scrollerEl.scrollTop(this.scrollTop); + } + }, + + + /* Events + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders the events onto the view. + // Should be overriden by subclasses. Subclasses should call the super-method afterwards. + renderEvents: function(events) { + this.segEach(function(seg) { + this.trigger('eventAfterRender', seg.event, seg.event, seg.el); + }); + this.trigger('eventAfterAllRender'); + }, + + + // Removes event elements from the view. + // Should be overridden by subclasses. Should call this super-method FIRST, then subclass DOM destruction. + destroyEvents: function() { + this.segEach(function(seg) { + this.trigger('eventDestroy', seg.event, seg.event, seg.el); + }); + }, + + + // Given an event and the default element used for rendering, returns the element that should actually be used. + // Basically runs events and elements through the eventRender hook. + resolveEventEl: function(event, el) { + var custom = this.trigger('eventRender', event, event, el); + + if (custom === false) { // means don't render at all + el = null; + } + else if (custom && custom !== true) { + el = $(custom); + } + + return el; + }, + + + // Hides all rendered event segments linked to the given event + showEvent: function(event) { + this.segEach(function(seg) { + seg.el.css('visibility', ''); + }, event); + }, + + + // Shows all rendered event segments linked to the given event + hideEvent: function(event) { + this.segEach(function(seg) { + seg.el.css('visibility', 'hidden'); + }, event); + }, + + + // Iterates through event segments. Goes through all by default. + // If the optional `event` argument is specified, only iterates through segments linked to that event. + // The `this` value of the callback function will be the view. + segEach: function(func, event) { + var segs = this.getSegs(); + var i; + + for (i = 0; i < segs.length; i++) { + if (!event || segs[i].event._id === event._id) { + func.call(this, segs[i]); + } + } + }, + + + // Retrieves all the rendered segment objects for the view + getSegs: function() { + // subclasses must implement + }, + + + /* Event Drag Visualization + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of an event hovering over the specified date. + // `end` is a Moment and might be null. + // `seg` might be null. if specified, it is the segment object of the event being dragged. + // otherwise, an external event from outside the calendar is being dragged. + renderDrag: function(start, end, seg) { + // subclasses should implement + }, + + + // Unrenders a visual indication of event hovering + destroyDrag: function() { + // subclasses should implement + }, + + + // Handler for accepting externally dragged events being dropped in the view. + // Gets called when jqui's 'dragstart' is fired. + documentDragStart: function(ev, ui) { + var _this = this; + var dropDate = null; + var dragListener; + + if (this.opt('droppable')) { // only listen if this setting is on + + // listener that tracks mouse movement over date-associated pixel regions + dragListener = new DragListener(this.coordMap, { + cellOver: function(cell, date) { + dropDate = date; + _this.renderDrag(date); + }, + cellOut: function() { + dropDate = null; + _this.destroyDrag(); + } + }); + + // gets called, only once, when jqui drag is finished + $(document).one('dragstop', function(ev, ui) { + _this.destroyDrag(); + if (dropDate) { + _this.trigger('drop', ev.target, dropDate, ev, ui); + } + }); + + dragListener.startDrag(ev); // start listening immediately + } + }, + + + /* Selection + ------------------------------------------------------------------------------------------------------------------*/ + + + // Selects a date range on the view. `start` and `end` are both Moments. + // `ev` is the native mouse event that begin the interaction. + select: function(start, end, ev) { + this.unselect(ev); + this.renderSelection(start, end); + this.reportSelection(start, end, ev); + }, + + + // Renders a visual indication of the selection + renderSelection: function(start, end) { + // subclasses should implement + }, + + + // Called when a new selection is made. Updates internal state and triggers handlers. + reportSelection: function(start, end, ev) { + this.isSelected = true; + this.trigger('select', null, start, end, ev); + }, + + + // Undoes a selection. updates in the internal state and triggers handlers. + // `ev` is the native mouse event that began the interaction. + unselect: function(ev) { + if (this.isSelected) { + this.isSelected = false; + this.destroySelection(); + this.trigger('unselect', null, ev); + } + }, + + + // Unrenders a visual indication of selection + destroySelection: function() { + // subclasses should implement + }, + + + // Handler for unselecting when the user clicks something and the 'unselectAuto' setting is on + documentMousedown: function(ev) { + var ignore; + + // is there a selection, and has the user made a proper left click? + if (this.isSelected && this.opt('unselectAuto') && isPrimaryMouseButton(ev)) { + + // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element + ignore = this.opt('unselectCancel'); + if (!ignore || !$(ev.target).closest(ignore).length) { + this.unselect(ev); + } + } + } + +}; + + +// We are mixing JavaScript OOP design patterns here by putting methods and member variables in the closed scope of the +// constructor. Going forward, methods should be part of the prototype. +function View(calendar) { + var t = this; + + // exports + t.calendar = calendar; + t.opt = opt; + t.trigger = trigger; + t.isEventDraggable = isEventDraggable; + t.isEventResizable = isEventResizable; + t.eventDrop = eventDrop; + t.eventResize = eventResize; + + // imports + var reportEventChange = calendar.reportEventChange; + + // locals + var options = calendar.options; + var nextDayThreshold = moment.duration(options.nextDayThreshold); + + + t.init(); // the "constructor" that concerns the prototype methods + + + function opt(name) { + var v = options[name]; + if ($.isPlainObject(v) && !isForcedAtomicOption(name)) { + return smartProperty(v, t.name); + } + return v; + } + + + function trigger(name, thisObj) { + return calendar.trigger.apply( + calendar, + [name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t]) + ); + } + + + + /* Event Editable Boolean Calculations + ------------------------------------------------------------------------------*/ + + + function isEventDraggable(event) { + var source = event.source || {}; + + return firstDefined( + event.startEditable, + source.startEditable, + opt('eventStartEditable'), + event.editable, + source.editable, + opt('editable') + ); + } + + + function isEventResizable(event) { + var source = event.source || {}; + + return firstDefined( + event.durationEditable, + source.durationEditable, + opt('eventDurationEditable'), + event.editable, + source.editable, + opt('editable') + ); + } + + + + /* Event Elements + ------------------------------------------------------------------------------*/ + + + // Compute the text that should be displayed on an event's element. + // Based off the settings of the view. Possible signatures: + // .getEventTimeText(event, formatStr) + // .getEventTimeText(startMoment, endMoment, formatStr) + // .getEventTimeText(startMoment, null, formatStr) + // `timeFormat` is used but the `formatStr` argument can be used to override. + t.getEventTimeText = function(event, formatStr) { + var start; + var end; + + if (typeof event === 'object' && typeof formatStr === 'object') { + // first two arguments are actually moments (or null). shift arguments. + start = event; + end = formatStr; + formatStr = arguments[2]; + } + else { + // otherwise, an event object was the first argument + start = event.start; + end = event.end; + } + + formatStr = formatStr || opt('timeFormat'); + + if (end && opt('displayEventEnd')) { + return calendar.formatRange(start, end, formatStr); + } + else { + return calendar.formatDate(start, formatStr); + } + }; + + + + /* Event Modification Reporting + ---------------------------------------------------------------------------------*/ + + + function eventDrop(el, event, newStart, ev) { + var mutateResult = calendar.mutateEvent(event, newStart, null); + + trigger( + 'eventDrop', + el, + event, + mutateResult.dateDelta, + function() { + mutateResult.undo(); + reportEventChange(); + }, + ev, + {} // jqui dummy + ); + + reportEventChange(); + } + + + function eventResize(el, event, newEnd, ev) { + var mutateResult = calendar.mutateEvent(event, null, newEnd); + + trigger( + 'eventResize', + el, + event, + mutateResult.durationDelta, + function() { + mutateResult.undo(); + reportEventChange(); + }, + ev, + {} // jqui dummy + ); + + reportEventChange(); + } + + + // ==================================================================================================== + // Utilities for day "cells" + // ==================================================================================================== + // The "basic" views are completely made up of day cells. + // The "agenda" views have day cells at the top "all day" slot. + // This was the obvious common place to put these utilities, but they should be abstracted out into + // a more meaningful class (like DayEventRenderer). + // ==================================================================================================== + + + // For determining how a given "cell" translates into a "date": + // + // 1. Convert the "cell" (row and column) into a "cell offset" (the # of the cell, cronologically from the first). + // Keep in mind that column indices are inverted with isRTL. This is taken into account. + // + // 2. Convert the "cell offset" to a "day offset" (the # of days since the first visible day in the view). + // + // 3. Convert the "day offset" into a "date" (a Moment). + // + // The reverse transformation happens when transforming a date into a cell. + + + // exports + t.isHiddenDay = isHiddenDay; + t.skipHiddenDays = skipHiddenDays; + t.getCellsPerWeek = getCellsPerWeek; + t.dateToCell = dateToCell; + t.dateToDayOffset = dateToDayOffset; + t.dayOffsetToCellOffset = dayOffsetToCellOffset; + t.cellOffsetToCell = cellOffsetToCell; + t.cellToDate = cellToDate; + t.cellToCellOffset = cellToCellOffset; + t.cellOffsetToDayOffset = cellOffsetToDayOffset; + t.dayOffsetToDate = dayOffsetToDate; + t.rangeToSegments = rangeToSegments; + t.isMultiDayEvent = isMultiDayEvent; + + + // internals + var hiddenDays = opt('hiddenDays') || []; // array of day-of-week indices that are hidden + var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool) + var cellsPerWeek; + var dayToCellMap = []; // hash from dayIndex -> cellIndex, for one week + var cellToDayMap = []; // hash from cellIndex -> dayIndex, for one week + var isRTL = opt('isRTL'); + + + // initialize important internal variables + (function() { + + if (opt('weekends') === false) { + hiddenDays.push(0, 6); // 0=sunday, 6=saturday + } + + // Loop through a hypothetical week and determine which + // days-of-week are hidden. Record in both hashes (one is the reverse of the other). + for (var dayIndex=0, cellIndex=0; dayIndex<7; dayIndex++) { + dayToCellMap[dayIndex] = cellIndex; + isHiddenDayHash[dayIndex] = $.inArray(dayIndex, hiddenDays) != -1; + if (!isHiddenDayHash[dayIndex]) { + cellToDayMap[cellIndex] = dayIndex; + cellIndex++; + } + } + + cellsPerWeek = cellIndex; + if (!cellsPerWeek) { + throw 'invalid hiddenDays'; // all days were hidden? bad. + } + + })(); + + + // Is the current day hidden? + // `day` is a day-of-week index (0-6), or a Moment + function isHiddenDay(day) { + if (moment.isMoment(day)) { + day = day.day(); + } + return isHiddenDayHash[day]; + } + + + function getCellsPerWeek() { + return cellsPerWeek; + } + + + // Incrementing the current day until it is no longer a hidden day, returning a copy. + // If the initial value of `date` is not a hidden day, don't do anything. + // Pass `isExclusive` as `true` if you are dealing with an end date. + // `inc` defaults to `1` (increment one day forward each time) + function skipHiddenDays(date, inc, isExclusive) { + var out = date.clone(); + inc = inc || 1; + while ( + isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7] + ) { + out.add(inc, 'days'); + } + return out; + } + + + // + // TRANSFORMATIONS: cell -> cell offset -> day offset -> date + // + + // cell -> date (combines all transformations) + // Possible arguments: + // - row, col + // - { row:#, col: # } + function cellToDate() { + var cellOffset = cellToCellOffset.apply(null, arguments); + var dayOffset = cellOffsetToDayOffset(cellOffset); + var date = dayOffsetToDate(dayOffset); + return date; + } + + // cell -> cell offset + // Possible arguments: + // - row, col + // - { row:#, col:# } + function cellToCellOffset(row, col) { + var colCnt = t.colCnt; + + // rtl variables. wish we could pre-populate these. but where? + var dis = isRTL ? -1 : 1; + var dit = isRTL ? colCnt - 1 : 0; + + if (typeof row == 'object') { + col = row.col; + row = row.row; + } + var cellOffset = row * colCnt + (col * dis + dit); // column, adjusted for RTL (dis & dit) + + return cellOffset; + } + + // cell offset -> day offset + function cellOffsetToDayOffset(cellOffset) { + var day0 = t.start.day(); // first date's day of week + cellOffset += dayToCellMap[day0]; // normlize cellOffset to beginning-of-week + return Math.floor(cellOffset / cellsPerWeek) * 7 + // # of days from full weeks + cellToDayMap[ // # of days from partial last week + (cellOffset % cellsPerWeek + cellsPerWeek) % cellsPerWeek // crazy math to handle negative cellOffsets + ] - + day0; // adjustment for beginning-of-week normalization + } + + // day offset -> date + function dayOffsetToDate(dayOffset) { + return t.start.clone().add(dayOffset, 'days'); + } + + + // + // TRANSFORMATIONS: date -> day offset -> cell offset -> cell + // + + // date -> cell (combines all transformations) + function dateToCell(date) { + var dayOffset = dateToDayOffset(date); + var cellOffset = dayOffsetToCellOffset(dayOffset); + var cell = cellOffsetToCell(cellOffset); + return cell; + } + + // date -> day offset + function dateToDayOffset(date) { + return date.clone().stripTime().diff(t.start, 'days'); + } + + // day offset -> cell offset + function dayOffsetToCellOffset(dayOffset) { + var day0 = t.start.day(); // first date's day of week + dayOffset += day0; // normalize dayOffset to beginning-of-week + return Math.floor(dayOffset / 7) * cellsPerWeek + // # of cells from full weeks + dayToCellMap[ // # of cells from partial last week + (dayOffset % 7 + 7) % 7 // crazy math to handle negative dayOffsets + ] - + dayToCellMap[day0]; // adjustment for beginning-of-week normalization + } + + // cell offset -> cell (object with row & col keys) + function cellOffsetToCell(cellOffset) { + var colCnt = t.colCnt; + + // rtl variables. wish we could pre-populate these. but where? + var dis = isRTL ? -1 : 1; + var dit = isRTL ? colCnt - 1 : 0; + + var row = Math.floor(cellOffset / colCnt); + var col = ((cellOffset % colCnt + colCnt) % colCnt) * dis + dit; // column, adjusted for RTL (dis & dit) + return { + row: row, + col: col + }; + } + + + // + // Converts a date range into an array of segment objects. + // "Segments" are horizontal stretches of time, sliced up by row. + // A segment object has the following properties: + // - row + // - cols + // - isStart + // - isEnd + // + function rangeToSegments(start, end) { + + var rowCnt = t.rowCnt; + var colCnt = t.colCnt; + var segments = []; // array of segments to return + + // day offset for given date range + var dayRange = computeDayRange(start, end); // convert to a whole-day range + var rangeDayOffsetStart = dateToDayOffset(dayRange.start); + var rangeDayOffsetEnd = dateToDayOffset(dayRange.end); // an exclusive value + + // first and last cell offset for the given date range + // "last" implies inclusivity + var rangeCellOffsetFirst = dayOffsetToCellOffset(rangeDayOffsetStart); + var rangeCellOffsetLast = dayOffsetToCellOffset(rangeDayOffsetEnd) - 1; + + // loop through all the rows in the view + for (var row=0; row
' + + '' + // needed for matchCellWidths + htmlEscape(this.opt('weekNumberTitle')) + + '' + + '' + + '' + // needed for matchCellWidths + this.calendar.calculateWeekNumber(this.cellToDate(row, 0)) + + '' + + 's of the "number" row in the DayGrid's content skeleton. + // The number row will only exist if either day numbers or week numbers are turned on. + numberCellHtml: function(row, col, date) { + var classes; + + if (!this.dayNumbersVisible) { // if there are week numbers but not day numbers + return ''; // will create an empty space above events :( + } + + classes = this.dayGrid.getDayClasses(date); + classes.unshift('fc-day-number'); + + return '' + + '' + + date.date() + + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + this.timeGrid.headHtml() + // render the day-of-week headers + '
' + + (this.dayGrid ? + '
' + + '
' : + '' + ) + + '
' + + '
' + + '
' + + '
'; + }, + + + // Generates the HTML that will go before the day-of week header cells. + // Queried by the TimeGrid subcomponent when generating rows. Ordering depends on isRTL. + headIntroHtml: function() { + var date; + var weekNumber; + var weekTitle; + var weekText; + + if (this.opt('weekNumbers')) { + date = this.cellToDate(0, 0); + weekNumber = this.calendar.calculateWeekNumber(date); + weekTitle = this.opt('weekNumberTitle'); + + if (this.opt('isRTL')) { + weekText = weekNumber + weekTitle; + } + else { + weekText = weekTitle + weekNumber; + } + + return '' + + '' + + '' + // needed for matchCellWidths + htmlEscape(weekText) + + '' + + ''; + } + else { + return ''; + } + }, + + + // Generates the HTML that goes before the all-day cells. + // Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL. + dayIntroHtml: function() { + return '' + + '' + + '' + // needed for matchCellWidths + (this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'))) + + '' + + ''; + }, + + + // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column. + slotBgIntroHtml: function() { + return ''; + }, + + + // Generates the HTML that goes before all other types of cells. + // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid. + // Queried by the TimeGrid and DayGrid subcomponents when generating rows. Ordering depends on isRTL. + introHtml: function() { + return ''; + }, + + + // Generates an HTML attribute string for setting the width of the axis, if it is known + axisStyleAttr: function() { + if (this.axisWidth !== null) { + return 'style="width:' + this.axisWidth + 'px"'; + } + return ''; + }, + + + /* Dimensions + ------------------------------------------------------------------------------------------------------------------*/ + + updateSize: function(isResize) { + if (isResize) { + this.timeGrid.resize(); + } + View.prototype.updateSize.call(this, isResize); + }, + + + // Refreshes the horizontal dimensions of the view + updateWidth: function() { + // make all axis cells line up, and record the width so newly created axis cells will have it + this.axisWidth = matchCellWidths(this.el.find('.fc-axis')); + }, + + + // Adjusts the vertical dimensions of the view to the specified values + setHeight: function(totalHeight, isAuto) { + var eventLimit; + var scrollerHeight; + + if (this.bottomRuleHeight === null) { + // calculate the height of the rule the very first time + this.bottomRuleHeight = this.bottomRuleEl.outerHeight(); + } + this.bottomRuleEl.hide(); // .show() will be called later if this
is necessary + + // reset all dimensions back to the original state + this.scrollerEl.css('overflow', ''); + unsetScroller(this.scrollerEl); + uncompensateScroll(this.noScrollRowEls); + + // limit number of events in the all-day area + if (this.dayGrid) { + this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed + + eventLimit = this.opt('eventLimit'); + if (eventLimit && typeof eventLimit !== 'number') { + eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number + } + if (eventLimit) { + this.dayGrid.limitRows(eventLimit); + } + } + + if (!isAuto) { // should we force dimensions of the scroll container, or let the contents be natural height? + + scrollerHeight = this.computeScrollerHeight(totalHeight); + if (setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars? + + // make the all-day and header rows lines up + compensateScroll(this.noScrollRowEls, getScrollbarWidths(this.scrollerEl)); + + // the scrollbar compensation might have changed text flow, which might affect height, so recalculate + // and reapply the desired height to the scroller. + scrollerHeight = this.computeScrollerHeight(totalHeight); + this.scrollerEl.height(scrollerHeight); + + this.restoreScroll(); + } + else { // no scrollbars + // still, force a height and display the bottom rule (marks the end of day) + this.scrollerEl.height(scrollerHeight).css('overflow', 'hidden'); // in case
goes outside + this.bottomRuleEl.show(); + } + } + }, + + + // Sets the scroll value of the scroller to the intial pre-configured state prior to allowing the user to change it. + resetScroll: function() { + var _this = this; + var scrollTime = moment.duration(this.opt('scrollTime')); + var top = this.timeGrid.computeTimeTop(scrollTime); + + // zoom can give weird floating-point values. rather scroll a little bit further + top = Math.ceil(top); + + if (top) { + top++; // to overcome top border that slots beyond the first have. looks better + } + + function scroll() { + _this.scrollerEl.scrollTop(top); + } + + scroll(); + setTimeout(scroll, 0); // overrides any previous scroll state made by the browser + }, + + + /* Events + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders events onto the view and populates the View's segment array + renderEvents: function(events) { + var dayEvents = []; + var timedEvents = []; + var daySegs = []; + var timedSegs; + var i; + + // separate the events into all-day and timed + for (i = 0; i < events.length; i++) { + if (events[i].allDay) { + dayEvents.push(events[i]); + } + else { + timedEvents.push(events[i]); + } + } + + // render the events in the subcomponents + timedSegs = this.timeGrid.renderEvents(timedEvents); + if (this.dayGrid) { + daySegs = this.dayGrid.renderEvents(dayEvents); + } + + // the all-day area is flexible and might have a lot of events, so shift the height + this.updateHeight(); + + // /View.prototype.renderEvents.call(this, events); // call the super-method + }, + + + // Retrieves all segment objects that are rendered in the view + getSegs: function() { + return this.timeGrid.getSegs().concat( + this.dayGrid ? this.dayGrid.getSegs() : [] + ); + }, + + + // Unrenders all event elements and clears internal segment data + destroyEvents: function() { + View.prototype.destroyEvents.call(this); // do this before the grids' segs have been cleared + + // if destroyEvents is being called as part of an event rerender, renderEvents will be called shortly + // after, so remember what the scroll value was so we can restore it. + this.recordScroll(); + + // destroy the events in the subcomponents + this.timeGrid.destroyEvents(); + if (this.dayGrid) { + this.dayGrid.destroyEvents(); + } + + // we DON'T need to call updateHeight() because: + // A) a renderEvents() call always happens after this, which will eventually call updateHeight() + // B) in IE8, this causes a flash whenever events are rerendered + }, + + + /* Event Dragging + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of an event being dragged over the view. + // A returned value of `true` signals that a mock "helper" event has been rendered. + renderDrag: function(start, end, seg) { + if (start.hasTime()) { + return this.timeGrid.renderDrag(start, end, seg); + } + else if (this.dayGrid) { + return this.dayGrid.renderDrag(start, end, seg); + } + }, + + + // Unrenders a visual indications of an event being dragged over the view + destroyDrag: function() { + this.timeGrid.destroyDrag(); + if (this.dayGrid) { + this.dayGrid.destroyDrag(); + } + }, + + + /* Selection + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of a selection + renderSelection: function(start, end) { + if (start.hasTime() || end.hasTime()) { + this.timeGrid.renderSelection(start, end); + } + else if (this.dayGrid) { + this.dayGrid.renderSelection(start, end); + } + }, + + + // Unrenders a visual indications of a selection + destroySelection: function() { + this.timeGrid.destroySelection(); + if (this.dayGrid) { + this.dayGrid.destroySelection(); + } + } + +}); + +;; + +/* A week view with an all-day cell area at the top, and a time grid below +----------------------------------------------------------------------------------------------------------------------*/ +// TODO: a WeekView mixin for calculating dates and titles + +fcViews.agendaWeek = AgendaWeekView; // register the view + +function AgendaWeekView(calendar) { + AgendaView.call(this, calendar); // call the super-constructor +} + + +AgendaWeekView.prototype = createObject(AgendaView.prototype); // define the super-class +$.extend(AgendaWeekView.prototype, { + + name: 'agendaWeek', + + + incrementDate: function(date, delta) { + return date.clone().stripTime().add(delta, 'weeks').startOf('week'); + }, + + + render: function(date) { + + this.intervalStart = date.clone().stripTime().startOf('week'); + this.intervalEnd = this.intervalStart.clone().add(1, 'weeks'); + + this.start = this.skipHiddenDays(this.intervalStart); + this.end = this.skipHiddenDays(this.intervalEnd, -1, true); + + this.title = this.calendar.formatRange( + this.start, + this.end.clone().subtract(1), // make inclusive by subtracting 1 ms + this.opt('titleFormat'), + ' \u2014 ' // emphasized dash + ); + + AgendaView.prototype.render.call(this, this.getCellsPerWeek()); // call the super-method + } + +}); + +;; + +/* A day view with an all-day cell area at the top, and a time grid below +----------------------------------------------------------------------------------------------------------------------*/ + +fcViews.agendaDay = AgendaDayView; // register the view + +function AgendaDayView(calendar) { + AgendaView.call(this, calendar); // call the super-constructor +} + + +AgendaDayView.prototype = createObject(AgendaView.prototype); // define the super-class +$.extend(AgendaDayView.prototype, { + + name: 'agendaDay', + + + incrementDate: function(date, delta) { + var out = date.clone().stripTime().add(delta, 'days'); + out = this.skipHiddenDays(out, delta < 0 ? -1 : 1); + return out; + }, + + + render: function(date) { + + this.start = this.intervalStart = date.clone().stripTime(); + this.end = this.intervalEnd = this.start.clone().add(1, 'days'); + + this.title = this.calendar.formatDate(this.start, this.opt('titleFormat')); + + AgendaView.prototype.render.call(this, 1); // call the super-method + } + +}); + +;; + +/* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week. +----------------------------------------------------------------------------------------------------------------------*/ + +function ResourceDayGrid(view) { + DayGrid.call(this, view); // call the super-constructor +} + + +ResourceDayGrid.prototype = createObject(DayGrid.prototype); // declare the super-class +$.extend(ResourceDayGrid.prototype, { + rangeToSegs: function(rangeStart, rangeEnd, resourceIds) { + var col, segments = []; + + var currentDate = this.view.calendar.getDate(); + if((rangeStart <= currentDate) && (currentDate < rangeEnd)) { + var view = this.view; + var resources = view.calendar.fetchResources(); + + for (col = 0; col < view.colCnt; col++) { + var resource = resources[col]; + if (resourceIds.indexOf(resource.id) > -1){ + segments.push({ + row: 0, + leftCol: col, + rightCol: col, + isStart: true, + isEnd: true + }); + } + } + } + + return segments; + } +}); +;; + +/* Event-rendering methods for the DayGrid class +----------------------------------------------------------------------------------------------------------------------*/ + +$.extend(ResourceDayGrid.prototype, { + eventToSegs: function(event, intervalStart, intervalEnd) { + var eventStart = event.start.clone().stripZone(); // normalize + var eventEnd = this.view.calendar.getEventEnd(event).stripZone(); // compute (if necessary) and normalize + var segs; + var i, seg; + + if (intervalStart && intervalEnd) { + seg = intersectionToSeg(eventStart, eventEnd, intervalStart, intervalEnd); + segs = seg ? [ seg ] : []; + } + else { + segs = this.rangeToSegs(eventStart, eventEnd, event.resources); // defined by the subclass + } + + // assign extra event-related properties to the segment objects + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + seg.event = event; + seg.eventStartMS = +eventStart; + seg.eventDurationMS = eventEnd - eventStart; + } + + return segs; + } +}); +;; + +/* A component that renders one or more columns of vertical time slots +----------------------------------------------------------------------------------------------------------------------*/ + +function ResourceTimeGrid(view) { + TimeGrid.call(this, view); // call the super-constructor +} + + +ResourceTimeGrid.prototype = createObject(TimeGrid.prototype); // define the super-class +$.extend(ResourceTimeGrid.prototype, { +// Slices up a date range into a segment for each column +// Each column represents a resource. An event can be assigned to multiple resources +// so we need to build segs accordingly + rangeToSegs: function(rangeStart, rangeEnd, resourceIds) { + var view = this.view; + var segs = []; + var seg; + var col; + var cellDate; + var colStart, colEnd; + + var resources = view.calendar.fetchResources(); + + // normalize + rangeStart = rangeStart.clone().stripZone(); + rangeEnd = rangeEnd.clone().stripZone(); + + for (col = 0; col < view.colCnt; col++) { + var resource = resources[col]; + if (resourceIds.indexOf(resource.id) > -1){ + cellDate = view.cellToDate(0, col); // use the View's cell system for this + colStart = cellDate.clone().time(this.minTime); + colEnd = cellDate.clone().time(this.maxTime); + seg = intersectionToSeg(rangeStart, rangeEnd, colStart, colEnd); + if (seg) { + seg.col = col; + segs.push(seg); + } + } + } + + return segs; + } + +}); + +;; + +/* Event-rendering methods for the TimeGrid class +----------------------------------------------------------------------------------------------------------------------*/ + +$.extend(ResourceTimeGrid.prototype, { +// Slices a single event into an array of event segments. + // When `intervalStart` and `intervalEnd` are specified, intersect the events with that interval. + // Otherwise, let the subclass decide how it wants to slice the segments over the grid. + eventToSegs: function(event, intervalStart, intervalEnd) { + var eventStart = event.start.clone().stripZone(); // normalize + var eventEnd = this.view.calendar.getEventEnd(event).stripZone(); // compute (if necessary) and normalize + var segs; + var i, seg; + + if (intervalStart && intervalEnd) { + seg = intersectionToSeg(eventStart, eventEnd, intervalStart, intervalEnd); + segs = seg ? [ seg ] : []; + } + else { + segs = this.rangeToSegs(eventStart, eventEnd, event.resources); // defined by the subclass + } + + // assign extra event-related properties to the segment objects + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + seg.event = event; + seg.eventStartMS = +eventStart; + seg.eventDurationMS = eventEnd - eventStart; + } + + return segs; + } +}); + +;; + +/* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically. +----------------------------------------------------------------------------------------------------------------------*/ +// Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on). +// Responsible for managing width/height. + +setDefaults({ + allDaySlot: true, + allDayText: 'all-day', + + scrollTime: '06:00:00', + + slotDuration: '00:30:00', + + axisFormat: generateAgendaAxisFormat, + timeFormat: { + agenda: generateAgendaTimeFormat + }, + + minTime: '00:00:00', + maxTime: '24:00:00', + slotEventOverlap: true +}); + +function ResourceView(calendar) { + View.call(this, calendar); // call the super-constructor + + // overrides - the view.js should expose these on the prototype then we wouldn't have to do this. + this.cellToDate = ResourceView.prototype.cellToDate; + + this.timeGrid = new ResourceTimeGrid(this); + + if (this.opt('allDaySlot')) { // should we display the "all-day" area? + this.dayGrid = new ResourceDayGrid(this); // the all-day subcomponent of this view + + // the coordinate grid will be a combination of both subcomponents' grids + this.coordMap = new ComboCoordMap([ + this.dayGrid.coordMap, + this.timeGrid.coordMap + ]); + } + else { + this.coordMap = this.timeGrid.coordMap; + } +} + + +ResourceView.prototype = createObject(AgendaView.prototype); // define the super-class +$.extend(ResourceView.prototype, { + + cellToDate: function() { + return this.start.clone(); + }, + + // fix the classes, etc. + headCellHtml: function(row, col, date) { + var view = this; + var calendar = view.calendar; + var resource = calendar.fetchResources()[col]; + return '' + + '' + + htmlEscape(resource.name) + + ''; + } + +}); + +;; + +/* A day view with an all-day cell area at the top, and a time grid below by resource +----------------------------------------------------------------------------------------------------------------------*/ + +fcViews.resourceDay = ResourceDayView; + +function ResourceDayView(calendar) { // TODO: make a ResourceView mixin + ResourceView.call(this, calendar); // call the super-constructor +} + +ResourceDayView.prototype = createObject(ResourceView.prototype); // define the super-class +$.extend(ResourceDayView.prototype, { + + name: 'resourceDay', + + + incrementDate: function(date, delta) { + var out = date.clone().stripTime().add(delta, 'days'); + out = this.skipHiddenDays(out, delta < 0 ? -1 : 1); + return out; + }, + + + render: function(date) { + this.start = this.intervalStart = date.clone().stripTime(); + this.end = this.intervalEnd = this.start.clone().add(1, 'days'); + + this.title = this.calendar.formatDate(this.start, this.opt('titleFormat')); + + ResourceView.prototype.render.call(this, this.calendar.fetchResources().length); // call the super-method + }, + + // Computes HTML classNames for a single-day cell + getDayClasses: function(date) { + var view = this.view; + var today = view.calendar.getNow().stripTime(); + var classes = [ 'fc-' + dayIDs[date.day()] ]; + + if ( + view.name === 'month' && + date.month() != view.intervalStart.month() + ) { + classes.push('fc-other-month'); + } + + if (date.isSame(today, 'day')) { + classes.push( + 'fc-todaysss', + view.highlightStateClass + ); + } + else if (date < today) { + classes.push('fc-past'); + } + else { + classes.push('fc-future'); + } + + return classes; + } + +}); +;; + }); \ No newline at end of file diff --git a/dist/cdnjs/2.1.1/fullcalendar.min.css b/dist/cdnjs/2.1.1/fullcalendar.min.css index 2858443..31aaeb4 100644 --- a/dist/cdnjs/2.1.1/fullcalendar.min.css +++ b/dist/cdnjs/2.1.1/fullcalendar.min.css @@ -1,5 +1,5 @@ -/*! - * FullCalendar v2.1.1 Stylesheet - * Docs & License: http://arshaw.com/fullcalendar/ - * (c) 2013 Adam Shaw +/*! + * FullCalendar v2.1.1 Stylesheet + * Docs & License: http://arshaw.com/fullcalendar/ + * (c) 2013 Adam Shaw */.fc{direction:ltr;text-align:left}.fc-rtl{text-align:right}body .fc{font-size:1em}.fc-unthemed .fc-popover,.fc-unthemed .fc-row,.fc-unthemed hr,.fc-unthemed tbody,.fc-unthemed td,.fc-unthemed th,.fc-unthemed thead{border-color:#ddd}.fc-unthemed .fc-popover{background-color:#fff}.fc-unthemed .fc-popover .fc-header,.fc-unthemed hr{background:#eee}.fc-unthemed .fc-popover .fc-header .fc-close{color:#666}.fc-unthemed .fc-today{background:#fcf8e3}.fc-highlight{background:#bce8f1;opacity:.3;filter:alpha(opacity=30)}.fc-icon{display:inline-block;font-size:2em;line-height:.5em;height:.5em;font-family:"Courier New",Courier,monospace}.fc-icon-left-single-arrow:after{content:"\02039";font-weight:700}.fc-icon-right-single-arrow:after{content:"\0203A";font-weight:700}.fc-icon-left-double-arrow:after{content:"\000AB"}.fc-icon-right-double-arrow:after{content:"\000BB"}.fc-icon-x:after{content:"\000D7"}.fc button{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;height:2.1em;padding:0 .6em;font-size:1em;white-space:nowrap;cursor:pointer}.fc button::-moz-focus-inner{margin:0;padding:0}.fc-state-default{border:1px solid}.fc-state-default.fc-corner-left{border-top-left-radius:4px;border-bottom-left-radius:4px}.fc-state-default.fc-corner-right{border-top-right-radius:4px;border-bottom-right-radius:4px}.fc button .fc-icon{position:relative;top:.05em;margin:0 .1em}.fc-state-default{background-color:#f5f5f5;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);color:#333;text-shadow:0 1px 1px rgba(255,255,255,.75);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05)}.fc-state-active,.fc-state-disabled,.fc-state-down,.fc-state-hover{color:#333;background-color:#e6e6e6}.fc-state-hover{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.fc-state-active,.fc-state-down{background-color:#ccc;background-image:none;box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05)}.fc-state-disabled{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);box-shadow:none}.fc-button-group{display:inline-block}.fc .fc-button-group>*{float:left;margin:0 0 0 -1px}.fc .fc-button-group>:first-child{margin-left:0}.fc-popover{position:absolute;box-shadow:0 2px 6px rgba(0,0,0,.15)}.fc-popover .fc-header{padding:2px 4px}.fc-popover .fc-header .fc-title{margin:0 2px}.fc-popover .fc-header .fc-close{cursor:pointer}.fc-ltr .fc-popover .fc-header .fc-title,.fc-rtl .fc-popover .fc-header .fc-close{float:left}.fc-ltr .fc-popover .fc-header .fc-close,.fc-rtl .fc-popover .fc-header .fc-title{float:right}.fc-unthemed .fc-popover{border-width:1px;border-style:solid}.fc-unthemed .fc-popover .fc-header .fc-close{font-size:25px;margin-top:4px}.fc-popover>.ui-widget-header+.ui-widget-content{border-top:0}.fc hr{height:0;margin:0;padding:0 0 2px;border-style:solid;border-width:1px 0}.fc-clear{clear:both}.fc-bg,.fc-helper-skeleton,.fc-highlight-skeleton{position:absolute;top:0;left:0;right:0}.fc-bg{bottom:0}.fc-bg table{height:100%}.fc table{width:100%;table-layout:fixed;border-collapse:collapse;border-spacing:0;font-size:1em}.fc th{text-align:center}.fc td,.fc th{border-style:solid;border-width:1px;padding:0;vertical-align:top}.fc td.fc-today{border-style:double}.fc .fc-row{border-style:solid;border-width:0}.fc-row table{border-left:0 hidden transparent;border-right:0 hidden transparent;border-bottom:0 hidden transparent}.fc-row:first-child table{border-top:0 hidden transparent}.fc-row{position:relative}.fc-row .fc-bg{z-index:1}.fc-row .fc-highlight-skeleton{z-index:2;bottom:0}.fc-row .fc-highlight-skeleton table{height:100%}.fc-row .fc-highlight-skeleton td{border-color:transparent}.fc-row .fc-content-skeleton{position:relative;z-index:3;padding-bottom:2px}.fc-row .fc-helper-skeleton{z-index:4}.fc-row .fc-content-skeleton td,.fc-row .fc-helper-skeleton td{background:0 0;border-color:transparent;border-bottom:0}.fc-row .fc-content-skeleton tbody td,.fc-row .fc-helper-skeleton tbody td{border-top:0}.fc-scroller{overflow-y:scroll;overflow-x:hidden}.fc-scroller>*{position:relative;width:100%;overflow:hidden}.fc-event{position:relative;display:block;font-size:.85em;line-height:1.3;border-radius:3px;border:1px solid #3a87ad;background-color:#3a87ad;font-weight:400}.fc-event,.fc-event:hover,.ui-widget .fc-event{color:#fff;text-decoration:none}.fc-event.fc-draggable,.fc-event[href]{cursor:pointer}.fc-day-grid-event{margin:1px 2px 0;padding:0 1px}.fc-ltr .fc-day-grid-event.fc-not-start,.fc-rtl .fc-day-grid-event.fc-not-end{margin-left:0;border-left-width:0;padding-left:1px;border-top-left-radius:0;border-bottom-left-radius:0}.fc-ltr .fc-day-grid-event.fc-not-end,.fc-rtl .fc-day-grid-event.fc-not-start{margin-right:0;border-right-width:0;padding-right:1px;border-top-right-radius:0;border-bottom-right-radius:0}.fc-day-grid-event>.fc-content{white-space:nowrap;overflow:hidden}.fc-day-grid-event .fc-time{font-weight:700}.fc-day-grid-event .fc-resizer{position:absolute;top:0;bottom:0;width:7px}.fc-ltr .fc-day-grid-event .fc-resizer{right:-3px;cursor:e-resize}.fc-rtl .fc-day-grid-event .fc-resizer{left:-3px;cursor:w-resize}a.fc-more{margin:1px 3px;font-size:.85em;cursor:pointer;text-decoration:none}a.fc-more:hover{text-decoration:underline}.fc-limited{display:none}.fc-day-grid .fc-row{z-index:1}.fc-more-popover{z-index:2;width:220px}.fc-more-popover .fc-event-container{padding:10px}.fc-toolbar{text-align:center;margin-bottom:1em}.fc-toolbar .fc-left{float:left}.fc-toolbar .fc-right{float:right}.fc-toolbar .fc-center{display:inline-block}.fc .fc-toolbar>*>*{float:left;margin-left:.75em}.fc .fc-toolbar>*>:first-child{margin-left:0}.fc-toolbar h2{margin:0}.fc-toolbar button{position:relative}.fc-toolbar .fc-state-hover,.fc-toolbar .ui-state-hover{z-index:2}.fc-toolbar .fc-state-down{z-index:3}.fc-toolbar .fc-state-active,.fc-toolbar .ui-state-active{z-index:4}.fc-toolbar button:focus{z-index:5}.fc-view-container *,.fc-view-container :after,.fc-view-container :before{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}.fc-view,.fc-view>table{position:relative;z-index:1}.fc-basicDay-view .fc-content-skeleton,.fc-basicWeek-view .fc-content-skeleton{padding-top:1px;padding-bottom:1em}.fc-basic-view tbody .fc-row{min-height:4em}.fc-row.fc-rigid{overflow:hidden}.fc-row.fc-rigid .fc-content-skeleton{position:absolute;top:0;left:0;right:0}.fc-basic-view .fc-day-number,.fc-basic-view .fc-week-number{padding:0 2px}.fc-basic-view td.fc-day-number,.fc-basic-view td.fc-week-number span{padding-top:2px;padding-bottom:2px}.fc-basic-view .fc-week-number{text-align:center}.fc-basic-view .fc-week-number span{display:inline-block;min-width:1.25em}.fc-ltr .fc-basic-view .fc-day-number{text-align:right}.fc-rtl .fc-basic-view .fc-day-number{text-align:left}.fc-day-number.fc-other-month{opacity:.3;filter:alpha(opacity=30)}.fc-agenda-view .fc-day-grid{position:relative;z-index:2}.fc-agenda-view .fc-day-grid .fc-row{min-height:3em}.fc-agenda-view .fc-day-grid .fc-row .fc-content-skeleton{padding-top:1px;padding-bottom:1em}.fc .fc-axis{vertical-align:middle;padding:0 4px;white-space:nowrap}.fc-ltr .fc-axis{text-align:right}.fc-rtl .fc-axis{text-align:left}.ui-widget td.fc-axis{font-weight:400}.fc-time-grid,.fc-time-grid-container{position:relative;z-index:1}.fc-time-grid{min-height:100%}.fc-time-grid table{border:0 hidden transparent}.fc-time-grid>.fc-bg{z-index:1}.fc-time-grid .fc-slats,.fc-time-grid>hr{position:relative;z-index:2}.fc-time-grid .fc-highlight-skeleton{z-index:3}.fc-time-grid .fc-content-skeleton{position:absolute;z-index:4;top:0;left:0;right:0}.fc-time-grid>.fc-helper-skeleton{z-index:5}.fc-slats td{height:1.5em;border-bottom:0}.fc-slats .fc-minor td{border-top-style:dotted}.fc-slats .ui-widget-content{background:0 0}.fc-time-grid .fc-highlight-container{position:relative}.fc-time-grid .fc-highlight{position:absolute;left:0;right:0}.fc-time-grid .fc-event-container{position:relative}.fc-ltr .fc-time-grid .fc-event-container{margin:0 2.5% 0 2px}.fc-rtl .fc-time-grid .fc-event-container{margin:0 2px 0 2.5%}.fc-time-grid .fc-event{position:absolute;z-index:1}.fc-time-grid-event.fc-not-start{border-top-width:0;padding-top:1px;border-top-left-radius:0;border-top-right-radius:0}.fc-time-grid-event.fc-not-end{border-bottom-width:0;padding-bottom:1px;border-bottom-left-radius:0;border-bottom-right-radius:0}.fc-time-grid-event{overflow:hidden}.fc-time-grid-event>.fc-content{position:relative;z-index:2}.fc-time-grid-event .fc-time,.fc-time-grid-event .fc-title{padding:0 1px}.fc-time-grid-event .fc-time{font-size:.85em;white-space:nowrap}.fc-time-grid-event .fc-bg{z-index:1;background:#fff;opacity:.25;filter:alpha(opacity=25)}.fc-time-grid-event.fc-short .fc-content{white-space:nowrap}.fc-time-grid-event.fc-short .fc-time,.fc-time-grid-event.fc-short .fc-title{display:inline-block;vertical-align:top}.fc-time-grid-event.fc-short .fc-time span{display:none}.fc-time-grid-event.fc-short .fc-time:before{content:attr(data-start)}.fc-time-grid-event.fc-short .fc-time:after{content:"\000A0-\000A0"}.fc-time-grid-event.fc-short .fc-title{font-size:.85em;padding:0}.fc-time-grid-event .fc-resizer{position:absolute;z-index:3;left:0;right:0;bottom:0;height:8px;overflow:hidden;line-height:8px;font-size:11px;font-family:monospace;text-align:center;cursor:s-resize}.fc-time-grid-event .fc-resizer:after{content:"="} \ No newline at end of file diff --git a/dist/cdnjs/2.1.1/fullcalendar.min.js b/dist/cdnjs/2.1.1/fullcalendar.min.js index aa83c57..93bbc04 100644 --- a/dist/cdnjs/2.1.1/fullcalendar.min.js +++ b/dist/cdnjs/2.1.1/fullcalendar.min.js @@ -3,6 +3,6 @@ * Docs & License: http://arshaw.com/fullcalendar/ * (c) 2013 Adam Shaw */ -(function(t){"function"==typeof define&&define.amd?define(["jquery","moment"],t):t(jQuery,moment)})(function(t,e){function i(t,e){return e.longDateFormat("LT").replace(":mm","(:mm)").replace(/(\Wmm)$/,"($1)").replace(/\s*a$/i,"t")}function n(t,e){var i=e.longDateFormat("L");return i=i.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g,""),t.isRTL?i+=" ddd":i="ddd "+i,i}function r(t){o(De,t)}function o(e){function i(i,n){t.isPlainObject(n)&&t.isPlainObject(e[i])&&!s(i)?e[i]=o({},e[i],n):void 0!==n&&(e[i]=n)}for(var n=1;arguments.length>n;n++)t.each(arguments[n],i);return e}function s(t){return/(Time|Duration)$/.test(t)}function l(i,n){function r(t){var i=e.localeData||e.langData;return i.call(e,t)||i.call(e,"en")}function s(t){ie?h()&&(p(),f(t)):l()}function l(){ne=K.theme?"ui":"fc",i.addClass("fc"),K.isRTL?i.addClass("fc-rtl"):i.addClass("fc-ltr"),K.theme?i.addClass("ui-widget"):i.addClass("fc-unthemed"),ie=t("
").prependTo(i),te=new a(q,K),ee=te.render(),ee&&i.prepend(ee),u(K.defaultView),K.handleWindowResize&&(se=L(v,K.windowResizeDelay),t(window).resize(se))}function d(){re&&re.destroy(),te.destroy(),ie.remove(),i.removeClass("fc fc-ltr fc-rtl fc-unthemed ui-widget"),t(window).unbind("resize",se)}function h(){return i.is(":visible")}function u(t){f(0,t)}function f(e,i){he++,re&&i&&re.name!==i&&(te.deactivateButton(re.name),I(),re.start&&re.destroy(),re.el.remove(),re=null),!re&&i&&(re=new xe[i](q),re.el=t("
").appendTo(ie),te.activateButton(i)),re&&(e&&(le=re.incrementDate(le,e)),re.start&&!e&&le.isWithin(re.intervalStart,re.intervalEnd)||h()&&(I(),re.start&&re.destroy(),re.render(le),Z(),C(),x(),b())),Z(),he--}function g(t){return h()?(t&&m(),he++,re.updateSize(!0),he--,!0):void 0}function p(){h()&&m()}function m(){oe="number"==typeof K.contentHeight?K.contentHeight:"number"==typeof K.height?K.height-(ee?ee.outerHeight(!0):0):Math.round(ie.width()/Math.max(K.aspectRatio,.5))}function v(t){!he&&t.target===window&&re.start&&g(!0)&&re.trigger("windowResize",de)}function y(){E(),S()}function w(){h()&&(I(),re.destroyEvents(),re.renderEvents(ue),Z())}function E(){I(),re.destroyEvents(),Z()}function b(){!K.lazyFetching||ae(re.start,re.end)?S():w()}function S(){ce(re.start,re.end)}function D(t){ue=t,w()}function T(){w()}function C(){te.updateTitle(re.title)}function x(){var t=q.getNow();t.isWithin(re.intervalStart,re.intervalEnd)?te.disableButton("today"):te.enableButton("today")}function k(t,e){t=q.moment(t),e=e?q.moment(e):t.hasTime()?t.clone().add(q.defaultTimedEventDuration):t.clone().add(q.defaultAllDayEventDuration),re.select(t,e)}function M(){re&&re.unselect()}function R(){f(-1)}function P(){f(1)}function G(){le.add(-1,"years"),f()}function N(){le.add(1,"years"),f()}function Y(){le=q.getNow(),f()}function A(t){le=q.moment(t),f()}function _(t){le.add(e.duration(t)),f()}function O(t,e){var i,n;e&&void 0!==xe[e]||(e=e||"day",i=te.getViewsWithButtons().join(" "),n=i.match(RegExp("\\w+"+z(e))),n||(n=i.match(/\w+Day/)),e=n?n[0]:"agendaDay"),le=t,u(e)}function F(){return le.clone()}function I(){ie.css({width:"100%",height:ie.height(),overflow:"hidden"})}function Z(){ie.css({width:"",height:"",overflow:""})}function B(){return q}function j(){return re}function X(t,e){return void 0===e?K[t]:(("height"==t||"contentHeight"==t||"aspectRatio"==t)&&(K[t]=e,g(!0)),void 0)}function $(t,e){return K[t]?K[t].apply(e||de,Array.prototype.slice.call(arguments,2)):void 0}var q=this;n=n||{};var U,K=o({},De,n);U=K.lang in Te?Te[K.lang]:Te[De.lang],U&&(K=o({},De,U,n)),K.isRTL&&(K=o({},De,Ce,U||{},n)),q.options=K,q.render=s,q.destroy=d,q.refetchEvents=y,q.reportEvents=D,q.reportEventChange=T,q.rerenderEvents=w,q.changeView=u,q.select=k,q.unselect=M,q.prev=R,q.next=P,q.prevYear=G,q.nextYear=N,q.today=Y,q.gotoDate=A,q.incrementDate=_,q.zoomTo=O,q.getDate=F,q.getCalendar=B,q.getView=j,q.option=X,q.trigger=$;var Q=H(r(K.lang));if(K.monthNames&&(Q._months=K.monthNames),K.monthNamesShort&&(Q._monthsShort=K.monthNamesShort),K.dayNames&&(Q._weekdays=K.dayNames),K.dayNamesShort&&(Q._weekdaysShort=K.dayNamesShort),null!=K.firstDay){var J=H(Q._week);J.dow=K.firstDay,Q._week=J}q.defaultAllDayEventDuration=e.duration(K.defaultAllDayEventDuration),q.defaultTimedEventDuration=e.duration(K.defaultTimedEventDuration),q.moment=function(){var t;return"local"===K.timezone?(t=He.moment.apply(null,arguments),t.hasTime()&&t.local()):t="UTC"===K.timezone?He.moment.utc.apply(null,arguments):He.moment.parseZone.apply(null,arguments),"_locale"in t?t._locale=Q:t._lang=Q,t},q.getIsAmbigTimezone=function(){return"local"!==K.timezone&&"UTC"!==K.timezone},q.rezoneDate=function(t){return q.moment(t.toArray())},q.getNow=function(){var t=K.now;return"function"==typeof t&&(t=t()),q.moment(t)},q.calculateWeekNumber=function(t){var e=K.weekNumberCalculation;return"function"==typeof e?e(t):"local"===e?t.week():"ISO"===e.toUpperCase()?t.isoWeek():void 0},q.getEventEnd=function(t){return t.end?t.end.clone():q.getDefaultEventEnd(t.allDay,t.start)},q.getDefaultEventEnd=function(t,e){var i=e.clone();return t?i.stripTime().add(q.defaultAllDayEventDuration):i.add(q.defaultTimedEventDuration),q.getIsAmbigTimezone()&&i.stripZone(),i},q.formatRange=function(t,e,i){return"function"==typeof i&&(i=i.call(q,K,Q)),W(t,e,i,null,K.isRTL)},q.formatDate=function(t,e){return"function"==typeof e&&(e=e.call(q,K,Q)),V(t,e)},c.call(q,K);var te,ee,ie,ne,re,oe,se,le,ae=q.isFetchNeeded,ce=q.fetchEvents,de=i[0],he=0,ue=[];le=null!=K.defaultDate?q.moment(K.defaultDate):q.getNow(),q.getSuggestedViewHeight=function(){return void 0===oe&&p(),oe},q.isHeightAuto=function(){return"auto"===K.contentHeight||"auto"===K.height}}function a(e,i){function n(){var e=i.header;return f=i.theme?"ui":"fc",e?g=t("
").append(o("left")).append(o("right")).append(o("center")).append('
'):void 0}function r(){g.remove()}function o(n){var r=t('
'),o=i.header[n];return o&&t.each(o.split(" "),function(){var n,o=t(),s=!0;t.each(this.split(","),function(n,r){var l,a,c,d,h,u,g,m;"title"==r?(o=o.add(t("

 

")),s=!1):(e[r]?l=function(){e[r]()}:xe[r]&&(l=function(){e.changeView(r)},p.push(r)),l&&(a=S(i.themeButtonIcons,r),c=S(i.buttonIcons,r),d=S(i.defaultButtonText,r),h=S(i.buttonText,r),u=h?R(h):a&&i.theme?"":c&&!i.theme?"":R(d||r),g=["fc-"+r+"-button",f+"-button",f+"-state-default"],m=t('").click(function(){m.hasClass(f+"-state-disabled")||(l(),(m.hasClass(f+"-state-active")||m.hasClass(f+"-state-disabled"))&&m.removeClass(f+"-state-hover"))}).mousedown(function(){m.not("."+f+"-state-active").not("."+f+"-state-disabled").addClass(f+"-state-down")}).mouseup(function(){m.removeClass(f+"-state-down")}).hover(function(){m.not("."+f+"-state-active").not("."+f+"-state-disabled").addClass(f+"-state-hover")},function(){m.removeClass(f+"-state-hover").removeClass(f+"-state-down")}),o=o.add(m)))}),s&&o.first().addClass(f+"-corner-left").end().last().addClass(f+"-corner-right").end(),o.length>1?(n=t("
"),s&&n.addClass("fc-button-group"),n.append(o),r.append(n)):r.append(o)}),r}function s(t){g.find("h2").text(t)}function l(t){g.find(".fc-"+t+"-button").addClass(f+"-state-active")}function a(t){g.find(".fc-"+t+"-button").removeClass(f+"-state-active")}function c(t){g.find(".fc-"+t+"-button").attr("disabled","disabled").addClass(f+"-state-disabled")}function d(t){g.find(".fc-"+t+"-button").removeAttr("disabled").removeClass(f+"-state-disabled")}function h(){return p}var u=this;u.render=n,u.destroy=r,u.updateTitle=s,u.activateButton=l,u.deactivateButton=a,u.disableButton=c,u.enableButton=d,u.getViewsWithButtons=h;var f,g=t(),p=[]}function c(e){function i(t,e){return!T||t.clone().stripZone()C.clone().stripZone()}function n(t,e){T=t,C=e,A=[];var i=++G,n=L.length;N=n;for(var o=0;n>o;o++)r(L[o],i)}function r(e,i){o(e,function(n){var r,o,s=t.isArray(e.events);if(i==G){if(n)for(r=0;n.length>r;r++)o=n[r],s||(o=w(o,e)),o&&A.push(o);N--,N||R(A)}})}function o(i,n){var r,s,l=He.sourceFetchers;for(r=0;l.length>r;r++){if(s=l[r].call(S,i,T.clone(),C.clone(),e.timezone,n),s===!0)return;if("object"==typeof s)return o(s,n),void 0}var a=i.events;if(a)t.isFunction(a)?(v(),a.call(S,T.clone(),C.clone(),e.timezone,function(t){n(t),y()})):t.isArray(a)?n(a):n();else{var c=i.url;if(c){var d,h=i.success,u=i.error,f=i.complete;d=t.isFunction(i.data)?i.data():i.data;var g=t.extend({},d||{}),p=M(i.startParam,e.startParam),m=M(i.endParam,e.endParam),w=M(i.timezoneParam,e.timezoneParam);p&&(g[p]=T.format()),m&&(g[m]=C.format()),e.timezone&&"local"!=e.timezone&&(g[w]=e.timezone),v(),t.ajax(t.extend({},ke,i,{data:g,success:function(e){e=e||[];var i=k(h,this,arguments);t.isArray(i)&&(e=i),n(e)},error:function(){k(u,this,arguments),n()},complete:function(){k(f,this,arguments),y()}}))}else n()}}function s(t){var e=l(t);e&&(L.push(e),N++,r(e,G))}function l(e){var i,n,r=He.sourceNormalizers;if(t.isFunction(e)||t.isArray(e)?i={events:e}:"string"==typeof e?i={url:e}:"object"==typeof e&&(i=t.extend({},e)),i){for(i.className?"string"==typeof i.className&&(i.className=i.className.split(/\s+/)):i.className=[],t.isArray(i.events)&&(i.origArray=i.events,i.events=t.map(i.events,function(t){return w(t,i)})),n=0;r.length>n;n++)r[n].call(S,i);return i}}function a(e){L=t.grep(L,function(t){return!c(t,e)}),A=t.grep(A,function(t){return!c(t.source,e)}),R(A)}function c(t,e){return t&&e&&h(t)==h(e)}function h(t){return("object"==typeof t?t.origArray||t.url||t.events:null)||t}function u(t){t.start=S.moment(t.start),t.end&&(t.end=S.moment(t.end)),E(t),f(t),R(A)}function f(t){var e,i,n,r;for(e=0;A.length>e;e++)if(i=A[e],i._id==t._id&&i!==t)for(n=0;V.length>n;n++)r=V[n],void 0!==t[r]&&(i[r]=t[r])}function g(t,e){var i=w(t);i&&(i.source||(e&&(z.events.push(i),i.source=z),A.push(i)),R(A))}function p(e){var i,n;for(null==e?e=function(){return!0}:t.isFunction(e)||(i=e+"",e=function(t){return t._id==i}),A=t.grep(A,e,!0),n=0;L.length>n;n++)t.isArray(L[n].events)&&(L[n].events=t.grep(L[n].events,e,!0));R(A)}function m(e){return t.isFunction(e)?t.grep(A,e):null!=e?(e+="",t.grep(A,function(t){return t._id==e})):A}function v(){Y++||H("loading",null,!0,x())}function y(){--Y||H("loading",null,!1,x())}function w(i,n){var r,o,s,l,a={};return e.eventDataTransform&&(i=e.eventDataTransform(i)),n&&n.eventDataTransform&&(i=n.eventDataTransform(i)),r=S.moment(i.start||i.date),r.isValid()&&(o=null,!i.end||(o=S.moment(i.end),o.isValid()))?(s=i.allDay,void 0===s&&(l=M(n?n.allDayDefault:void 0,e.allDayDefault),s=void 0!==l?l:!(r.hasTime()||o&&o.hasTime())),s?(r.hasTime()&&r.stripTime(),o&&o.hasTime()&&o.stripTime()):(r.hasTime()||(r=S.rezoneDate(r)),o&&!o.hasTime()&&(o=S.rezoneDate(o))),t.extend(a,i),n&&(a.source=n),a._id=i._id||(void 0===i.id?"_fc"+Me++:i.id+""),a.className=i.className?"string"==typeof i.className?i.className.split(/\s+/):i.className:[],a.resources?"string"==typeof a.resources&&(a.resources=a.resources.split(/\s+/)):a.resources=[],a.allDay=s,a.start=r,a.end=o,e.forceEventDuration&&!a.end&&(a.end=P(a)),d(a),a):void 0}function E(t,e,i){var n,r,o,s,l=t._allDay,a=t._start,c=t._end,d=!1;return e||i||(e=t.start,i=t.end),n=t.allDay!=l?t.allDay:!(e||i).hasTime(),n&&(e&&(e=e.clone().stripTime()),i&&(i=i.clone().stripTime())),e&&(r=n?D(e,a.clone().stripTime()):D(e,a)),n!=l?d=!0:i&&(o=D(i||S.getDefaultEventEnd(n,e||a),e||a).subtract(D(c||S.getDefaultEventEnd(l,a),a))),s=b(m(t._id),d,n,r,o),{dateDelta:r,durationDelta:o,undo:s}}function b(i,n,r,o,s){var l=S.getIsAmbigTimezone(),a=[];return t.each(i,function(t,i){var c=i._allDay,h=i._start,u=i._end,f=null!=r?r:c,g=h.clone(),p=!n&&u?u.clone():null;f?(g.stripTime(),p&&p.stripTime()):(g.hasTime()||(g=S.rezoneDate(g)),p&&!p.hasTime()&&(p=S.rezoneDate(p))),p||!e.forceEventDuration&&!+s||(p=S.getDefaultEventEnd(f,g)),g.add(o),p&&p.add(o).add(s),l&&(+o||+s)&&(g.stripZone(),p&&p.stripZone()),i.allDay=f,i.start=g,i.end=p,d(i),a.push(function(){i.allDay=c,i.start=h,i.end=u,d(i)})}),function(){for(var t=0;a.length>t;t++)a[t]()}}var S=this;S.isFetchNeeded=i,S.fetchEvents=n,S.addEventSource=s,S.removeEventSource=a,S.updateEvent=u,S.renderEvent=g,S.removeEvents=p,S.clientEvents=m,S.mutateEvent=E;var T,C,H=S.trigger,x=S.getView,R=S.reportEvents,P=S.getEventEnd,z={events:[]},L=[z],G=0,N=0,Y=0,A=[];t.each((e.events?[e.events]:[]).concat(e.eventSources||[]),function(t,e){var i=l(e);i&&L.push(i)});var V=["title","url","allDay","className","editable","color","backgroundColor","borderColor","textColor"]}function d(t){t._allDay=t.allDay,t._start=t.start.clone(),t._end=t.end?t.end.clone():null}function h(t,e){e.left&&t.css({"border-left-width":1,"margin-left":e.left-1}),e.right&&t.css({"border-right-width":1,"margin-right":e.right-1})}function u(t){t.css({"margin-left":"","margin-right":"","border-left-width":"","border-right-width":""})}function f(e,i,n){var r=Math.floor(i/e.length),o=Math.floor(i-r*(e.length-1)),s=[],l=[],a=[],c=0;g(e),e.each(function(i,n){var d=i===e.length-1?o:r,h=t(n).outerHeight(!0);d>h?(s.push(n),l.push(h),a.push(t(n).height())):c+=h}),n&&(i-=c,r=Math.floor(i/s.length),o=Math.floor(i-r*(s.length-1))),t(s).each(function(e,i){var n=e===s.length-1?o:r,c=l[e],d=a[e],h=n-(c-d);n>c&&t(i).height(h)})}function g(t){t.height("")}function p(e){var i=0;return e.find("> *").each(function(e,n){var r=t(n).outerWidth();r>i&&(i=r)}),i++,e.width(i),i}function m(t,e){return t.height(e).addClass("fc-scroller"),t[0].scrollHeight-1>t[0].clientHeight?!0:(v(t),!1)}function v(t){t.height("").removeClass("fc-scroller")}function y(e){var i=e.css("position"),n=e.parents().filter(function(){var e=t(this);return/(auto|scroll)/.test(e.css("overflow")+e.css("overflow-y")+e.css("overflow-x"))}).eq(0);return"fixed"!==i&&n.length?n:t(e[0].ownerDocument||document)}function w(t){var e=t.offset().left,i=e+t.width(),n=t.children(),r=n.offset().left,o=r+n.outerWidth();return{left:r-e,right:i-o}}function E(t){return 1==t.which&&!t.ctrlKey}function b(t,e,i,n){var r,o,s,l;return e>i&&n>t?(t>=i?(r=t.clone(),s=!0):(r=i.clone(),s=!1),n>=e?(o=e.clone(),l=!0):(o=n.clone(),l=!1),{start:r,end:o,isStart:s,isEnd:l}):void 0}function S(t,e){if(t=t||{},void 0!==t[e])return t[e];for(var i,n=e.split(/(?=[A-Z])/),r=n.length-1;r>=0;r--)if(i=t[n[r].toLowerCase()],void 0!==i)return i;return t["default"]}function D(t,i){return e.duration({days:t.clone().stripTime().diff(i.clone().stripTime(),"days"),ms:t.time()-i.time()})}function T(t){return"[object Date]"===Object.prototype.toString.call(t)||t instanceof Date}function C(t,e){return t-e}function H(t){var e=function(){};return e.prototype=t,new e}function x(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i])}function k(e,i,n){if(t.isFunction(e)&&(e=[e]),e){var r,o;for(r=0;e.length>r;r++)o=e[r].apply(i,n)||o;return o}}function M(){for(var t=0;arguments.length>t;t++)if(void 0!==arguments[t])return arguments[t]}function R(t){return(t+"").replace(/&/g,"&").replace(//g,">").replace(/'/g,"'").replace(/"/g,""").replace(/\n/g,"
")}function P(t){return t.replace(/&.*?;/g,"")}function z(t){return t.charAt(0).toUpperCase()+t.slice(1)}function L(t,e){var i,n,r,o,s=function(){var l=+new Date-o;e>l&&l>0?i=setTimeout(s,e-l):(i=null,t.apply(r,n),i||(r=n=null))};return function(){r=this,n=arguments,o=+new Date,i||(i=setTimeout(s,e))}}function G(i,n,r){var o,s,l,a,c=i[0],d=1==i.length&&"string"==typeof c;return e.isMoment(c)?(a=e.apply(null,i),c._ambigTime&&(a._ambigTime=!0),c._ambigZone&&(a._ambigZone=!0)):T(c)||void 0===c?a=e.apply(null,i):(o=!1,s=!1,d?Pe.test(c)?(c+="-01",i=[c],o=!0,s=!0):(l=ze.exec(c))&&(o=!l[5],s=!0):t.isArray(c)&&(s=!0),a=n?e.utc.apply(e,i):e.apply(null,i),o?(a._ambigTime=!0,a._ambigZone=!0):r&&(s?a._ambigZone=!0:d&&a.zone(c))),new N(a)}function N(t){x(this,t)}function Y(t,e){var i,n=[],r=!1,o=!1;for(i=0;t.length>i;i++)n.push(He.moment.parseZone(t[i])),r=r||n[i]._ambigTime,o=o||n[i]._ambigZone;for(i=0;n.length>i;i++)r&&!e?n[i].stripTime():o&&n[i].stripZone();return n}function A(t,i){return e.fn.format.call(t,i)}function V(t,e){return _(t,Z(e))}function _(t,e){var i,n="";for(i=0;e.length>i;i++)n+=O(t,e[i]);return n}function O(t,e){var i,n;return"string"==typeof e?e:(i=e.token)?Le[i]?Le[i](t):A(t,i):e.maybe&&(n=_(t,e.maybe),n.match(/[1-9]/))?n:""}function W(t,e,i,n,r){var o;return t=He.moment.parseZone(t),e=He.moment.parseZone(e),o=(t.localeData||t.lang).call(t),i=o.longDateFormat(i)||i,n=n||" - ",F(t,e,Z(i),n,r)}function F(t,e,i,n,r){var o,s,l,a,c="",d="",h="",u="",f="";for(s=0;i.length>s&&(o=I(t,e,i[s]),o!==!1);s++)c+=o;for(l=i.length-1;l>s&&(o=I(t,e,i[l]),o!==!1);l--)d=o+d;for(a=s;l>=a;a++)h+=O(t,i[a]),u+=O(e,i[a]);return(h||u)&&(f=r?u+n+h:h+n+u),c+f+d}function I(t,e,i){var n,r;return"string"==typeof i?i:(n=i.token)&&(r=Ge[n.charAt(0)],r&&t.isSame(e,r))?A(t,n):!1}function Z(t){return t in Ne?Ne[t]:Ne[t]=B(t)}function B(t){for(var e,i=[],n=/\[([^\]]*)\]|\(([^\)]*)\)|(LT|(\w)\4*o?)|([^\w\[\(]+)/g;e=n.exec(t);)e[1]?i.push(e[1]):e[2]?i.push({maybe:B(e[2])}):e[3]?i.push({token:e[3]}):e[5]&&i.push(e[5]);return i}function j(t){this.options=t||{}}function X(t){this.grid=t}function $(t){this.coordMaps=t}function q(t,e){this.coordMap=t,this.options=e||{}}function U(t,e){return t||e?t&&e?t.grid===e.grid&&t.row===e.row&&t.col===e.col:!1:!0}function K(e,i){this.options=i=i||{},this.sourceEl=e,this.parentEl=i.parentEl?t(i.parentEl):e.parent()}function Q(t){this.view=t}function J(t){Q.call(this,t),this.coordMap=new X(this)}function te(t,e){return t.eventStartMS-e.eventStartMS||e.eventDurationMS-t.eventDurationMS||e.event.allDay-t.event.allDay||(t.event.title||"").localeCompare(e.event.title)}function ee(t){J.call(this,t)}function ie(t,e){var i,n;for(i=0;e.length>i;i++)if(n=e[i],n.leftCol<=t.rightCol&&n.rightCol>=t.leftCol)return!0;return!1}function ne(t,e){return t.leftCol-e.leftCol}function re(t){J.call(this,t)}function oe(t){var e,i,n;if(t.sort(te),e=se(t),le(e),i=e[0]){for(n=0;i.length>n;n++)ae(i[n]);for(n=0;i.length>n;n++)ce(i[n],0,0)}}function se(t){var e,i,n,r=[];for(e=0;t.length>e;e++){for(i=t[e],n=0;r.length>n&&de(i,r[n]).length;n++);i.level=n,(r[n]||(r[n]=[])).push(i)}return r}function le(t){var e,i,n,r,o;for(e=0;t.length>e;e++)for(i=t[e],n=0;i.length>n;n++)for(r=i[n],r.forwardSegs=[],o=e+1;t.length>o;o++)de(r,t[o],r.forwardSegs)}function ae(t){var e,i,n=t.forwardSegs,r=0;if(void 0===t.forwardPressure){for(e=0;n.length>e;e++)i=n[e],ae(i),r=Math.max(r,1+i.forwardPressure);t.forwardPressure=r}}function ce(t,e,i){var n,r=t.forwardSegs;if(void 0===t.forwardCoord)for(r.length?(r.sort(ue),ce(r[0],e+1,i),t.forwardCoord=r[0].backwardCoord):t.forwardCoord=1,t.backwardCoord=t.forwardCoord-(t.forwardCoord-i)/(e+1),n=0;r.length>n;n++)ce(r[n],0,t.forwardCoord)}function de(t,e,i){i=i||[];for(var n=0;e.length>n;n++)he(t,e[n])&&i.push(e[n]);return i}function he(t,e){return t.bottom>e.top&&t.topd;d++){var h=d*n,u=h+n-1,f=Math.max(a,h),g=Math.min(c,u);if(g>=f){var m=E(f),v=E(g),b=[m.col,v.col].sort(),S=p(f)==s,T=p(g)+1==l;r.push({row:d,leftCol:b[0],rightCol:b[1],isStart:S,isEnd:T})}}return r}function D(t,e){var i,n,r=t.clone().stripTime();return e&&(i=e.clone().stripTime(),n=+e.time(),n&&n>=k&&i.add(1,"days")),(!e||r>=i)&&(i=r.clone().add(1,"days")),{start:r,end:i}}function T(t){var e=D(t.start,t.end);return e.end.diff(e.start,"days")>1}var C=this;C.calendar=i,C.opt=n,C.trigger=r,C.isEventDraggable=o,C.isEventResizable=l,C.eventDrop=a,C.eventResize=c;var H=i.reportEventChange,x=i.options,k=e.duration(x.nextDayThreshold);C.init(),C.getEventTimeText=function(t,e){var r,o;return"object"==typeof t&&"object"==typeof e?(r=t,o=e,e=arguments[2]):(r=t.start,o=t.end),e=e||n("timeFormat"),o&&n("displayEventEnd")?i.formatRange(r,o,e):i.formatDate(r,e)},C.isHiddenDay=d,C.skipHiddenDays=u,C.getCellsPerWeek=h,C.dateToCell=v,C.dateToDayOffset=y,C.dayOffsetToCellOffset=w,C.cellOffsetToCell=E,C.cellToDate=f,C.cellToCellOffset=g,C.cellOffsetToDayOffset=p,C.dayOffsetToDate=m,C.rangeToSegments=b,C.isMultiDayEvent=T;var R,P=n("hiddenDays")||[],z=[],L=[],G=[],N=n("isRTL");(function(){n("weekends")===!1&&P.push(0,6);for(var e=0,i=0;7>e;e++)L[e]=i,z[e]=-1!=t.inArray(e,P),z[e]||(G[i]=e,i++);if(R=i,!R)throw"invalid hiddenDays"})()}function ge(t){fe.call(this,t),this.dayGrid=new ee(this),this.coordMap=this.dayGrid.coordMap}function pe(t){ge.call(this,t)}function me(t){ge.call(this,t)}function ve(t){ge.call(this,t)}function ye(t,e){return e.longDateFormat("LT").replace(":mm","(:mm)").replace(/(\Wmm)$/,"($1)").replace(/\s*a$/i,"a")}function we(t,e){return e.longDateFormat("LT").replace(/\s*a$/i,"")}function Ee(t){fe.call(this,t),this.timeGrid=new re(this),this.opt("allDaySlot")?(this.dayGrid=new ee(this),this.coordMap=new $([this.dayGrid.coordMap,this.timeGrid.coordMap])):this.coordMap=this.timeGrid.coordMap}function be(t){Ee.call(this,t)}function Se(t){Ee.call(this,t)}var De={lang:"en",defaultTimedEventDuration:"02:00:00",defaultAllDayEventDuration:{days:1},forceEventDuration:!1,nextDayThreshold:"09:00:00",defaultView:"month",aspectRatio:1.35,header:{left:"title",center:"",right:"today prev,next"},weekends:!0,weekNumbers:!1,weekNumberTitle:"W",weekNumberCalculation:"local",lazyFetching:!0,startParam:"start",endParam:"end",timezoneParam:"timezone",timezone:!1,titleFormat:{month:"MMMM YYYY",week:"ll",day:"LL"},columnFormat:{month:"ddd",week:n,day:"dddd"},timeFormat:{"default":i},displayEventEnd:{month:!1,basicWeek:!1,"default":!0},isRTL:!1,defaultButtonText:{prev:"prev",next:"next",prevYear:"prev year",nextYear:"next year",today:"today",month:"month",week:"week",day:"day"},buttonIcons:{prev:"left-single-arrow",next:"right-single-arrow",prevYear:"left-double-arrow",nextYear:"right-double-arrow"},theme:!1,themeButtonIcons:{prev:"circle-triangle-w",next:"circle-triangle-e",prevYear:"seek-prev",nextYear:"seek-next"},dragOpacity:.75,dragRevertDuration:500,dragScroll:!0,unselectAuto:!0,dropAccept:"*",eventLimit:!1,eventLimitText:"more",eventLimitClick:"popover",dayPopoverFormat:"LL",handleWindowResize:!0,windowResizeDelay:200},Te={en:{columnFormat:{week:"ddd M/D"},dayPopoverFormat:"dddd, MMMM D"}},Ce={header:{left:"next,prev today",center:"",right:"title"},buttonIcons:{prev:"right-single-arrow",next:"left-single-arrow",prevYear:"right-double-arrow",nextYear:"left-double-arrow"},themeButtonIcons:{prev:"circle-triangle-e",next:"circle-triangle-w",nextYear:"seek-prev",prevYear:"seek-next"}},He=t.fullCalendar={version:"2.1.1"},xe=He.views={};t.fn.fullCalendar=function(e){var i=Array.prototype.slice.call(arguments,1),n=this;return this.each(function(r,o){var s,a=t(o),c=a.data("fullCalendar");"string"==typeof e?c&&t.isFunction(c[e])&&(s=c[e].apply(c,i),r||(n=s),"destroy"===e&&a.removeData("fullCalendar")):c||(c=new l(a,e),a.data("fullCalendar",c),c.render())}),n},He.langs=Te,He.datepickerLang=function(e,i,n){var r=Te[e];r||(r=Te[e]={}),o(r,{isRTL:n.isRTL,weekNumberTitle:n.weekHeader,titleFormat:{month:n.showMonthAfterYear?"YYYY["+n.yearSuffix+"] MMMM":"MMMM YYYY["+n.yearSuffix+"]"},defaultButtonText:{prev:P(n.prevText),next:P(n.nextText),today:P(n.currentText)}}),t.datepicker&&(t.datepicker.regional[i]=t.datepicker.regional[e]=n,t.datepicker.regional.en=t.datepicker.regional[""],t.datepicker.setDefaults(n))},He.lang=function(t,e){var i;e&&(i=Te[t],i||(i=Te[t]={}),o(i,e||{})),De.lang=t},He.sourceNormalizers=[],He.sourceFetchers=[];var ke={dataType:"json",cache:!1},Me=1,Re=["sun","mon","tue","wed","thu","fri","sat"];He.applyAll=k;var Pe=/^\s*\d{4}-\d\d$/,ze=/^\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+)?)?)?)?)?$/;He.moment=function(){return G(arguments)},He.moment.utc=function(){var t=G(arguments,!0);return t.hasTime()&&t.utc(),t},He.moment.parseZone=function(){return G(arguments,!0,!0)},N.prototype=H(e.fn),N.prototype.clone=function(){return G([this])},N.prototype.time=function(t){if(null==t)return e.duration({hours:this.hours(),minutes:this.minutes(),seconds:this.seconds(),milliseconds:this.milliseconds()});delete this._ambigTime,e.isDuration(t)||e.isMoment(t)||(t=e.duration(t));var i=0;return e.isDuration(t)&&(i=24*Math.floor(t.asDays())),this.hours(i+t.hours()).minutes(t.minutes()).seconds(t.seconds()).milliseconds(t.milliseconds())},N.prototype.stripTime=function(){var t=this.toArray();return e.fn.utc.call(this),this.year(t[0]).month(t[1]).date(t[2]).hours(0).minutes(0).seconds(0).milliseconds(0),this._ambigTime=!0,this._ambigZone=!0,this},N.prototype.hasTime=function(){return!this._ambigTime},N.prototype.stripZone=function(){var t=this.toArray(),i=this._ambigTime;return e.fn.utc.call(this),this.year(t[0]).month(t[1]).date(t[2]).hours(t[3]).minutes(t[4]).seconds(t[5]).milliseconds(t[6]),i&&(this._ambigTime=!0),this._ambigZone=!0,this},N.prototype.hasZone=function(){return!this._ambigZone},N.prototype.zone=function(t){return null!=t&&(delete this._ambigTime,delete this._ambigZone),e.fn.zone.apply(this,arguments)},N.prototype.local=function(){var t=this.toArray(),i=this._ambigZone;return delete this._ambigTime,delete this._ambigZone,e.fn.local.apply(this,arguments),i&&this.year(t[0]).month(t[1]).date(t[2]).hours(t[3]).minutes(t[4]).seconds(t[5]).milliseconds(t[6]),this},N.prototype.utc=function(){return delete this._ambigTime,delete this._ambigZone,e.fn.utc.apply(this,arguments)},N.prototype.format=function(){return arguments[0]?V(this,arguments[0]):this._ambigTime?A(this,"YYYY-MM-DD"):this._ambigZone?A(this,"YYYY-MM-DD[T]HH:mm:ss"):A(this)},N.prototype.toISOString=function(){return this._ambigTime?A(this,"YYYY-MM-DD"):this._ambigZone?A(this,"YYYY-MM-DD[T]HH:mm:ss"):e.fn.toISOString.apply(this,arguments)},N.prototype.isWithin=function(t,e){var i=Y([this,t,e]);return i[0]>=i[1]&&i[0]').addClass(i.className||"").css({top:0,left:0}).append(i.content).appendTo(i.parentEl),this.el.on("click",".fc-close",function(){e.hide()}),i.autoHide&&t(document).on("mousedown",this.documentMousedownProxy=t.proxy(this,"documentMousedown"))},documentMousedown:function(e){this.el&&!t(e.target).closest(this.el).length&&this.hide()},destroy:function(){this.hide(),this.el&&(this.el.remove(),this.el=null),t(document).off("mousedown",this.documentMousedownProxy)},position:function(){var e,i,n,r,o,s=this.options,l=this.el.offsetParent().offset(),a=this.el.outerWidth(),c=this.el.outerHeight(),d=t(window),h=y(this.el);r=s.top||0,o=void 0!==s.left?s.left:void 0!==s.right?s.right-a:0,h.is(window)||h.is(document)?(h=d,e=0,i=0):(n=h.offset(),e=n.top,i=n.left),e+=d.scrollTop(),i+=d.scrollLeft(),s.viewportConstrain!==!1&&(r=Math.min(r,e+h.outerHeight()-c-this.margin),r=Math.max(r,e+this.margin),o=Math.min(o,i+h.outerWidth()-a-this.margin),o=Math.max(o,i+this.margin)),this.el.css({top:r-l.top,left:o-l.left})},trigger:function(t){this.options[t]&&this.options[t].apply(this,Array.prototype.slice.call(arguments,1))}},X.prototype={grid:null,rows:null,cols:null,containerEl:null,minX:null,maxX:null,minY:null,maxY:null,build:function(){this.grid.buildCoords(this.rows=[],this.cols=[]),this.computeBounds()},getCell:function(t,e){var i,n=null,r=this.rows,o=this.cols,s=-1,l=-1;if(this.inBounds(t,e)){for(i=0;r.length>i;i++)if(e>=r[i][0]&&r[i][1]>e){s=i;break}for(i=0;o.length>i;i++)if(t>=o[i][0]&&o[i][1]>t){l=i;break}s>=0&&l>=0&&(n={row:s,col:l},n.grid=this.grid,n.date=this.grid.getCellDate(n))}return n},computeBounds:function(){var t;this.containerEl&&(t=this.containerEl.offset(),this.minX=t.left,this.maxX=t.left+this.containerEl.outerWidth(),this.minY=t.top,this.maxY=t.top+this.containerEl.outerHeight())},inBounds:function(t,e){return this.containerEl?t>=this.minX&&this.maxX>t&&e>=this.minY&&this.maxY>e:!0}},$.prototype={coordMaps:null,build:function(){var t,e=this.coordMaps;for(t=0;e.length>t;t++)e[t].build()},getCell:function(t,e){var i,n=this.coordMaps,r=null;for(i=0;n.length>i&&!r;i++)r=n[i].getCell(t,e);return r}},q.prototype={coordMap:null,options:null,isListening:!1,isDragging:!1,origCell:null,origDate:null,cell:null,date:null,mouseX0:null,mouseY0:null,mousemoveProxy:null,mouseupProxy:null,scrollEl:null,scrollBounds:null,scrollTopVel:null,scrollLeftVel:null,scrollIntervalId:null,scrollHandlerProxy:null,scrollSensitivity:30,scrollSpeed:200,scrollIntervalMs:50,mousedown:function(t){E(t)&&(t.preventDefault(),this.startListening(t),this.options.distance||this.startDrag(t))},startListening:function(e){var i,n;this.isListening||(e&&this.options.scroll&&(i=y(t(e.target)),i.is(window)||i.is(document)||(this.scrollEl=i,this.scrollHandlerProxy=L(t.proxy(this,"scrollHandler"),100),this.scrollEl.on("scroll",this.scrollHandlerProxy))),this.computeCoords(),e&&(n=this.getCell(e),this.origCell=n,this.origDate=n?n.date:null,this.mouseX0=e.pageX,this.mouseY0=e.pageY),t(document).on("mousemove",this.mousemoveProxy=t.proxy(this,"mousemove")).on("mouseup",this.mouseupProxy=t.proxy(this,"mouseup")).on("selectstart",this.preventDefault),this.isListening=!0,this.trigger("listenStart",e))},computeCoords:function(){this.coordMap.build(),this.computeScrollBounds()},mousemove:function(t){var e,i;this.isDragging||(e=this.options.distance||1,i=Math.pow(t.pageX-this.mouseX0,2)+Math.pow(t.pageY-this.mouseY0,2),i>=e*e&&this.startDrag(t)),this.isDragging&&this.drag(t) -},startDrag:function(t){var e;this.isListening||this.startListening(),this.isDragging||(this.isDragging=!0,this.trigger("dragStart",t),e=this.getCell(t),e&&this.cellOver(e,!0))},drag:function(t){var e;this.isDragging&&(e=this.getCell(t),U(e,this.cell)||(this.cell&&this.cellOut(),e&&this.cellOver(e)),this.dragScroll(t))},cellOver:function(t){this.cell=t,this.date=t.date,this.trigger("cellOver",t,t.date)},cellOut:function(){this.cell&&(this.trigger("cellOut",this.cell),this.cell=null,this.date=null)},mouseup:function(t){this.stopDrag(t),this.stopListening(t)},stopDrag:function(t){this.isDragging&&(this.stopScrolling(),this.trigger("dragStop",t),this.isDragging=!1)},stopListening:function(e){this.isListening&&(this.scrollEl&&(this.scrollEl.off("scroll",this.scrollHandlerProxy),this.scrollHandlerProxy=null),t(document).off("mousemove",this.mousemoveProxy).off("mouseup",this.mouseupProxy).off("selectstart",this.preventDefault),this.mousemoveProxy=null,this.mouseupProxy=null,this.isListening=!1,this.trigger("listenStop",e),this.origCell=this.cell=null,this.origDate=this.date=null)},getCell:function(t){return this.coordMap.getCell(t.pageX,t.pageY)},trigger:function(t){this.options[t]&&this.options[t].apply(this,Array.prototype.slice.call(arguments,1))},preventDefault:function(t){t.preventDefault()},computeScrollBounds:function(){var t,e=this.scrollEl;e&&(t=e.offset(),this.scrollBounds={top:t.top,left:t.left,bottom:t.top+e.outerHeight(),right:t.left+e.outerWidth()})},dragScroll:function(t){var e,i,n,r,o=this.scrollSensitivity,s=this.scrollBounds,l=0,a=0;s&&(e=(o-(t.pageY-s.top))/o,i=(o-(s.bottom-t.pageY))/o,n=(o-(t.pageX-s.left))/o,r=(o-(s.right-t.pageX))/o,e>=0&&1>=e?l=-1*e*this.scrollSpeed:i>=0&&1>=i&&(l=i*this.scrollSpeed),n>=0&&1>=n?a=-1*n*this.scrollSpeed:r>=0&&1>=r&&(a=r*this.scrollSpeed)),this.setScrollVel(l,a)},setScrollVel:function(e,i){this.scrollTopVel=e,this.scrollLeftVel=i,this.constrainScrollVel(),!this.scrollTopVel&&!this.scrollLeftVel||this.scrollIntervalId||(this.scrollIntervalId=setInterval(t.proxy(this,"scrollIntervalFunc"),this.scrollIntervalMs))},constrainScrollVel:function(){var t=this.scrollEl;0>this.scrollTopVel?0>=t.scrollTop()&&(this.scrollTopVel=0):this.scrollTopVel>0&&t.scrollTop()+t[0].clientHeight>=t[0].scrollHeight&&(this.scrollTopVel=0),0>this.scrollLeftVel?0>=t.scrollLeft()&&(this.scrollLeftVel=0):this.scrollLeftVel>0&&t.scrollLeft()+t[0].clientWidth>=t[0].scrollWidth&&(this.scrollLeftVel=0)},scrollIntervalFunc:function(){var t=this.scrollEl,e=this.scrollIntervalMs/1e3;this.scrollTopVel&&t.scrollTop(t.scrollTop()+this.scrollTopVel*e),this.scrollLeftVel&&t.scrollLeft(t.scrollLeft()+this.scrollLeftVel*e),this.constrainScrollVel(),this.scrollTopVel||this.scrollLeftVel||this.stopScrolling()},stopScrolling:function(){this.scrollIntervalId&&(clearInterval(this.scrollIntervalId),this.scrollIntervalId=null,this.computeCoords())},scrollHandler:function(){this.scrollIntervalId||this.computeCoords()}},K.prototype={options:null,sourceEl:null,el:null,parentEl:null,top0:null,left0:null,mouseY0:null,mouseX0:null,topDelta:null,leftDelta:null,mousemoveProxy:null,isFollowing:!1,isHidden:!1,isAnimating:!1,start:function(e){this.isFollowing||(this.isFollowing=!0,this.mouseY0=e.pageY,this.mouseX0=e.pageX,this.topDelta=0,this.leftDelta=0,this.isHidden||this.updatePosition(),t(document).on("mousemove",this.mousemoveProxy=t.proxy(this,"mousemove")))},stop:function(e,i){function n(){this.isAnimating=!1,r.destroyEl(),this.top0=this.left0=null,i&&i()}var r=this,o=this.options.revertDuration;this.isFollowing&&!this.isAnimating&&(this.isFollowing=!1,t(document).off("mousemove",this.mousemoveProxy),e&&o&&!this.isHidden?(this.isAnimating=!0,this.el.animate({top:this.top0,left:this.left0},{duration:o,complete:n})):n())},getEl:function(){var t=this.el;return t||(this.sourceEl.width(),t=this.el=this.sourceEl.clone().css({position:"absolute",visibility:"",display:this.isHidden?"none":"",margin:0,right:"auto",bottom:"auto",width:this.sourceEl.width(),height:this.sourceEl.height(),opacity:this.options.opacity||"",zIndex:this.options.zIndex}).appendTo(this.parentEl)),t},destroyEl:function(){this.el&&(this.el.remove(),this.el=null)},updatePosition:function(){var t,e;this.getEl(),null===this.top0&&(this.sourceEl.width(),t=this.sourceEl.offset(),e=this.el.offsetParent().offset(),this.top0=t.top-e.top,this.left0=t.left-e.left),this.el.css({top:this.top0+this.topDelta,left:this.left0+this.leftDelta})},mousemove:function(t){this.topDelta=t.pageY-this.mouseY0,this.leftDelta=t.pageX-this.mouseX0,this.isHidden||this.updatePosition()},hide:function(){this.isHidden||(this.isHidden=!0,this.el&&this.el.hide())},show:function(){this.isHidden&&(this.isHidden=!1,this.updatePosition(),this.getEl().show())}},Q.prototype={view:null,cellHtml:"",rowHtml:function(t,e){var i,n,r=this.view,o=this.getHtmlRenderer("cell",t),s="";for(e=e||0,i=0;r.colCnt>i;i++)n=r.cellToDate(e,i),s+=o(e,i,n);return s=this.bookendCells(s,t,e),""+s+""},bookendCells:function(t,e,i){var n=this.view,r=this.getHtmlRenderer("intro",e)(i||0),o=this.getHtmlRenderer("outro",e)(i||0),s=n.opt("isRTL"),l=s?o:r,a=s?r:o;return"string"==typeof t?l+t+a:t.prepend(l).append(a)},getHtmlRenderer:function(t,e){var i,n,r,o,s=this.view;return i=t+"Html",e&&(n=e+z(t)+"Html"),n&&(o=s[n])?r=s:n&&(o=this[n])?r=this:(o=s[i])?r=s:(o=this[i])&&(r=this),"function"==typeof o?function(){return o.apply(r,arguments)||""}:function(){return o||""}}},J.prototype=H(Q.prototype),t.extend(J.prototype,{el:null,coordMap:null,cellDuration:null,render:function(){this.bindHandlers()},destroy:function(){},buildCoords:function(){},getCellDate:function(){},getCellDayEl:function(){},rangeToSegs:function(){},bindHandlers:function(){var e=this;this.el.on("mousedown",function(i){t(i.target).is(".fc-event-container *, .fc-more")||t(i.target).closest(".fc-popover").length||e.dayMousedown(i)}),this.bindSegHandlers()},dayMousedown:function(t){var e,i,n,r=this,o=this.view,s=o.opt("selectable"),l=null,a=new q(this.coordMap,{scroll:o.opt("dragScroll"),dragStart:function(){o.unselect()},cellOver:function(t,o){a.origDate&&(n=r.getCellDayEl(t),l=[o,a.origDate].sort(C),e=l[0],i=l[1].clone().add(r.cellDuration),s&&r.renderSelection(e,i))},cellOut:function(){l=null,r.destroySelection()},listenStop:function(t){l&&(l[0].isSame(l[1])&&o.trigger("dayClick",n[0],e,t),s&&o.reportSelection(e,i,t))}});a.mousedown(t)},renderDrag:function(){},destroyDrag:function(){},renderResize:function(){},destroyResize:function(){},renderRangeHelper:function(t,e,i){var n,r=this.view;!e&&r.opt("forceEventDuration")&&(e=r.calendar.getDefaultEventEnd(!t.hasTime(),t)),n=i?H(i.event):{},n.start=t,n.end=e,n.allDay=!(t.hasTime()||e&&e.hasTime()),n.className=(n.className||[]).concat("fc-helper"),i||(n.editable=!1),this.renderHelper(n,i)},renderHelper:function(){},destroyHelper:function(){},renderSelection:function(t,e){this.renderHighlight(t,e)},destroySelection:function(){this.destroyHighlight()},renderHighlight:function(){},destroyHighlight:function(){},headHtml:function(){return'
'+""+""+this.rowHtml("head")+""+"
"+"
"},headCellHtml:function(t,e,i){var n=this.view,r=n.calendar,o=n.opt("columnFormat");return''+R(r.formatDate(i,o))+""},bgCellHtml:function(t,e,i){var n=this.view,r=this.getDayClasses(i);return r.unshift("fc-day",n.widgetContentClass),''},getDayClasses:function(t){var e=this.view,i=e.calendar.getNow().stripTime(),n=["fc-"+Re[t.day()]];return"month"===e.name&&t.month()!=e.intervalStart.month()&&n.push("fc-other-month"),t.isSame(i,"day")?n.push("fc-today",e.highlightStateClass):i>t?n.push("fc-past"):n.push("fc-future"),n}}),t.extend(J.prototype,{mousedOverSeg:null,isDraggingSeg:!1,isResizingSeg:!1,renderEvents:function(){},getSegs:function(){},destroyEvents:function(){this.triggerSegMouseout()},renderSegs:function(e,i){var n,r=this.view,o="",s=[];for(n=0;e.length>n;n++)o+=this.renderSegHtml(e[n],i);return t(o).each(function(i,n){var o=e[i],l=r.resolveEventEl(o.event,t(n));l&&(l.data("fc-seg",o),o.el=l,s.push(o))}),s},renderSegHtml:function(){},eventsToSegs:function(e,i,n){var r=this;return t.map(e,function(t){return r.eventToSegs(t,i,n)})},eventToSegs:function(t,e,i){var n,r,o,s=t.start.clone().stripZone(),l=this.view.calendar.getEventEnd(t).stripZone();for(e&&i?(o=b(s,l,e,i),n=o?[o]:[]):n=this.rangeToSegs(s,l),r=0;n.length>r;r++)o=n[r],o.event=t,o.eventStartMS=+s,o.eventDurationMS=l-s;return n},bindSegHandlers:function(){var e=this,i=this.view;t.each({mouseenter:function(t,i){e.triggerSegMouseover(t,i)},mouseleave:function(t,i){e.triggerSegMouseout(t,i)},click:function(t,e){return i.trigger("eventClick",this,t.event,e)},mousedown:function(n,r){t(r.target).is(".fc-resizer")&&i.isEventResizable(n.event)?e.segResizeMousedown(n,r):i.isEventDraggable(n.event)&&e.segDragMousedown(n,r)}},function(i,n){e.el.on(i,".fc-event-container > *",function(i){var r=t(this).data("fc-seg");return!r||e.isDraggingSeg||e.isResizingSeg?void 0:n.call(this,r,i)})})},triggerSegMouseover:function(t,e){this.mousedOverSeg||(this.mousedOverSeg=t,this.view.trigger("eventMouseover",t.el[0],t.event,e))},triggerSegMouseout:function(t,e){e=e||{},this.mousedOverSeg&&(t=t||this.mousedOverSeg,this.mousedOverSeg=null,this.view.trigger("eventMouseout",t.el[0],t.event,e))},segDragMousedown:function(t,e){var i,n,r=this,o=this.view,s=t.el,l=t.event,a=new K(t.el,{parentEl:o.el,opacity:o.opt("dragOpacity"),revertDuration:o.opt("dragRevertDuration"),zIndex:2}),c=new q(o.coordMap,{distance:5,scroll:o.opt("dragScroll"),listenStart:function(t){a.hide(),a.start(t)},dragStart:function(e){r.triggerSegMouseout(t,e),r.isDraggingSeg=!0,o.hideEvent(l),o.trigger("eventDragStart",s[0],l,e,{})},cellOver:function(e,s){var l=t.cellDate||c.origDate,d=r.computeDraggedEventDates(t,l,s);i=d.start,n=d.end,o.renderDrag(i,n,t)?a.hide():a.show()},cellOut:function(){i=null,o.destroyDrag(),a.show()},dragStop:function(t){var e=i&&!i.isSame(l.start);a.stop(!e,function(){r.isDraggingSeg=!1,o.destroyDrag(),o.showEvent(l),o.trigger("eventDragStop",s[0],l,t,{}),e&&o.eventDrop(s[0],l,i,t)})},listenStop:function(){a.stop()}});c.mousedown(e)},computeDraggedEventDates:function(t,e,i){var n,r,o,s=this.view,l=t.event,a=l.start,c=s.calendar.getEventEnd(l);return i.hasTime()===e.hasTime()?(n=D(i,e),r=a.clone().add(n),o=null===l.end?null:c.clone().add(n)):(r=i,o=null),{start:r,end:o}},segResizeMousedown:function(t,e){function i(){r.destroyResize(),o.showEvent(l)}var n,r=this,o=this.view,s=t.el,l=t.event,a=l.start,c=o.calendar.getEventEnd(l),d=null;n=new q(this.coordMap,{distance:5,scroll:o.opt("dragScroll"),dragStart:function(e){r.triggerSegMouseout(t,e),r.isResizingSeg=!0,o.trigger("eventResizeStart",s[0],l,e,{})},cellOver:function(e,n){n.isBefore(a)&&(n=a),d=n.clone().add(r.cellDuration),d.isSame(c)?(d=null,i()):(r.renderResize(a,d,t),o.hideEvent(l))},cellOut:function(){d=null,i()},dragStop:function(t){r.isResizingSeg=!1,i(),o.trigger("eventResizeStop",s[0],l,t,{}),d&&o.eventResize(s[0],l,d,t)}}),n.mousedown(e)},getSegClasses:function(t,e,i){var n=t.event,r=["fc-event",t.isStart?"fc-start":"fc-not-start",t.isEnd?"fc-end":"fc-not-end"].concat(n.className,n.source?n.source.className:[]);return e&&r.push("fc-draggable"),i&&r.push("fc-resizable"),r},getEventSkinCss:function(t){var e=this.view,i=t.source||{},n=t.color,r=i.color,o=e.opt("eventColor"),s=t.backgroundColor||n||i.backgroundColor||r||e.opt("eventBackgroundColor")||o,l=t.borderColor||n||i.borderColor||r||e.opt("eventBorderColor")||o,a=t.textColor||i.textColor||e.opt("eventTextColor"),c=[];return s&&c.push("background-color:"+s),l&&c.push("border-color:"+l),a&&c.push("color:"+a),c.join(";")}}),ee.prototype=H(J.prototype),t.extend(ee.prototype,{numbersVisible:!1,cellDuration:e.duration({days:1}),bottomCoordPadding:0,rowEls:null,dayEls:null,helperEls:null,highlightEls:null,render:function(e){var i,n=this.view,r="";for(i=0;n.rowCnt>i;i++)r+=this.dayRowHtml(i,e);this.el.html(r),this.rowEls=this.el.find(".fc-row"),this.dayEls=this.el.find(".fc-day"),this.dayEls.each(function(e,i){var r=n.cellToDate(Math.floor(e/n.colCnt),e%n.colCnt);n.trigger("dayRender",null,r,t(i))}),J.prototype.render.call(this)},destroy:function(){this.destroySegPopover()},dayRowHtml:function(t,e){var i=this.view,n=["fc-row","fc-week",i.widgetContentClass];return e&&n.push("fc-rigid"),'
'+'
'+""+this.rowHtml("day",t)+"
"+"
"+'
'+""+(this.numbersVisible?""+this.rowHtml("number",t)+"":"")+"
"+"
"+"
"},dayCellHtml:function(t,e,i){return this.bgCellHtml(t,e,i)},buildCoords:function(e,i){var n,r,o,s=this.view.colCnt;this.dayEls.slice(0,s).each(function(e,s){n=t(s),r=n.offset().left,e&&(o[1]=r),o=[r],i[e]=o}),o[1]=r+n.outerWidth(),this.rowEls.each(function(i,s){n=t(s),r=n.offset().top,i&&(o[1]=r),o=[r],e[i]=o}),o[1]=r+n.outerHeight()+this.bottomCoordPadding},getCellDate:function(t){return this.view.cellToDate(t)},getCellDayEl:function(t){return this.dayEls.eq(t.row*this.view.colCnt+t.col)},rangeToSegs:function(t,e){return this.view.rangeToSegments(t,e)},renderDrag:function(t,e,i){var n;return this.renderHighlight(t,e||this.view.calendar.getDefaultEventEnd(!0,t)),i&&!i.el.closest(this.el).length?(this.renderRangeHelper(t,e,i),n=this.view.opt("dragOpacity"),void 0!==n&&this.helperEls.css("opacity",n),!0):void 0},destroyDrag:function(){this.destroyHighlight(),this.destroyHelper()},renderResize:function(t,e,i){this.renderHighlight(t,e),this.renderRangeHelper(t,e,i)},destroyResize:function(){this.destroyHighlight(),this.destroyHelper()},renderHelper:function(e,i){var n=[],r=this.renderEventRows([e]);this.rowEls.each(function(e,o){var s,l=t(o),a=t('
');s=i&&i.row===e?i.el.position().top:l.find(".fc-content-skeleton tbody").position().top,a.css("top",s).find("table").append(r[e].tbodyEl),l.append(a),n.push(a[0])}),this.helperEls=t(n)},destroyHelper:function(){this.helperEls&&(this.helperEls.remove(),this.helperEls=null)},renderHighlight:function(e,i){var n,r,o,s=this.rangeToSegs(e,i),l=[];for(n=0;s.length>n;n++)r=s[n],o=t(this.highlightSkeletonHtml(r.leftCol,r.rightCol+1)),o.appendTo(this.rowEls[r.row]),l.push(o[0]);this.highlightEls=t(l)},destroyHighlight:function(){this.highlightEls&&(this.highlightEls.remove(),this.highlightEls=null)},highlightSkeletonHtml:function(t,e){var i=this.view.colCnt,n="";return t>0&&(n+='"),m=[],v=[],y=[];for(r=0;g>r;r++){if(o=f[r],s=0,l=t(""),m.push([]),v.push([]),y.push([]),o)for(a=0;o.length>a;a++){for(c=o[a],n(c.leftCol),d=t('",l+=""+(s?"":n)+'",c.add(this.slotDuration);return l},processOptions:function(){var t=this.view,i=t.opt("slotDuration"),n=t.opt("snapDuration");i=e.duration(i),n=n?e.duration(n):i,this.slotDuration=i,this.snapDuration=n,this.cellDuration=n,this.minTime=e.duration(t.opt("minTime")),this.maxTime=e.duration(t.opt("maxTime"))},rangeToSegs:function(t,e){var i,n,r,o,s,l=this.view,a=[];for(t=t.clone().stripZone(),e=e.clone().stripZone(),n=0;l.colCnt>n;n++)r=l.cellToDate(0,n),o=r.clone().time(this.minTime),s=r.clone().time(this.maxTime),i=b(t,e,o,s),i&&(i.col=n,a.push(i));return a},resize:function(){this.computeSlatTops(),this.updateSegVerticals()},buildCoords:function(i,n){var r,o,s=this.view.colCnt,l=this.el.offset().top,a=e.duration(+this.minTime),c=null;for(this.dayEls.slice(0,s).each(function(e,i){r=t(i),o=r.offset().left,c&&(c[1]=o),c=[o],n[e]=c}),c[1]=o+r.outerWidth(),c=null;this.maxTime>a;)o=l+this.computeTimeTop(a),c&&(c[1]=o),c=[o],i.push(c),a.add(this.snapDuration);c[1]=l+this.computeTimeTop(a)},getCellDate:function(t){var e=this.view,i=e.calendar;return i.rezoneDate(e.cellToDate(0,t.col).time(this.minTime+this.snapDuration*t.row))},getCellDayEl:function(t){return this.dayEls.eq(t.col)},computeDateTop:function(t,i){return this.computeTimeTop(e.duration(t.clone().stripZone()-i.clone().stripTime()))},computeTimeTop:function(t){var e,i,n,r,o=(t-this.minTime)/this.slotDuration;return o=Math.max(0,o),o=Math.min(this.slatEls.length,o),e=Math.floor(o),i=o-e,n=this.slatTops[e],i?(r=this.slatTops[e+1],n+(r-n)*i):n},computeSlatTops:function(){var e,i=[];this.slatEls.each(function(n,r){e=t(r).position().top,i.push(e)}),i.push(e+this.slatEls.last().outerHeight()),this.slatTops=i},renderDrag:function(t,e,i){var n;return i?(this.renderRangeHelper(t,e,i),n=this.view.opt("dragOpacity"),void 0!==n&&this.helperEl.css("opacity",n),!0):(this.renderHighlight(t,e||this.view.calendar.getDefaultEventEnd(!1,t)),void 0)},destroyDrag:function(){this.destroyHelper(),this.destroyHighlight()},renderResize:function(t,e,i){this.renderRangeHelper(t,e,i)},destroyResize:function(){this.destroyHelper()},renderHelper:function(e,i){var n,r,o,s=this.renderEventTable([e]),l=s.tableEl,a=s.segs;for(n=0;a.length>n;n++)r=a[n],i&&i.col===r.col&&(o=i.el,r.el.css({left:o.css("left"),right:o.css("right"),"margin-left":o.css("margin-left"),"margin-right":o.css("margin-right")}));this.helperEl=t('
').append(l).appendTo(this.el)},destroyHelper:function(){this.helperEl&&(this.helperEl.remove(),this.helperEl=null)},renderSelection:function(t,e){this.view.opt("selectHelper")?this.renderRangeHelper(t,e):this.renderHighlight(t,e)},destroySelection:function(){this.destroyHelper(),this.destroyHighlight()},renderHighlight:function(e,i){this.highlightEl=t(this.highlightSkeletonHtml(e,i)).appendTo(this.el)},destroyHighlight:function(){this.highlightEl&&(this.highlightEl.remove(),this.highlightEl=null)},highlightSkeletonHtml:function(t,e){var i,n,r,o,s,l=this.view,a=this.rangeToSegs(t,e),c="",d=0;for(i=0;a.length>i;i++)n=a[i],n.col>d&&(c+='
",d++;return l.colCnt>d&&(c+='":void 0},numberIntroHtml:function(t){return this.weekNumbersVisible?'":void 0},dayIntroHtml:function(){return this.weekNumbersVisible?'":void 0},introHtml:function(){return this.weekNumbersVisible?'":void 0},numberCellHtml:function(t,e,i){var n;return this.dayNumbersVisible?(n=this.dayGrid.getDayClasses(i),n.unshift("fc-day-number"),'"):""):'"},dayIntroHtml:function(){return'"},slotBgIntroHtml:function(){return'"},introHtml:function(){return'"},axisStyleAttr:function(){return null!==this.axisWidth?'style="width:'+this.axisWidth+'px"':""},updateSize:function(t){t&&this.timeGrid.resize(),fe.prototype.updateSize.call(this,t)},updateWidth:function(){this.axisWidth=p(this.el.find(".fc-axis"))},setHeight:function(t,e){var i,n;null===this.bottomRuleHeight&&(this.bottomRuleHeight=this.bottomRuleEl.outerHeight()),this.bottomRuleEl.hide(),this.scrollerEl.css("overflow",""),v(this.scrollerEl),u(this.noScrollRowEls),this.dayGrid&&(this.dayGrid.destroySegPopover(),i=this.opt("eventLimit"),i&&"number"!=typeof i&&(i=Ye),i&&this.dayGrid.limitRows(i)),e||(n=this.computeScrollerHeight(t),m(this.scrollerEl,n)?(h(this.noScrollRowEls,w(this.scrollerEl)),n=this.computeScrollerHeight(t),this.scrollerEl.height(n),this.restoreScroll()):(this.scrollerEl.height(n).css("overflow","hidden"),this.bottomRuleEl.show()))},resetScroll:function(){function t(){i.scrollerEl.scrollTop(r)}var i=this,n=e.duration(this.opt("scrollTime")),r=this.timeGrid.computeTimeTop(n);r=Math.ceil(r),r&&r++,t(),setTimeout(t,0)},renderEvents:function(t){var e,i,n=[],r=[],o=[];for(i=0;t.length>i;i++)t[i].allDay?n.push(t[i]):r.push(t[i]);e=this.timeGrid.renderEvents(r),this.dayGrid&&(o=this.dayGrid.renderEvents(n)),this.updateHeight(),fe.prototype.renderEvents.call(this,t)},getSegs:function(){return this.timeGrid.getSegs().concat(this.dayGrid?this.dayGrid.getSegs():[])},destroyEvents:function(){fe.prototype.destroyEvents.call(this),this.recordScroll(),this.timeGrid.destroyEvents(),this.dayGrid&&this.dayGrid.destroyEvents()},renderDrag:function(t,e,i){return t.hasTime()?this.timeGrid.renderDrag(t,e,i):this.dayGrid?this.dayGrid.renderDrag(t,e,i):void 0},destroyDrag:function(){this.timeGrid.destroyDrag(),this.dayGrid&&this.dayGrid.destroyDrag()},renderSelection:function(t,e){t.hasTime()||e.hasTime()?this.timeGrid.renderSelection(t,e):this.dayGrid&&this.dayGrid.renderSelection(t,e)},destroySelection:function(){this.timeGrid.destroySelection(),this.dayGrid&&this.dayGrid.destroySelection()}}),xe.agendaWeek=be,be.prototype=H(Ee.prototype),t.extend(be.prototype,{name:"agendaWeek",incrementDate:function(t,e){return t.clone().stripTime().add(e,"weeks").startOf("week")},render:function(t){this.intervalStart=t.clone().stripTime().startOf("week"),this.intervalEnd=this.intervalStart.clone().add(1,"weeks"),this.start=this.skipHiddenDays(this.intervalStart),this.end=this.skipHiddenDays(this.intervalEnd,-1,!0),this.title=this.calendar.formatRange(this.start,this.end.clone().subtract(1),this.opt("titleFormat")," — "),Ee.prototype.render.call(this,this.getCellsPerWeek())}}),xe.agendaDay=Se,Se.prototype=H(Ee.prototype),t.extend(Se.prototype,{name:"agendaDay",incrementDate:function(t,e){var i=t.clone().stripTime().add(e,"days");return i=this.skipHiddenDays(i,0>e?-1:1)},render:function(t){this.start=this.intervalStart=t.clone().stripTime(),this.end=this.intervalEnd=this.start.clone().add(1,"days"),this.title=this.calendar.formatDate(this.start,this.opt("titleFormat")),Ee.prototype.render.call(this,1)}})}); \ No newline at end of file +(function(t){"function"==typeof define&&define.amd?define(["jquery","moment"],t):t(jQuery,moment)})(function(t,e){function i(t,e){return e.longDateFormat("LT").replace(":mm","(:mm)").replace(/(\Wmm)$/,"($1)").replace(/\s*a$/i,"t")}function n(t,e){var i=e.longDateFormat("L");return i=i.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g,""),t.isRTL?i+=" ddd":i="ddd "+i,i}function r(t){o(ke,t)}function o(e){function i(i,n){t.isPlainObject(n)&&t.isPlainObject(e[i])&&!s(i)?e[i]=o({},e[i],n):void 0!==n&&(e[i]=n)}for(var n=1;arguments.length>n;n++)t.each(arguments[n],i);return e}function s(t){return/(Time|Duration)$/.test(t)}function l(i,n){function r(t){var i=e.localeData||e.langData;return i.call(e,t)||i.call(e,"en")}function s(t){ne?u()&&(m(),p(t)):l()}function l(){re=Q.theme?"ui":"fc",i.addClass("fc"),Q.isRTL?i.addClass("fc-rtl"):i.addClass("fc-ltr"),Q.theme?i.addClass("ui-widget"):i.addClass("fc-unthemed"),ne=t("
").prependTo(i),ee=new a(U,Q),ie=ee.render(),ie&&i.prepend(ie),f(Q.defaultView),Q.handleWindowResize&&(le=L(y,Q.windowResizeDelay),t(window).resize(le))}function d(){oe&&oe.destroy(),ee.destroy(),ne.remove(),i.removeClass("fc fc-ltr fc-rtl fc-unthemed ui-widget"),t(window).unbind("resize",le)}function u(){return i.is(":visible")}function f(t){p(0,t)}function p(e,i){ue++,oe&&i&&oe.name!==i&&(ee.deactivateButton(oe.name),I(),oe.start&&oe.destroy(),oe.el.remove(),oe=null),!oe&&i&&(oe=new ze[i](U),oe.el=t("
").appendTo(ne),ee.activateButton(i)),oe&&(e&&(ae=oe.incrementDate(ae,e)),oe.start&&!e&&ae.isWithin(oe.intervalStart,oe.intervalEnd)||u()&&(I(),oe.start&&oe.destroy(),oe.render(ae),B(),H(),k(),S())),B(),ue--}function g(t){return u()?(t&&v(),ue++,oe.updateSize(!0),ue--,!0):void 0}function m(){u()&&v()}function v(){se="number"==typeof Q.contentHeight?Q.contentHeight:"number"==typeof Q.height?Q.height-(ie?ie.outerHeight(!0):0):Math.round(ne.width()/Math.max(Q.aspectRatio,.5))}function y(t){!ue&&t.target===window&&oe.start&&g(!0)&&oe.trigger("windowResize",he)}function w(){D(),b()}function E(){u()&&(I(),oe.destroyEvents(),oe.renderEvents(fe),B())}function D(){I(),oe.destroyEvents(),B()}function S(){!Q.lazyFetching||ce(oe.start,oe.end)?b():E()}function b(){de(oe.start,oe.end)}function T(t){fe=t,E()}function C(){E()}function H(){ee.updateTitle(oe.title)}function k(){var t=U.getNow();t.isWithin(oe.intervalStart,oe.intervalEnd)?ee.disableButton("today"):ee.enableButton("today")}function M(t,e){t=U.moment(t),e=e?U.moment(e):t.hasTime()?t.clone().add(U.defaultTimedEventDuration):t.clone().add(U.defaultAllDayEventDuration),oe.select(t,e)}function R(){oe&&oe.unselect()}function P(){p(-1)}function z(){p(1)}function N(){ae.add(-1,"years"),p()}function _(){ae.add(1,"years"),p()}function A(){ae=U.getNow(),p()}function Y(t){ae=U.moment(t),p()}function O(t){ae.add(e.duration(t)),p()}function F(t,e){var i,n;e&&void 0!==ze[e]||(e=e||"day",i=ee.getViewsWithButtons().join(" "),n=i.match(RegExp("\\w+"+G(e))),n||(n=i.match(/\w+Day/)),e=n?n[0]:"agendaDay"),ae=t,f(e)}function Z(){return ae.clone()}function I(){ne.css({width:"100%",height:ne.height(),overflow:"hidden"})}function B(){ne.css({width:"",height:"",overflow:""})}function j(){return U}function X(){return oe}function $(t,e){return void 0===e?Q[t]:(("height"==t||"contentHeight"==t||"aspectRatio"==t)&&(Q[t]=e,g(!0)),void 0)}function q(t,e){return Q[t]?Q[t].apply(e||he,Array.prototype.slice.call(arguments,2)):void 0}var U=this;n=n||{};var K,Q=o({},ke,n);K=Q.lang in Me?Me[Q.lang]:Me[ke.lang],K&&(Q=o({},ke,K,n)),Q.isRTL&&(Q=o({},ke,Re,K||{},n)),U.options=Q,U.render=s,U.destroy=d,U.refetchEvents=w,U.reportEvents=T,U.reportEventChange=C,U.rerenderEvents=E,U.changeView=f,U.select=M,U.unselect=R,U.prev=P,U.next=z,U.prevYear=N,U.nextYear=_,U.today=A,U.gotoDate=Y,U.incrementDate=O,U.zoomTo=F,U.getDate=Z,U.getCalendar=j,U.getView=X,U.option=$,U.trigger=q;var J=x(r(Q.lang));if(Q.monthNames&&(J._months=Q.monthNames),Q.monthNamesShort&&(J._monthsShort=Q.monthNamesShort),Q.dayNames&&(J._weekdays=Q.dayNames),Q.dayNamesShort&&(J._weekdaysShort=Q.dayNamesShort),null!=Q.firstDay){var te=x(J._week);te.dow=Q.firstDay,J._week=te}U.defaultAllDayEventDuration=e.duration(Q.defaultAllDayEventDuration),U.defaultTimedEventDuration=e.duration(Q.defaultTimedEventDuration),U.moment=function(){var t;return"local"===Q.timezone?(t=Pe.moment.apply(null,arguments),t.hasTime()&&t.local()):t="UTC"===Q.timezone?Pe.moment.utc.apply(null,arguments):Pe.moment.parseZone.apply(null,arguments),"_locale"in t?t._locale=J:t._lang=J,t},U.getIsAmbigTimezone=function(){return"local"!==Q.timezone&&"UTC"!==Q.timezone},U.rezoneDate=function(t){return U.moment(t.toArray())},U.getNow=function(){var t=Q.now;return"function"==typeof t&&(t=t()),U.moment(t)},U.calculateWeekNumber=function(t){var e=Q.weekNumberCalculation;return"function"==typeof e?e(t):"local"===e?t.week():"ISO"===e.toUpperCase()?t.isoWeek():void 0},U.getEventEnd=function(t){return t.end?t.end.clone():U.getDefaultEventEnd(t.allDay,t.start)},U.getDefaultEventEnd=function(t,e){var i=e.clone();return t?i.stripTime().add(U.defaultAllDayEventDuration):i.add(U.defaultTimedEventDuration),U.getIsAmbigTimezone()&&i.stripZone(),i},U.formatRange=function(t,e,i){return"function"==typeof i&&(i=i.call(U,Q,J)),W(t,e,i,null,Q.isRTL)},U.formatDate=function(t,e){return"function"==typeof e&&(e=e.call(U,Q,J)),V(t,e)},c.call(U,Q),h.call(U,Q);var ee,ie,ne,re,oe,se,le,ae,ce=U.isFetchNeeded,de=U.fetchEvents,he=i[0],ue=0,fe=[];ae=null!=Q.defaultDate?U.moment(Q.defaultDate):U.getNow(),U.getSuggestedViewHeight=function(){return void 0===se&&m(),se},U.isHeightAuto=function(){return"auto"===Q.contentHeight||"auto"===Q.height}}function a(e,i){function n(){var e=i.header;return f=i.theme?"ui":"fc",e?p=t("
").append(o("left")).append(o("right")).append(o("center")).append('
'):void 0}function r(){p.remove()}function o(n){var r=t('
'),o=i.header[n];return o&&t.each(o.split(" "),function(){var n,o=t(),s=!0;t.each(this.split(","),function(n,r){var l,a,c,d,h,u,p,m;"title"==r?(o=o.add(t("

 

")),s=!1):(e[r]?l=function(){e[r]()}:ze[r]&&(l=function(){e.changeView(r)},g.push(r)),l&&(a=b(i.themeButtonIcons,r),c=b(i.buttonIcons,r),d=b(i.defaultButtonText,r),h=b(i.buttonText,r),u=h?P(h):a&&i.theme?"":c&&!i.theme?"":P(d||r),p=["fc-"+r+"-button",f+"-button",f+"-state-default"],m=t('").click(function(){m.hasClass(f+"-state-disabled")||(l(),(m.hasClass(f+"-state-active")||m.hasClass(f+"-state-disabled"))&&m.removeClass(f+"-state-hover"))}).mousedown(function(){m.not("."+f+"-state-active").not("."+f+"-state-disabled").addClass(f+"-state-down")}).mouseup(function(){m.removeClass(f+"-state-down")}).hover(function(){m.not("."+f+"-state-active").not("."+f+"-state-disabled").addClass(f+"-state-hover")},function(){m.removeClass(f+"-state-hover").removeClass(f+"-state-down")}),o=o.add(m)))}),s&&o.first().addClass(f+"-corner-left").end().last().addClass(f+"-corner-right").end(),o.length>1?(n=t("
"),s&&n.addClass("fc-button-group"),n.append(o),r.append(n)):r.append(o)}),r}function s(t){p.find("h2").text(t)}function l(t){p.find(".fc-"+t+"-button").addClass(f+"-state-active")}function a(t){p.find(".fc-"+t+"-button").removeClass(f+"-state-active")}function c(t){p.find(".fc-"+t+"-button").attr("disabled","disabled").addClass(f+"-state-disabled")}function d(t){p.find(".fc-"+t+"-button").removeAttr("disabled").removeClass(f+"-state-disabled")}function h(){return g}var u=this;u.render=n,u.destroy=r,u.updateTitle=s,u.activateButton=l,u.deactivateButton=a,u.disableButton=c,u.enableButton=d,u.getViewsWithButtons=h;var f,p=t(),g=[]}function c(e){function i(t,e){return!b||t.clone().stripZone()C.clone().stripZone()}function n(t,e){b=t,C=e,A=[];var i=++L,n=G.length;N=n;for(var o=0;n>o;o++)r(G[o],i)}function r(e,i){o(e,function(n){var r,o,s=t.isArray(e.events);if(i==L){if(n)for(r=0;n.length>r;r++)o=n[r],s||(o=w(o,e)),o&&A.push(o);N--,N||k(A)}})}function o(i,n){var r,s,l=Pe.sourceFetchers;for(r=0;l.length>r;r++){if(s=l[r].call(S,i,b.clone(),C.clone(),e.timezone,n),s===!0)return;if("object"==typeof s)return o(s,n),void 0}var a=i.events;if(a)t.isFunction(a)?(v(),a.call(S,b.clone(),C.clone(),e.timezone,function(t){n(t),y()})):t.isArray(a)?n(a):n();else{var c=i.url;if(c){var d,h=i.success,u=i.error,f=i.complete;d=t.isFunction(i.data)?i.data():i.data;var p=t.extend({},d||{}),g=R(i.startParam,e.startParam),m=R(i.endParam,e.endParam),w=R(i.timezoneParam,e.timezoneParam);g&&(p[g]=b.format()),m&&(p[m]=C.format()),e.timezone&&"local"!=e.timezone&&(p[w]=e.timezone),v(),t.ajax(t.extend({},Ge,i,{data:p,success:function(e){e=e||[];var i=M(h,this,arguments);t.isArray(i)&&(e=i),n(e)},error:function(){M(u,this,arguments),n()},complete:function(){M(f,this,arguments),y()}}))}else n()}}function s(t){var e=l(t);e&&(G.push(e),N++,r(e,L))}function l(e){var i,n,r=Pe.sourceNormalizers;if(t.isFunction(e)||t.isArray(e)?i={events:e}:"string"==typeof e?i={url:e}:"object"==typeof e&&(i=t.extend({},e)),i){for(i.className?"string"==typeof i.className&&(i.className=i.className.split(/\s+/)):i.className=[],t.isArray(i.events)&&(i.origArray=i.events,i.events=t.map(i.events,function(t){return w(t,i)})),n=0;r.length>n;n++)r[n].call(S,i);return i}}function a(e){G=t.grep(G,function(t){return!c(t,e)}),A=t.grep(A,function(t){return!c(t.source,e)}),k(A)}function c(t,e){return t&&e&&h(t)==h(e)}function h(t){return("object"==typeof t?t.origArray||t.url||t.events:null)||t}function u(t){t.start=S.moment(t.start),t.end&&(t.end=S.moment(t.end)),E(t),f(t),k(A)}function f(t){var e,i,n,r;for(e=0;A.length>e;e++)if(i=A[e],i._id==t._id&&i!==t)for(n=0;Y.length>n;n++)r=Y[n],void 0!==t[r]&&(i[r]=t[r])}function p(t,e){var i=w(t);i&&(i.source||(e&&(z.events.push(i),i.source=z),A.push(i)),k(A))}function g(e){var i,n;for(null==e?e=function(){return!0}:t.isFunction(e)||(i=e+"",e=function(t){return t._id==i}),A=t.grep(A,e,!0),n=0;G.length>n;n++)t.isArray(G[n].events)&&(G[n].events=t.grep(G[n].events,e,!0));k(A)}function m(e){return t.isFunction(e)?t.grep(A,e):null!=e?(e+="",t.grep(A,function(t){return t._id==e})):A}function v(){_++||H("loading",null,!0,x())}function y(){--_||H("loading",null,!1,x())}function w(i,n){var r,o,s,l,a={};return e.eventDataTransform&&(i=e.eventDataTransform(i)),n&&n.eventDataTransform&&(i=n.eventDataTransform(i)),r=S.moment(i.start||i.date),r.isValid()&&(o=null,!i.end||(o=S.moment(i.end),o.isValid()))?(s=i.allDay,void 0===s&&(l=R(n?n.allDayDefault:void 0,e.allDayDefault),s=void 0!==l?l:!(r.hasTime()||o&&o.hasTime())),s?(r.hasTime()&&r.stripTime(),o&&o.hasTime()&&o.stripTime()):(r.hasTime()||(r=S.rezoneDate(r)),o&&!o.hasTime()&&(o=S.rezoneDate(o))),t.extend(a,i),n&&(a.source=n),a._id=i._id||(void 0===i.id?"_fc"+Le++:i.id+""),a.className=i.className?"string"==typeof i.className?i.className.split(/\s+/):i.className:[],a.resources?"string"==typeof a.resources&&(a.resources=a.resources.split(/\s+/)):a.resources=[],a.allDay=s,a.start=r,a.end=o,e.forceEventDuration&&!a.end&&(a.end=P(a)),d(a),a):void 0}function E(t,e,i){var n,r,o,s,l=t._allDay,a=t._start,c=t._end,d=!1;return e||i||(e=t.start,i=t.end),n=t.allDay!=l?t.allDay:!(e||i).hasTime(),n&&(e&&(e=e.clone().stripTime()),i&&(i=i.clone().stripTime())),e&&(r=n?T(e,a.clone().stripTime()):T(e,a)),n!=l?d=!0:i&&(o=T(i||S.getDefaultEventEnd(n,e||a),e||a).subtract(T(c||S.getDefaultEventEnd(l,a),a))),s=D(m(t._id),d,n,r,o),{dateDelta:r,durationDelta:o,undo:s}}function D(i,n,r,o,s){var l=S.getIsAmbigTimezone(),a=[];return t.each(i,function(t,i){var c=i._allDay,h=i._start,u=i._end,f=null!=r?r:c,p=h.clone(),g=!n&&u?u.clone():null;f?(p.stripTime(),g&&g.stripTime()):(p.hasTime()||(p=S.rezoneDate(p)),g&&!g.hasTime()&&(g=S.rezoneDate(g))),g||!e.forceEventDuration&&!+s||(g=S.getDefaultEventEnd(f,p)),p.add(o),g&&g.add(o).add(s),l&&(+o||+s)&&(p.stripZone(),g&&g.stripZone()),i.allDay=f,i.start=p,i.end=g,d(i),a.push(function(){i.allDay=c,i.start=h,i.end=u,d(i)})}),function(){for(var t=0;a.length>t;t++)a[t]()}}var S=this;S.isFetchNeeded=i,S.fetchEvents=n,S.addEventSource=s,S.removeEventSource=a,S.updateEvent=u,S.renderEvent=p,S.removeEvents=g,S.clientEvents=m,S.mutateEvent=E;var b,C,H=S.trigger,x=S.getView,k=S.reportEvents,P=S.getEventEnd,z={events:[]},G=[z],L=0,N=0,_=0,A=[];t.each((e.events?[e.events]:[]).concat(e.eventSources||[]),function(t,e){var i=l(e);i&&G.push(i)});var Y=["title","url","allDay","className","editable","color","backgroundColor","borderColor","textColor"]}function d(t){t._allDay=t.allDay,t._start=t.start.clone(),t._end=t.end?t.end.clone():null}function h(e){function i(e){h=[];var i;if(t.isFunction(e))i={resources:e},h.push(i),c=void 0;else if("string"==typeof e)i={url:e},h.push(i),c=void 0;else if("object"==typeof e&&null!=e){for(var n=0;e.length>n;n++){var r=e[n];o(r),i={resources:r},h.push(i)}c=void 0}}function n(i,n){var o;if(i=i!==void 0?i:!0,!i||void 0===o){o=[];for(var s=h.length,l=0;s>l;l++){var a=r(h[l],n);o=o.concat(a)}}return t.isFunction(e.resourceFilter)?t.grep(o,e.resourceFilter):o}function r(i,n){var r=i.resources;if(r){if(t.isFunction(r))return r()}else{var o=i.url;if(o){var s={};if("object"==typeof n){var l=e.startParam,a=e.endParam;l&&(s[l]=Math.round(+n.intervalStart/1e3)),a&&(s[a]=Math.round(+n.intervalEnd/1e3))}t.ajax(t.extend({},Ge,i,{data:s,dataType:"json",cache:!1,success:function(t){t=t||[],r=t},error:function(){},async:!1}))}}return r}function o(t){t.className?"string"==typeof t.className&&(t.className=t.className.split(/\s+/)):t.className=[];for(var e=Pe.sourceNormalizers,i=0;e.length>i;i++)e[i](t)}function s(t,e,i,n){var r,o,s,c,d=t._allDay,h=t._start,u=t._end,f=!1;return i||n||(i=t.start,n=t.end),r=t.allDay!=d?t.allDay:!(i||n).hasTime(),r&&(i&&(i=i.clone().stripTime()),n&&(n=n.clone().stripTime())),i&&(o=r?T(i,h.clone().stripTime()):T(i,h)),r!=d?f=!0:n&&(s=T(n||a.getDefaultEventEnd(r,i||h),i||h).subtract(T(u||a.getDefaultEventEnd(d,h),h))),c=l(a.clientEvents(t._id),f,r,o,s,e),{dateDelta:o,durationDelta:s,undo:c}}function l(i,n,r,o,s,l){var c=a.getIsAmbigTimezone(),h=[];return t.each(i,function(t,i){var u=i.resources,f=i._allDay,p=i._start,g=i._end,m=null!=r?r:f,v=p.clone(),y=!n&&g?g.clone():null;m?(v.stripTime(),y&&y.stripTime()):(v.hasTime()||(v=a.rezoneDate(v)),y&&!y.hasTime()&&(y=a.rezoneDate(y))),y||!e.forceEventDuration&&!+s||(y=a.getDefaultEventEnd(m,v)),v.add(o),y&&y.add(o).add(s),c&&(+o||+s)&&(v.stripZone(),y&&y.stripZone()),i.allDay=m,i.start=v,i.end=y,i.resources=l,d(i),h.push(function(){i.allDay=f,i.start=p,i.end=g,i.resources=u,d(i)})}),function(){for(var t=0;h.length>t;t++)h[t]()}}var a=this;a.fetchResources=n,a.setResources=i,a.mutateResourceEvent=s;var c,h=[];i(e.resources)}function u(t,e){e.left&&t.css({"border-left-width":1,"margin-left":e.left-1}),e.right&&t.css({"border-right-width":1,"margin-right":e.right-1})}function f(t){t.css({"margin-left":"","margin-right":"","border-left-width":"","border-right-width":""})}function p(e,i,n){var r=Math.floor(i/e.length),o=Math.floor(i-r*(e.length-1)),s=[],l=[],a=[],c=0;g(e),e.each(function(i,n){var d=i===e.length-1?o:r,h=t(n).outerHeight(!0);d>h?(s.push(n),l.push(h),a.push(t(n).height())):c+=h}),n&&(i-=c,r=Math.floor(i/s.length),o=Math.floor(i-r*(s.length-1))),t(s).each(function(e,i){var n=e===s.length-1?o:r,c=l[e],d=a[e],h=n-(c-d);n>c&&t(i).height(h)})}function g(t){t.height("")}function m(e){var i=0;return e.find("> *").each(function(e,n){var r=t(n).outerWidth();r>i&&(i=r)}),i++,e.width(i),i}function v(t,e){return t.height(e).addClass("fc-scroller"),t[0].scrollHeight-1>t[0].clientHeight?!0:(y(t),!1)}function y(t){t.height("").removeClass("fc-scroller")}function w(e){var i=e.css("position"),n=e.parents().filter(function(){var e=t(this);return/(auto|scroll)/.test(e.css("overflow")+e.css("overflow-y")+e.css("overflow-x"))}).eq(0);return"fixed"!==i&&n.length?n:t(e[0].ownerDocument||document)}function E(t){var e=t.offset().left,i=e+t.width(),n=t.children(),r=n.offset().left,o=r+n.outerWidth();return{left:r-e,right:i-o}}function D(t){return 1==t.which&&!t.ctrlKey}function S(t,e,i,n){var r,o,s,l;return e>i&&n>t?(t>=i?(r=t.clone(),s=!0):(r=i.clone(),s=!1),n>=e?(o=e.clone(),l=!0):(o=n.clone(),l=!1),{start:r,end:o,isStart:s,isEnd:l}):void 0}function b(t,e){if(t=t||{},void 0!==t[e])return t[e];for(var i,n=e.split(/(?=[A-Z])/),r=n.length-1;r>=0;r--)if(i=t[n[r].toLowerCase()],void 0!==i)return i;return t["default"]}function T(t,i){return e.duration({days:t.clone().stripTime().diff(i.clone().stripTime(),"days"),ms:t.time()-i.time()})}function C(t){return"[object Date]"===Object.prototype.toString.call(t)||t instanceof Date}function H(t,e){return t-e}function x(t){var e=function(){};return e.prototype=t,new e}function k(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i])}function M(e,i,n){if(t.isFunction(e)&&(e=[e]),e){var r,o;for(r=0;e.length>r;r++)o=e[r].apply(i,n)||o;return o}}function R(){for(var t=0;arguments.length>t;t++)if(void 0!==arguments[t])return arguments[t]}function P(t){return(t+"").replace(/&/g,"&").replace(//g,">").replace(/'/g,"'").replace(/"/g,""").replace(/\n/g,"
")}function z(t){return t.replace(/&.*?;/g,"")}function G(t){return t.charAt(0).toUpperCase()+t.slice(1)}function L(t,e){var i,n,r,o,s=function(){var l=+new Date-o;e>l&&l>0?i=setTimeout(s,e-l):(i=null,t.apply(r,n),i||(r=n=null))};return function(){r=this,n=arguments,o=+new Date,i||(i=setTimeout(s,e))}}function N(i,n,r){var o,s,l,a,c=i[0],d=1==i.length&&"string"==typeof c;return e.isMoment(c)?(a=e.apply(null,i),c._ambigTime&&(a._ambigTime=!0),c._ambigZone&&(a._ambigZone=!0)):C(c)||void 0===c?a=e.apply(null,i):(o=!1,s=!1,d?_e.test(c)?(c+="-01",i=[c],o=!0,s=!0):(l=Ae.exec(c))&&(o=!l[5],s=!0):t.isArray(c)&&(s=!0),a=n?e.utc.apply(e,i):e.apply(null,i),o?(a._ambigTime=!0,a._ambigZone=!0):r&&(s?a._ambigZone=!0:d&&a.zone(c))),new _(a)}function _(t){k(this,t)}function A(t,e){var i,n=[],r=!1,o=!1;for(i=0;t.length>i;i++)n.push(Pe.moment.parseZone(t[i])),r=r||n[i]._ambigTime,o=o||n[i]._ambigZone;for(i=0;n.length>i;i++)r&&!e?n[i].stripTime():o&&n[i].stripZone();return n}function Y(t,i){return e.fn.format.call(t,i)}function V(t,e){return O(t,B(e))}function O(t,e){var i,n="";for(i=0;e.length>i;i++)n+=F(t,e[i]);return n}function F(t,e){var i,n;return"string"==typeof e?e:(i=e.token)?Ye[i]?Ye[i](t):Y(t,i):e.maybe&&(n=O(t,e.maybe),n.match(/[1-9]/))?n:""}function W(t,e,i,n,r){var o;return t=Pe.moment.parseZone(t),e=Pe.moment.parseZone(e),o=(t.localeData||t.lang).call(t),i=o.longDateFormat(i)||i,n=n||" - ",Z(t,e,B(i),n,r)}function Z(t,e,i,n,r){var o,s,l,a,c="",d="",h="",u="",f="";for(s=0;i.length>s&&(o=I(t,e,i[s]),o!==!1);s++)c+=o;for(l=i.length-1;l>s&&(o=I(t,e,i[l]),o!==!1);l--)d=o+d;for(a=s;l>=a;a++)h+=F(t,i[a]),u+=F(e,i[a]);return(h||u)&&(f=r?u+n+h:h+n+u),c+f+d}function I(t,e,i){var n,r;return"string"==typeof i?i:(n=i.token)&&(r=Ve[n.charAt(0)],r&&t.isSame(e,r))?Y(t,n):!1}function B(t){return t in Oe?Oe[t]:Oe[t]=j(t)}function j(t){for(var e,i=[],n=/\[([^\]]*)\]|\(([^\)]*)\)|(LT|(\w)\4*o?)|([^\w\[\(]+)/g;e=n.exec(t);)e[1]?i.push(e[1]):e[2]?i.push({maybe:j(e[2])}):e[3]?i.push({token:e[3]}):e[5]&&i.push(e[5]);return i}function X(t){this.options=t||{}}function $(t){this.grid=t}function q(t){this.coordMaps=t}function U(t,e){this.coordMap=t,this.options=e||{}}function K(t,e){return t||e?t&&e?t.grid===e.grid&&t.row===e.row&&t.col===e.col:!1:!0}function Q(e,i){this.options=i=i||{},this.sourceEl=e,this.parentEl=i.parentEl?t(i.parentEl):e.parent()}function J(t){this.view=t}function te(t){J.call(this,t),this.coordMap=new $(this)}function ee(t,e){return t.eventStartMS-e.eventStartMS||e.eventDurationMS-t.eventDurationMS||e.event.allDay-t.event.allDay||(t.event.title||"").localeCompare(e.event.title)}function ie(t){te.call(this,t)}function ne(t,e){var i,n;for(i=0;e.length>i;i++)if(n=e[i],n.leftCol<=t.rightCol&&n.rightCol>=t.leftCol)return!0;return!1}function re(t,e){return t.leftCol-e.leftCol}function oe(t){te.call(this,t)}function se(t){var e,i,n;if(t.sort(ee),e=le(t),ae(e),i=e[0]){for(n=0;i.length>n;n++)ce(i[n]);for(n=0;i.length>n;n++)de(i[n],0,0)}}function le(t){var e,i,n,r=[];for(e=0;t.length>e;e++){for(i=t[e],n=0;r.length>n&&he(i,r[n]).length;n++);i.level=n,(r[n]||(r[n]=[])).push(i)}return r}function ae(t){var e,i,n,r,o;for(e=0;t.length>e;e++)for(i=t[e],n=0;i.length>n;n++)for(r=i[n],r.forwardSegs=[],o=e+1;t.length>o;o++)he(r,t[o],r.forwardSegs)}function ce(t){var e,i,n=t.forwardSegs,r=0;if(void 0===t.forwardPressure){for(e=0;n.length>e;e++)i=n[e],ce(i),r=Math.max(r,1+i.forwardPressure);t.forwardPressure=r}}function de(t,e,i){var n,r=t.forwardSegs;if(void 0===t.forwardCoord)for(r.length?(r.sort(fe),de(r[0],e+1,i),t.forwardCoord=r[0].backwardCoord):t.forwardCoord=1,t.backwardCoord=t.forwardCoord-(t.forwardCoord-i)/(e+1),n=0;r.length>n;n++)de(r[n],0,t.forwardCoord)}function he(t,e,i){i=i||[];for(var n=0;e.length>n;n++)ue(t,e[n])&&i.push(e[n]);return i}function ue(t,e){return t.bottom>e.top&&t.topd;d++){var h=d*n,u=h+n-1,f=Math.max(a,h),p=Math.min(c,u);if(p>=f){var m=E(f),v=E(p),D=[m.col,v.col].sort(),b=g(f)==s,T=g(p)+1==l;r.push({row:d,leftCol:D[0],rightCol:D[1],isStart:b,isEnd:T})}}return r}function S(t,e){var i,n,r=t.clone().stripTime();return e&&(i=e.clone().stripTime(),n=+e.time(),n&&n>=k&&i.add(1,"days")),(!e||r>=i)&&(i=r.clone().add(1,"days")),{start:r,end:i}}function T(t){var e=S(t.start,t.end);return e.end.diff(e.start,"days")>1}var C=this;C.calendar=i,C.opt=n,C.trigger=r,C.isEventDraggable=o,C.isEventResizable=l,C.eventDrop=a,C.eventResize=c;var H=i.reportEventChange,x=i.options,k=e.duration(x.nextDayThreshold);C.init(),C.getEventTimeText=function(t,e){var r,o;return"object"==typeof t&&"object"==typeof e?(r=t,o=e,e=arguments[2]):(r=t.start,o=t.end),e=e||n("timeFormat"),o&&n("displayEventEnd")?i.formatRange(r,o,e):i.formatDate(r,e)},C.isHiddenDay=d,C.skipHiddenDays=u,C.getCellsPerWeek=h,C.dateToCell=v,C.dateToDayOffset=y,C.dayOffsetToCellOffset=w,C.cellOffsetToCell=E,C.cellToDate=f,C.cellToCellOffset=p,C.cellOffsetToDayOffset=g,C.dayOffsetToDate=m,C.rangeToSegments=D,C.isMultiDayEvent=T;var M,P=n("hiddenDays")||[],z=[],G=[],L=[],N=n("isRTL");(function(){n("weekends")===!1&&P.push(0,6);for(var e=0,i=0;7>e;e++)G[e]=i,z[e]=-1!=t.inArray(e,P),z[e]||(L[i]=e,i++);if(M=i,!M)throw"invalid hiddenDays"})()}function ge(t){pe.call(this,t),this.dayGrid=new ie(this),this.coordMap=this.dayGrid.coordMap}function me(t){ge.call(this,t)}function ve(t){ge.call(this,t)}function ye(t){ge.call(this,t)}function we(t,e){return e.longDateFormat("LT").replace(":mm","(:mm)").replace(/(\Wmm)$/,"($1)").replace(/\s*a$/i,"a")}function Ee(t,e){return e.longDateFormat("LT").replace(/\s*a$/i,"")}function De(t){pe.call(this,t),this.timeGrid=new oe(this),this.opt("allDaySlot")?(this.dayGrid=new ie(this),this.coordMap=new q([this.dayGrid.coordMap,this.timeGrid.coordMap])):this.coordMap=this.timeGrid.coordMap}function Se(t){De.call(this,t)}function be(t){De.call(this,t)}function Te(t){ie.call(this,t)}function Ce(t){oe.call(this,t)}function He(t){pe.call(this,t),this.cellToDate=He.prototype.cellToDate,this.timeGrid=new Ce(this),this.opt("allDaySlot")?(this.dayGrid=new Te(this),this.coordMap=new q([this.dayGrid.coordMap,this.timeGrid.coordMap])):this.coordMap=this.timeGrid.coordMap}function xe(t){He.call(this,t)}var ke={lang:"en",defaultTimedEventDuration:"02:00:00",defaultAllDayEventDuration:{days:1},forceEventDuration:!1,nextDayThreshold:"09:00:00",defaultView:"month",aspectRatio:1.35,header:{left:"title",center:"",right:"today prev,next"},weekends:!0,weekNumbers:!1,weekNumberTitle:"W",weekNumberCalculation:"local",lazyFetching:!0,startParam:"start",endParam:"end",timezoneParam:"timezone",timezone:!1,titleFormat:{month:"MMMM YYYY",week:"ll",day:"LL"},columnFormat:{month:"ddd",week:n,day:"dddd"},timeFormat:{"default":i},displayEventEnd:{month:!1,basicWeek:!1,"default":!0},isRTL:!1,defaultButtonText:{prev:"prev",next:"next",prevYear:"prev year",nextYear:"next year",today:"today",month:"month",week:"week",day:"day"},buttonIcons:{prev:"left-single-arrow",next:"right-single-arrow",prevYear:"left-double-arrow",nextYear:"right-double-arrow"},theme:!1,themeButtonIcons:{prev:"circle-triangle-w",next:"circle-triangle-e",prevYear:"seek-prev",nextYear:"seek-next"},dragOpacity:.75,dragRevertDuration:500,dragScroll:!0,unselectAuto:!0,dropAccept:"*",eventLimit:!1,eventLimitText:"more",eventLimitClick:"popover",dayPopoverFormat:"LL",handleWindowResize:!0,windowResizeDelay:200},Me={en:{columnFormat:{week:"ddd M/D"},dayPopoverFormat:"dddd, MMMM D"}},Re={header:{left:"next,prev today",center:"",right:"title"},buttonIcons:{prev:"right-single-arrow",next:"left-single-arrow",prevYear:"right-double-arrow",nextYear:"left-double-arrow"},themeButtonIcons:{prev:"circle-triangle-e",next:"circle-triangle-w",nextYear:"seek-prev",prevYear:"seek-next"}},Pe=t.fullCalendar={version:"2.1.1"},ze=Pe.views={};t.fn.fullCalendar=function(e){var i=Array.prototype.slice.call(arguments,1),n=this;return this.each(function(r,o){var s,a=t(o),c=a.data("fullCalendar");"string"==typeof e?c&&t.isFunction(c[e])&&(s=c[e].apply(c,i),r||(n=s),"destroy"===e&&a.removeData("fullCalendar")):c||(c=new l(a,e),a.data("fullCalendar",c),c.render())}),n},Pe.langs=Me,Pe.datepickerLang=function(e,i,n){var r=Me[e];r||(r=Me[e]={}),o(r,{isRTL:n.isRTL,weekNumberTitle:n.weekHeader,titleFormat:{month:n.showMonthAfterYear?"YYYY["+n.yearSuffix+"] MMMM":"MMMM YYYY["+n.yearSuffix+"]"},defaultButtonText:{prev:z(n.prevText),next:z(n.nextText),today:z(n.currentText)}}),t.datepicker&&(t.datepicker.regional[i]=t.datepicker.regional[e]=n,t.datepicker.regional.en=t.datepicker.regional[""],t.datepicker.setDefaults(n))},Pe.lang=function(t,e){var i;e&&(i=Me[t],i||(i=Me[t]={}),o(i,e||{})),ke.lang=t},Pe.sourceNormalizers=[],Pe.sourceFetchers=[];var Ge={dataType:"json",cache:!1},Le=1,Ne=["sun","mon","tue","wed","thu","fri","sat"];Pe.applyAll=M;var _e=/^\s*\d{4}-\d\d$/,Ae=/^\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+)?)?)?)?)?$/;Pe.moment=function(){return N(arguments)},Pe.moment.utc=function(){var t=N(arguments,!0);return t.hasTime()&&t.utc(),t},Pe.moment.parseZone=function(){return N(arguments,!0,!0)},_.prototype=x(e.fn),_.prototype.clone=function(){return N([this])},_.prototype.time=function(t){if(null==t)return e.duration({hours:this.hours(),minutes:this.minutes(),seconds:this.seconds(),milliseconds:this.milliseconds()});delete this._ambigTime,e.isDuration(t)||e.isMoment(t)||(t=e.duration(t));var i=0;return e.isDuration(t)&&(i=24*Math.floor(t.asDays())),this.hours(i+t.hours()).minutes(t.minutes()).seconds(t.seconds()).milliseconds(t.milliseconds())},_.prototype.stripTime=function(){var t=this.toArray();return e.fn.utc.call(this),this.year(t[0]).month(t[1]).date(t[2]).hours(0).minutes(0).seconds(0).milliseconds(0),this._ambigTime=!0,this._ambigZone=!0,this},_.prototype.hasTime=function(){return!this._ambigTime},_.prototype.stripZone=function(){var t=this.toArray(),i=this._ambigTime;return e.fn.utc.call(this),this.year(t[0]).month(t[1]).date(t[2]).hours(t[3]).minutes(t[4]).seconds(t[5]).milliseconds(t[6]),i&&(this._ambigTime=!0),this._ambigZone=!0,this},_.prototype.hasZone=function(){return!this._ambigZone},_.prototype.zone=function(t){return null!=t&&(delete this._ambigTime,delete this._ambigZone),e.fn.zone.apply(this,arguments)},_.prototype.local=function(){var t=this.toArray(),i=this._ambigZone;return delete this._ambigTime,delete this._ambigZone,e.fn.local.apply(this,arguments),i&&this.year(t[0]).month(t[1]).date(t[2]).hours(t[3]).minutes(t[4]).seconds(t[5]).milliseconds(t[6]),this},_.prototype.utc=function(){return delete this._ambigTime,delete this._ambigZone,e.fn.utc.apply(this,arguments)},_.prototype.format=function(){return arguments[0]?V(this,arguments[0]):this._ambigTime?Y(this,"YYYY-MM-DD"):this._ambigZone?Y(this,"YYYY-MM-DD[T]HH:mm:ss"):Y(this)},_.prototype.toISOString=function(){return this._ambigTime?Y(this,"YYYY-MM-DD"):this._ambigZone?Y(this,"YYYY-MM-DD[T]HH:mm:ss"):e.fn.toISOString.apply(this,arguments)},_.prototype.isWithin=function(t,e){var i=A([this,t,e]);return i[0]>=i[1]&&i[0]').addClass(i.className||"").css({top:0,left:0}).append(i.content).appendTo(i.parentEl),this.el.on("click",".fc-close",function(){e.hide()}),i.autoHide&&t(document).on("mousedown",this.documentMousedownProxy=t.proxy(this,"documentMousedown"))},documentMousedown:function(e){this.el&&!t(e.target).closest(this.el).length&&this.hide()},destroy:function(){this.hide(),this.el&&(this.el.remove(),this.el=null),t(document).off("mousedown",this.documentMousedownProxy)},position:function(){var e,i,n,r,o,s=this.options,l=this.el.offsetParent().offset(),a=this.el.outerWidth(),c=this.el.outerHeight(),d=t(window),h=w(this.el);r=s.top||0,o=void 0!==s.left?s.left:void 0!==s.right?s.right-a:0,h.is(window)||h.is(document)?(h=d,e=0,i=0):(n=h.offset(),e=n.top,i=n.left),e+=d.scrollTop(),i+=d.scrollLeft(),s.viewportConstrain!==!1&&(r=Math.min(r,e+h.outerHeight()-c-this.margin),r=Math.max(r,e+this.margin),o=Math.min(o,i+h.outerWidth()-a-this.margin),o=Math.max(o,i+this.margin)),this.el.css({top:r-l.top,left:o-l.left}) +},trigger:function(t){this.options[t]&&this.options[t].apply(this,Array.prototype.slice.call(arguments,1))}},$.prototype={grid:null,rows:null,cols:null,containerEl:null,minX:null,maxX:null,minY:null,maxY:null,build:function(){this.grid.buildCoords(this.rows=[],this.cols=[]),this.computeBounds()},getCell:function(t,e){var i,n=null,r=this.rows,o=this.cols,s=-1,l=-1;if(this.inBounds(t,e)){for(i=0;r.length>i;i++)if(e>=r[i][0]&&r[i][1]>e){s=i;break}for(i=0;o.length>i;i++)if(t>=o[i][0]&&o[i][1]>t){l=i;break}s>=0&&l>=0&&(n={row:s,col:l},n.grid=this.grid,n.date=this.grid.getCellDate(n))}return n},computeBounds:function(){var t;this.containerEl&&(t=this.containerEl.offset(),this.minX=t.left,this.maxX=t.left+this.containerEl.outerWidth(),this.minY=t.top,this.maxY=t.top+this.containerEl.outerHeight())},inBounds:function(t,e){return this.containerEl?t>=this.minX&&this.maxX>t&&e>=this.minY&&this.maxY>e:!0}},q.prototype={coordMaps:null,build:function(){var t,e=this.coordMaps;for(t=0;e.length>t;t++)e[t].build()},getCell:function(t,e){var i,n=this.coordMaps,r=null;for(i=0;n.length>i&&!r;i++)r=n[i].getCell(t,e);return r}},U.prototype={coordMap:null,options:null,isListening:!1,isDragging:!1,origCell:null,origDate:null,cell:null,date:null,mouseX0:null,mouseY0:null,mousemoveProxy:null,mouseupProxy:null,scrollEl:null,scrollBounds:null,scrollTopVel:null,scrollLeftVel:null,scrollIntervalId:null,scrollHandlerProxy:null,scrollSensitivity:30,scrollSpeed:200,scrollIntervalMs:50,mousedown:function(t){D(t)&&(t.preventDefault(),this.startListening(t),this.options.distance||this.startDrag(t))},startListening:function(e){var i,n;this.isListening||(e&&this.options.scroll&&(i=w(t(e.target)),i.is(window)||i.is(document)||(this.scrollEl=i,this.scrollHandlerProxy=L(t.proxy(this,"scrollHandler"),100),this.scrollEl.on("scroll",this.scrollHandlerProxy))),this.computeCoords(),e&&(n=this.getCell(e),this.origCell=n,this.origDate=n?n.date:null,this.mouseX0=e.pageX,this.mouseY0=e.pageY),t(document).on("mousemove",this.mousemoveProxy=t.proxy(this,"mousemove")).on("mouseup",this.mouseupProxy=t.proxy(this,"mouseup")).on("selectstart",this.preventDefault),this.isListening=!0,this.trigger("listenStart",e))},computeCoords:function(){this.coordMap.build(),this.computeScrollBounds()},mousemove:function(t){var e,i;this.isDragging||(e=this.options.distance||1,i=Math.pow(t.pageX-this.mouseX0,2)+Math.pow(t.pageY-this.mouseY0,2),i>=e*e&&this.startDrag(t)),this.isDragging&&this.drag(t)},startDrag:function(t){var e;this.isListening||this.startListening(),this.isDragging||(this.isDragging=!0,this.trigger("dragStart",t),e=this.getCell(t),e&&this.cellOver(e,!0))},drag:function(t){var e;this.isDragging&&(e=this.getCell(t),K(e,this.cell)||(this.cell&&this.cellOut(),e&&this.cellOver(e)),this.dragScroll(t))},cellOver:function(t){this.cell=t,this.date=t.date,this.trigger("cellOver",t,t.date)},cellOut:function(){this.cell&&(this.trigger("cellOut",this.cell),this.cell=null,this.date=null)},mouseup:function(t){this.stopDrag(t),this.stopListening(t)},stopDrag:function(t){this.isDragging&&(this.stopScrolling(),this.trigger("dragStop",t),this.isDragging=!1)},stopListening:function(e){this.isListening&&(this.scrollEl&&(this.scrollEl.off("scroll",this.scrollHandlerProxy),this.scrollHandlerProxy=null),t(document).off("mousemove",this.mousemoveProxy).off("mouseup",this.mouseupProxy).off("selectstart",this.preventDefault),this.mousemoveProxy=null,this.mouseupProxy=null,this.isListening=!1,this.trigger("listenStop",e),this.origCell=this.cell=null,this.origDate=this.date=null)},getCell:function(t){return this.coordMap.getCell(t.pageX,t.pageY)},trigger:function(t){this.options[t]&&this.options[t].apply(this,Array.prototype.slice.call(arguments,1))},preventDefault:function(t){t.preventDefault()},computeScrollBounds:function(){var t,e=this.scrollEl;e&&(t=e.offset(),this.scrollBounds={top:t.top,left:t.left,bottom:t.top+e.outerHeight(),right:t.left+e.outerWidth()})},dragScroll:function(t){var e,i,n,r,o=this.scrollSensitivity,s=this.scrollBounds,l=0,a=0;s&&(e=(o-(t.pageY-s.top))/o,i=(o-(s.bottom-t.pageY))/o,n=(o-(t.pageX-s.left))/o,r=(o-(s.right-t.pageX))/o,e>=0&&1>=e?l=-1*e*this.scrollSpeed:i>=0&&1>=i&&(l=i*this.scrollSpeed),n>=0&&1>=n?a=-1*n*this.scrollSpeed:r>=0&&1>=r&&(a=r*this.scrollSpeed)),this.setScrollVel(l,a)},setScrollVel:function(e,i){this.scrollTopVel=e,this.scrollLeftVel=i,this.constrainScrollVel(),!this.scrollTopVel&&!this.scrollLeftVel||this.scrollIntervalId||(this.scrollIntervalId=setInterval(t.proxy(this,"scrollIntervalFunc"),this.scrollIntervalMs))},constrainScrollVel:function(){var t=this.scrollEl;0>this.scrollTopVel?0>=t.scrollTop()&&(this.scrollTopVel=0):this.scrollTopVel>0&&t.scrollTop()+t[0].clientHeight>=t[0].scrollHeight&&(this.scrollTopVel=0),0>this.scrollLeftVel?0>=t.scrollLeft()&&(this.scrollLeftVel=0):this.scrollLeftVel>0&&t.scrollLeft()+t[0].clientWidth>=t[0].scrollWidth&&(this.scrollLeftVel=0)},scrollIntervalFunc:function(){var t=this.scrollEl,e=this.scrollIntervalMs/1e3;this.scrollTopVel&&t.scrollTop(t.scrollTop()+this.scrollTopVel*e),this.scrollLeftVel&&t.scrollLeft(t.scrollLeft()+this.scrollLeftVel*e),this.constrainScrollVel(),this.scrollTopVel||this.scrollLeftVel||this.stopScrolling()},stopScrolling:function(){this.scrollIntervalId&&(clearInterval(this.scrollIntervalId),this.scrollIntervalId=null,this.computeCoords())},scrollHandler:function(){this.scrollIntervalId||this.computeCoords()}},Q.prototype={options:null,sourceEl:null,el:null,parentEl:null,top0:null,left0:null,mouseY0:null,mouseX0:null,topDelta:null,leftDelta:null,mousemoveProxy:null,isFollowing:!1,isHidden:!1,isAnimating:!1,start:function(e){this.isFollowing||(this.isFollowing=!0,this.mouseY0=e.pageY,this.mouseX0=e.pageX,this.topDelta=0,this.leftDelta=0,this.isHidden||this.updatePosition(),t(document).on("mousemove",this.mousemoveProxy=t.proxy(this,"mousemove")))},stop:function(e,i){function n(){this.isAnimating=!1,r.destroyEl(),this.top0=this.left0=null,i&&i()}var r=this,o=this.options.revertDuration;this.isFollowing&&!this.isAnimating&&(this.isFollowing=!1,t(document).off("mousemove",this.mousemoveProxy),e&&o&&!this.isHidden?(this.isAnimating=!0,this.el.animate({top:this.top0,left:this.left0},{duration:o,complete:n})):n())},getEl:function(){var t=this.el;return t||(this.sourceEl.width(),t=this.el=this.sourceEl.clone().css({position:"absolute",visibility:"",display:this.isHidden?"none":"",margin:0,right:"auto",bottom:"auto",width:this.sourceEl.width(),height:this.sourceEl.height(),opacity:this.options.opacity||"",zIndex:this.options.zIndex}).appendTo(this.parentEl)),t},destroyEl:function(){this.el&&(this.el.remove(),this.el=null)},updatePosition:function(){var t,e;this.getEl(),null===this.top0&&(this.sourceEl.width(),t=this.sourceEl.offset(),e=this.el.offsetParent().offset(),this.top0=t.top-e.top,this.left0=t.left-e.left),this.el.css({top:this.top0+this.topDelta,left:this.left0+this.leftDelta})},mousemove:function(t){this.topDelta=t.pageY-this.mouseY0,this.leftDelta=t.pageX-this.mouseX0,this.isHidden||this.updatePosition()},hide:function(){this.isHidden||(this.isHidden=!0,this.el&&this.el.hide())},show:function(){this.isHidden&&(this.isHidden=!1,this.updatePosition(),this.getEl().show())}},J.prototype={view:null,cellHtml:"
"+s+""},bookendCells:function(t,e,i){var n=this.view,r=this.getHtmlRenderer("intro",e)(i||0),o=this.getHtmlRenderer("outro",e)(i||0),s=n.opt("isRTL"),l=s?o:r,a=s?r:o;return"string"==typeof t?l+t+a:t.prepend(l).append(a)},getHtmlRenderer:function(t,e){var i,n,r,o,s=this.view;return i=t+"Html",e&&(n=e+G(t)+"Html"),n&&(o=s[n])?r=s:n&&(o=this[n])?r=this:(o=s[i])?r=s:(o=this[i])&&(r=this),"function"==typeof o?function(){return o.apply(r,arguments)||""}:function(){return o||""}}},te.prototype=x(J.prototype),t.extend(te.prototype,{el:null,coordMap:null,cellDuration:null,render:function(){this.bindHandlers()},destroy:function(){},buildCoords:function(){},getCellDate:function(){},getCellDayEl:function(){},rangeToSegs:function(){},bindHandlers:function(){var e=this;this.el.on("mousedown",function(i){t(i.target).is(".fc-event-container *, .fc-more")||t(i.target).closest(".fc-popover").length||e.dayMousedown(i)}),this.bindSegHandlers()},dayMousedown:function(t){var e,i,n,r=this,o=this.view,s=o.opt("selectable"),l=null,a=new U(this.coordMap,{scroll:o.opt("dragScroll"),dragStart:function(){o.unselect()},cellOver:function(t,o){a.origDate&&(n=r.getCellDayEl(t),l=[o,a.origDate].sort(H),e=l[0],i=l[1].clone().add(r.cellDuration),s&&r.renderSelection(e,i))},cellOut:function(){l=null,r.destroySelection()},listenStop:function(t){l&&(l[0].isSame(l[1])&&o.trigger("dayClick",n[0],e,t),s&&o.reportSelection(e,i,t))}});a.mousedown(t)},renderDrag:function(){},destroyDrag:function(){},renderResize:function(){},destroyResize:function(){},renderRangeHelper:function(t,e,i){var n,r=this.view;!e&&r.opt("forceEventDuration")&&(e=r.calendar.getDefaultEventEnd(!t.hasTime(),t)),n=i?x(i.event):{},n.start=t,n.end=e,n.allDay=!(t.hasTime()||e&&e.hasTime()),n.className=(n.className||[]).concat("fc-helper"),i||(n.editable=!1),this.renderHelper(n,i)},renderHelper:function(){},destroyHelper:function(){},renderSelection:function(t,e){this.renderHighlight(t,e)},destroySelection:function(){this.destroyHighlight()},renderHighlight:function(){},destroyHighlight:function(){},headHtml:function(){return'
'+"
'),e>t&&(n+=''),i>e&&(n+=''),n=this.bookendCells(n,"highlight"),'
'+n+""+"
"+"
"}}),t.extend(ee.prototype,{segs:null,rowStructs:null,renderEvents:function(e){var i=this.rowStructs=this.renderEventRows(e),n=[];this.rowEls.each(function(e,r){t(r).find(".fc-content-skeleton > table").append(i[e].tbodyEl),n.push.apply(n,i[e].segs)}),this.segs=n},getSegs:function(){return(this.segs||[]).concat(this.popoverSegs||[])},destroyEvents:function(){var t,e;for(J.prototype.destroyEvents.call(this),t=this.rowStructs||[];e=t.pop();)e.tbodyEl.remove();this.segs=null,this.destroySegPopover()},renderEventRows:function(t){var e,i,n=this.eventsToSegs(t),r=[];for(n=this.renderSegs(n),e=this.groupSegRows(n),i=0;e.length>i;i++)r.push(this.renderEventRow(i,e[i]));return r},renderSegHtml:function(t,e){var i,n=this.view,r=n.opt("isRTL"),o=t.event,s=n.isEventDraggable(o),l=!e&&o.allDay&&t.isEnd&&n.isEventResizable(o),a=this.getSegClasses(t,s,l),c=this.getEventSkinCss(o),d="";return a.unshift("fc-day-grid-event"),!o.allDay&&t.isStart&&(d=''+R(n.getEventTimeText(o))+""),i=''+(R(o.title||"")||" ")+"",'"+'
'+(r?i+" "+d:d+" "+i)+"
"+(l?'
':"")+""},renderEventRow:function(e,i){function n(e){for(;e>s;)d=(y[r-1]||[])[s],d?d.attr("rowspan",parseInt(d.attr("rowspan")||1,10)+1):(d=t("
"),l.append(d)),v[r][s]=d,y[r][s]=d,s++}var r,o,s,l,a,c,d,h=this.view,u=h.colCnt,f=this.buildSegLevels(i),g=Math.max(1,f.length),p=t("
').append(c.el),c.leftCol!=c.rightCol?d.attr("colspan",c.rightCol-c.leftCol+1):y[r][s]=d;c.rightCol>=s;)v[r][s]=d,m[r][s]=c,s++;l.append(d)}n(u),this.bookendCells(l,"eventSkeleton"),p.append(l)}return{row:e,tbodyEl:p,cellMatrix:v,segMatrix:m,segLevels:f,segs:i}},buildSegLevels:function(t){var e,i,n,r=[];for(t.sort(te),e=0;t.length>e;e++){for(i=t[e],n=0;r.length>n&&ie(i,r[n]);n++);i.level=n,(r[n]||(r[n]=[])).push(i)}for(n=0;r.length>n;n++)r[n].sort(ne);return r},groupSegRows:function(t){var e,i=this.view,n=[];for(e=0;i.rowCnt>e;e++)n.push([]);for(e=0;t.length>e;e++)n[t[e].row].push(t[e]);return n}}),t.extend(ee.prototype,{segPopover:null,popoverSegs:null,destroySegPopover:function(){this.segPopover&&this.segPopover.hide()},limitRows:function(t){var e,i,n=this.rowStructs||[];for(e=0;n.length>e;e++)this.unlimitRow(e),i=t?"number"==typeof t?t:this.computeRowLevelLimit(e):!1,i!==!1&&this.limitRow(e,i)},computeRowLevelLimit:function(t){var e,i,n=this.rowEls.eq(t),r=n.height(),o=this.rowStructs[t].tbodyEl.children();for(e=0;o.length>e;e++)if(i=o.eq(e).removeClass("fc-limited"),i.position().top+i.outerHeight()>r)return e;return!1},limitRow:function(e,i){function n(n){for(;n>T;)r={row:e,col:T},d=E.getCellSegs(r,i),d.length&&(f=s[i-1][T],w=E.renderMoreLink(r,d),y=t("
").append(w),f.append(y),D.push(y[0])),T++}var r,o,s,l,a,c,d,h,u,f,g,p,m,v,y,w,E=this,b=this.view,S=this.rowStructs[e],D=[],T=0;if(i&&S.segLevels.length>i){for(o=S.segLevels[i-1],s=S.cellMatrix,l=S.tbodyEl.children().slice(i).addClass("fc-limited").get(),a=0;o.length>a;a++){for(c=o[a],n(c.leftCol),u=[],h=0;c.rightCol>=T;)r={row:e,col:T},d=this.getCellSegs(r,i),u.push(d),h+=d.length,T++;if(h){for(f=s[i-1][c.leftCol],g=f.attr("rowspan")||1,p=[],m=0;u.length>m;m++)v=t('
').attr("rowspan",g),d=u[m],r={row:e,col:c.leftCol+m},w=this.renderMoreLink(r,[c].concat(d)),y=t("
").append(w),v.append(y),p.push(v[0]),D.push(v[0]);f.addClass("fc-limited").after(t(p)),l.push(f[0])}}n(b.colCnt),S.moreEls=t(D),S.limitedEls=t(l)}},unlimitRow:function(t){var e=this.rowStructs[t];e.moreEls&&(e.moreEls.remove(),e.moreEls=null),e.limitedEls&&(e.limitedEls.removeClass("fc-limited"),e.limitedEls=null)},renderMoreLink:function(e,i){var n=this,r=this.view;return t('').text(this.getMoreLinkText(i.length)).on("click",function(o){var s=r.opt("eventLimitClick"),l=r.cellToDate(e),a=t(this),c=n.getCellDayEl(e),d=n.getCellSegs(e),h=n.resliceDaySegs(d,l),u=n.resliceDaySegs(i,l);"function"==typeof s&&(s=r.trigger("eventLimitClick",null,{date:l,dayEl:c,moreEl:a,segs:h,hiddenSegs:u},o)),"popover"===s?n.showSegPopover(l,e,a,h):"string"==typeof s&&r.calendar.zoomTo(l,s)})},showSegPopover:function(t,e,i,n){var r,o,s=this,l=this.view,a=i.parent();r=1==l.rowCnt?this.view.el:this.rowEls.eq(e.row),o={className:"fc-more-popover",content:this.renderSegPopoverContent(t,n),parentEl:this.el,top:r.offset().top,autoHide:!0,viewportConstrain:l.opt("popoverViewportConstrain"),hide:function(){s.segPopover.destroy(),s.segPopover=null,s.popoverSegs=null}},l.opt("isRTL")?o.right=a.offset().left+a.outerWidth()+1:o.left=a.offset().left-1,this.segPopover=new j(o),this.segPopover.show()},renderSegPopoverContent:function(e,i){var n,r=this.view,o=r.opt("theme"),s=e.format(r.opt("dayPopoverFormat")),l=t('
'+''+''+R(s)+""+'
'+"
"+'
'+'
'+"
"),a=l.find(".fc-event-container");for(i=this.renderSegs(i,!0),this.popoverSegs=i,n=0;i.length>n;n++)i[n].cellDate=e,a.append(i[n].el);return l},resliceDaySegs:function(e,i){var n=t.map(e,function(t){return t.event}),r=i.clone().stripTime(),o=r.clone().add(1,"days");return this.eventsToSegs(n,r,o)},getMoreLinkText:function(t){var e=this.view,i=e.opt("eventLimitText");return"function"==typeof i?i(t):"+"+t+" "+i},getCellSegs:function(t,e){for(var i,n=this.rowStructs[t.row].segMatrix,r=e||0,o=[];n.length>r;)i=n[r][t.col],i&&o.push(i),r++;return o}}),re.prototype=H(J.prototype),t.extend(re.prototype,{slotDuration:null,snapDuration:null,minTime:null,maxTime:null,dayEls:null,slatEls:null,slatTops:null,highlightEl:null,helperEl:null,render:function(){this.processOptions(),this.el.html(this.renderHtml()),this.dayEls=this.el.find(".fc-day"),this.slatEls=this.el.find(".fc-slats tr"),this.computeSlatTops(),J.prototype.render.call(this)},renderHtml:function(){return'
'+this.rowHtml("slotBg")+"
"+"
"+'
'+""+this.slatRowHtml()+"
"+"
"},slotBgCellHtml:function(t,e,i){return this.bgCellHtml(t,e,i)},slatRowHtml:function(){for(var t,i,n,r=this.view,o=r.calendar,s=r.opt("isRTL"),l="",a=0===this.slotDuration.asMinutes()%15,c=e.duration(+this.minTime);this.maxTime>c;)t=r.start.clone().time(c),i=t.minutes(),n='
"+(a&&i?"":""+R(o.formatDate(t,r.opt("axisFormat")))+"")+"
'+(s?n:"")+"
',d=n.col),r=l.cellToDate(0,d),o=this.computeDateTop(n.start,r),s=this.computeDateTop(n.end,r),c+='
'+"
"+"
'),c=this.bookendCells(c,"highlight"),'
'+c+""+"
"+"
"}}),t.extend(re.prototype,{segs:null,eventSkeletonEl:null,renderEvents:function(e){var i=this.renderEventTable(e);this.eventSkeletonEl=t('
').append(i.tableEl),this.el.append(this.eventSkeletonEl),this.segs=i.segs},getSegs:function(){return this.segs||[]},destroyEvents:function(){J.prototype.destroyEvents.call(this),this.eventSkeletonEl&&(this.eventSkeletonEl.remove(),this.eventSkeletonEl=null),this.segs=null},renderEventTable:function(e){var i,n,r,o,s,l,a=t("
"),c=a.find("tr"),d=this.eventsToSegs(e);for(d=this.renderSegs(d),i=this.groupSegCols(d),this.computeSegVerticals(d),o=0;i.length>o;o++){for(s=i[o],oe(s),l=t('
'),n=0;s.length>n;n++)r=s[n],r.el.css(this.generateSegPositionCss(r)),30>r.bottom-r.top&&r.el.addClass("fc-short"),l.append(r.el);c.append(t("
").append(l))}return this.bookendCells(c,"eventSkeleton"),{tableEl:a,segs:d}},updateSegVerticals:function(){var t,e=this.segs;if(e)for(this.computeSegVerticals(e),t=0;e.length>t;t++)e[t].el.css(this.generateSegVerticalCss(e[t]))},computeSegVerticals:function(t){var e,i;for(e=0;t.length>e;e++)i=t[e],i.top=this.computeDateTop(i.start,i.start),i.bottom=this.computeDateTop(i.end,i.start)},renderSegHtml:function(t,e){var i,n,r,o=this.view,s=t.event,l=o.isEventDraggable(s),a=!e&&t.isEnd&&o.isEventResizable(s),c=this.getSegClasses(t,l,a),d=this.getEventSkinCss(s);return c.unshift("fc-time-grid-event"),o.isMultiDayEvent(s)?(t.isStart||t.isEnd)&&(i=o.getEventTimeText(t.start,t.end),n=o.getEventTimeText(t.start,t.end,"LT"),r=o.getEventTimeText(t.start,null)):(i=o.getEventTimeText(s),n=o.getEventTimeText(s,"LT"),r=o.getEventTimeText(s.start,null)),'"+'
'+(i?'
"+""+R(i)+""+"
":"")+(s.title?'
'+R(s.title)+"
":"")+"
"+'
'+(a?'
':"")+""},generateSegPositionCss:function(t){var e,i,n=this.view,r=n.opt("isRTL"),o=n.opt("slotEventOverlap"),s=t.backwardCoord,l=t.forwardCoord,a=this.generateSegVerticalCss(t);return o&&(l=Math.min(1,s+2*(l-s))),r?(e=1-l,i=s):(e=s,i=1-l),a.zIndex=t.level+1,a.left=100*e+"%",a.right=100*i+"%",o&&t.forwardPressure&&(a[r?"marginLeft":"marginRight"]=20),a},generateSegVerticalCss:function(t){return{top:t.top,bottom:-t.bottom}},groupSegCols:function(t){var e,i=this.view,n=[];for(e=0;i.colCnt>e;e++)n.push([]);for(e=0;t.length>e;e++)n[t[e].col].push(t[e]);return n}}),fe.prototype={calendar:null,coordMap:null,el:null,start:null,end:null,intervalStart:null,intervalEnd:null,rowCnt:null,colCnt:null,isSelected:!1,scrollerEl:null,scrollTop:null,widgetHeaderClass:null,widgetContentClass:null,highlightStateClass:null,documentMousedownProxy:null,documentDragStartProxy:null,init:function(){var e=this.opt("theme")?"ui":"fc";this.widgetHeaderClass=e+"-widget-header",this.widgetContentClass=e+"-widget-content",this.highlightStateClass=e+"-state-highlight",this.documentMousedownProxy=t.proxy(this,"documentMousedown"),this.documentDragStartProxy=t.proxy(this,"documentDragStart")},render:function(){this.updateSize(),this.trigger("viewRender",this,this,this.el),t(document).on("mousedown",this.documentMousedownProxy).on("dragstart",this.documentDragStartProxy)},destroy:function(){this.unselect(),this.trigger("viewDestroy",this,this,this.el),this.destroyEvents(),this.el.empty(),t(document).off("mousedown",this.documentMousedownProxy).off("dragstart",this.documentDragStartProxy)},incrementDate:function(){},updateSize:function(t){t&&this.recordScroll(),this.updateHeight(),this.updateWidth()},updateWidth:function(){},updateHeight:function(){var t=this.calendar;this.setHeight(t.getSuggestedViewHeight(),t.isHeightAuto())},setHeight:function(){},computeScrollerHeight:function(t){var e,i=this.el.add(this.scrollerEl);return i.css({position:"relative",left:-1}),e=this.el.outerHeight()-this.scrollerEl.height(),i.css({position:"",left:""}),t-e},recordScroll:function(){this.scrollerEl&&(this.scrollTop=this.scrollerEl.scrollTop())},restoreScroll:function(){null!==this.scrollTop&&this.scrollerEl.scrollTop(this.scrollTop)},renderEvents:function(){this.segEach(function(t){this.trigger("eventAfterRender",t.event,t.event,t.el)}),this.trigger("eventAfterAllRender")},destroyEvents:function(){this.segEach(function(t){this.trigger("eventDestroy",t.event,t.event,t.el)})},resolveEventEl:function(e,i){var n=this.trigger("eventRender",e,e,i);return n===!1?i=null:n&&n!==!0&&(i=t(n)),i},showEvent:function(t){this.segEach(function(t){t.el.css("visibility","")},t)},hideEvent:function(t){this.segEach(function(t){t.el.css("visibility","hidden")},t)},segEach:function(t,e){var i,n=this.getSegs();for(i=0;n.length>i;i++)e&&n[i].event._id!==e._id||t.call(this,n[i])},getSegs:function(){},renderDrag:function(){},destroyDrag:function(){},documentDragStart:function(e){var i,n=this,r=null;this.opt("droppable")&&(i=new q(this.coordMap,{cellOver:function(t,e){r=e,n.renderDrag(e)},cellOut:function(){r=null,n.destroyDrag()}}),t(document).one("dragstop",function(t,e){n.destroyDrag(),r&&n.trigger("drop",t.target,r,t,e)}),i.startDrag(e))},select:function(t,e,i){this.unselect(i),this.renderSelection(t,e),this.reportSelection(t,e,i)},renderSelection:function(){},reportSelection:function(t,e,i){this.isSelected=!0,this.trigger("select",null,t,e,i)},unselect:function(t){this.isSelected&&(this.isSelected=!1,this.destroySelection(),this.trigger("unselect",null,t))},destroySelection:function(){},documentMousedown:function(e){var i;this.isSelected&&this.opt("unselectAuto")&&E(e)&&(i=this.opt("unselectCancel"),i&&t(e.target).closest(i).length||this.unselect(e))}},ge.prototype=H(fe.prototype),t.extend(ge.prototype,{dayGrid:null,dayNumbersVisible:!1,weekNumbersVisible:!1,weekNumberWidth:null,headRowEl:null,render:function(t,e,i){this.rowCnt=t,this.colCnt=e,this.dayNumbersVisible=i,this.weekNumbersVisible=this.opt("weekNumbers"),this.dayGrid.numbersVisible=this.dayNumbersVisible||this.weekNumbersVisible,this.el.addClass("fc-basic-view").html(this.renderHtml()),this.headRowEl=this.el.find("thead .fc-row"),this.scrollerEl=this.el.find(".fc-day-grid-container"),this.dayGrid.coordMap.containerEl=this.scrollerEl,this.dayGrid.el=this.el.find(".fc-day-grid"),this.dayGrid.render(this.hasRigidRows()),fe.prototype.render.call(this)},destroy:function(){this.dayGrid.destroy(),fe.prototype.destroy.call(this)},renderHtml:function(){return'"+""+""+""+""+'"+""+""+"
'+this.dayGrid.headHtml()+"
'+'
'+'
'+"
"+"
" -},headIntroHtml:function(){return this.weekNumbersVisible?'
"+""+R(this.opt("weekNumberTitle"))+""+""+""+this.calendar.calculateWeekNumber(this.cellToDate(t,0))+""+"'+i.date()+""},weekNumberStyleAttr:function(){return null!==this.weekNumberWidth?'style="width:'+this.weekNumberWidth+'px"':""},hasRigidRows:function(){var t=this.opt("eventLimit");return t&&"number"!=typeof t},updateWidth:function(){this.weekNumbersVisible&&(this.weekNumberWidth=p(this.el.find(".fc-week-number")))},setHeight:function(t,e){var i,n=this.opt("eventLimit");v(this.scrollerEl),u(this.headRowEl),this.dayGrid.destroySegPopover(),n&&"number"==typeof n&&this.dayGrid.limitRows(n),i=this.computeScrollerHeight(t),this.setGridHeight(i,e),n&&"number"!=typeof n&&this.dayGrid.limitRows(n),!e&&m(this.scrollerEl,i)&&(h(this.headRowEl,w(this.scrollerEl)),i=this.computeScrollerHeight(t),this.scrollerEl.height(i),this.restoreScroll())},setGridHeight:function(t,e){e?g(this.dayGrid.rowEls):f(this.dayGrid.rowEls,t,!0)},renderEvents:function(t){this.dayGrid.renderEvents(t),this.updateHeight(),fe.prototype.renderEvents.call(this,t)},getSegs:function(){return this.dayGrid.getSegs()},destroyEvents:function(){fe.prototype.destroyEvents.call(this),this.recordScroll(),this.dayGrid.destroyEvents()},renderDrag:function(t,e,i){return this.dayGrid.renderDrag(t,e,i)},destroyDrag:function(){this.dayGrid.destroyDrag()},renderSelection:function(t,e){this.dayGrid.renderSelection(t,e)},destroySelection:function(){this.dayGrid.destroySelection()}}),r({fixedWeekCount:!0}),xe.month=pe,pe.prototype=H(ge.prototype),t.extend(pe.prototype,{name:"month",incrementDate:function(t,e){return t.clone().stripTime().add(e,"months").startOf("month")},render:function(t){var e;this.intervalStart=t.clone().stripTime().startOf("month"),this.intervalEnd=this.intervalStart.clone().add(1,"months"),this.start=this.intervalStart.clone(),this.start=this.skipHiddenDays(this.start),this.start.startOf("week"),this.start=this.skipHiddenDays(this.start),this.end=this.intervalEnd.clone(),this.end=this.skipHiddenDays(this.end,-1,!0),this.end.add((7-this.end.weekday())%7,"days"),this.end=this.skipHiddenDays(this.end,-1,!0),e=Math.ceil(this.end.diff(this.start,"weeks",!0)),this.isFixedWeeks()&&(this.end.add(6-e,"weeks"),e=6),this.title=this.calendar.formatDate(this.intervalStart,this.opt("titleFormat")),ge.prototype.render.call(this,e,this.getCellsPerWeek(),!0)},setGridHeight:function(t,e){e=e||"variable"===this.opt("weekMode"),e&&(t*=this.rowCnt/6),f(this.dayGrid.rowEls,t,!e)},isFixedWeeks:function(){var t=this.opt("weekMode");return t?"fixed"===t:this.opt("fixedWeekCount")}}),xe.basicWeek=me,me.prototype=H(ge.prototype),t.extend(me.prototype,{name:"basicWeek",incrementDate:function(t,e){return t.clone().stripTime().add(e,"weeks").startOf("week")},render:function(t){this.intervalStart=t.clone().stripTime().startOf("week"),this.intervalEnd=this.intervalStart.clone().add(1,"weeks"),this.start=this.skipHiddenDays(this.intervalStart),this.end=this.skipHiddenDays(this.intervalEnd,-1,!0),this.title=this.calendar.formatRange(this.start,this.end.clone().subtract(1),this.opt("titleFormat")," — "),ge.prototype.render.call(this,1,this.getCellsPerWeek(),!1)}}),xe.basicDay=ve,ve.prototype=H(ge.prototype),t.extend(ve.prototype,{name:"basicDay",incrementDate:function(t,e){var i=t.clone().stripTime().add(e,"days");return i=this.skipHiddenDays(i,0>e?-1:1)},render:function(t){this.start=this.intervalStart=t.clone().stripTime(),this.end=this.intervalEnd=this.start.clone().add(1,"days"),this.title=this.calendar.formatDate(this.start,this.opt("titleFormat")),ge.prototype.render.call(this,1,1,!1)}}),r({allDaySlot:!0,allDayText:"all-day",scrollTime:"06:00:00",slotDuration:"00:30:00",axisFormat:ye,timeFormat:{agenda:we},minTime:"00:00:00",maxTime:"24:00:00",slotEventOverlap:!0});var Ye=5;Ee.prototype=H(fe.prototype),t.extend(Ee.prototype,{timeGrid:null,dayGrid:null,axisWidth:null,noScrollRowEls:null,bottomRuleEl:null,bottomRuleHeight:null,render:function(e){this.rowCnt=1,this.colCnt=e,this.el.addClass("fc-agenda-view").html(this.renderHtml()),this.scrollerEl=this.el.find(".fc-time-grid-container"),this.timeGrid.coordMap.containerEl=this.scrollerEl,this.timeGrid.el=this.el.find(".fc-time-grid"),this.timeGrid.render(),this.bottomRuleEl=t('
').appendTo(this.timeGrid.el),this.dayGrid&&(this.dayGrid.el=this.el.find(".fc-day-grid"),this.dayGrid.render(),this.dayGrid.bottomCoordPadding=this.dayGrid.el.next("hr").outerHeight()),this.noScrollRowEls=this.el.find(".fc-row:not(.fc-scroller *)"),fe.prototype.render.call(this),this.resetScroll()},destroy:function(){this.timeGrid.destroy(),this.dayGrid&&this.dayGrid.destroy(),fe.prototype.destroy.call(this)},renderHtml:function(){return'"+""+""+""+""+'"+""+""+"
'+this.timeGrid.headHtml()+"
'+(this.dayGrid?'

':"")+'
'+'
'+"
"+"
"},headIntroHtml:function(){var t,e,i,n;return this.opt("weekNumbers")?(t=this.cellToDate(0,0),e=this.calendar.calculateWeekNumber(t),i=this.opt("weekNumberTitle"),n=this.opt("isRTL")?e+i:i+e,'
"+""+R(n)+""+""+""+(this.opt("allDayHtml")||R(this.opt("allDayText")))+""+"",rowHtml:function(t,e){var i,n,r=this.view,o=this.getHtmlRenderer("cell",t),s="";for(e=e||0,i=0;r.colCnt>i;i++)n=r.cellToDate(e,i),s+=o(e,i,n);return s=this.bookendCells(s,t,e),"
"+""+this.rowHtml("head")+""+"
"+"
"},headCellHtml:function(t,e,i){var n=this.view,r=n.calendar,o=n.opt("columnFormat");return''+P(r.formatDate(i,o))+""},bgCellHtml:function(t,e,i){var n=this.view,r=this.getDayClasses(i);return r.unshift("fc-day",n.widgetContentClass),''},getDayClasses:function(t){var e=this.view,i=e.calendar.getNow().stripTime(),n=["fc-"+Ne[t.day()]];return"month"===e.name&&t.month()!=e.intervalStart.month()&&n.push("fc-other-month"),t.isSame(i,"day")?n.push("fc-today",e.highlightStateClass):i>t?n.push("fc-past"):n.push("fc-future"),n}}),t.extend(te.prototype,{mousedOverSeg:null,isDraggingSeg:!1,isResizingSeg:!1,renderEvents:function(){},getSegs:function(){},destroyEvents:function(){this.triggerSegMouseout()},renderSegs:function(e,i){var n,r=this.view,o="",s=[];for(n=0;e.length>n;n++)o+=this.renderSegHtml(e[n],i);return t(o).each(function(i,n){var o=e[i],l=r.resolveEventEl(o.event,t(n));l&&(l.data("fc-seg",o),o.el=l,s.push(o))}),s},renderSegHtml:function(){},eventsToSegs:function(e,i,n){var r=this;return t.map(e,function(t){return r.eventToSegs(t,i,n)})},eventToSegs:function(t,e,i){var n,r,o,s=t.start.clone().stripZone(),l=this.view.calendar.getEventEnd(t).stripZone();for(e&&i?(o=S(s,l,e,i),n=o?[o]:[]):n=this.rangeToSegs(s,l),r=0;n.length>r;r++)o=n[r],o.event=t,o.eventStartMS=+s,o.eventDurationMS=l-s;return n},bindSegHandlers:function(){var e=this,i=this.view;t.each({mouseenter:function(t,i){e.triggerSegMouseover(t,i)},mouseleave:function(t,i){e.triggerSegMouseout(t,i)},click:function(t,e){return i.trigger("eventClick",this,t.event,e)},mousedown:function(n,r){t(r.target).is(".fc-resizer")&&i.isEventResizable(n.event)?e.segResizeMousedown(n,r):i.isEventDraggable(n.event)&&e.segDragMousedown(n,r)}},function(i,n){e.el.on(i,".fc-event-container > *",function(i){var r=t(this).data("fc-seg");return!r||e.isDraggingSeg||e.isResizingSeg?void 0:n.call(this,r,i)})})},triggerSegMouseover:function(t,e){this.mousedOverSeg||(this.mousedOverSeg=t,this.view.trigger("eventMouseover",t.el[0],t.event,e))},triggerSegMouseout:function(t,e){e=e||{},this.mousedOverSeg&&(t=t||this.mousedOverSeg,this.mousedOverSeg=null,this.view.trigger("eventMouseout",t.el[0],t.event,e))},segDragMousedown:function(t,e){var i,n,r=this,o=this.view,s=t.el,l=t.event,a=new Q(t.el,{parentEl:o.el,opacity:o.opt("dragOpacity"),revertDuration:o.opt("dragRevertDuration"),zIndex:2}),c=new U(o.coordMap,{distance:5,scroll:o.opt("dragScroll"),listenStart:function(t){a.hide(),a.start(t)},dragStart:function(e){r.triggerSegMouseout(t,e),r.isDraggingSeg=!0,o.hideEvent(l),o.trigger("eventDragStart",s[0],l,e,{})},cellOver:function(e,s){var l=t.cellDate||c.origDate,d=r.computeDraggedEventDates(t,l,s);i=d.start,n=d.end,o.renderDrag(i,n,t)?a.hide():a.show()},cellOut:function(){i=null,o.destroyDrag(),a.show()},dragStop:function(t){var e=i&&!i.isSame(l.start);a.stop(!e,function(){r.isDraggingSeg=!1,o.destroyDrag(),o.showEvent(l),o.trigger("eventDragStop",s[0],l,t,{}),e&&o.eventDrop(s[0],l,i,t)})},listenStop:function(){a.stop()}});c.mousedown(e)},computeDraggedEventDates:function(t,e,i){var n,r,o,s=this.view,l=t.event,a=l.start,c=s.calendar.getEventEnd(l);return i.hasTime()===e.hasTime()?(n=T(i,e),r=a.clone().add(n),o=null===l.end?null:c.clone().add(n)):(r=i,o=null),{start:r,end:o}},segResizeMousedown:function(t,e){function i(){r.destroyResize(),o.showEvent(l)}var n,r=this,o=this.view,s=t.el,l=t.event,a=l.start,c=o.calendar.getEventEnd(l),d=null;n=new U(this.coordMap,{distance:5,scroll:o.opt("dragScroll"),dragStart:function(e){r.triggerSegMouseout(t,e),r.isResizingSeg=!0,o.trigger("eventResizeStart",s[0],l,e,{})},cellOver:function(e,n){n.isBefore(a)&&(n=a),d=n.clone().add(r.cellDuration),d.isSame(c)?(d=null,i()):(r.renderResize(a,d,t),o.hideEvent(l))},cellOut:function(){d=null,i()},dragStop:function(t){r.isResizingSeg=!1,i(),o.trigger("eventResizeStop",s[0],l,t,{}),d&&o.eventResize(s[0],l,d,t)}}),n.mousedown(e)},getSegClasses:function(t,e,i){var n=t.event,r=["fc-event",t.isStart?"fc-start":"fc-not-start",t.isEnd?"fc-end":"fc-not-end"].concat(n.className,n.source?n.source.className:[]);return e&&r.push("fc-draggable"),i&&r.push("fc-resizable"),r},getEventSkinCss:function(t){var e=this.view,i=t.source||{},n=t.color,r=i.color,o=e.opt("eventColor"),s=t.backgroundColor||n||i.backgroundColor||r||e.opt("eventBackgroundColor")||o,l=t.borderColor||n||i.borderColor||r||e.opt("eventBorderColor")||o,a=t.textColor||i.textColor||e.opt("eventTextColor"),c=[];return s&&c.push("background-color:"+s),l&&c.push("border-color:"+l),a&&c.push("color:"+a),c.join(";")}}),ie.prototype=x(te.prototype),t.extend(ie.prototype,{numbersVisible:!1,cellDuration:e.duration({days:1}),bottomCoordPadding:0,rowEls:null,dayEls:null,helperEls:null,highlightEls:null,render:function(e){var i,n=this.view,r="";for(i=0;n.rowCnt>i;i++)r+=this.dayRowHtml(i,e);this.el.html(r),this.rowEls=this.el.find(".fc-row"),this.dayEls=this.el.find(".fc-day"),this.dayEls.each(function(e,i){var r=n.cellToDate(Math.floor(e/n.colCnt),e%n.colCnt);n.trigger("dayRender",null,r,t(i))}),te.prototype.render.call(this)},destroy:function(){this.destroySegPopover()},dayRowHtml:function(t,e){var i=this.view,n=["fc-row","fc-week",i.widgetContentClass];return e&&n.push("fc-rigid"),'
'+'
'+""+this.rowHtml("day",t)+"
"+"
"+'
'+""+(this.numbersVisible?""+this.rowHtml("number",t)+"":"")+"
"+"
"+"
"},dayCellHtml:function(t,e,i){return this.bgCellHtml(t,e,i)},buildCoords:function(e,i){var n,r,o,s=this.view.colCnt;this.dayEls.slice(0,s).each(function(e,s){n=t(s),r=n.offset().left,e&&(o[1]=r),o=[r],i[e]=o}),o[1]=r+n.outerWidth(),this.rowEls.each(function(i,s){n=t(s),r=n.offset().top,i&&(o[1]=r),o=[r],e[i]=o}),o[1]=r+n.outerHeight()+this.bottomCoordPadding},getCellDate:function(t){return this.view.cellToDate(t)},getCellDayEl:function(t){return this.dayEls.eq(t.row*this.view.colCnt+t.col)},rangeToSegs:function(t,e){return this.view.rangeToSegments(t,e)},renderDrag:function(t,e,i){var n;return this.renderHighlight(t,e||this.view.calendar.getDefaultEventEnd(!0,t)),i&&!i.el.closest(this.el).length?(this.renderRangeHelper(t,e,i),n=this.view.opt("dragOpacity"),void 0!==n&&this.helperEls.css("opacity",n),!0):void 0},destroyDrag:function(){this.destroyHighlight(),this.destroyHelper()},renderResize:function(t,e,i){this.renderHighlight(t,e),this.renderRangeHelper(t,e,i)},destroyResize:function(){this.destroyHighlight(),this.destroyHelper()},renderHelper:function(e,i){var n=[],r=this.renderEventRows([e]);this.rowEls.each(function(e,o){var s,l=t(o),a=t('
');s=i&&i.row===e?i.el.position().top:l.find(".fc-content-skeleton tbody").position().top,a.css("top",s).find("table").append(r[e].tbodyEl),l.append(a),n.push(a[0])}),this.helperEls=t(n)},destroyHelper:function(){this.helperEls&&(this.helperEls.remove(),this.helperEls=null)},renderHighlight:function(e,i){var n,r,o,s=this.rangeToSegs(e,i),l=[];for(n=0;s.length>n;n++)r=s[n],o=t(this.highlightSkeletonHtml(r.leftCol,r.rightCol+1)),o.appendTo(this.rowEls[r.row]),l.push(o[0]);this.highlightEls=t(l)},destroyHighlight:function(){this.highlightEls&&(this.highlightEls.remove(),this.highlightEls=null)},highlightSkeletonHtml:function(t,e){var i=this.view.colCnt,n="";return t>0&&(n+='"),m=[],v=[],y=[];for(r=0;p>r;r++){if(o=f[r],s=0,l=t(""),m.push([]),v.push([]),y.push([]),o)for(a=0;o.length>a;a++){for(c=o[a],n(c.leftCol),d=t('",l+=""+(s?"":n)+'",c.add(this.slotDuration);return l},processOptions:function(){var t=this.view,i=t.opt("slotDuration"),n=t.opt("snapDuration");i=e.duration(i),n=n?e.duration(n):i,this.slotDuration=i,this.snapDuration=n,this.cellDuration=n,this.minTime=e.duration(t.opt("minTime")),this.maxTime=e.duration(t.opt("maxTime"))},rangeToSegs:function(t,e){var i,n,r,o,s,l=this.view,a=[];for(t=t.clone().stripZone(),e=e.clone().stripZone(),n=0;l.colCnt>n;n++)r=l.cellToDate(0,n),o=r.clone().time(this.minTime),s=r.clone().time(this.maxTime),i=S(t,e,o,s),i&&(i.col=n,a.push(i));return a},resize:function(){this.computeSlatTops(),this.updateSegVerticals()},buildCoords:function(i,n){var r,o,s=this.view.colCnt,l=this.el.offset().top,a=e.duration(+this.minTime),c=null;for(this.dayEls.slice(0,s).each(function(e,i){r=t(i),o=r.offset().left,c&&(c[1]=o),c=[o],n[e]=c}),c[1]=o+r.outerWidth(),c=null;this.maxTime>a;)o=l+this.computeTimeTop(a),c&&(c[1]=o),c=[o],i.push(c),a.add(this.snapDuration);c[1]=l+this.computeTimeTop(a)},getCellDate:function(t){var e=this.view,i=e.calendar;return i.rezoneDate(e.cellToDate(0,t.col).time(this.minTime+this.snapDuration*t.row))},getCellDayEl:function(t){return this.dayEls.eq(t.col)},computeDateTop:function(t,i){return this.computeTimeTop(e.duration(t.clone().stripZone()-i.clone().stripTime()))},computeTimeTop:function(t){var e,i,n,r,o=(t-this.minTime)/this.slotDuration;return o=Math.max(0,o),o=Math.min(this.slatEls.length,o),e=Math.floor(o),i=o-e,n=this.slatTops[e],i?(r=this.slatTops[e+1],n+(r-n)*i):n},computeSlatTops:function(){var e,i=[];this.slatEls.each(function(n,r){e=t(r).position().top,i.push(e)}),i.push(e+this.slatEls.last().outerHeight()),this.slatTops=i},renderDrag:function(t,e,i){var n;return i?(this.renderRangeHelper(t,e,i),n=this.view.opt("dragOpacity"),void 0!==n&&this.helperEl.css("opacity",n),!0):(this.renderHighlight(t,e||this.view.calendar.getDefaultEventEnd(!1,t)),void 0)},destroyDrag:function(){this.destroyHelper(),this.destroyHighlight()},renderResize:function(t,e,i){this.renderRangeHelper(t,e,i)},destroyResize:function(){this.destroyHelper()},renderHelper:function(e,i){var n,r,o,s=this.renderEventTable([e]),l=s.tableEl,a=s.segs;for(n=0;a.length>n;n++)r=a[n],i&&i.col===r.col&&(o=i.el,r.el.css({left:o.css("left"),right:o.css("right"),"margin-left":o.css("margin-left"),"margin-right":o.css("margin-right")}));this.helperEl=t('
').append(l).appendTo(this.el)},destroyHelper:function(){this.helperEl&&(this.helperEl.remove(),this.helperEl=null)},renderSelection:function(t,e){this.view.opt("selectHelper")?this.renderRangeHelper(t,e):this.renderHighlight(t,e)},destroySelection:function(){this.destroyHelper(),this.destroyHighlight()},renderHighlight:function(e,i){this.highlightEl=t(this.highlightSkeletonHtml(e,i)).appendTo(this.el)},destroyHighlight:function(){this.highlightEl&&(this.highlightEl.remove(),this.highlightEl=null)},highlightSkeletonHtml:function(t,e){var i,n,r,o,s,l=this.view,a=this.rangeToSegs(t,e),c="",d=0;for(i=0;a.length>i;i++)n=a[i],n.col>d&&(c+='
",d++;return l.colCnt>d&&(c+='":void 0},numberIntroHtml:function(t){return this.weekNumbersVisible?'":void 0},dayIntroHtml:function(){return this.weekNumbersVisible?'":void 0},introHtml:function(){return this.weekNumbersVisible?'":void 0},numberCellHtml:function(t,e,i){var n;return this.dayNumbersVisible?(n=this.dayGrid.getDayClasses(i),n.unshift("fc-day-number"),'"):""):'"},dayIntroHtml:function(){return'"},slotBgIntroHtml:function(){return'"},introHtml:function(){return'"},axisStyleAttr:function(){return null!==this.axisWidth?'style="width:'+this.axisWidth+'px"':""},updateSize:function(t){t&&this.timeGrid.resize(),pe.prototype.updateSize.call(this,t)},updateWidth:function(){this.axisWidth=m(this.el.find(".fc-axis"))},setHeight:function(t,e){var i,n;null===this.bottomRuleHeight&&(this.bottomRuleHeight=this.bottomRuleEl.outerHeight()),this.bottomRuleEl.hide(),this.scrollerEl.css("overflow",""),y(this.scrollerEl),f(this.noScrollRowEls),this.dayGrid&&(this.dayGrid.destroySegPopover(),i=this.opt("eventLimit"),i&&"number"!=typeof i&&(i=Fe),i&&this.dayGrid.limitRows(i)),e||(n=this.computeScrollerHeight(t),v(this.scrollerEl,n)?(u(this.noScrollRowEls,E(this.scrollerEl)),n=this.computeScrollerHeight(t),this.scrollerEl.height(n),this.restoreScroll()):(this.scrollerEl.height(n).css("overflow","hidden"),this.bottomRuleEl.show()))},resetScroll:function(){function t(){i.scrollerEl.scrollTop(r)}var i=this,n=e.duration(this.opt("scrollTime")),r=this.timeGrid.computeTimeTop(n);r=Math.ceil(r),r&&r++,t(),setTimeout(t,0)},renderEvents:function(t){var e,i,n=[],r=[],o=[];for(i=0;t.length>i;i++)t[i].allDay?n.push(t[i]):r.push(t[i]);e=this.timeGrid.renderEvents(r),this.dayGrid&&(o=this.dayGrid.renderEvents(n)),this.updateHeight()},getSegs:function(){return this.timeGrid.getSegs().concat(this.dayGrid?this.dayGrid.getSegs():[])},destroyEvents:function(){pe.prototype.destroyEvents.call(this),this.recordScroll(),this.timeGrid.destroyEvents(),this.dayGrid&&this.dayGrid.destroyEvents()},renderDrag:function(t,e,i){return t.hasTime()?this.timeGrid.renderDrag(t,e,i):this.dayGrid?this.dayGrid.renderDrag(t,e,i):void 0},destroyDrag:function(){this.timeGrid.destroyDrag(),this.dayGrid&&this.dayGrid.destroyDrag()},renderSelection:function(t,e){t.hasTime()||e.hasTime()?this.timeGrid.renderSelection(t,e):this.dayGrid&&this.dayGrid.renderSelection(t,e)},destroySelection:function(){this.timeGrid.destroySelection(),this.dayGrid&&this.dayGrid.destroySelection()}}),ze.agendaWeek=Se,Se.prototype=x(De.prototype),t.extend(Se.prototype,{name:"agendaWeek",incrementDate:function(t,e){return t.clone().stripTime().add(e,"weeks").startOf("week")},render:function(t){this.intervalStart=t.clone().stripTime().startOf("week"),this.intervalEnd=this.intervalStart.clone().add(1,"weeks"),this.start=this.skipHiddenDays(this.intervalStart),this.end=this.skipHiddenDays(this.intervalEnd,-1,!0),this.title=this.calendar.formatRange(this.start,this.end.clone().subtract(1),this.opt("titleFormat")," — "),De.prototype.render.call(this,this.getCellsPerWeek())}}),ze.agendaDay=be,be.prototype=x(De.prototype),t.extend(be.prototype,{name:"agendaDay",incrementDate:function(t,e){var i=t.clone().stripTime().add(e,"days");return i=this.skipHiddenDays(i,0>e?-1:1)},render:function(t){this.start=this.intervalStart=t.clone().stripTime(),this.end=this.intervalEnd=this.start.clone().add(1,"days"),this.title=this.calendar.formatDate(this.start,this.opt("titleFormat")),De.prototype.render.call(this,1)}}),Te.prototype=x(ie.prototype),t.extend(Te.prototype,{rangeToSegs:function(t,e,i){var n,r=[],o=this.view.calendar.getDate();if(o>=t&&e>o){var s=this.view,l=s.calendar.fetchResources();for(n=0;s.colCnt>n;n++){var a=l[n];i.indexOf(a.id)>-1&&r.push({row:0,leftCol:n,rightCol:n,isStart:!0,isEnd:!0})}}return r}}),t.extend(Te.prototype,{eventToSegs:function(t,e,i){var n,r,o,s=t.start.clone().stripZone(),l=this.view.calendar.getEventEnd(t).stripZone();for(e&&i?(o=S(s,l,e,i),n=o?[o]:[]):n=this.rangeToSegs(s,l,t.resources),r=0;n.length>r;r++)o=n[r],o.event=t,o.eventStartMS=+s,o.eventDurationMS=l-s;return n}}),Ce.prototype=x(oe.prototype),t.extend(Ce.prototype,{rangeToSegs:function(t,e,i){var n,r,o,s,l,a=this.view,c=[],d=a.calendar.fetchResources();for(t=t.clone().stripZone(),e=e.clone().stripZone(),r=0;a.colCnt>r;r++){var h=d[r];i.indexOf(h.id)>-1&&(o=a.cellToDate(0,r),s=o.clone().time(this.minTime),l=o.clone().time(this.maxTime),n=S(t,e,s,l),n&&(n.col=r,c.push(n)))}return c}}),t.extend(Ce.prototype,{eventToSegs:function(t,e,i){var n,r,o,s=t.start.clone().stripZone(),l=this.view.calendar.getEventEnd(t).stripZone();for(e&&i?(o=S(s,l,e,i),n=o?[o]:[]):n=this.rangeToSegs(s,l,t.resources),r=0;n.length>r;r++)o=n[r],o.event=t,o.eventStartMS=+s,o.eventDurationMS=l-s;return n}}),r({allDaySlot:!0,allDayText:"all-day",scrollTime:"06:00:00",slotDuration:"00:30:00",axisFormat:we,timeFormat:{agenda:Ee},minTime:"00:00:00",maxTime:"24:00:00",slotEventOverlap:!0}),He.prototype=x(De.prototype),t.extend(He.prototype,{cellToDate:function(){return this.start.clone()},headCellHtml:function(t,e){var i=this,n=i.calendar,r=n.fetchResources()[e];return'"}}),ze.resourceDay=xe,xe.prototype=x(He.prototype),t.extend(xe.prototype,{name:"resourceDay",incrementDate:function(t,e){var i=t.clone().stripTime().add(e,"days");return i=this.skipHiddenDays(i,0>e?-1:1)},render:function(t){this.start=this.intervalStart=t.clone().stripTime(),this.end=this.intervalEnd=this.start.clone().add(1,"days"),this.title=this.calendar.formatDate(this.start,this.opt("titleFormat")),He.prototype.render.call(this,this.calendar.fetchResources().length)},getDayClasses:function(t){var e=this.view,i=e.calendar.getNow().stripTime(),n=["fc-"+Ne[t.day()]];return"month"===e.name&&t.month()!=e.intervalStart.month()&&n.push("fc-other-month"),t.isSame(i,"day")?n.push("fc-todaysss",e.highlightStateClass):i>t?n.push("fc-past"):n.push("fc-future"),n}})}); \ No newline at end of file diff --git a/dist/cdnjs/2.1.1/fullcalendar.print.css b/dist/cdnjs/2.1.1/fullcalendar.print.css index dc073b7..c276fe3 100644 --- a/dist/cdnjs/2.1.1/fullcalendar.print.css +++ b/dist/cdnjs/2.1.1/fullcalendar.print.css @@ -1,201 +1,201 @@ -/*! - * FullCalendar v2.1.1 Print Stylesheet - * Docs & License: http://arshaw.com/fullcalendar/ - * (c) 2013 Adam Shaw - */ - -/* - * Include this stylesheet on your page to get a more printer-friendly calendar. - * When including this stylesheet, use the media='print' attribute of the tag. - * Make sure to include this stylesheet IN ADDITION to the regular fullcalendar.css. - */ - -.fc { - max-width: 100% !important; -} - - -/* Global Event Restyling ---------------------------------------------------------------------------------------------------*/ - -.fc-event { - background: #fff !important; - color: #000 !important; - page-break-inside: avoid; -} - -.fc-event .fc-resizer { - display: none; -} - - -/* Table & Day-Row Restyling ---------------------------------------------------------------------------------------------------*/ - -th, -td, -hr, -thead, -tbody, -.fc-row { - border-color: #ccc !important; - background: #fff !important; -} - -/* kill the overlaid, absolutely-positioned common components */ -.fc-bg, -.fc-highlight-skeleton, -.fc-helper-skeleton { - display: none; -} - -/* don't force a min-height on rows (for DayGrid) */ -.fc tbody .fc-row { - height: auto !important; /* undo height that JS set in distributeHeight */ - min-height: 0 !important; /* undo the min-height from each view's specific stylesheet */ -} - -.fc tbody .fc-row .fc-content-skeleton { - position: static; /* undo .fc-rigid */ - padding-bottom: 0 !important; /* use a more border-friendly method for this... */ -} - -.fc tbody .fc-row .fc-content-skeleton tbody tr:last-child td { /* only works in newer browsers */ - padding-bottom: 1em; /* ...gives space within the skeleton. also ensures min height in a way */ -} - -.fc tbody .fc-row .fc-content-skeleton table { - /* provides a min-height for the row, but only effective for IE, which exaggerates this value, - making it look more like 3em. for other browers, it will already be this tall */ - height: 1em; -} - - -/* Undo month-view event limiting. Display all events and hide the "more" links ---------------------------------------------------------------------------------------------------*/ - -.fc-more-cell, -.fc-more { - display: none !important; -} - -.fc tr.fc-limited { - display: table-row !important; -} - -.fc td.fc-limited { - display: table-cell !important; -} - -.fc-popover { - display: none; /* never display the "more.." popover in print mode */ -} - - -/* TimeGrid Restyling ---------------------------------------------------------------------------------------------------*/ - -/* undo the min-height 100% trick used to fill the container's height */ -.fc-time-grid { - min-height: 0 !important; -} - -/* don't display the side axis at all ("all-day" and time cells) */ -.fc-agenda-view .fc-axis { - display: none; -} - -/* don't display the horizontal lines */ -.fc-slats, -.fc-time-grid hr { /* this hr is used when height is underused and needs to be filled */ - display: none !important; /* important overrides inline declaration */ -} - -/* let the container that holds the events be naturally positioned and create real height */ -.fc-time-grid .fc-content-skeleton { - position: static; -} - -/* in case there are no events, we still want some height */ -.fc-time-grid .fc-content-skeleton table { - height: 4em; -} - -/* kill the horizontal spacing made by the event container. event margins will be done below */ -.fc-time-grid .fc-event-container { - margin: 0 !important; -} - - -/* TimeGrid *Event* Restyling ---------------------------------------------------------------------------------------------------*/ - -/* naturally position events, vertically stacking them */ -.fc-time-grid .fc-event { - position: static !important; - margin: 3px 2px !important; -} - -/* for events that continue to a future day, give the bottom border back */ -.fc-time-grid .fc-event.fc-not-end { - border-bottom-width: 1px !important; -} - -/* indicate the event continues via "..." text */ -.fc-time-grid .fc-event.fc-not-end:after { - content: "..."; -} - -/* for events that are continuations from previous days, give the top border back */ -.fc-time-grid .fc-event.fc-not-start { - border-top-width: 1px !important; -} - -/* indicate the event is a continuation via "..." text */ -.fc-time-grid .fc-event.fc-not-start:before { - content: "..."; -} - -/* time */ - -/* undo a previous declaration and let the time text span to a second line */ -.fc-time-grid .fc-event .fc-time { - white-space: normal !important; -} - -/* hide the the time that is normally displayed... */ -.fc-time-grid .fc-event .fc-time span { - display: none; -} - -/* ...replace it with a more verbose version (includes AM/PM) stored in an html attribute */ -.fc-time-grid .fc-event .fc-time:after { - content: attr(data-full); -} - - -/* Vertical Scroller & Containers ---------------------------------------------------------------------------------------------------*/ - -/* kill the scrollbars and allow natural height */ -.fc-scroller, -.fc-day-grid-container, /* these divs might be assigned height, which we need to cleared */ -.fc-time-grid-container { /* */ - overflow: visible !important; - height: auto !important; -} - -/* kill the horizontal border/padding used to compensate for scrollbars */ -.fc-row { - border: 0 !important; - margin: 0 !important; -} - - -/* Button Controls ---------------------------------------------------------------------------------------------------*/ - -.fc-button-group, -.fc button { - display: none; /* don't display any button-related controls */ -} +/*! + * FullCalendar v2.1.1 Print Stylesheet + * Docs & License: http://arshaw.com/fullcalendar/ + * (c) 2013 Adam Shaw + */ + +/* + * Include this stylesheet on your page to get a more printer-friendly calendar. + * When including this stylesheet, use the media='print' attribute of the tag. + * Make sure to include this stylesheet IN ADDITION to the regular fullcalendar.css. + */ + +.fc { + max-width: 100% !important; +} + + +/* Global Event Restyling +--------------------------------------------------------------------------------------------------*/ + +.fc-event { + background: #fff !important; + color: #000 !important; + page-break-inside: avoid; +} + +.fc-event .fc-resizer { + display: none; +} + + +/* Table & Day-Row Restyling +--------------------------------------------------------------------------------------------------*/ + +th, +td, +hr, +thead, +tbody, +.fc-row { + border-color: #ccc !important; + background: #fff !important; +} + +/* kill the overlaid, absolutely-positioned common components */ +.fc-bg, +.fc-highlight-skeleton, +.fc-helper-skeleton { + display: none; +} + +/* don't force a min-height on rows (for DayGrid) */ +.fc tbody .fc-row { + height: auto !important; /* undo height that JS set in distributeHeight */ + min-height: 0 !important; /* undo the min-height from each view's specific stylesheet */ +} + +.fc tbody .fc-row .fc-content-skeleton { + position: static; /* undo .fc-rigid */ + padding-bottom: 0 !important; /* use a more border-friendly method for this... */ +} + +.fc tbody .fc-row .fc-content-skeleton tbody tr:last-child td { /* only works in newer browsers */ + padding-bottom: 1em; /* ...gives space within the skeleton. also ensures min height in a way */ +} + +.fc tbody .fc-row .fc-content-skeleton table { + /* provides a min-height for the row, but only effective for IE, which exaggerates this value, + making it look more like 3em. for other browers, it will already be this tall */ + height: 1em; +} + + +/* Undo month-view event limiting. Display all events and hide the "more" links +--------------------------------------------------------------------------------------------------*/ + +.fc-more-cell, +.fc-more { + display: none !important; +} + +.fc tr.fc-limited { + display: table-row !important; +} + +.fc td.fc-limited { + display: table-cell !important; +} + +.fc-popover { + display: none; /* never display the "more.." popover in print mode */ +} + + +/* TimeGrid Restyling +--------------------------------------------------------------------------------------------------*/ + +/* undo the min-height 100% trick used to fill the container's height */ +.fc-time-grid { + min-height: 0 !important; +} + +/* don't display the side axis at all ("all-day" and time cells) */ +.fc-agenda-view .fc-axis { + display: none; +} + +/* don't display the horizontal lines */ +.fc-slats, +.fc-time-grid hr { /* this hr is used when height is underused and needs to be filled */ + display: none !important; /* important overrides inline declaration */ +} + +/* let the container that holds the events be naturally positioned and create real height */ +.fc-time-grid .fc-content-skeleton { + position: static; +} + +/* in case there are no events, we still want some height */ +.fc-time-grid .fc-content-skeleton table { + height: 4em; +} + +/* kill the horizontal spacing made by the event container. event margins will be done below */ +.fc-time-grid .fc-event-container { + margin: 0 !important; +} + + +/* TimeGrid *Event* Restyling +--------------------------------------------------------------------------------------------------*/ + +/* naturally position events, vertically stacking them */ +.fc-time-grid .fc-event { + position: static !important; + margin: 3px 2px !important; +} + +/* for events that continue to a future day, give the bottom border back */ +.fc-time-grid .fc-event.fc-not-end { + border-bottom-width: 1px !important; +} + +/* indicate the event continues via "..." text */ +.fc-time-grid .fc-event.fc-not-end:after { + content: "..."; +} + +/* for events that are continuations from previous days, give the top border back */ +.fc-time-grid .fc-event.fc-not-start { + border-top-width: 1px !important; +} + +/* indicate the event is a continuation via "..." text */ +.fc-time-grid .fc-event.fc-not-start:before { + content: "..."; +} + +/* time */ + +/* undo a previous declaration and let the time text span to a second line */ +.fc-time-grid .fc-event .fc-time { + white-space: normal !important; +} + +/* hide the the time that is normally displayed... */ +.fc-time-grid .fc-event .fc-time span { + display: none; +} + +/* ...replace it with a more verbose version (includes AM/PM) stored in an html attribute */ +.fc-time-grid .fc-event .fc-time:after { + content: attr(data-full); +} + + +/* Vertical Scroller & Containers +--------------------------------------------------------------------------------------------------*/ + +/* kill the scrollbars and allow natural height */ +.fc-scroller, +.fc-day-grid-container, /* these divs might be assigned height, which we need to cleared */ +.fc-time-grid-container { /* */ + overflow: visible !important; + height: auto !important; +} + +/* kill the horizontal border/padding used to compensate for scrollbars */ +.fc-row { + border: 0 !important; + margin: 0 !important; +} + + +/* Button Controls +--------------------------------------------------------------------------------------------------*/ + +.fc-button-group, +.fc button { + display: none; /* don't display any button-related controls */ +} diff --git a/dist/cdnjs/2.1.1/gcal.js b/dist/cdnjs/2.1.1/gcal.js index 754fb72..c7f3b3b 100644 --- a/dist/cdnjs/2.1.1/gcal.js +++ b/dist/cdnjs/2.1.1/gcal.js @@ -1,100 +1,100 @@ -/*! - * FullCalendar v2.1.1 Google Calendar Plugin - * Docs & License: http://arshaw.com/fullcalendar/ - * (c) 2013 Adam Shaw - */ - -(function(factory) { - if (typeof define === 'function' && define.amd) { - define([ 'jquery' ], factory); - } - else { - factory(jQuery); - } -})(function($) { - - -var fc = $.fullCalendar; -var applyAll = fc.applyAll; - - -fc.sourceNormalizers.push(function(sourceOptions) { - if (sourceOptions.dataType == 'gcal' || - sourceOptions.dataType === undefined && - (sourceOptions.url || '').match(/^(http|https):\/\/www.google.com\/calendar\/feeds\//)) { - sourceOptions.dataType = 'gcal'; - if (sourceOptions.editable === undefined) { - sourceOptions.editable = false; - } - } -}); - - -fc.sourceFetchers.push(function(sourceOptions, start, end, timezone) { - if (sourceOptions.dataType == 'gcal') { - return transformOptions(sourceOptions, start, end, timezone); - } -}); - - -function transformOptions(sourceOptions, start, end, timezone) { - - var success = sourceOptions.success; - var data = $.extend({}, sourceOptions.data || {}, { - singleevents: true, - 'max-results': 9999 - }); - - return $.extend({}, sourceOptions, { - url: sourceOptions.url.replace(/\/basic$/, '/full') + '?alt=json-in-script&callback=?', - dataType: 'jsonp', - data: data, - timezoneParam: 'ctz', - startParam: 'start-min', - endParam: 'start-max', - success: function(data) { - var events = []; - if (data.feed.entry) { - $.each(data.feed.entry, function(i, entry) { - - var url; - $.each(entry.link, function(i, link) { - if (link.type == 'text/html') { - url = link.href; - if (timezone && timezone != 'local') { - url += (url.indexOf('?') == -1 ? '?' : '&') + 'ctz=' + encodeURIComponent(timezone); - } - } - }); - - events.push({ - id: entry.gCal$uid.value, - title: entry.title.$t, - start: entry.gd$when[0].startTime, - end: entry.gd$when[0].endTime, - url: url, - location: entry.gd$where[0].valueString, - description: entry.content.$t - }); - - }); - } - var args = [events].concat(Array.prototype.slice.call(arguments, 1)); - var res = applyAll(success, this, args); - if ($.isArray(res)) { - return res; - } - return events; - } - }); - -} - - -// legacy -fc.gcalFeed = function(url, sourceOptions) { - return $.extend({}, sourceOptions, { url: url, dataType: 'gcal' }); -}; - - -}); +/*! + * FullCalendar v2.1.1 Google Calendar Plugin + * Docs & License: http://arshaw.com/fullcalendar/ + * (c) 2013 Adam Shaw + */ + +(function(factory) { + if (typeof define === 'function' && define.amd) { + define([ 'jquery' ], factory); + } + else { + factory(jQuery); + } +})(function($) { + + +var fc = $.fullCalendar; +var applyAll = fc.applyAll; + + +fc.sourceNormalizers.push(function(sourceOptions) { + if (sourceOptions.dataType == 'gcal' || + sourceOptions.dataType === undefined && + (sourceOptions.url || '').match(/^(http|https):\/\/www.google.com\/calendar\/feeds\//)) { + sourceOptions.dataType = 'gcal'; + if (sourceOptions.editable === undefined) { + sourceOptions.editable = false; + } + } +}); + + +fc.sourceFetchers.push(function(sourceOptions, start, end, timezone) { + if (sourceOptions.dataType == 'gcal') { + return transformOptions(sourceOptions, start, end, timezone); + } +}); + + +function transformOptions(sourceOptions, start, end, timezone) { + + var success = sourceOptions.success; + var data = $.extend({}, sourceOptions.data || {}, { + singleevents: true, + 'max-results': 9999 + }); + + return $.extend({}, sourceOptions, { + url: sourceOptions.url.replace(/\/basic$/, '/full') + '?alt=json-in-script&callback=?', + dataType: 'jsonp', + data: data, + timezoneParam: 'ctz', + startParam: 'start-min', + endParam: 'start-max', + success: function(data) { + var events = []; + if (data.feed.entry) { + $.each(data.feed.entry, function(i, entry) { + + var url; + $.each(entry.link, function(i, link) { + if (link.type == 'text/html') { + url = link.href; + if (timezone && timezone != 'local') { + url += (url.indexOf('?') == -1 ? '?' : '&') + 'ctz=' + encodeURIComponent(timezone); + } + } + }); + + events.push({ + id: entry.gCal$uid.value, + title: entry.title.$t, + start: entry.gd$when[0].startTime, + end: entry.gd$when[0].endTime, + url: url, + location: entry.gd$where[0].valueString, + description: entry.content.$t + }); + + }); + } + var args = [events].concat(Array.prototype.slice.call(arguments, 1)); + var res = applyAll(success, this, args); + if ($.isArray(res)) { + return res; + } + return events; + } + }); + +} + + +// legacy +fc.gcalFeed = function(url, sourceOptions) { + return $.extend({}, sourceOptions, { url: url, dataType: 'gcal' }); +}; + + +}); diff --git a/dist/cdnjs/2.1.1/lang/is.js b/dist/cdnjs/2.1.1/lang/is.js index 7bbdebf..4601695 100644 --- a/dist/cdnjs/2.1.1/lang/is.js +++ b/dist/cdnjs/2.1.1/lang/is.js @@ -1 +1 @@ -(function(e){"function"==typeof define&&define.amd?define(["jquery","moment"],e):e(jQuery,moment)})(function(e,t){function n(e){return 11===e%100?!0:1===e%10?!1:!0}function i(e,t,i,r){var a=e+" ";switch(i){case"s":return t||r?"nokkrar sekúndur":"nokkrum sekúndum";case"m":return t?"mínúta":"mínútu";case"mm":return n(e)?a+(t||r?"mínútur":"mínútum"):t?a+"mínúta":a+"mínútu";case"hh":return n(e)?a+(t||r?"klukkustundir":"klukkustundum"):a+"klukkustund";case"d":return t?"dagur":r?"dag":"degi";case"dd":return n(e)?t?a+"dagar":a+(r?"daga":"dögum"):t?a+"dagur":a+(r?"dag":"degi");case"M":return t?"mánuður":r?"mánuð":"mánuði";case"MM":return n(e)?t?a+"mánuðir":a+(r?"mánuði":"mánuðum"):t?a+"mánuður":a+(r?"mánuð":"mánuði");case"y":return t||r?"ár":"ári";case"yy":return n(e)?a+(t||r?"ár":"árum"):a+(t||r?"ár":"ári")}}(t.defineLocale||t.lang).call(t,"is",{months:"janúar_febrúar_mars_apríl_maí_júní_júlí_ágúst_september_október_nóvember_desember".split("_"),monthsShort:"jan_feb_mar_apr_maí_jún_júl_ágú_sep_okt_nóv_des".split("_"),weekdays:"sunnudagur_mánudagur_þriðjudagur_miðvikudagur_fimmtudagur_föstudagur_laugardagur".split("_"),weekdaysShort:"sun_mán_þri_mið_fim_fös_lau".split("_"),weekdaysMin:"Su_Má_Þr_Mi_Fi_Fö_La".split("_"),longDateFormat:{LT:"H:mm",L:"DD/MM/YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] LT",LLLL:"dddd, D. MMMM YYYY [kl.] LT"},calendar:{sameDay:"[í dag kl.] LT",nextDay:"[á morgun kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[í gær kl.] LT",lastWeek:"[síðasta] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"eftir %s",past:"fyrir %s síðan",s:i,m:i,mm:i,h:"klukkustund",hh:i,d:i,dd:i,M:i,MM:i,y:i,yy:i},ordinal:"%d.",week:{dow:1,doy:4}}),e.fullCalendar.datepickerLang("is","is",{closeText:"Loka",prevText:"< Fyrri",nextText:"Næsti >",currentText:"Í dag",monthNames:["Janúar","Febrúar","Mars","Apríl","Maí","Júní","Júlí","Ágúst","September","Október","Nóvember","Desember"],monthNamesShort:["Jan","Feb","Mar","Apr","Maí","Jún","Júl","Ágú","Sep","Okt","Nóv","Des"],dayNames:["Sunnudagur","Mánudagur","Þriðjudagur","Miðvikudagur","Fimmtudagur","Föstudagur","Laugardagur"],dayNamesShort:["Sun","Mán","Þri","Mið","Fim","Fös","Lau"],dayNamesMin:["Su","Má","Þr","Mi","Fi","Fö","La"],weekHeader:"Vika",dateFormat:"dd.mm.yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.lang("is",{defaultButtonText:{month:"Mánuður",week:"Vika",day:"Dagur",list:"Dagskrá"},allDayHtml:"Allan
daginn",eventLimitText:"meira"})}); \ No newline at end of file +(function(e){"function"==typeof define&&define.amd?define(["jquery","moment"],e):e(jQuery,moment)})(function(e,t){function n(e){return 11===e%100?!0:1===e%10?!1:!0}function r(e,t,r,i){var a=e+" ";switch(r){case"s":return t||i?"nokkrar sekúndur":"nokkrum sekúndum";case"m":return t?"mínúta":"mínútu";case"mm":return n(e)?a+(t||i?"mínútur":"mínútum"):t?a+"mínúta":a+"mínútu";case"hh":return n(e)?a+(t||i?"klukkustundir":"klukkustundum"):a+"klukkustund";case"d":return t?"dagur":i?"dag":"degi";case"dd":return n(e)?t?a+"dagar":a+(i?"daga":"dögum"):t?a+"dagur":a+(i?"dag":"degi");case"M":return t?"mánuður":i?"mánuð":"mánuði";case"MM":return n(e)?t?a+"mánuðir":a+(i?"mánuði":"mánuðum"):t?a+"mánuður":a+(i?"mánuð":"mánuði");case"y":return t||i?"ár":"ári";case"yy":return n(e)?a+(t||i?"ár":"árum"):a+(t||i?"ár":"ári")}}(t.defineLocale||t.lang).call(t,"is",{months:"janúar_febrúar_mars_apríl_maí_júní_júlí_ágúst_september_október_nóvember_desember".split("_"),monthsShort:"jan_feb_mar_apr_maí_jún_júl_ágú_sep_okt_nóv_des".split("_"),weekdays:"sunnudagur_mánudagur_þriðjudagur_miðvikudagur_fimmtudagur_föstudagur_laugardagur".split("_"),weekdaysShort:"sun_mán_þri_mið_fim_fös_lau".split("_"),weekdaysMin:"Su_Má_Þr_Mi_Fi_Fö_La".split("_"),longDateFormat:{LT:"H:mm",L:"DD/MM/YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] LT",LLLL:"dddd, D. MMMM YYYY [kl.] LT"},calendar:{sameDay:"[í dag kl.] LT",nextDay:"[á morgun kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[í gær kl.] LT",lastWeek:"[síðasta] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"eftir %s",past:"fyrir %s síðan",s:r,m:r,mm:r,h:"klukkustund",hh:r,d:r,dd:r,M:r,MM:r,y:r,yy:r},ordinal:"%d.",week:{dow:1,doy:4}}),e.fullCalendar.datepickerLang("is","is",{closeText:"Loka",prevText:"< Fyrri",nextText:"Næsti >",currentText:"Í dag",monthNames:["Janúar","Febrúar","Mars","Apríl","Maí","Júní","Júlí","Ágúst","September","Október","Nóvember","Desember"],monthNamesShort:["Jan","Feb","Mar","Apr","Maí","Jún","Júl","Ágú","Sep","Okt","Nóv","Des"],dayNames:["Sunnudagur","Mánudagur","Þriðjudagur","Miðvikudagur","Fimmtudagur","Föstudagur","Laugardagur"],dayNamesShort:["Sun","Mán","Þri","Mið","Fim","Fös","Lau"],dayNamesMin:["Su","Má","Þr","Mi","Fi","Fö","La"],weekHeader:"Vika",dateFormat:"dd.mm.yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.lang("is",{defaultButtonText:{month:"Mánuður",week:"Vika",day:"Dagur",list:"Dagskrá"},allDayHtml:"Allan
daginn",eventLimitText:"meira"})}); \ No newline at end of file diff --git a/dist/fullcalendar-2.1.1.zip b/dist/fullcalendar-2.1.1.zip index a98d177..6c17f1c 100644 Binary files a/dist/fullcalendar-2.1.1.zip and b/dist/fullcalendar-2.1.1.zip differ diff --git a/dist/fullcalendar.css b/dist/fullcalendar.css index ab8b6e6..a5b9d33 100644 --- a/dist/fullcalendar.css +++ b/dist/fullcalendar.css @@ -1,933 +1,933 @@ -/*! - * FullCalendar v2.1.1 Stylesheet - * Docs & License: http://arshaw.com/fullcalendar/ - * (c) 2013 Adam Shaw - */ - - -.fc { - direction: ltr; - text-align: left; -} - -.fc-rtl { - text-align: right; -} - -body .fc { /* extra precedence to overcome jqui */ - font-size: 1em; -} - - -/* Colors ---------------------------------------------------------------------------------------------------*/ - -.fc-unthemed th, -.fc-unthemed td, -.fc-unthemed hr, -.fc-unthemed thead, -.fc-unthemed tbody, -.fc-unthemed .fc-row, -.fc-unthemed .fc-popover { - border-color: #ddd; -} - -.fc-unthemed .fc-popover { - background-color: #fff; -} - -.fc-unthemed hr, -.fc-unthemed .fc-popover .fc-header { - background: #eee; -} - -.fc-unthemed .fc-popover .fc-header .fc-close { - color: #666; -} - -.fc-unthemed .fc-today { - background: #fcf8e3; -} - -.fc-highlight { /* when user is selecting cells */ - background: #bce8f1; - opacity: .3; - filter: alpha(opacity=30); /* for IE */ -} - - -/* Icons (inline elements with styled text that mock arrow icons) ---------------------------------------------------------------------------------------------------*/ - -.fc-icon { - display: inline-block; - font-size: 2em; - line-height: .5em; - height: .5em; /* will make the total height 1em */ - font-family: "Courier New", Courier, monospace; -} - -.fc-icon-left-single-arrow:after { - content: "\02039"; - font-weight: bold; -} - -.fc-icon-right-single-arrow:after { - content: "\0203A"; - font-weight: bold; -} - -.fc-icon-left-double-arrow:after { - content: "\000AB"; -} - -.fc-icon-right-double-arrow:after { - content: "\000BB"; -} - -.fc-icon-x:after { - content: "\000D7"; -} - - -/* Buttons (styled rows. -----------------------------------------------------------------------------------------------------------------------*/ -// It leverages methods of the subclass and the View to determine custom rendering behavior for each row "type" -// (such as highlight rows, day rows, helper rows, etc). - -function RowRenderer(view) { - this.view = view; -} - - -RowRenderer.prototype = { - - view: null, // a View object - cellHtml: '' + cellHtml + ''; - }, - - - // Applies the "intro" and "outro" HTML to the given cells. - // Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro. - // `cells` can be an HTML string of element - // `row` is an optional row number. - bookendCells: function(cells, rowType, row) { - var view = this.view; - var intro = this.getHtmlRenderer('intro', rowType)(row || 0); - var outro = this.getHtmlRenderer('outro', rowType)(row || 0); - var isRTL = view.opt('isRTL'); - var prependHtml = isRTL ? outro : intro; - var appendHtml = isRTL ? intro : outro; - - if (typeof cells === 'string') { - return prependHtml + cells + appendHtml; - } - else { // a jQuery element - return cells.prepend(prependHtml).append(appendHtml); - } - }, - - - // Returns an HTML-rendering function given a specific `rendererName` (like cell, intro, or outro) and a specific - // `rowType` (like day, eventSkeleton, helperSkeleton), which is optional. - // If a renderer for the specific rowType doesn't exist, it will fall back to a generic renderer. - // We will query the View object first for any custom rendering functions, then the methods of the subclass. - getHtmlRenderer: function(rendererName, rowType) { - var view = this.view; - var generalName; // like "cellHtml" - var specificName; // like "dayCellHtml". based on rowType - var provider; // either the View or the RowRenderer subclass, whichever provided the method - var renderer; - - generalName = rendererName + 'Html'; - if (rowType) { - specificName = rowType + capitaliseFirstLetter(rendererName) + 'Html'; - } - - if (specificName && (renderer = view[specificName])) { - provider = view; - } - else if (specificName && (renderer = this[specificName])) { - provider = this; - } - else if ((renderer = view[generalName])) { - provider = view; - } - else if ((renderer = this[generalName])) { - provider = this; - } - - if (typeof renderer === 'function') { - return function(row) { - return renderer.apply(provider, arguments) || ''; // use correct `this` and always return a string - }; - } - - // the rendered can be a plain string as well. if not specified, always an empty string. - return function() { - return renderer || ''; - }; - } - -}; - -;; - -/* An abstract class comprised of a "grid" of cells that each represent a specific datetime -----------------------------------------------------------------------------------------------------------------------*/ - -function Grid(view) { - RowRenderer.call(this, view); // call the super-constructor - this.coordMap = new GridCoordMap(this); -} - - -Grid.prototype = createObject(RowRenderer.prototype); // declare the super-class -$.extend(Grid.prototype, { - - el: null, // the containing element - coordMap: null, // a GridCoordMap that converts pixel values to datetimes - cellDuration: null, // a cell's duration. subclasses must assign this ASAP - - - // Renders the grid into the `el` element. - // Subclasses should override and call this super-method when done. - render: function() { - this.bindHandlers(); - }, - - - // Called when the grid's resources need to be cleaned up - destroy: function() { - // subclasses can implement - }, - - - /* Coordinates & Cells - ------------------------------------------------------------------------------------------------------------------*/ - - - // Populates the given empty arrays with the y and x coordinates of the cells - buildCoords: function(rows, cols) { - // subclasses must implement - }, - - - // Given a cell object, returns the date for that cell - getCellDate: function(cell) { - // subclasses must implement - }, - - - // Given a cell object, returns the element that represents the cell's whole-day - getCellDayEl: function(cell) { - // subclasses must implement - }, - - - // Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects - rangeToSegs: function(start, end) { - // subclasses must implement - }, - - - /* Handlers - ------------------------------------------------------------------------------------------------------------------*/ - - - // Attach handlers to `this.el`, using bubbling to listen to all ancestors. - // We don't need to undo any of this in a "destroy" method, because the view will simply remove `this.el` from the - // DOM and jQuery will be smart enough to garbage collect the handlers. - bindHandlers: function() { - var _this = this; - - this.el.on('mousedown', function(ev) { - if ( - !$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link - !$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one) - ) { - _this.dayMousedown(ev); - } - }); - - this.bindSegHandlers(); // attach event-element-related handlers. in Grid.events.js - }, - - - // Process a mousedown on an element that represents a day. For day clicking and selecting. - dayMousedown: function(ev) { - var _this = this; - var view = this.view; - var isSelectable = view.opt('selectable'); - var dates = null; // the inclusive dates of the selection. will be null if no selection - var start; // the inclusive start of the selection - var end; // the *exclusive* end of the selection - var dayEl; - - // this listener tracks a mousedown on a day element, and a subsequent drag. - // if the drag ends on the same day, it is a 'dayClick'. - // if 'selectable' is enabled, this listener also detects selections. - var dragListener = new DragListener(this.coordMap, { - //distance: 5, // needs more work if we want dayClick to fire correctly - scroll: view.opt('dragScroll'), - dragStart: function() { - view.unselect(); // since we could be rendering a new selection, we want to clear any old one - }, - cellOver: function(cell, date) { - if (dragListener.origDate) { // click needs to have started on a cell - - dayEl = _this.getCellDayEl(cell); - - dates = [ date, dragListener.origDate ].sort(dateCompare); - start = dates[0]; - end = dates[1].clone().add(_this.cellDuration); - - if (isSelectable) { - _this.renderSelection(start, end); - } - } - }, - cellOut: function(cell, date) { - dates = null; - _this.destroySelection(); - }, - listenStop: function(ev) { - if (dates) { // started and ended on a cell? - if (dates[0].isSame(dates[1])) { - view.trigger('dayClick', dayEl[0], start, ev); - } - if (isSelectable) { - // the selection will already have been rendered. just report it - view.reportSelection(start, end, ev); - } - } - } - }); - - dragListener.mousedown(ev); // start listening, which will eventually initiate a dragStart - }, - - - /* Event Dragging - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of a event being dragged over the given date(s). - // `end` can be null, as well as `seg`. See View's documentation on renderDrag for more info. - // A returned value of `true` signals that a mock "helper" event has been rendered. - renderDrag: function(start, end, seg) { - // subclasses must implement - }, - - - // Unrenders a visual indication of an event being dragged - destroyDrag: function() { - // subclasses must implement - }, - - - /* Event Resizing - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event being resized. - // `start` and `end` are the updated dates of the event. `seg` is the original segment object involved in the drag. - renderResize: function(start, end, seg) { - // subclasses must implement - }, - - - // Unrenders a visual indication of an event being resized. - destroyResize: function() { - // subclasses must implement - }, - - - /* Event Helper - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a mock event over the given date(s). - // `end` can be null, in which case the mock event that is rendered will have a null end time. - // `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging. - renderRangeHelper: function(start, end, sourceSeg) { - var view = this.view; - var fakeEvent; - - // compute the end time if forced to do so (this is what EventManager does) - if (!end && view.opt('forceEventDuration')) { - end = view.calendar.getDefaultEventEnd(!start.hasTime(), start); - } - - fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible - fakeEvent.start = start; - fakeEvent.end = end; - fakeEvent.allDay = !(start.hasTime() || (end && end.hasTime())); // freshly compute allDay - - // this extra className will be useful for differentiating real events from mock events in CSS - fakeEvent.className = (fakeEvent.className || []).concat('fc-helper'); - - // if something external is being dragged in, don't render a resizer - if (!sourceSeg) { - fakeEvent.editable = false; - } - - this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering - }, - - - // Renders a mock event - renderHelper: function(event, sourceSeg) { - // subclasses must implement - }, - - - // Unrenders a mock event - destroyHelper: function() { - // subclasses must implement - }, - - - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses. - renderSelection: function(start, end) { - this.renderHighlight(start, end); - }, - - - // Unrenders any visual indications of a selection. Will unrender a highlight by default. - destroySelection: function() { - this.destroyHighlight(); - }, - - - /* Highlight - ------------------------------------------------------------------------------------------------------------------*/ - - - // Puts visual emphasis on a certain date range - renderHighlight: function(start, end) { - // subclasses should implement - }, - - - // Removes visual emphasis on a date range - destroyHighlight: function() { - // subclasses should implement - }, - - - - /* Generic rendering utilities for subclasses - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a day-of-week header row - headHtml: function() { - return '' + - '
' + - '
'),e>t&&(n+=''),i>e&&(n+=''),n=this.bookendCells(n,"highlight"),'
'+n+""+"
"+"
"}}),t.extend(ie.prototype,{segs:null,rowStructs:null,renderEvents:function(e){var i=this.rowStructs=this.renderEventRows(e),n=[];this.rowEls.each(function(e,r){t(r).find(".fc-content-skeleton > table").append(i[e].tbodyEl),n.push.apply(n,i[e].segs)}),this.segs=n},getSegs:function(){return(this.segs||[]).concat(this.popoverSegs||[])},destroyEvents:function(){var t,e;for(te.prototype.destroyEvents.call(this),t=this.rowStructs||[];e=t.pop();)e.tbodyEl.remove();this.segs=null,this.destroySegPopover()},renderEventRows:function(t){var e,i,n=this.eventsToSegs(t),r=[];for(n=this.renderSegs(n),e=this.groupSegRows(n),i=0;e.length>i;i++)r.push(this.renderEventRow(i,e[i]));return r},renderSegHtml:function(t,e){var i,n=this.view,r=n.opt("isRTL"),o=t.event,s=n.isEventDraggable(o),l=!e&&o.allDay&&t.isEnd&&n.isEventResizable(o),a=this.getSegClasses(t,s,l),c=this.getEventSkinCss(o),d="";return a.unshift("fc-day-grid-event"),!o.allDay&&t.isStart&&(d=''+P(n.getEventTimeText(o))+""),i=''+(P(o.title||"")||" ")+"",'"+'
'+(r?i+" "+d:d+" "+i)+"
"+(l?'
':"")+""},renderEventRow:function(e,i){function n(e){for(;e>s;)d=(y[r-1]||[])[s],d?d.attr("rowspan",parseInt(d.attr("rowspan")||1,10)+1):(d=t("
"),l.append(d)),v[r][s]=d,y[r][s]=d,s++}var r,o,s,l,a,c,d,h=this.view,u=h.colCnt,f=this.buildSegLevels(i),p=Math.max(1,f.length),g=t("
').append(c.el),c.leftCol!=c.rightCol?d.attr("colspan",c.rightCol-c.leftCol+1):y[r][s]=d;c.rightCol>=s;)v[r][s]=d,m[r][s]=c,s++;l.append(d)}n(u),this.bookendCells(l,"eventSkeleton"),g.append(l)}return{row:e,tbodyEl:g,cellMatrix:v,segMatrix:m,segLevels:f,segs:i}},buildSegLevels:function(t){var e,i,n,r=[];for(t.sort(ee),e=0;t.length>e;e++){for(i=t[e],n=0;r.length>n&&ne(i,r[n]);n++);i.level=n,(r[n]||(r[n]=[])).push(i)}for(n=0;r.length>n;n++)r[n].sort(re);return r},groupSegRows:function(t){var e,i=this.view,n=[];for(e=0;i.rowCnt>e;e++)n.push([]);for(e=0;t.length>e;e++)n[t[e].row].push(t[e]);return n}}),t.extend(ie.prototype,{segPopover:null,popoverSegs:null,destroySegPopover:function(){this.segPopover&&this.segPopover.hide()},limitRows:function(t){var e,i,n=this.rowStructs||[];for(e=0;n.length>e;e++)this.unlimitRow(e),i=t?"number"==typeof t?t:this.computeRowLevelLimit(e):!1,i!==!1&&this.limitRow(e,i)},computeRowLevelLimit:function(t){var e,i,n=this.rowEls.eq(t),r=n.height(),o=this.rowStructs[t].tbodyEl.children();for(e=0;o.length>e;e++)if(i=o.eq(e).removeClass("fc-limited"),i.position().top+i.outerHeight()>r)return e;return!1},limitRow:function(e,i){function n(n){for(;n>T;)r={row:e,col:T},d=E.getCellSegs(r,i),d.length&&(f=s[i-1][T],w=E.renderMoreLink(r,d),y=t("
").append(w),f.append(y),b.push(y[0])),T++}var r,o,s,l,a,c,d,h,u,f,p,g,m,v,y,w,E=this,D=this.view,S=this.rowStructs[e],b=[],T=0;if(i&&S.segLevels.length>i){for(o=S.segLevels[i-1],s=S.cellMatrix,l=S.tbodyEl.children().slice(i).addClass("fc-limited").get(),a=0;o.length>a;a++){for(c=o[a],n(c.leftCol),u=[],h=0;c.rightCol>=T;)r={row:e,col:T},d=this.getCellSegs(r,i),u.push(d),h+=d.length,T++;if(h){for(f=s[i-1][c.leftCol],p=f.attr("rowspan")||1,g=[],m=0;u.length>m;m++)v=t('
').attr("rowspan",p),d=u[m],r={row:e,col:c.leftCol+m},w=this.renderMoreLink(r,[c].concat(d)),y=t("
").append(w),v.append(y),g.push(v[0]),b.push(v[0]);f.addClass("fc-limited").after(t(g)),l.push(f[0])}}n(D.colCnt),S.moreEls=t(b),S.limitedEls=t(l)}},unlimitRow:function(t){var e=this.rowStructs[t];e.moreEls&&(e.moreEls.remove(),e.moreEls=null),e.limitedEls&&(e.limitedEls.removeClass("fc-limited"),e.limitedEls=null)},renderMoreLink:function(e,i){var n=this,r=this.view;return t('').text(this.getMoreLinkText(i.length)).on("click",function(o){var s=r.opt("eventLimitClick"),l=r.cellToDate(e),a=t(this),c=n.getCellDayEl(e),d=n.getCellSegs(e),h=n.resliceDaySegs(d,l),u=n.resliceDaySegs(i,l);"function"==typeof s&&(s=r.trigger("eventLimitClick",null,{date:l,dayEl:c,moreEl:a,segs:h,hiddenSegs:u},o)),"popover"===s?n.showSegPopover(l,e,a,h):"string"==typeof s&&r.calendar.zoomTo(l,s)})},showSegPopover:function(t,e,i,n){var r,o,s=this,l=this.view,a=i.parent();r=1==l.rowCnt?this.view.el:this.rowEls.eq(e.row),o={className:"fc-more-popover",content:this.renderSegPopoverContent(t,n),parentEl:this.el,top:r.offset().top,autoHide:!0,viewportConstrain:l.opt("popoverViewportConstrain"),hide:function(){s.segPopover.destroy(),s.segPopover=null,s.popoverSegs=null}},l.opt("isRTL")?o.right=a.offset().left+a.outerWidth()+1:o.left=a.offset().left-1,this.segPopover=new X(o),this.segPopover.show()},renderSegPopoverContent:function(e,i){var n,r=this.view,o=r.opt("theme"),s=e.format(r.opt("dayPopoverFormat")),l=t('
'+''+''+P(s)+""+'
'+"
"+'
'+'
'+"
"),a=l.find(".fc-event-container");for(i=this.renderSegs(i,!0),this.popoverSegs=i,n=0;i.length>n;n++)i[n].cellDate=e,a.append(i[n].el);return l},resliceDaySegs:function(e,i){var n=t.map(e,function(t){return t.event}),r=i.clone().stripTime(),o=r.clone().add(1,"days");return this.eventsToSegs(n,r,o)},getMoreLinkText:function(t){var e=this.view,i=e.opt("eventLimitText");return"function"==typeof i?i(t):"+"+t+" "+i},getCellSegs:function(t,e){for(var i,n=this.rowStructs[t.row].segMatrix,r=e||0,o=[];n.length>r;)i=n[r][t.col],i&&o.push(i),r++;return o}}),oe.prototype=x(te.prototype),t.extend(oe.prototype,{slotDuration:null,snapDuration:null,minTime:null,maxTime:null,dayEls:null,slatEls:null,slatTops:null,highlightEl:null,helperEl:null,render:function(){this.processOptions(),this.el.html(this.renderHtml()),this.dayEls=this.el.find(".fc-day"),this.slatEls=this.el.find(".fc-slats tr"),this.computeSlatTops(),te.prototype.render.call(this)},renderHtml:function(){return'
'+this.rowHtml("slotBg")+"
"+"
"+'
'+""+this.slatRowHtml()+"
"+"
"},slotBgCellHtml:function(t,e,i){return this.bgCellHtml(t,e,i)},slatRowHtml:function(){for(var t,i,n,r=this.view,o=r.calendar,s=r.opt("isRTL"),l="",a=0===this.slotDuration.asMinutes()%15,c=e.duration(+this.minTime);this.maxTime>c;)t=r.start.clone().time(c),i=t.minutes(),n='
"+(a&&i?"":""+P(o.formatDate(t,r.opt("axisFormat")))+"")+"
'+(s?n:"")+"
',d=n.col),r=l.cellToDate(0,d),o=this.computeDateTop(n.start,r),s=this.computeDateTop(n.end,r),c+='
'+"
"+"
'),c=this.bookendCells(c,"highlight"),'
'+c+""+"
"+"
"}}),t.extend(oe.prototype,{segs:null,eventSkeletonEl:null,renderEvents:function(e){var i=this.renderEventTable(e);this.eventSkeletonEl=t('
').append(i.tableEl),this.el.append(this.eventSkeletonEl),this.segs=i.segs},getSegs:function(){return this.segs||[]},destroyEvents:function(){te.prototype.destroyEvents.call(this),this.eventSkeletonEl&&(this.eventSkeletonEl.remove(),this.eventSkeletonEl=null),this.segs=null},renderEventTable:function(e){var i,n,r,o,s,l,a=t("
"),c=a.find("tr"),d=this.eventsToSegs(e);for(d=this.renderSegs(d),i=this.groupSegCols(d),this.computeSegVerticals(d),o=0;i.length>o;o++){for(s=i[o],se(s),l=t('
'),n=0;s.length>n;n++)r=s[n],r.el.css(this.generateSegPositionCss(r)),30>r.bottom-r.top&&r.el.addClass("fc-short"),l.append(r.el);c.append(t("
").append(l))}return this.bookendCells(c,"eventSkeleton"),{tableEl:a,segs:d}},updateSegVerticals:function(){var t,e=this.segs;if(e)for(this.computeSegVerticals(e),t=0;e.length>t;t++)e[t].el.css(this.generateSegVerticalCss(e[t]))},computeSegVerticals:function(t){var e,i;for(e=0;t.length>e;e++)i=t[e],i.top=this.computeDateTop(i.start,i.start),i.bottom=this.computeDateTop(i.end,i.start)},renderSegHtml:function(t,e){var i,n,r,o=this.view,s=t.event,l=o.isEventDraggable(s),a=!e&&t.isEnd&&o.isEventResizable(s),c=this.getSegClasses(t,l,a),d=this.getEventSkinCss(s);return c.unshift("fc-time-grid-event"),o.isMultiDayEvent(s)?(t.isStart||t.isEnd)&&(i=o.getEventTimeText(t.start,t.end),n=o.getEventTimeText(t.start,t.end,"LT"),r=o.getEventTimeText(t.start,null)):(i=o.getEventTimeText(s),n=o.getEventTimeText(s,"LT"),r=o.getEventTimeText(s.start,null)),'"+'
'+(i?'
"+""+P(i)+""+"
":"")+(s.title?'
'+P(s.title)+"
":"")+"
"+'
'+(a?'
':"")+""},generateSegPositionCss:function(t){var e,i,n=this.view,r=n.opt("isRTL"),o=n.opt("slotEventOverlap"),s=t.backwardCoord,l=t.forwardCoord,a=this.generateSegVerticalCss(t);return o&&(l=Math.min(1,s+2*(l-s))),r?(e=1-l,i=s):(e=s,i=1-l),a.zIndex=t.level+1,a.left=100*e+"%",a.right=100*i+"%",o&&t.forwardPressure&&(a[r?"marginLeft":"marginRight"]=20),a},generateSegVerticalCss:function(t){return{top:t.top,bottom:-t.bottom}},groupSegCols:function(t){var e,i=this.view,n=[];for(e=0;i.colCnt>e;e++)n.push([]);for(e=0;t.length>e;e++)n[t[e].col].push(t[e]);return n}}),pe.prototype={calendar:null,coordMap:null,el:null,start:null,end:null,intervalStart:null,intervalEnd:null,rowCnt:null,colCnt:null,isSelected:!1,scrollerEl:null,scrollTop:null,widgetHeaderClass:null,widgetContentClass:null,highlightStateClass:null,documentMousedownProxy:null,documentDragStartProxy:null,init:function(){var e=this.opt("theme")?"ui":"fc";this.widgetHeaderClass=e+"-widget-header",this.widgetContentClass=e+"-widget-content",this.highlightStateClass=e+"-state-highlight",this.documentMousedownProxy=t.proxy(this,"documentMousedown"),this.documentDragStartProxy=t.proxy(this,"documentDragStart")},render:function(){this.updateSize(),this.trigger("viewRender",this,this,this.el),t(document).on("mousedown",this.documentMousedownProxy).on("dragstart",this.documentDragStartProxy)},destroy:function(){this.unselect(),this.trigger("viewDestroy",this,this,this.el),this.destroyEvents(),this.el.empty(),t(document).off("mousedown",this.documentMousedownProxy).off("dragstart",this.documentDragStartProxy)},incrementDate:function(){},updateSize:function(t){t&&this.recordScroll(),this.updateHeight(),this.updateWidth()},updateWidth:function(){},updateHeight:function(){var t=this.calendar;this.setHeight(t.getSuggestedViewHeight(),t.isHeightAuto())},setHeight:function(){},computeScrollerHeight:function(t){var e,i=this.el.add(this.scrollerEl);return i.css({position:"relative",left:-1}),e=this.el.outerHeight()-this.scrollerEl.height(),i.css({position:"",left:""}),t-e},recordScroll:function(){this.scrollerEl&&(this.scrollTop=this.scrollerEl.scrollTop()) +},restoreScroll:function(){null!==this.scrollTop&&this.scrollerEl.scrollTop(this.scrollTop)},renderEvents:function(){this.segEach(function(t){this.trigger("eventAfterRender",t.event,t.event,t.el)}),this.trigger("eventAfterAllRender")},destroyEvents:function(){this.segEach(function(t){this.trigger("eventDestroy",t.event,t.event,t.el)})},resolveEventEl:function(e,i){var n=this.trigger("eventRender",e,e,i);return n===!1?i=null:n&&n!==!0&&(i=t(n)),i},showEvent:function(t){this.segEach(function(t){t.el.css("visibility","")},t)},hideEvent:function(t){this.segEach(function(t){t.el.css("visibility","hidden")},t)},segEach:function(t,e){var i,n=this.getSegs();for(i=0;n.length>i;i++)e&&n[i].event._id!==e._id||t.call(this,n[i])},getSegs:function(){},renderDrag:function(){},destroyDrag:function(){},documentDragStart:function(e){var i,n=this,r=null;this.opt("droppable")&&(i=new U(this.coordMap,{cellOver:function(t,e){r=e,n.renderDrag(e)},cellOut:function(){r=null,n.destroyDrag()}}),t(document).one("dragstop",function(t,e){n.destroyDrag(),r&&n.trigger("drop",t.target,r,t,e)}),i.startDrag(e))},select:function(t,e,i){this.unselect(i),this.renderSelection(t,e),this.reportSelection(t,e,i)},renderSelection:function(){},reportSelection:function(t,e,i){this.isSelected=!0,this.trigger("select",null,t,e,i)},unselect:function(t){this.isSelected&&(this.isSelected=!1,this.destroySelection(),this.trigger("unselect",null,t))},destroySelection:function(){},documentMousedown:function(e){var i;this.isSelected&&this.opt("unselectAuto")&&D(e)&&(i=this.opt("unselectCancel"),i&&t(e.target).closest(i).length||this.unselect(e))}},ge.prototype=x(pe.prototype),t.extend(ge.prototype,{dayGrid:null,dayNumbersVisible:!1,weekNumbersVisible:!1,weekNumberWidth:null,headRowEl:null,render:function(t,e,i){this.rowCnt=t,this.colCnt=e,this.dayNumbersVisible=i,this.weekNumbersVisible=this.opt("weekNumbers"),this.dayGrid.numbersVisible=this.dayNumbersVisible||this.weekNumbersVisible,this.el.addClass("fc-basic-view").html(this.renderHtml()),this.headRowEl=this.el.find("thead .fc-row"),this.scrollerEl=this.el.find(".fc-day-grid-container"),this.dayGrid.coordMap.containerEl=this.scrollerEl,this.dayGrid.el=this.el.find(".fc-day-grid"),this.dayGrid.render(this.hasRigidRows()),pe.prototype.render.call(this)},destroy:function(){this.dayGrid.destroy(),pe.prototype.destroy.call(this)},renderHtml:function(){return'"+""+""+""+""+'"+""+""+"
'+this.dayGrid.headHtml()+"
'+'
'+'
'+"
"+"
"},headIntroHtml:function(){return this.weekNumbersVisible?'
"+""+P(this.opt("weekNumberTitle"))+""+""+""+this.calendar.calculateWeekNumber(this.cellToDate(t,0))+""+"'+i.date()+""},weekNumberStyleAttr:function(){return null!==this.weekNumberWidth?'style="width:'+this.weekNumberWidth+'px"':""},hasRigidRows:function(){var t=this.opt("eventLimit");return t&&"number"!=typeof t},updateWidth:function(){this.weekNumbersVisible&&(this.weekNumberWidth=m(this.el.find(".fc-week-number")))},setHeight:function(t,e){var i,n=this.opt("eventLimit");y(this.scrollerEl),f(this.headRowEl),this.dayGrid.destroySegPopover(),n&&"number"==typeof n&&this.dayGrid.limitRows(n),i=this.computeScrollerHeight(t),this.setGridHeight(i,e),n&&"number"!=typeof n&&this.dayGrid.limitRows(n),!e&&v(this.scrollerEl,i)&&(u(this.headRowEl,E(this.scrollerEl)),i=this.computeScrollerHeight(t),this.scrollerEl.height(i),this.restoreScroll())},setGridHeight:function(t,e){e?g(this.dayGrid.rowEls):p(this.dayGrid.rowEls,t,!0)},renderEvents:function(t){this.dayGrid.renderEvents(t),this.updateHeight(),pe.prototype.renderEvents.call(this,t)},getSegs:function(){return this.dayGrid.getSegs()},destroyEvents:function(){pe.prototype.destroyEvents.call(this),this.recordScroll(),this.dayGrid.destroyEvents()},renderDrag:function(t,e,i){return this.dayGrid.renderDrag(t,e,i)},destroyDrag:function(){this.dayGrid.destroyDrag()},renderSelection:function(t,e){this.dayGrid.renderSelection(t,e)},destroySelection:function(){this.dayGrid.destroySelection()}}),r({fixedWeekCount:!0}),ze.month=me,me.prototype=x(ge.prototype),t.extend(me.prototype,{name:"month",incrementDate:function(t,e){return t.clone().stripTime().add(e,"months").startOf("month")},render:function(t){var e;this.intervalStart=t.clone().stripTime().startOf("month"),this.intervalEnd=this.intervalStart.clone().add(1,"months"),this.start=this.intervalStart.clone(),this.start=this.skipHiddenDays(this.start),this.start.startOf("week"),this.start=this.skipHiddenDays(this.start),this.end=this.intervalEnd.clone(),this.end=this.skipHiddenDays(this.end,-1,!0),this.end.add((7-this.end.weekday())%7,"days"),this.end=this.skipHiddenDays(this.end,-1,!0),e=Math.ceil(this.end.diff(this.start,"weeks",!0)),this.isFixedWeeks()&&(this.end.add(6-e,"weeks"),e=6),this.title=this.calendar.formatDate(this.intervalStart,this.opt("titleFormat")),ge.prototype.render.call(this,e,this.getCellsPerWeek(),!0)},setGridHeight:function(t,e){e=e||"variable"===this.opt("weekMode"),e&&(t*=this.rowCnt/6),p(this.dayGrid.rowEls,t,!e)},isFixedWeeks:function(){var t=this.opt("weekMode");return t?"fixed"===t:this.opt("fixedWeekCount")}}),ze.basicWeek=ve,ve.prototype=x(ge.prototype),t.extend(ve.prototype,{name:"basicWeek",incrementDate:function(t,e){return t.clone().stripTime().add(e,"weeks").startOf("week")},render:function(t){this.intervalStart=t.clone().stripTime().startOf("week"),this.intervalEnd=this.intervalStart.clone().add(1,"weeks"),this.start=this.skipHiddenDays(this.intervalStart),this.end=this.skipHiddenDays(this.intervalEnd,-1,!0),this.title=this.calendar.formatRange(this.start,this.end.clone().subtract(1),this.opt("titleFormat")," — "),ge.prototype.render.call(this,1,this.getCellsPerWeek(),!1)}}),ze.basicDay=ye,ye.prototype=x(ge.prototype),t.extend(ye.prototype,{name:"basicDay",incrementDate:function(t,e){var i=t.clone().stripTime().add(e,"days");return i=this.skipHiddenDays(i,0>e?-1:1)},render:function(t){this.start=this.intervalStart=t.clone().stripTime(),this.end=this.intervalEnd=this.start.clone().add(1,"days"),this.title=this.calendar.formatDate(this.start,this.opt("titleFormat")),ge.prototype.render.call(this,1,1,!1)}}),r({allDaySlot:!0,allDayText:"all-day",scrollTime:"06:00:00",slotDuration:"00:30:00",axisFormat:we,timeFormat:{agenda:Ee},minTime:"00:00:00",maxTime:"24:00:00",slotEventOverlap:!0});var Fe=5;De.prototype=x(pe.prototype),t.extend(De.prototype,{timeGrid:null,dayGrid:null,axisWidth:null,noScrollRowEls:null,bottomRuleEl:null,bottomRuleHeight:null,render:function(e){this.rowCnt=1,this.colCnt=e,this.el.addClass("fc-agenda-view").html(this.renderHtml()),this.scrollerEl=this.el.find(".fc-time-grid-container"),this.timeGrid.coordMap.containerEl=this.scrollerEl,this.timeGrid.el=this.el.find(".fc-time-grid"),this.timeGrid.render(),this.bottomRuleEl=t('
').appendTo(this.timeGrid.el),this.dayGrid&&(this.dayGrid.el=this.el.find(".fc-day-grid"),this.dayGrid.render(),this.dayGrid.bottomCoordPadding=this.dayGrid.el.next("hr").outerHeight()),this.noScrollRowEls=this.el.find(".fc-row:not(.fc-scroller *)"),pe.prototype.render.call(this),this.resetScroll()},destroy:function(){this.timeGrid.destroy(),this.dayGrid&&this.dayGrid.destroy(),pe.prototype.destroy.call(this)},renderHtml:function(){return'"+""+""+""+""+'"+""+""+"
'+this.timeGrid.headHtml()+"
'+(this.dayGrid?'

':"")+'
'+'
'+"
"+"
"},headIntroHtml:function(){var t,e,i,n;return this.opt("weekNumbers")?(t=this.cellToDate(0,0),e=this.calendar.calculateWeekNumber(t),i=this.opt("weekNumberTitle"),n=this.opt("isRTL")?e+i:i+e,'
"+""+P(n)+""+""+""+(this.opt("allDayHtml")||P(this.opt("allDayText")))+""+"'+P(r.name)+"s are globally styled */ - border-color: transparent; - - /* don't put a border between events and/or the day number */ - border-bottom: 0; -} - -.fc-row .fc-content-skeleton tbody td, /* cells with events inside (so NOT the day number cell) */ -.fc-row .fc-helper-skeleton tbody td { - /* don't put a border between event cells */ - border-top: 0; -} - - -/* Scrolling Container ---------------------------------------------------------------------------------------------------*/ - -.fc-scroller { /* this class goes on elements for guaranteed vertical scrollbars */ - overflow-y: scroll; - overflow-x: hidden; -} - -.fc-scroller > * { /* we expect an immediate inner element */ - position: relative; /* re-scope all positions */ - width: 100%; /* hack to force re-sizing this inner element when scrollbars appear/disappear */ - overflow: hidden; /* don't let negative margins or absolute positioning create further scroll */ -} - - -/* Global Event Styles ---------------------------------------------------------------------------------------------------*/ - -.fc-event { - position: relative; /* for resize handle and other inner positioning */ - display: block; /* make the tag block */ - font-size: .85em; - line-height: 1.3; - border-radius: 3px; - border: 1px solid #3a87ad; /* default BORDER color */ - background-color: #3a87ad; /* default BACKGROUND color */ - font-weight: normal; /* undo jqui's ui-widget-header bold */ -} - -/* overpower some of bootstrap's and jqui's styles on tags */ -.fc-event, -.fc-event:hover, -.ui-widget .fc-event { - color: #fff; /* default TEXT color */ - text-decoration: none; /* if has an href */ -} - -.fc-event[href], -.fc-event.fc-draggable { - cursor: pointer; /* give events with links and draggable events a hand mouse pointer */ -} - - -/* DayGrid events ----------------------------------------------------------------------------------------------------- -We use the full "fc-day-grid-event" class instead of using descendants because the event won't -be a descendant of the grid when it is being dragged. -*/ - -.fc-day-grid-event { - margin: 1px 2px 0; /* spacing between events and edges */ - padding: 0 1px; -} - -/* events that are continuing to/from another week. kill rounded corners and butt up against edge */ - -.fc-ltr .fc-day-grid-event.fc-not-start, -.fc-rtl .fc-day-grid-event.fc-not-end { - margin-left: 0; - border-left-width: 0; - padding-left: 1px; /* replace the border with padding */ - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} - -.fc-ltr .fc-day-grid-event.fc-not-end, -.fc-rtl .fc-day-grid-event.fc-not-start { - margin-right: 0; - border-right-width: 0; - padding-right: 1px; /* replace the border with padding */ - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} - -.fc-day-grid-event > .fc-content { /* force events to be one-line tall */ - white-space: nowrap; - overflow: hidden; -} - -.fc-day-grid-event .fc-time { - font-weight: bold; -} - -/* resize handle (outside of fc-content, so can go outside of bounds) */ - -.fc-day-grid-event .fc-resizer { - position: absolute; - top: 0; - bottom: 0; - width: 7px; -} - -.fc-ltr .fc-day-grid-event .fc-resizer { - right: -3px; - cursor: e-resize; -} - -.fc-rtl .fc-day-grid-event .fc-resizer { - left: -3px; - cursor: w-resize; -} - - -/* Event Limiting ---------------------------------------------------------------------------------------------------*/ - -/* "more" link that represents hidden events */ - -a.fc-more { - margin: 1px 3px; - font-size: .85em; - cursor: pointer; - text-decoration: none; -} - -a.fc-more:hover { - text-decoration: underline; -} - -.fc-limited { /* rows and cells that are hidden because of a "more" link */ - display: none; -} - -/* popover that appears when "more" link is clicked */ - -.fc-day-grid .fc-row { - z-index: 1; /* make the "more" popover one higher than this */ -} - -.fc-more-popover { - z-index: 2; - width: 220px; -} - -.fc-more-popover .fc-event-container { - padding: 10px; -} - -/* Toolbar ---------------------------------------------------------------------------------------------------*/ - -.fc-toolbar { - text-align: center; - margin-bottom: 1em; -} - -.fc-toolbar .fc-left { - float: left; -} - -.fc-toolbar .fc-right { - float: right; -} - -.fc-toolbar .fc-center { - display: inline-block; -} - -/* the things within each left/right/center section */ -.fc .fc-toolbar > * > * { /* extra precedence to override button border margins */ - float: left; - margin-left: .75em; -} - -/* the first thing within each left/center/right section */ -.fc .fc-toolbar > * > :first-child { /* extra precedence to override button border margins */ - margin-left: 0; -} - -/* title text */ - -.fc-toolbar h2 { - margin: 0; -} - -/* button layering (for border precedence) */ - -.fc-toolbar button { - position: relative; -} - -.fc-toolbar .fc-state-hover, -.fc-toolbar .ui-state-hover { - z-index: 2; -} - -.fc-toolbar .fc-state-down { - z-index: 3; -} - -.fc-toolbar .fc-state-active, -.fc-toolbar .ui-state-active { - z-index: 4; -} - -.fc-toolbar button:focus { - z-index: 5; -} - - -/* View Structure ---------------------------------------------------------------------------------------------------*/ - -/* undo twitter bootstrap's box-sizing rules. normalizes positioning techniques */ -/* don't do this for the toolbar because we'll want bootstrap to style those buttons as some pt */ -.fc-view-container *, -.fc-view-container *:before, -.fc-view-container *:after { - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; - box-sizing: content-box; -} - -.fc-view, /* scope positioning and z-index's for everything within the view */ -.fc-view > table { /* so dragged elements can be above the view's main element */ - position: relative; - z-index: 1; -} - -/* BasicView ---------------------------------------------------------------------------------------------------*/ - -/* day row structure */ - -.fc-basicWeek-view .fc-content-skeleton, -.fc-basicDay-view .fc-content-skeleton { - /* we are sure there are no day numbers in these views, so... */ - padding-top: 1px; /* add a pixel to make sure there are 2px padding above events */ - padding-bottom: 1em; /* ensure a space at bottom of cell for user selecting/clicking */ -} - -.fc-basic-view tbody .fc-row { - min-height: 4em; /* ensure that all rows are at least this tall */ -} - -/* a "rigid" row will take up a constant amount of height because content-skeleton is absolute */ - -.fc-row.fc-rigid { - overflow: hidden; -} - -.fc-row.fc-rigid .fc-content-skeleton { - position: absolute; - top: 0; - left: 0; - right: 0; -} - -/* week and day number styling */ - -.fc-basic-view .fc-week-number, -.fc-basic-view .fc-day-number { - padding: 0 2px; -} - -.fc-basic-view td.fc-week-number span, -.fc-basic-view td.fc-day-number { - padding-top: 2px; - padding-bottom: 2px; -} - -.fc-basic-view .fc-week-number { - text-align: center; -} - -.fc-basic-view .fc-week-number span { - /* work around the way we do column resizing and ensure a minimum width */ - display: inline-block; - min-width: 1.25em; -} - -.fc-ltr .fc-basic-view .fc-day-number { - text-align: right; -} - -.fc-rtl .fc-basic-view .fc-day-number { - text-align: left; -} - -.fc-day-number.fc-other-month { - opacity: 0.3; - filter: alpha(opacity=30); /* for IE */ - /* opacity with small font can sometimes look too faded - might want to set the 'color' property instead - making day-numbers bold also fixes the problem */ -} - -/* AgendaView all-day area ---------------------------------------------------------------------------------------------------*/ - -.fc-agenda-view .fc-day-grid { - position: relative; - z-index: 2; /* so the "more.." popover will be over the time grid */ -} - -.fc-agenda-view .fc-day-grid .fc-row { - min-height: 3em; /* all-day section will never get shorter than this */ -} - -.fc-agenda-view .fc-day-grid .fc-row .fc-content-skeleton { - padding-top: 1px; /* add a pixel to make sure there are 2px padding above events */ - padding-bottom: 1em; /* give space underneath events for clicking/selecting days */ -} - - -/* TimeGrid axis running down the side (for both the all-day area and the slot area) ---------------------------------------------------------------------------------------------------*/ - -.fc .fc-axis { /* .fc to overcome default cell styles */ - vertical-align: middle; - padding: 0 4px; - white-space: nowrap; -} - -.fc-ltr .fc-axis { - text-align: right; -} - -.fc-rtl .fc-axis { - text-align: left; -} - -.ui-widget td.fc-axis { - font-weight: normal; /* overcome jqui theme making it bold */ -} - - -/* TimeGrid Structure ---------------------------------------------------------------------------------------------------*/ - -.fc-time-grid-container, /* so scroll container's z-index is below all-day */ -.fc-time-grid { /* so slats/bg/content/etc positions get scoped within here */ - position: relative; - z-index: 1; -} - -.fc-time-grid { - min-height: 100%; /* so if height setting is 'auto', .fc-bg stretches to fill height */ -} - -.fc-time-grid table { /* don't put outer borders on slats/bg/content/etc */ - border: 0 hidden transparent; -} - -.fc-time-grid > .fc-bg { - z-index: 1; -} - -.fc-time-grid .fc-slats, -.fc-time-grid > hr { /* the
AgendaView injects when grid is shorter than scroller */ - position: relative; - z-index: 2; -} - -.fc-time-grid .fc-highlight-skeleton { - z-index: 3; -} - -.fc-time-grid .fc-content-skeleton { - position: absolute; - z-index: 4; - top: 0; - left: 0; - right: 0; -} - -.fc-time-grid > .fc-helper-skeleton { - z-index: 5; -} - - -/* TimeGrid Slats (lines that run horizontally) ---------------------------------------------------------------------------------------------------*/ - -.fc-slats td { - height: 1.5em; - border-bottom: 0; /* each cell is responsible for its top border */ -} - -.fc-slats .fc-minor td { - border-top-style: dotted; -} - -.fc-slats .ui-widget-content { /* for jqui theme */ - background: none; /* see through to fc-bg */ -} - - -/* TimeGrid Highlighting Slots ---------------------------------------------------------------------------------------------------*/ - -.fc-time-grid .fc-highlight-container { /* a div within a cell within the fc-highlight-skeleton */ - position: relative; /* scopes the left/right of the fc-highlight to be in the column */ -} - -.fc-time-grid .fc-highlight { - position: absolute; - left: 0; - right: 0; - /* top and bottom will be in by JS */ -} - - -/* TimeGrid Event Containment ---------------------------------------------------------------------------------------------------*/ - -.fc-time-grid .fc-event-container { /* a div within a cell within the fc-content-skeleton */ - position: relative; -} - -.fc-ltr .fc-time-grid .fc-event-container { /* space on the sides of events for LTR (default) */ - margin: 0 2.5% 0 2px; -} - -.fc-rtl .fc-time-grid .fc-event-container { /* space on the sides of events for RTL */ - margin: 0 2px 0 2.5%; -} - -.fc-time-grid .fc-event { - position: absolute; - z-index: 1; /* scope inner z-index's */ -} - - -/* TimeGrid Event Styling ----------------------------------------------------------------------------------------------------- -We use the full "fc-time-grid-event" class instead of using descendants because the event won't -be a descendant of the grid when it is being dragged. -*/ - -.fc-time-grid-event.fc-not-start { /* events that are continuing from another day */ - /* replace space made by the top border with padding */ - border-top-width: 0; - padding-top: 1px; - - /* remove top rounded corners */ - border-top-left-radius: 0; - border-top-right-radius: 0; -} - -.fc-time-grid-event.fc-not-end { - /* replace space made by the top border with padding */ - border-bottom-width: 0; - padding-bottom: 1px; - - /* remove bottom rounded corners */ - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} - -.fc-time-grid-event { - overflow: hidden; /* don't let the bg flow over rounded corners */ -} - -.fc-time-grid-event > .fc-content { /* contains the time and title, but no bg and resizer */ - position: relative; - z-index: 2; /* above the bg */ -} - -.fc-time-grid-event .fc-time, -.fc-time-grid-event .fc-title { - padding: 0 1px; -} - -.fc-time-grid-event .fc-time { - font-size: .85em; - white-space: nowrap; -} - -.fc-time-grid-event .fc-bg { - z-index: 1; - background: #fff; - opacity: .25; - filter: alpha(opacity=25); /* for IE */ -} - -/* short mode, where time and title are on the same line */ - -.fc-time-grid-event.fc-short .fc-content { - /* don't wrap to second line (now that contents will be inline) */ - white-space: nowrap; -} - -.fc-time-grid-event.fc-short .fc-time, -.fc-time-grid-event.fc-short .fc-title { - /* put the time and title on the same line */ - display: inline-block; - vertical-align: top; -} - -.fc-time-grid-event.fc-short .fc-time span { - display: none; /* don't display the full time text... */ -} - -.fc-time-grid-event.fc-short .fc-time:before { - content: attr(data-start); /* ...instead, display only the start time */ -} - -.fc-time-grid-event.fc-short .fc-time:after { - content: "\000A0-\000A0"; /* seperate with a dash, wrapped in nbsp's */ -} - -.fc-time-grid-event.fc-short .fc-title { - font-size: .85em; /* make the title text the same size as the time */ - padding: 0; /* undo padding from above */ -} - -/* resizer */ - -.fc-time-grid-event .fc-resizer { - position: absolute; - z-index: 3; /* above content */ - left: 0; - right: 0; - bottom: 0; - height: 8px; - overflow: hidden; - line-height: 8px; - font-size: 11px; - font-family: monospace; - text-align: center; - cursor: s-resize; -} - -.fc-time-grid-event .fc-resizer:after { - content: "="; -} +/*! + * FullCalendar v2.1.1 Stylesheet + * Docs & License: http://arshaw.com/fullcalendar/ + * (c) 2013 Adam Shaw + */ + + +.fc { + direction: ltr; + text-align: left; +} + +.fc-rtl { + text-align: right; +} + +body .fc { /* extra precedence to overcome jqui */ + font-size: 1em; +} + + +/* Colors +--------------------------------------------------------------------------------------------------*/ + +.fc-unthemed th, +.fc-unthemed td, +.fc-unthemed hr, +.fc-unthemed thead, +.fc-unthemed tbody, +.fc-unthemed .fc-row, +.fc-unthemed .fc-popover { + border-color: #ddd; +} + +.fc-unthemed .fc-popover { + background-color: #fff; +} + +.fc-unthemed hr, +.fc-unthemed .fc-popover .fc-header { + background: #eee; +} + +.fc-unthemed .fc-popover .fc-header .fc-close { + color: #666; +} + +.fc-unthemed .fc-today { + background: #fcf8e3; +} + +.fc-highlight { /* when user is selecting cells */ + background: #bce8f1; + opacity: .3; + filter: alpha(opacity=30); /* for IE */ +} + + +/* Icons (inline elements with styled text that mock arrow icons) +--------------------------------------------------------------------------------------------------*/ + +.fc-icon { + display: inline-block; + font-size: 2em; + line-height: .5em; + height: .5em; /* will make the total height 1em */ + font-family: "Courier New", Courier, monospace; +} + +.fc-icon-left-single-arrow:after { + content: "\02039"; + font-weight: bold; +} + +.fc-icon-right-single-arrow:after { + content: "\0203A"; + font-weight: bold; +} + +.fc-icon-left-double-arrow:after { + content: "\000AB"; +} + +.fc-icon-right-double-arrow:after { + content: "\000BB"; +} + +.fc-icon-x:after { + content: "\000D7"; +} + + +/* Buttons (styled
s are globally styled */ + border-color: transparent; + + /* don't put a border between events and/or the day number */ + border-bottom: 0; +} + +.fc-row .fc-content-skeleton tbody td, /* cells with events inside (so NOT the day number cell) */ +.fc-row .fc-helper-skeleton tbody td { + /* don't put a border between event cells */ + border-top: 0; +} + + +/* Scrolling Container +--------------------------------------------------------------------------------------------------*/ + +.fc-scroller { /* this class goes on elements for guaranteed vertical scrollbars */ + overflow-y: scroll; + overflow-x: hidden; +} + +.fc-scroller > * { /* we expect an immediate inner element */ + position: relative; /* re-scope all positions */ + width: 100%; /* hack to force re-sizing this inner element when scrollbars appear/disappear */ + overflow: hidden; /* don't let negative margins or absolute positioning create further scroll */ +} + + +/* Global Event Styles +--------------------------------------------------------------------------------------------------*/ + +.fc-event { + position: relative; /* for resize handle and other inner positioning */ + display: block; /* make the tag block */ + font-size: .85em; + line-height: 1.3; + border-radius: 3px; + border: 1px solid #3a87ad; /* default BORDER color */ + background-color: #3a87ad; /* default BACKGROUND color */ + font-weight: normal; /* undo jqui's ui-widget-header bold */ +} + +/* overpower some of bootstrap's and jqui's styles on tags */ +.fc-event, +.fc-event:hover, +.ui-widget .fc-event { + color: #fff; /* default TEXT color */ + text-decoration: none; /* if has an href */ +} + +.fc-event[href], +.fc-event.fc-draggable { + cursor: pointer; /* give events with links and draggable events a hand mouse pointer */ +} + + +/* DayGrid events +---------------------------------------------------------------------------------------------------- +We use the full "fc-day-grid-event" class instead of using descendants because the event won't +be a descendant of the grid when it is being dragged. +*/ + +.fc-day-grid-event { + margin: 1px 2px 0; /* spacing between events and edges */ + padding: 0 1px; +} + +/* events that are continuing to/from another week. kill rounded corners and butt up against edge */ + +.fc-ltr .fc-day-grid-event.fc-not-start, +.fc-rtl .fc-day-grid-event.fc-not-end { + margin-left: 0; + border-left-width: 0; + padding-left: 1px; /* replace the border with padding */ + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.fc-ltr .fc-day-grid-event.fc-not-end, +.fc-rtl .fc-day-grid-event.fc-not-start { + margin-right: 0; + border-right-width: 0; + padding-right: 1px; /* replace the border with padding */ + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.fc-day-grid-event > .fc-content { /* force events to be one-line tall */ + white-space: nowrap; + overflow: hidden; +} + +.fc-day-grid-event .fc-time { + font-weight: bold; +} + +/* resize handle (outside of fc-content, so can go outside of bounds) */ + +.fc-day-grid-event .fc-resizer { + position: absolute; + top: 0; + bottom: 0; + width: 7px; +} + +.fc-ltr .fc-day-grid-event .fc-resizer { + right: -3px; + cursor: e-resize; +} + +.fc-rtl .fc-day-grid-event .fc-resizer { + left: -3px; + cursor: w-resize; +} + + +/* Event Limiting +--------------------------------------------------------------------------------------------------*/ + +/* "more" link that represents hidden events */ + +a.fc-more { + margin: 1px 3px; + font-size: .85em; + cursor: pointer; + text-decoration: none; +} + +a.fc-more:hover { + text-decoration: underline; +} + +.fc-limited { /* rows and cells that are hidden because of a "more" link */ + display: none; +} + +/* popover that appears when "more" link is clicked */ + +.fc-day-grid .fc-row { + z-index: 1; /* make the "more" popover one higher than this */ +} + +.fc-more-popover { + z-index: 2; + width: 220px; +} + +.fc-more-popover .fc-event-container { + padding: 10px; +} + +/* Toolbar +--------------------------------------------------------------------------------------------------*/ + +.fc-toolbar { + text-align: center; + margin-bottom: 1em; +} + +.fc-toolbar .fc-left { + float: left; +} + +.fc-toolbar .fc-right { + float: right; +} + +.fc-toolbar .fc-center { + display: inline-block; +} + +/* the things within each left/right/center section */ +.fc .fc-toolbar > * > * { /* extra precedence to override button border margins */ + float: left; + margin-left: .75em; +} + +/* the first thing within each left/center/right section */ +.fc .fc-toolbar > * > :first-child { /* extra precedence to override button border margins */ + margin-left: 0; +} + +/* title text */ + +.fc-toolbar h2 { + margin: 0; +} + +/* button layering (for border precedence) */ + +.fc-toolbar button { + position: relative; +} + +.fc-toolbar .fc-state-hover, +.fc-toolbar .ui-state-hover { + z-index: 2; +} + +.fc-toolbar .fc-state-down { + z-index: 3; +} + +.fc-toolbar .fc-state-active, +.fc-toolbar .ui-state-active { + z-index: 4; +} + +.fc-toolbar button:focus { + z-index: 5; +} + + +/* View Structure +--------------------------------------------------------------------------------------------------*/ + +/* undo twitter bootstrap's box-sizing rules. normalizes positioning techniques */ +/* don't do this for the toolbar because we'll want bootstrap to style those buttons as some pt */ +.fc-view-container *, +.fc-view-container *:before, +.fc-view-container *:after { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +.fc-view, /* scope positioning and z-index's for everything within the view */ +.fc-view > table { /* so dragged elements can be above the view's main element */ + position: relative; + z-index: 1; +} + +/* BasicView +--------------------------------------------------------------------------------------------------*/ + +/* day row structure */ + +.fc-basicWeek-view .fc-content-skeleton, +.fc-basicDay-view .fc-content-skeleton { + /* we are sure there are no day numbers in these views, so... */ + padding-top: 1px; /* add a pixel to make sure there are 2px padding above events */ + padding-bottom: 1em; /* ensure a space at bottom of cell for user selecting/clicking */ +} + +.fc-basic-view tbody .fc-row { + min-height: 4em; /* ensure that all rows are at least this tall */ +} + +/* a "rigid" row will take up a constant amount of height because content-skeleton is absolute */ + +.fc-row.fc-rigid { + overflow: hidden; +} + +.fc-row.fc-rigid .fc-content-skeleton { + position: absolute; + top: 0; + left: 0; + right: 0; +} + +/* week and day number styling */ + +.fc-basic-view .fc-week-number, +.fc-basic-view .fc-day-number { + padding: 0 2px; +} + +.fc-basic-view td.fc-week-number span, +.fc-basic-view td.fc-day-number { + padding-top: 2px; + padding-bottom: 2px; +} + +.fc-basic-view .fc-week-number { + text-align: center; +} + +.fc-basic-view .fc-week-number span { + /* work around the way we do column resizing and ensure a minimum width */ + display: inline-block; + min-width: 1.25em; +} + +.fc-ltr .fc-basic-view .fc-day-number { + text-align: right; +} + +.fc-rtl .fc-basic-view .fc-day-number { + text-align: left; +} + +.fc-day-number.fc-other-month { + opacity: 0.3; + filter: alpha(opacity=30); /* for IE */ + /* opacity with small font can sometimes look too faded + might want to set the 'color' property instead + making day-numbers bold also fixes the problem */ +} + +/* AgendaView all-day area +--------------------------------------------------------------------------------------------------*/ + +.fc-agenda-view .fc-day-grid { + position: relative; + z-index: 2; /* so the "more.." popover will be over the time grid */ +} + +.fc-agenda-view .fc-day-grid .fc-row { + min-height: 3em; /* all-day section will never get shorter than this */ +} + +.fc-agenda-view .fc-day-grid .fc-row .fc-content-skeleton { + padding-top: 1px; /* add a pixel to make sure there are 2px padding above events */ + padding-bottom: 1em; /* give space underneath events for clicking/selecting days */ +} + + +/* TimeGrid axis running down the side (for both the all-day area and the slot area) +--------------------------------------------------------------------------------------------------*/ + +.fc .fc-axis { /* .fc to overcome default cell styles */ + vertical-align: middle; + padding: 0 4px; + white-space: nowrap; +} + +.fc-ltr .fc-axis { + text-align: right; +} + +.fc-rtl .fc-axis { + text-align: left; +} + +.ui-widget td.fc-axis { + font-weight: normal; /* overcome jqui theme making it bold */ +} + + +/* TimeGrid Structure +--------------------------------------------------------------------------------------------------*/ + +.fc-time-grid-container, /* so scroll container's z-index is below all-day */ +.fc-time-grid { /* so slats/bg/content/etc positions get scoped within here */ + position: relative; + z-index: 1; +} + +.fc-time-grid { + min-height: 100%; /* so if height setting is 'auto', .fc-bg stretches to fill height */ +} + +.fc-time-grid table { /* don't put outer borders on slats/bg/content/etc */ + border: 0 hidden transparent; +} + +.fc-time-grid > .fc-bg { + z-index: 1; +} + +.fc-time-grid .fc-slats, +.fc-time-grid > hr { /* the
AgendaView injects when grid is shorter than scroller */ + position: relative; + z-index: 2; +} + +.fc-time-grid .fc-highlight-skeleton { + z-index: 3; +} + +.fc-time-grid .fc-content-skeleton { + position: absolute; + z-index: 4; + top: 0; + left: 0; + right: 0; +} + +.fc-time-grid > .fc-helper-skeleton { + z-index: 5; +} + + +/* TimeGrid Slats (lines that run horizontally) +--------------------------------------------------------------------------------------------------*/ + +.fc-slats td { + height: 1.5em; + border-bottom: 0; /* each cell is responsible for its top border */ +} + +.fc-slats .fc-minor td { + border-top-style: dotted; +} + +.fc-slats .ui-widget-content { /* for jqui theme */ + background: none; /* see through to fc-bg */ +} + + +/* TimeGrid Highlighting Slots +--------------------------------------------------------------------------------------------------*/ + +.fc-time-grid .fc-highlight-container { /* a div within a cell within the fc-highlight-skeleton */ + position: relative; /* scopes the left/right of the fc-highlight to be in the column */ +} + +.fc-time-grid .fc-highlight { + position: absolute; + left: 0; + right: 0; + /* top and bottom will be in by JS */ +} + + +/* TimeGrid Event Containment +--------------------------------------------------------------------------------------------------*/ + +.fc-time-grid .fc-event-container { /* a div within a cell within the fc-content-skeleton */ + position: relative; +} + +.fc-ltr .fc-time-grid .fc-event-container { /* space on the sides of events for LTR (default) */ + margin: 0 2.5% 0 2px; +} + +.fc-rtl .fc-time-grid .fc-event-container { /* space on the sides of events for RTL */ + margin: 0 2px 0 2.5%; +} + +.fc-time-grid .fc-event { + position: absolute; + z-index: 1; /* scope inner z-index's */ +} + + +/* TimeGrid Event Styling +---------------------------------------------------------------------------------------------------- +We use the full "fc-time-grid-event" class instead of using descendants because the event won't +be a descendant of the grid when it is being dragged. +*/ + +.fc-time-grid-event.fc-not-start { /* events that are continuing from another day */ + /* replace space made by the top border with padding */ + border-top-width: 0; + padding-top: 1px; + + /* remove top rounded corners */ + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.fc-time-grid-event.fc-not-end { + /* replace space made by the top border with padding */ + border-bottom-width: 0; + padding-bottom: 1px; + + /* remove bottom rounded corners */ + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.fc-time-grid-event { + overflow: hidden; /* don't let the bg flow over rounded corners */ +} + +.fc-time-grid-event > .fc-content { /* contains the time and title, but no bg and resizer */ + position: relative; + z-index: 2; /* above the bg */ +} + +.fc-time-grid-event .fc-time, +.fc-time-grid-event .fc-title { + padding: 0 1px; +} + +.fc-time-grid-event .fc-time { + font-size: .85em; + white-space: nowrap; +} + +.fc-time-grid-event .fc-bg { + z-index: 1; + background: #fff; + opacity: .25; + filter: alpha(opacity=25); /* for IE */ +} + +/* short mode, where time and title are on the same line */ + +.fc-time-grid-event.fc-short .fc-content { + /* don't wrap to second line (now that contents will be inline) */ + white-space: nowrap; +} + +.fc-time-grid-event.fc-short .fc-time, +.fc-time-grid-event.fc-short .fc-title { + /* put the time and title on the same line */ + display: inline-block; + vertical-align: top; +} + +.fc-time-grid-event.fc-short .fc-time span { + display: none; /* don't display the full time text... */ +} + +.fc-time-grid-event.fc-short .fc-time:before { + content: attr(data-start); /* ...instead, display only the start time */ +} + +.fc-time-grid-event.fc-short .fc-time:after { + content: "\000A0-\000A0"; /* seperate with a dash, wrapped in nbsp's */ +} + +.fc-time-grid-event.fc-short .fc-title { + font-size: .85em; /* make the title text the same size as the time */ + padding: 0; /* undo padding from above */ +} + +/* resizer */ + +.fc-time-grid-event .fc-resizer { + position: absolute; + z-index: 3; /* above content */ + left: 0; + right: 0; + bottom: 0; + height: 8px; + overflow: hidden; + line-height: 8px; + font-size: 11px; + font-family: monospace; + text-align: center; + cursor: s-resize; +} + +.fc-time-grid-event .fc-resizer:after { + content: "="; +} diff --git a/dist/fullcalendar.js b/dist/fullcalendar.js index 6984157..9cadab6 100644 --- a/dist/fullcalendar.js +++ b/dist/fullcalendar.js @@ -1,8399 +1,8996 @@ -/*! - * FullCalendar v2.1.1 - * Docs & License: http://arshaw.com/fullcalendar/ - * (c) 2013 Adam Shaw - */ - -(function(factory) { - if (typeof define === 'function' && define.amd) { - define([ 'jquery', 'moment' ], factory); - } - else { - factory(jQuery, moment); - } -})(function($, moment) { - -;; - -var defaults = { - - lang: 'en', - - defaultTimedEventDuration: '02:00:00', - defaultAllDayEventDuration: { days: 1 }, - forceEventDuration: false, - nextDayThreshold: '09:00:00', // 9am - - // display - defaultView: 'month', - aspectRatio: 1.35, - header: { - left: 'title', - center: '', - right: 'today prev,next' - }, - weekends: true, - weekNumbers: false, - - weekNumberTitle: 'W', - weekNumberCalculation: 'local', - - //editable: false, - - // event ajax - lazyFetching: true, - startParam: 'start', - endParam: 'end', - timezoneParam: 'timezone', - - timezone: false, - - //allDayDefault: undefined, - - // time formats - titleFormat: { - month: 'MMMM YYYY', // like "September 1986". each language will override this - week: 'll', // like "Sep 4 1986" - day: 'LL' // like "September 4 1986" - }, - columnFormat: { - month: 'ddd', // like "Sat" - week: generateWeekColumnFormat, - day: 'dddd' // like "Saturday" - }, - timeFormat: { // for event elements - 'default': generateShortTimeFormat - }, - - displayEventEnd: { - month: false, - basicWeek: false, - 'default': true - }, - - // locale - isRTL: false, - defaultButtonText: { - prev: "prev", - next: "next", - prevYear: "prev year", - nextYear: "next year", - today: 'today', - month: 'month', - week: 'week', - day: 'day' - }, - - buttonIcons: { - prev: 'left-single-arrow', - next: 'right-single-arrow', - prevYear: 'left-double-arrow', - nextYear: 'right-double-arrow' - }, - - // jquery-ui theming - theme: false, - themeButtonIcons: { - prev: 'circle-triangle-w', - next: 'circle-triangle-e', - prevYear: 'seek-prev', - nextYear: 'seek-next' - }, - - dragOpacity: .75, - dragRevertDuration: 500, - dragScroll: true, - - //selectable: false, - unselectAuto: true, - - dropAccept: '*', - - eventLimit: false, - eventLimitText: 'more', - eventLimitClick: 'popover', - dayPopoverFormat: 'LL', - - handleWindowResize: true, - windowResizeDelay: 200 // milliseconds before a rerender happens - -}; - - -function generateShortTimeFormat(options, langData) { - return langData.longDateFormat('LT') - .replace(':mm', '(:mm)') - .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs - .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand -} - - -function generateWeekColumnFormat(options, langData) { - var format = langData.longDateFormat('L'); // for the format like "MM/DD/YYYY" - format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, ''); // strip the year off the edge, as well as other misc non-whitespace chars - if (options.isRTL) { - format += ' ddd'; // for RTL, add day-of-week to end - } - else { - format = 'ddd ' + format; // for LTR, add day-of-week to beginning - } - return format; -} - - -var langOptionHash = { - en: { - columnFormat: { - week: 'ddd M/D' // override for english. different from the generated default, which is MM/DD - }, - dayPopoverFormat: 'dddd, MMMM D' - } -}; - - -// right-to-left defaults -var rtlDefaults = { - header: { - left: 'next,prev today', - center: '', - right: 'title' - }, - buttonIcons: { - prev: 'right-single-arrow', - next: 'left-single-arrow', - prevYear: 'right-double-arrow', - nextYear: 'left-double-arrow' - }, - themeButtonIcons: { - prev: 'circle-triangle-e', - next: 'circle-triangle-w', - nextYear: 'seek-prev', - prevYear: 'seek-next' - } -}; - -;; - -var fc = $.fullCalendar = { version: "2.1.1" }; -var fcViews = fc.views = {}; - - -$.fn.fullCalendar = function(options) { - var args = Array.prototype.slice.call(arguments, 1); // for a possible method call - var res = this; // what this function will return (this jQuery object by default) - - this.each(function(i, _element) { // loop each DOM element involved - var element = $(_element); - var calendar = element.data('fullCalendar'); // get the existing calendar object (if any) - var singleRes; // the returned value of this single method call - - // a method call - if (typeof options === 'string') { - if (calendar && $.isFunction(calendar[options])) { - singleRes = calendar[options].apply(calendar, args); - if (!i) { - res = singleRes; // record the first method call result - } - if (options === 'destroy') { // for the destroy method, must remove Calendar object data - element.removeData('fullCalendar'); - } - } - } - // a new calendar initialization - else if (!calendar) { // don't initialize twice - calendar = new Calendar(element, options); - element.data('fullCalendar', calendar); - calendar.render(); - } - }); - - return res; -}; - - -// function for adding/overriding defaults -function setDefaults(d) { - mergeOptions(defaults, d); -} - - -// Recursively combines option hash-objects. -// Better than `$.extend(true, ...)` because arrays are not traversed/copied. -// -// called like: -// mergeOptions(target, obj1, obj2, ...) -// -function mergeOptions(target) { - - function mergeIntoTarget(name, value) { - if ($.isPlainObject(value) && $.isPlainObject(target[name]) && !isForcedAtomicOption(name)) { - // merge into a new object to avoid destruction - target[name] = mergeOptions({}, target[name], value); // combine. `value` object takes precedence - } - else if (value !== undefined) { // only use values that are set and not undefined - target[name] = value; - } - } - - for (var i=1; i").prependTo(element); - - header = new Header(t, options); - headerElement = header.render(); - if (headerElement) { - element.prepend(headerElement); - } - - changeView(options.defaultView); - - if (options.handleWindowResize) { - windowResizeProxy = debounce(windowResize, options.windowResizeDelay); // prevents rapid calls - $(window).resize(windowResizeProxy); - } - } - - - function destroy() { - - if (currentView) { - currentView.destroy(); - } - - header.destroy(); - content.remove(); - element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget'); - - $(window).unbind('resize', windowResizeProxy); - } - - - function elementVisible() { - return element.is(':visible'); - } - - - - // View Rendering - // ----------------------------------------------------------------------------------- - - - function changeView(viewName) { - renderView(0, viewName); - } - - - // Renders a view because of a date change, view-type change, or for the first time - function renderView(delta, viewName) { - ignoreWindowResize++; - - // if viewName is changing, destroy the old view - if (currentView && viewName && currentView.name !== viewName) { - header.deactivateButton(currentView.name); - freezeContentHeight(); // prevent a scroll jump when view element is removed - if (currentView.start) { // rendered before? - currentView.destroy(); - } - currentView.el.remove(); - currentView = null; - } - - // if viewName changed, or the view was never created, create a fresh view - if (!currentView && viewName) { - currentView = new fcViews[viewName](t); - currentView.el = $("
").appendTo(content); - header.activateButton(viewName); - } - - if (currentView) { - - // let the view determine what the delta means - if (delta) { - date = currentView.incrementDate(date, delta); - } - - // render or rerender the view - if ( - !currentView.start || // never rendered before - delta || // explicit date window change - !date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change - ) { - if (elementVisible()) { - - freezeContentHeight(); - if (currentView.start) { // rendered before? - currentView.destroy(); - } - currentView.render(date); - unfreezeContentHeight(); - - // need to do this after View::render, so dates are calculated - updateTitle(); - updateTodayButton(); - - getAndRenderEvents(); - } - } - } - - unfreezeContentHeight(); // undo any lone freezeContentHeight calls - ignoreWindowResize--; - } - - - - // Resizing - // ----------------------------------------------------------------------------------- - - - t.getSuggestedViewHeight = function() { - if (suggestedViewHeight === undefined) { - calcSize(); - } - return suggestedViewHeight; - }; - - - t.isHeightAuto = function() { - return options.contentHeight === 'auto' || options.height === 'auto'; - }; - - - function updateSize(shouldRecalc) { - if (elementVisible()) { - - if (shouldRecalc) { - _calcSize(); - } - - ignoreWindowResize++; - currentView.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto() - ignoreWindowResize--; - - return true; // signal success - } - } - - - function calcSize() { - if (elementVisible()) { - _calcSize(); - } - } - - - function _calcSize() { // assumes elementVisible - if (typeof options.contentHeight === 'number') { // exists and not 'auto' - suggestedViewHeight = options.contentHeight; - } - else if (typeof options.height === 'number') { // exists and not 'auto' - suggestedViewHeight = options.height - (headerElement ? headerElement.outerHeight(true) : 0); - } - else { - suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5)); - } - } - - - function windowResize(ev) { - if ( - !ignoreWindowResize && - ev.target === window && // so we don't process jqui "resize" events that have bubbled up - currentView.start // view has already been rendered - ) { - if (updateSize(true)) { - currentView.trigger('windowResize', _element); - } - } - } - - - - /* Event Fetching/Rendering - -----------------------------------------------------------------------------*/ - // TODO: going forward, most of this stuff should be directly handled by the view - - - function refetchEvents() { // can be called as an API method - destroyEvents(); // so that events are cleared before user starts waiting for AJAX - fetchAndRenderEvents(); - } - - - function renderEvents() { // destroys old events if previously rendered - if (elementVisible()) { - freezeContentHeight(); - currentView.destroyEvents(); // no performance cost if never rendered - currentView.renderEvents(events); - unfreezeContentHeight(); - } - } - - - function destroyEvents() { - freezeContentHeight(); - currentView.destroyEvents(); - unfreezeContentHeight(); - } - - - function getAndRenderEvents() { - if (!options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) { - fetchAndRenderEvents(); - } - else { - renderEvents(); - } - } - - - function fetchAndRenderEvents() { - fetchEvents(currentView.start, currentView.end); - // ... will call reportEvents - // ... which will call renderEvents - } - - - // called when event data arrives - function reportEvents(_events) { - events = _events; - renderEvents(); - } - - - // called when a single event's data has been changed - function reportEventChange() { - renderEvents(); - } - - - - /* Header Updating - -----------------------------------------------------------------------------*/ - - - function updateTitle() { - header.updateTitle(currentView.title); - } - - - function updateTodayButton() { - var now = t.getNow(); - if (now.isWithin(currentView.intervalStart, currentView.intervalEnd)) { - header.disableButton('today'); - } - else { - header.enableButton('today'); - } - } - - - - /* Selection - -----------------------------------------------------------------------------*/ - - - function select(start, end) { - - start = t.moment(start); - if (end) { - end = t.moment(end); - } - else if (start.hasTime()) { - end = start.clone().add(t.defaultTimedEventDuration); - } - else { - end = start.clone().add(t.defaultAllDayEventDuration); - } - - currentView.select(start, end); - } - - - function unselect() { // safe to be called before renderView - if (currentView) { - currentView.unselect(); - } - } - - - - /* Date - -----------------------------------------------------------------------------*/ - - - function prev() { - renderView(-1); - } - - - function next() { - renderView(1); - } - - - function prevYear() { - date.add(-1, 'years'); - renderView(); - } - - - function nextYear() { - date.add(1, 'years'); - renderView(); - } - - - function today() { - date = t.getNow(); - renderView(); - } - - - function gotoDate(dateInput) { - date = t.moment(dateInput); - renderView(); - } - - - function incrementDate(delta) { - date.add(moment.duration(delta)); - renderView(); - } - - - // Forces navigation to a view for the given date. - // `viewName` can be a specific view name or a generic one like "week" or "day". - function zoomTo(newDate, viewName) { - var viewStr; - var match; - - if (!viewName || fcViews[viewName] === undefined) { // a general view name, or "auto" - viewName = viewName || 'day'; - viewStr = header.getViewsWithButtons().join(' '); // space-separated string of all the views in the header - - // try to match a general view name, like "week", against a specific one, like "agendaWeek" - match = viewStr.match(new RegExp('\\w+' + capitaliseFirstLetter(viewName))); - - // fall back to the day view being used in the header - if (!match) { - match = viewStr.match(/\w+Day/); - } - - viewName = match ? match[0] : 'agendaDay'; // fall back to agendaDay - } - - date = newDate; - changeView(viewName); - } - - - function getDate() { - return date.clone(); - } - - - - /* Height "Freezing" - -----------------------------------------------------------------------------*/ - - - function freezeContentHeight() { - content.css({ - width: '100%', - height: content.height(), - overflow: 'hidden' - }); - } - - - function unfreezeContentHeight() { - content.css({ - width: '', - height: '', - overflow: '' - }); - } - - - - /* Misc - -----------------------------------------------------------------------------*/ - - - function getCalendar() { - return t; - } - - - function getView() { - return currentView; - } - - - function option(name, value) { - if (value === undefined) { - return options[name]; - } - if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') { - options[name] = value; - updateSize(true); // true = allow recalculation of height - } - } - - - function trigger(name, thisObj) { - if (options[name]) { - return options[name].apply( - thisObj || _element, - Array.prototype.slice.call(arguments, 2) - ); - } - } - -} - -;; - -/* Top toolbar area with buttons and title -----------------------------------------------------------------------------------------------------------------------*/ -// TODO: rename all header-related things to "toolbar" - -function Header(calendar, options) { - var t = this; - - // exports - t.render = render; - t.destroy = destroy; - t.updateTitle = updateTitle; - t.activateButton = activateButton; - t.deactivateButton = deactivateButton; - t.disableButton = disableButton; - t.enableButton = enableButton; - t.getViewsWithButtons = getViewsWithButtons; - - // locals - var el = $(); - var viewsWithButtons = []; - var tm; - - - function render() { - var sections = options.header; - - tm = options.theme ? 'ui' : 'fc'; - - if (sections) { - el = $("
") - .append(renderSection('left')) - .append(renderSection('right')) - .append(renderSection('center')) - .append('
'); - - return el; - } - } - - - function destroy() { - el.remove(); - } - - - function renderSection(position) { - var sectionEl = $('
'); - var buttonStr = options.header[position]; - - if (buttonStr) { - $.each(buttonStr.split(' '), function(i) { - var groupChildren = $(); - var isOnlyButtons = true; - var groupEl; - - $.each(this.split(','), function(j, buttonName) { - var buttonClick; - var themeIcon; - var normalIcon; - var defaultText; - var customText; - var innerHtml; - var classes; - var button; - - if (buttonName == 'title') { - groupChildren = groupChildren.add($('

 

')); // we always want it to take up height - isOnlyButtons = false; - } - else { - if (calendar[buttonName]) { // a calendar method - buttonClick = function() { - calendar[buttonName](); - }; - } - else if (fcViews[buttonName]) { // a view name - buttonClick = function() { - calendar.changeView(buttonName); - }; - viewsWithButtons.push(buttonName); - } - if (buttonClick) { - - // smartProperty allows different text per view button (ex: "Agenda Week" vs "Basic Week") - themeIcon = smartProperty(options.themeButtonIcons, buttonName); - normalIcon = smartProperty(options.buttonIcons, buttonName); - defaultText = smartProperty(options.defaultButtonText, buttonName); - customText = smartProperty(options.buttonText, buttonName); - - if (customText) { - innerHtml = htmlEscape(customText); - } - else if (themeIcon && options.theme) { - innerHtml = ""; - } - else if (normalIcon && !options.theme) { - innerHtml = ""; - } - else { - innerHtml = htmlEscape(defaultText || buttonName); - } - - classes = [ - 'fc-' + buttonName + '-button', - tm + '-button', - tm + '-state-default' - ]; - - button = $( // type="button" so that it doesn't submit a form - '' - ) - .click(function() { - // don't process clicks for disabled buttons - if (!button.hasClass(tm + '-state-disabled')) { - - buttonClick(); - - // after the click action, if the button becomes the "active" tab, or disabled, - // it should never have a hover class, so remove it now. - if ( - button.hasClass(tm + '-state-active') || - button.hasClass(tm + '-state-disabled') - ) { - button.removeClass(tm + '-state-hover'); - } - } - }) - .mousedown(function() { - // the *down* effect (mouse pressed in). - // only on buttons that are not the "active" tab, or disabled - button - .not('.' + tm + '-state-active') - .not('.' + tm + '-state-disabled') - .addClass(tm + '-state-down'); - }) - .mouseup(function() { - // undo the *down* effect - button.removeClass(tm + '-state-down'); - }) - .hover( - function() { - // the *hover* effect. - // only on buttons that are not the "active" tab, or disabled - button - .not('.' + tm + '-state-active') - .not('.' + tm + '-state-disabled') - .addClass(tm + '-state-hover'); - }, - function() { - // undo the *hover* effect - button - .removeClass(tm + '-state-hover') - .removeClass(tm + '-state-down'); // if mouseleave happens before mouseup - } - ); - - groupChildren = groupChildren.add(button); - } - } - }); - - if (isOnlyButtons) { - groupChildren - .first().addClass(tm + '-corner-left').end() - .last().addClass(tm + '-corner-right').end(); - } - - if (groupChildren.length > 1) { - groupEl = $('
'); - if (isOnlyButtons) { - groupEl.addClass('fc-button-group'); - } - groupEl.append(groupChildren); - sectionEl.append(groupEl); - } - else { - sectionEl.append(groupChildren); // 1 or 0 children - } - }); - } - - return sectionEl; - } - - - function updateTitle(text) { - el.find('h2').text(text); - } - - - function activateButton(buttonName) { - el.find('.fc-' + buttonName + '-button') - .addClass(tm + '-state-active'); - } - - - function deactivateButton(buttonName) { - el.find('.fc-' + buttonName + '-button') - .removeClass(tm + '-state-active'); - } - - - function disableButton(buttonName) { - el.find('.fc-' + buttonName + '-button') - .attr('disabled', 'disabled') - .addClass(tm + '-state-disabled'); - } - - - function enableButton(buttonName) { - el.find('.fc-' + buttonName + '-button') - .removeAttr('disabled') - .removeClass(tm + '-state-disabled'); - } - - - function getViewsWithButtons() { - return viewsWithButtons; - } - -} - -;; - -fc.sourceNormalizers = []; -fc.sourceFetchers = []; - -var ajaxDefaults = { - dataType: 'json', - cache: false -}; - -var eventGUID = 1; - - -function EventManager(options) { // assumed to be a calendar - var t = this; - - - // exports - t.isFetchNeeded = isFetchNeeded; - t.fetchEvents = fetchEvents; - t.addEventSource = addEventSource; - t.removeEventSource = removeEventSource; - t.updateEvent = updateEvent; - t.renderEvent = renderEvent; - t.removeEvents = removeEvents; - t.clientEvents = clientEvents; - t.mutateEvent = mutateEvent; - - - // imports - var trigger = t.trigger; - var getView = t.getView; - var reportEvents = t.reportEvents; - var getEventEnd = t.getEventEnd; - - - // locals - var stickySource = { events: [] }; - var sources = [ stickySource ]; - var rangeStart, rangeEnd; - var currentFetchID = 0; - var pendingSourceCnt = 0; - var loadingLevel = 0; - var cache = []; - - - $.each( - (options.events ? [ options.events ] : []).concat(options.eventSources || []), - function(i, sourceInput) { - var source = buildEventSource(sourceInput); - if (source) { - sources.push(source); - } - } - ); - - - - /* Fetching - -----------------------------------------------------------------------------*/ - - - function isFetchNeeded(start, end) { - return !rangeStart || // nothing has been fetched yet? - // or, a part of the new range is outside of the old range? (after normalizing) - start.clone().stripZone() < rangeStart.clone().stripZone() || - end.clone().stripZone() > rangeEnd.clone().stripZone(); - } - - - function fetchEvents(start, end) { - rangeStart = start; - rangeEnd = end; - cache = []; - var fetchID = ++currentFetchID; - var len = sources.length; - pendingSourceCnt = len; - for (var i=0; i cells, find the cell with the largest natural width and set the widths of all the -// cells to be that width. -// PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline -function matchCellWidths(els) { - var maxInnerWidth = 0; - - els.find('> *').each(function(i, innerEl) { - var innerWidth = $(innerEl).outerWidth(); - if (innerWidth > maxInnerWidth) { - maxInnerWidth = innerWidth; - } - }); - - maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance - - els.width(maxInnerWidth); - - return maxInnerWidth; -} - - -// Turns a container element into a scroller if its contents is taller than the allotted height. -// Returns true if the element is now a scroller, false otherwise. -// NOTE: this method is best because it takes weird zooming dimensions into account -function setPotentialScroller(containerEl, height) { - containerEl.height(height).addClass('fc-scroller'); - - // are scrollbars needed? - if (containerEl[0].scrollHeight - 1 > containerEl[0].clientHeight) { // !!! -1 because IE is often off-by-one :( - return true; - } - - unsetScroller(containerEl); // undo - return false; -} - - -// Takes an element that might have been a scroller, and turns it back into a normal element. -function unsetScroller(containerEl) { - containerEl.height('').removeClass('fc-scroller'); -} - - -/* General DOM Utilities -----------------------------------------------------------------------------------------------------------------------*/ - - -// borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51 -function getScrollParent(el) { - var position = el.css('position'), - scrollParent = el.parents().filter(function() { - var parent = $(this); - return (/(auto|scroll)/).test( - parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x') - ); - }).eq(0); - - return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent; -} - - -// Given a container element, return an object with the pixel values of the left/right scrollbars. -// Left scrollbars might occur on RTL browsers (IE maybe?) but I have not tested. -// PREREQUISITE: container element must have a single child with display:block -function getScrollbarWidths(container) { - var containerLeft = container.offset().left; - var containerRight = containerLeft + container.width(); - var inner = container.children(); - var innerLeft = inner.offset().left; - var innerRight = innerLeft + inner.outerWidth(); - - return { - left: innerLeft - containerLeft, - right: containerRight - innerRight - }; -} - - -// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac) -function isPrimaryMouseButton(ev) { - return ev.which == 1 && !ev.ctrlKey; -} - - -/* FullCalendar-specific Misc Utilities -----------------------------------------------------------------------------------------------------------------------*/ - - -// Creates a basic segment with the intersection of the two ranges. Returns undefined if no intersection. -// Expects all dates to be normalized to the same timezone beforehand. -function intersectionToSeg(subjectStart, subjectEnd, intervalStart, intervalEnd) { - var segStart, segEnd; - var isStart, isEnd; - - if (subjectEnd > intervalStart && subjectStart < intervalEnd) { // in bounds at all? - - if (subjectStart >= intervalStart) { - segStart = subjectStart.clone(); - isStart = true; - } - else { - segStart = intervalStart.clone(); - isStart = false; - } - - if (subjectEnd <= intervalEnd) { - segEnd = subjectEnd.clone(); - isEnd = true; - } - else { - segEnd = intervalEnd.clone(); - isEnd = false; - } - - return { - start: segStart, - end: segEnd, - isStart: isStart, - isEnd: isEnd - }; - } -} - - -function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object - obj = obj || {}; - if (obj[name] !== undefined) { - return obj[name]; - } - var parts = name.split(/(?=[A-Z])/), - i = parts.length - 1, res; - for (; i>=0; i--) { - res = obj[parts[i].toLowerCase()]; - if (res !== undefined) { - return res; - } - } - return obj['default']; -} - - -/* Date Utilities -----------------------------------------------------------------------------------------------------------------------*/ - -var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ]; - - -// Diffs the two moments into a Duration where full-days are recorded first, then the remaining time. -// Moments will have their timezones normalized. -function dayishDiff(a, b) { - return moment.duration({ - days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'), - ms: a.time() - b.time() - }); -} - - -function isNativeDate(input) { - return Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date; -} - - -function dateCompare(a, b) { // works with Moments and native Dates - return a - b; -} - - -/* General Utilities -----------------------------------------------------------------------------------------------------------------------*/ - -fc.applyAll = applyAll; // export - - -// Create an object that has the given prototype. Just like Object.create -function createObject(proto) { - var f = function() {}; - f.prototype = proto; - return new f(); -} - - -// Copies specifically-owned (non-protoype) properties of `b` onto `a`. -// FYI, $.extend would copy *all* properties of `b` onto `a`. -function extend(a, b) { - for (var i in b) { - if (b.hasOwnProperty(i)) { - a[i] = b[i]; - } - } -} - - -function applyAll(functions, thisObj, args) { - if ($.isFunction(functions)) { - functions = [ functions ]; - } - if (functions) { - var i; - var ret; - for (i=0; i/g, '>') - .replace(/'/g, ''') - .replace(/"/g, '"') - .replace(/\n/g, '
'); -} - - -function stripHtmlEntities(text) { - return text.replace(/&.*?;/g, ''); -} - - -function capitaliseFirstLetter(str) { - return str.charAt(0).toUpperCase() + str.slice(1); -} - - -// Returns a function, that, as long as it continues to be invoked, will not -// be triggered. The function will be called after it stops being called for -// N milliseconds. -// https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714 -function debounce(func, wait) { - var timeoutId; - var args; - var context; - var timestamp; // of most recent call - var later = function() { - var last = +new Date() - timestamp; - if (last < wait && last > 0) { - timeoutId = setTimeout(later, wait - last); - } - else { - timeoutId = null; - func.apply(context, args); - if (!timeoutId) { - context = args = null; - } - } - }; - - return function() { - context = this; - args = arguments; - timestamp = +new Date(); - if (!timeoutId) { - timeoutId = setTimeout(later, wait); - } - }; -} - -;; - -var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/; -var ambigTimeOrZoneRegex = - /^\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+)?)?)?)?)?$/; - - -// Creating -// ------------------------------------------------------------------------------------------------- - -// Creates a new moment, similar to the vanilla moment(...) constructor, but with -// extra features (ambiguous time, enhanced formatting). When gived an existing moment, -// it will function as a clone (and retain the zone of the moment). Anything else will -// result in a moment in the local zone. -fc.moment = function() { - return makeMoment(arguments); -}; - -// Sames as fc.moment, but forces the resulting moment to be in the UTC timezone. -fc.moment.utc = function() { - var mom = makeMoment(arguments, true); - - // Force it into UTC because makeMoment doesn't guarantee it. - if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone - mom.utc(); - } - - return mom; -}; - -// Same as fc.moment, but when given an ISO8601 string, the timezone offset is preserved. -// ISO8601 strings with no timezone offset will become ambiguously zoned. -fc.moment.parseZone = function() { - return makeMoment(arguments, true, true); -}; - -// Builds an FCMoment from args. When given an existing moment, it clones. When given a native -// Date, or called with no arguments (the current time), the resulting moment will be local. -// Anything else needs to be "parsed" (a string or an array), and will be affected by: -// parseAsUTC - if there is no zone information, should we parse the input in UTC? -// parseZone - if there is zone information, should we force the zone of the moment? -function makeMoment(args, parseAsUTC, parseZone) { - var input = args[0]; - var isSingleString = args.length == 1 && typeof input === 'string'; - var isAmbigTime; - var isAmbigZone; - var ambigMatch; - var output; // an object with fields for the new FCMoment object - - if (moment.isMoment(input)) { - output = moment.apply(null, args); // clone it - - // the ambig properties have not been preserved in the clone, so reassign them - if (input._ambigTime) { - output._ambigTime = true; - } - if (input._ambigZone) { - output._ambigZone = true; - } - } - else if (isNativeDate(input) || input === undefined) { - output = moment.apply(null, args); // will be local - } - else { // "parsing" is required - isAmbigTime = false; - isAmbigZone = false; - - if (isSingleString) { - if (ambigDateOfMonthRegex.test(input)) { - // accept strings like '2014-05', but convert to the first of the month - input += '-01'; - args = [ input ]; // for when we pass it on to moment's constructor - isAmbigTime = true; - isAmbigZone = true; - } - else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) { - isAmbigTime = !ambigMatch[5]; // no time part? - isAmbigZone = true; - } - } - else if ($.isArray(input)) { - // arrays have no timezone information, so assume ambiguous zone - isAmbigZone = true; - } - // otherwise, probably a string with a format - - if (parseAsUTC) { - output = moment.utc.apply(moment, args); - } - else { - output = moment.apply(null, args); - } - - if (isAmbigTime) { - output._ambigTime = true; - output._ambigZone = true; // ambiguous time always means ambiguous zone - } - else if (parseZone) { // let's record the inputted zone somehow - if (isAmbigZone) { - output._ambigZone = true; - } - else if (isSingleString) { - output.zone(input); // if not a valid zone, will assign UTC - } - } - } - - return new FCMoment(output); -} - -// Our subclass of Moment. -// Accepts an object with the internal Moment properties that should be copied over to -// `this` object (most likely another Moment object). The values in this data must not -// be referenced by anything else (two moments sharing a Date object for example). -function FCMoment(internalData) { - extend(this, internalData); -} - -// Chain the prototype to Moment's -FCMoment.prototype = createObject(moment.fn); - -// We need this because Moment's implementation won't create an FCMoment, -// nor will it copy over the ambig flags. -FCMoment.prototype.clone = function() { - return makeMoment([ this ]); -}; - - -// Time-of-day -// ------------------------------------------------------------------------------------------------- - -// GETTER -// Returns a Duration with the hours/minutes/seconds/ms values of the moment. -// If the moment has an ambiguous time, a duration of 00:00 will be returned. -// -// SETTER -// You can supply a Duration, a Moment, or a Duration-like argument. -// When setting the time, and the moment has an ambiguous time, it then becomes unambiguous. -FCMoment.prototype.time = function(time) { - if (time == null) { // getter - return moment.duration({ - hours: this.hours(), - minutes: this.minutes(), - seconds: this.seconds(), - milliseconds: this.milliseconds() - }); - } - else { // setter - - delete this._ambigTime; // mark that the moment now has a time - - if (!moment.isDuration(time) && !moment.isMoment(time)) { - time = moment.duration(time); - } - - // The day value should cause overflow (so 24 hours becomes 00:00:00 of next day). - // Only for Duration times, not Moment times. - var dayHours = 0; - if (moment.isDuration(time)) { - dayHours = Math.floor(time.asDays()) * 24; - } - - // We need to set the individual fields. - // Can't use startOf('day') then add duration. In case of DST at start of day. - return this.hours(dayHours + time.hours()) - .minutes(time.minutes()) - .seconds(time.seconds()) - .milliseconds(time.milliseconds()); - } -}; - -// Converts the moment to UTC, stripping out its time-of-day and timezone offset, -// but preserving its YMD. A moment with a stripped time will display no time -// nor timezone offset when .format() is called. -FCMoment.prototype.stripTime = function() { - var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array - - // set the internal UTC flag - moment.fn.utc.call(this); // call the original method, because we don't want to affect _ambigZone - - this.year(a[0]) // TODO: find a way to do this in one shot - .month(a[1]) - .date(a[2]) - .hours(0) - .minutes(0) - .seconds(0) - .milliseconds(0); - - // Mark the time as ambiguous. This needs to happen after the .utc() call, which calls .zone(), which - // clears all ambig flags. Same concept with the .year/month/date calls in the case of moment-timezone. - this._ambigTime = true; - this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset - - return this; // for chaining -}; - -// Returns if the moment has a non-ambiguous time (boolean) -FCMoment.prototype.hasTime = function() { - return !this._ambigTime; -}; - - -// Timezone -// ------------------------------------------------------------------------------------------------- - -// Converts the moment to UTC, stripping out its timezone offset, but preserving its -// YMD and time-of-day. A moment with a stripped timezone offset will display no -// timezone offset when .format() is called. -FCMoment.prototype.stripZone = function() { - var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array - var wasAmbigTime = this._ambigTime; - - moment.fn.utc.call(this); // set the internal UTC flag - - this.year(a[0]) // TODO: find a way to do this in one shot - .month(a[1]) - .date(a[2]) - .hours(a[3]) - .minutes(a[4]) - .seconds(a[5]) - .milliseconds(a[6]); - - if (wasAmbigTime) { - // the above call to .utc()/.zone() unfortunately clears the ambig flags, so reassign - this._ambigTime = true; - } - - // Mark the zone as ambiguous. This needs to happen after the .utc() call, which calls .zone(), which - // clears all ambig flags. Same concept with the .year/month/date calls in the case of moment-timezone. - this._ambigZone = true; - - return this; // for chaining -}; - -// Returns of the moment has a non-ambiguous timezone offset (boolean) -FCMoment.prototype.hasZone = function() { - return !this._ambigZone; -}; - -// this method implicitly marks a zone -FCMoment.prototype.zone = function(tzo) { - - if (tzo != null) { - // FYI, the delete statements need to be before the .zone() call or else chaos ensues - // for reasons I don't understand. - delete this._ambigTime; - delete this._ambigZone; - } - - return moment.fn.zone.apply(this, arguments); -}; - -// this method implicitly marks a zone -FCMoment.prototype.local = function() { - var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array - var wasAmbigZone = this._ambigZone; - - // will happen anyway via .local()/.zone(), but don't want to rely on internal implementation - delete this._ambigTime; - delete this._ambigZone; - - moment.fn.local.apply(this, arguments); - - if (wasAmbigZone) { - // If the moment was ambiguously zoned, the date fields were stored as UTC. - // We want to preserve these, but in local time. - this.year(a[0]) // TODO: find a way to do this in one shot - .month(a[1]) - .date(a[2]) - .hours(a[3]) - .minutes(a[4]) - .seconds(a[5]) - .milliseconds(a[6]); - } - - return this; // for chaining -}; - -// this method implicitly marks a zone -FCMoment.prototype.utc = function() { - - // will happen anyway via .local()/.zone(), but don't want to rely on internal implementation - delete this._ambigTime; - delete this._ambigZone; - - return moment.fn.utc.apply(this, arguments); -}; - - -// Formatting -// ------------------------------------------------------------------------------------------------- - -FCMoment.prototype.format = function() { - if (arguments[0]) { - return formatDate(this, arguments[0]); // our extended formatting - } - if (this._ambigTime) { - return momentFormat(this, 'YYYY-MM-DD'); - } - if (this._ambigZone) { - return momentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss'); - } - return momentFormat(this); // default moment original formatting -}; - -FCMoment.prototype.toISOString = function() { - if (this._ambigTime) { - return momentFormat(this, 'YYYY-MM-DD'); - } - if (this._ambigZone) { - return momentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss'); - } - return moment.fn.toISOString.apply(this, arguments); -}; - - -// Querying -// ------------------------------------------------------------------------------------------------- - -// Is the moment within the specified range? `end` is exclusive. -FCMoment.prototype.isWithin = function(start, end) { - var a = commonlyAmbiguate([ this, start, end ]); - return a[0] >= a[1] && a[0] < a[2]; -}; - -// When isSame is called with units, timezone ambiguity is normalized before the comparison happens. -// If no units are specified, the two moments must be identically the same, with matching ambig flags. -FCMoment.prototype.isSame = function(input, units) { - var a; - - if (units) { - a = commonlyAmbiguate([ this, input ], true); // normalize timezones but don't erase times - return moment.fn.isSame.call(a[0], a[1], units); - } - else { - input = fc.moment.parseZone(input); // normalize input - return moment.fn.isSame.call(this, input) && - Boolean(this._ambigTime) === Boolean(input._ambigTime) && - Boolean(this._ambigZone) === Boolean(input._ambigZone); - } -}; - -// Make these query methods work with ambiguous moments -$.each([ - 'isBefore', - 'isAfter' -], function(i, methodName) { - FCMoment.prototype[methodName] = function(input, units) { - var a = commonlyAmbiguate([ this, input ]); - return moment.fn[methodName].call(a[0], a[1], units); - }; -}); - - -// Misc Internals -// ------------------------------------------------------------------------------------------------- - -// given an array of moment-like inputs, return a parallel array w/ moments similarly ambiguated. -// for example, of one moment has ambig time, but not others, all moments will have their time stripped. -// set `preserveTime` to `true` to keep times, but only normalize zone ambiguity. -function commonlyAmbiguate(inputs, preserveTime) { - var outputs = []; - var anyAmbigTime = false; - var anyAmbigZone = false; - var i; - - for (i=0; i "MMMM D YYYY" - formatStr = localeData.longDateFormat(formatStr) || formatStr; - // BTW, this is not important for `formatDate` because it is impossible to put custom tokens - // or non-zero areas in Moment's localized format strings. - - separator = separator || ' - '; - - return formatRangeWithChunks( - date1, - date2, - getFormatStringChunks(formatStr), - separator, - isRTL - ); -} -fc.formatRange = formatRange; // expose - - -function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) { - var chunkStr; // the rendering of the chunk - var leftI; - var leftStr = ''; - var rightI; - var rightStr = ''; - var middleI; - var middleStr1 = ''; - var middleStr2 = ''; - var middleStr = ''; - - // Start at the leftmost side of the formatting string and continue until you hit a token - // that is not the same between dates. - for (leftI=0; leftIleftI; rightI--) { - chunkStr = formatSimilarChunk(date1, date2, chunks[rightI]); - if (chunkStr === false) { - break; - } - rightStr = chunkStr + rightStr; - } - - // The area in the middle is different for both of the dates. - // Collect them distinctly so we can jam them together later. - for (middleI=leftI; middleI<=rightI; middleI++) { - middleStr1 += formatDateWithChunk(date1, chunks[middleI]); - middleStr2 += formatDateWithChunk(date2, chunks[middleI]); - } - - if (middleStr1 || middleStr2) { - if (isRTL) { - middleStr = middleStr2 + separator + middleStr1; - } - else { - middleStr = middleStr1 + separator + middleStr2; - } - } - - return leftStr + middleStr + rightStr; -} - - -var similarUnitMap = { - Y: 'year', - M: 'month', - D: 'day', // day of month - d: 'day', // day of week - // prevents a separator between anything time-related... - A: 'second', // AM/PM - a: 'second', // am/pm - T: 'second', // A/P - t: 'second', // a/p - H: 'second', // hour (24) - h: 'second', // hour (12) - m: 'second', // minute - s: 'second' // second -}; -// TODO: week maybe? - - -// Given a formatting chunk, and given that both dates are similar in the regard the -// formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`. -function formatSimilarChunk(date1, date2, chunk) { - var token; - var unit; - - if (typeof chunk === 'string') { // a literal string - return chunk; - } - else if ((token = chunk.token)) { - unit = similarUnitMap[token.charAt(0)]; - // are the dates the same for this unit of measurement? - if (unit && date1.isSame(date2, unit)) { - return momentFormat(date1, token); // would be the same if we used `date2` - // BTW, don't support custom tokens - } - } - - return false; // the chunk is NOT the same for the two dates - // BTW, don't support splitting on non-zero areas -} - - -// Chunking Utils -// ------------------------------------------------------------------------------------------------- - - -var formatStringChunkCache = {}; - - -function getFormatStringChunks(formatStr) { - if (formatStr in formatStringChunkCache) { - return formatStringChunkCache[formatStr]; - } - return (formatStringChunkCache[formatStr] = chunkFormatString(formatStr)); -} - - -// Break the formatting string into an array of chunks -function chunkFormatString(formatStr) { - var chunks = []; - var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LT|(\w)\4*o?)|([^\w\[\(]+)/g; // TODO: more descrimination - var match; - - while ((match = chunker.exec(formatStr))) { - if (match[1]) { // a literal string inside [ ... ] - chunks.push(match[1]); - } - else if (match[2]) { // non-zero formatting inside ( ... ) - chunks.push({ maybe: chunkFormatString(match[2]) }); - } - else if (match[3]) { // a formatting token - chunks.push({ token: match[3] }); - } - else if (match[5]) { // an unenclosed literal string - chunks.push(match[5]); - } - } - - return chunks; -} - -;; - -/* A rectangular panel that is absolutely positioned over other content ------------------------------------------------------------------------------------------------------------------------- -Options: - - className (string) - - content (HTML string or jQuery element set) - - parentEl - - top - - left - - right (the x coord of where the right edge should be. not a "CSS" right) - - autoHide (boolean) - - show (callback) - - hide (callback) -*/ - -function Popover(options) { - this.options = options || {}; -} - - -Popover.prototype = { - - isHidden: true, - options: null, - el: null, // the container element for the popover. generated by this object - documentMousedownProxy: null, // document mousedown handler bound to `this` - margin: 10, // the space required between the popover and the edges of the scroll container - - - // Shows the popover on the specified position. Renders it if not already - show: function() { - if (this.isHidden) { - if (!this.el) { - this.render(); - } - this.el.show(); - this.position(); - this.isHidden = false; - this.trigger('show'); - } - }, - - - // Hides the popover, through CSS, but does not remove it from the DOM - hide: function() { - if (!this.isHidden) { - this.el.hide(); - this.isHidden = true; - this.trigger('hide'); - } - }, - - - // Creates `this.el` and renders content inside of it - render: function() { - var _this = this; - var options = this.options; - - this.el = $('
') - .addClass(options.className || '') - .css({ - // position initially to the top left to avoid creating scrollbars - top: 0, - left: 0 - }) - .append(options.content) - .appendTo(options.parentEl); - - // when a click happens on anything inside with a 'fc-close' className, hide the popover - this.el.on('click', '.fc-close', function() { - _this.hide(); - }); - - if (options.autoHide) { - $(document).on('mousedown', this.documentMousedownProxy = $.proxy(this, 'documentMousedown')); - } - }, - - - // Triggered when the user clicks *anywhere* in the document, for the autoHide feature - documentMousedown: function(ev) { - // only hide the popover if the click happened outside the popover - if (this.el && !$(ev.target).closest(this.el).length) { - this.hide(); - } - }, - - - // Hides and unregisters any handlers - destroy: function() { - this.hide(); - - if (this.el) { - this.el.remove(); - this.el = null; - } - - $(document).off('mousedown', this.documentMousedownProxy); - }, - - - // Positions the popover optimally, using the top/left/right options - position: function() { - var options = this.options; - var origin = this.el.offsetParent().offset(); - var width = this.el.outerWidth(); - var height = this.el.outerHeight(); - var windowEl = $(window); - var viewportEl = getScrollParent(this.el); - var viewportTop; - var viewportLeft; - var viewportOffset; - var top; // the "position" (not "offset") values for the popover - var left; // - - // compute top and left - top = options.top || 0; - if (options.left !== undefined) { - left = options.left; - } - else if (options.right !== undefined) { - left = options.right - width; // derive the left value from the right value - } - else { - left = 0; - } - - if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result - viewportEl = windowEl; - viewportTop = 0; // the window is always at the top left - viewportLeft = 0; // (and .offset() won't work if called here) - } - else { - viewportOffset = viewportEl.offset(); - viewportTop = viewportOffset.top; - viewportLeft = viewportOffset.left; - } - - // if the window is scrolled, it causes the visible area to be further down - viewportTop += windowEl.scrollTop(); - viewportLeft += windowEl.scrollLeft(); - - // constrain to the view port. if constrained by two edges, give precedence to top/left - if (options.viewportConstrain !== false) { - top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin); - top = Math.max(top, viewportTop + this.margin); - left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin); - left = Math.max(left, viewportLeft + this.margin); - } - - this.el.css({ - top: top - origin.top, - left: left - origin.left - }); - }, - - - // Triggers a callback. Calls a function in the option hash of the same name. - // Arguments beyond the first `name` are forwarded on. - // TODO: better code reuse for this. Repeat code - trigger: function(name) { - if (this.options[name]) { - this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); - } - } - -}; - -;; - -/* A "coordinate map" converts pixel coordinates into an associated cell, which has an associated date ------------------------------------------------------------------------------------------------------------------------- -Common interface: - - CoordMap.prototype = { - build: function() {}, - getCell: function(x, y) {} - }; - -*/ - -/* Coordinate map for a grid component -----------------------------------------------------------------------------------------------------------------------*/ - -function GridCoordMap(grid) { - this.grid = grid; -} - - -GridCoordMap.prototype = { - - grid: null, // reference to the Grid - rows: null, // the top-to-bottom y coordinates. including the bottom of the last item - cols: null, // the left-to-right x coordinates. including the right of the last item - - containerEl: null, // container element that all coordinates are constrained to. optionally assigned - minX: null, - maxX: null, // exclusive - minY: null, - maxY: null, // exclusive - - - // Queries the grid for the coordinates of all the cells - build: function() { - this.grid.buildCoords( - this.rows = [], - this.cols = [] - ); - this.computeBounds(); - }, - - - // Given a coordinate of the document, gets the associated cell. If no cell is underneath, returns null - getCell: function(x, y) { - var cell = null; - var rows = this.rows; - var cols = this.cols; - var r = -1; - var c = -1; - var i; - - if (this.inBounds(x, y)) { - - for (i = 0; i < rows.length; i++) { - if (y >= rows[i][0] && y < rows[i][1]) { - r = i; - break; - } - } - - for (i = 0; i < cols.length; i++) { - if (x >= cols[i][0] && x < cols[i][1]) { - c = i; - break; - } - } - - if (r >= 0 && c >= 0) { - cell = { row: r, col: c }; - cell.grid = this.grid; - cell.date = this.grid.getCellDate(cell); - } - } - - return cell; - }, - - - // If there is a containerEl, compute the bounds into min/max values - computeBounds: function() { - var containerOffset; - - if (this.containerEl) { - containerOffset = this.containerEl.offset(); - this.minX = containerOffset.left; - this.maxX = containerOffset.left + this.containerEl.outerWidth(); - this.minY = containerOffset.top; - this.maxY = containerOffset.top + this.containerEl.outerHeight(); - } - }, - - - // Determines if the given coordinates are in bounds. If no `containerEl`, always true - inBounds: function(x, y) { - if (this.containerEl) { - return x >= this.minX && x < this.maxX && y >= this.minY && y < this.maxY; - } - return true; - } - -}; - - -/* Coordinate map that is a combination of multiple other coordinate maps -----------------------------------------------------------------------------------------------------------------------*/ - -function ComboCoordMap(coordMaps) { - this.coordMaps = coordMaps; -} - - -ComboCoordMap.prototype = { - - coordMaps: null, // an array of CoordMaps - - - // Builds all coordMaps - build: function() { - var coordMaps = this.coordMaps; - var i; - - for (i = 0; i < coordMaps.length; i++) { - coordMaps[i].build(); - } - }, - - - // Queries all coordMaps for the cell underneath the given coordinates, returning the first result - getCell: function(x, y) { - var coordMaps = this.coordMaps; - var cell = null; - var i; - - for (i = 0; i < coordMaps.length && !cell; i++) { - cell = coordMaps[i].getCell(x, y); - } - - return cell; - } - -}; -;; - -/* Tracks mouse movements over a CoordMap and raises events about which cell the mouse is over. -----------------------------------------------------------------------------------------------------------------------*/ -// TODO: implement scrolling - -function DragListener(coordMap, options) { - this.coordMap = coordMap; - this.options = options || {}; -} - - -DragListener.prototype = { - - coordMap: null, - options: null, - - isListening: false, - isDragging: false, - - // the cell/date the mouse was over when listening started - origCell: null, - origDate: null, - - // the cell/date the mouse is over - cell: null, - date: null, - - // coordinates of the initial mousedown - mouseX0: null, - mouseY0: null, - - // handler attached to the document, bound to the DragListener's `this` - mousemoveProxy: null, - mouseupProxy: null, - - scrollEl: null, - scrollBounds: null, // { top, bottom, left, right } - scrollTopVel: null, // pixels per second - scrollLeftVel: null, // pixels per second - scrollIntervalId: null, // ID of setTimeout for scrolling animation loop - scrollHandlerProxy: null, // this-scoped function for handling when scrollEl is scrolled - - scrollSensitivity: 30, // pixels from edge for scrolling to start - scrollSpeed: 200, // pixels per second, at maximum speed - scrollIntervalMs: 50, // millisecond wait between scroll increment - - - // Call this when the user does a mousedown. Will probably lead to startListening - mousedown: function(ev) { - if (isPrimaryMouseButton(ev)) { - - ev.preventDefault(); // prevents native selection in most browsers - - this.startListening(ev); - - // start the drag immediately if there is no minimum distance for a drag start - if (!this.options.distance) { - this.startDrag(ev); - } - } - }, - - - // Call this to start tracking mouse movements - startListening: function(ev) { - var scrollParent; - var cell; - - if (!this.isListening) { - - // grab scroll container and attach handler - if (ev && this.options.scroll) { - scrollParent = getScrollParent($(ev.target)); - if (!scrollParent.is(window) && !scrollParent.is(document)) { - this.scrollEl = scrollParent; - - // scope to `this`, and use `debounce` to make sure rapid calls don't happen - this.scrollHandlerProxy = debounce($.proxy(this, 'scrollHandler'), 100); - this.scrollEl.on('scroll', this.scrollHandlerProxy); - } - } - - this.computeCoords(); // relies on `scrollEl` - - // get info on the initial cell, date, and coordinates - if (ev) { - cell = this.getCell(ev); - this.origCell = cell; - this.origDate = cell ? cell.date : null; - - this.mouseX0 = ev.pageX; - this.mouseY0 = ev.pageY; - } - - $(document) - .on('mousemove', this.mousemoveProxy = $.proxy(this, 'mousemove')) - .on('mouseup', this.mouseupProxy = $.proxy(this, 'mouseup')) - .on('selectstart', this.preventDefault); // prevents native selection in IE<=8 - - this.isListening = true; - this.trigger('listenStart', ev); - } - }, - - - // Recomputes the drag-critical positions of elements - computeCoords: function() { - this.coordMap.build(); - this.computeScrollBounds(); - }, - - - // Called when the user moves the mouse - mousemove: function(ev) { - var minDistance; - var distanceSq; // current distance from mouseX0/mouseY0, squared - - if (!this.isDragging) { // if not already dragging... - // then start the drag if the minimum distance criteria is met - minDistance = this.options.distance || 1; - distanceSq = Math.pow(ev.pageX - this.mouseX0, 2) + Math.pow(ev.pageY - this.mouseY0, 2); - if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem - this.startDrag(ev); - } - } - - if (this.isDragging) { - this.drag(ev); // report a drag, even if this mousemove initiated the drag - } - }, - - - // Call this to initiate a legitimate drag. - // This function is called internally from this class, but can also be called explicitly from outside - startDrag: function(ev) { - var cell; - - if (!this.isListening) { // startDrag must have manually initiated - this.startListening(); - } - - if (!this.isDragging) { - this.isDragging = true; - this.trigger('dragStart', ev); - - // report the initial cell the mouse is over - cell = this.getCell(ev); - if (cell) { - this.cellOver(cell, true); - } - } - }, - - - // Called while the mouse is being moved and when we know a legitimate drag is taking place - drag: function(ev) { - var cell; - - if (this.isDragging) { - cell = this.getCell(ev); - - if (!isCellsEqual(cell, this.cell)) { // a different cell than before? - if (this.cell) { - this.cellOut(); - } - if (cell) { - this.cellOver(cell); - } - } - - this.dragScroll(ev); // will possibly cause scrolling - } - }, - - - // Called when a the mouse has just moved over a new cell - cellOver: function(cell) { - this.cell = cell; - this.date = cell.date; - this.trigger('cellOver', cell, cell.date); - }, - - - // Called when the mouse has just moved out of a cell - cellOut: function() { - if (this.cell) { - this.trigger('cellOut', this.cell); - this.cell = null; - this.date = null; - } - }, - - - // Called when the user does a mouseup - mouseup: function(ev) { - this.stopDrag(ev); - this.stopListening(ev); - }, - - - // Called when the drag is over. Will not cause listening to stop however. - // A concluding 'cellOut' event will NOT be triggered. - stopDrag: function(ev) { - if (this.isDragging) { - this.stopScrolling(); - this.trigger('dragStop', ev); - this.isDragging = false; - } - }, - - - // Call this to stop listening to the user's mouse events - stopListening: function(ev) { - if (this.isListening) { - - // remove the scroll handler if there is a scrollEl - if (this.scrollEl) { - this.scrollEl.off('scroll', this.scrollHandlerProxy); - this.scrollHandlerProxy = null; - } - - $(document) - .off('mousemove', this.mousemoveProxy) - .off('mouseup', this.mouseupProxy) - .off('selectstart', this.preventDefault); - - this.mousemoveProxy = null; - this.mouseupProxy = null; - - this.isListening = false; - this.trigger('listenStop', ev); - - this.origCell = this.cell = null; - this.origDate = this.date = null; - } - }, - - - // Gets the cell underneath the coordinates for the given mouse event - getCell: function(ev) { - return this.coordMap.getCell(ev.pageX, ev.pageY); - }, - - - // Triggers a callback. Calls a function in the option hash of the same name. - // Arguments beyond the first `name` are forwarded on. - trigger: function(name) { - if (this.options[name]) { - this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); - } - }, - - - // Stops a given mouse event from doing it's native browser action. In our case, text selection. - preventDefault: function(ev) { - ev.preventDefault(); - }, - - - /* Scrolling - ------------------------------------------------------------------------------------------------------------------*/ - - - // Computes and stores the bounding rectangle of scrollEl - computeScrollBounds: function() { - var el = this.scrollEl; - var offset; - - if (el) { - offset = el.offset(); - this.scrollBounds = { - top: offset.top, - left: offset.left, - bottom: offset.top + el.outerHeight(), - right: offset.left + el.outerWidth() - }; - } - }, - - - // Called when the dragging is in progress and scrolling should be updated - dragScroll: function(ev) { - var sensitivity = this.scrollSensitivity; - var bounds = this.scrollBounds; - var topCloseness, bottomCloseness; - var leftCloseness, rightCloseness; - var topVel = 0; - var leftVel = 0; - - if (bounds) { // only scroll if scrollEl exists - - // compute closeness to edges. valid range is from 0.0 - 1.0 - topCloseness = (sensitivity - (ev.pageY - bounds.top)) / sensitivity; - bottomCloseness = (sensitivity - (bounds.bottom - ev.pageY)) / sensitivity; - leftCloseness = (sensitivity - (ev.pageX - bounds.left)) / sensitivity; - rightCloseness = (sensitivity - (bounds.right - ev.pageX)) / sensitivity; - - // translate vertical closeness into velocity. - // mouse must be completely in bounds for velocity to happen. - if (topCloseness >= 0 && topCloseness <= 1) { - topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up - } - else if (bottomCloseness >= 0 && bottomCloseness <= 1) { - topVel = bottomCloseness * this.scrollSpeed; - } - - // translate horizontal closeness into velocity - if (leftCloseness >= 0 && leftCloseness <= 1) { - leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left - } - else if (rightCloseness >= 0 && rightCloseness <= 1) { - leftVel = rightCloseness * this.scrollSpeed; - } - } - - this.setScrollVel(topVel, leftVel); - }, - - - // Sets the speed-of-scrolling for the scrollEl - setScrollVel: function(topVel, leftVel) { - - this.scrollTopVel = topVel; - this.scrollLeftVel = leftVel; - - this.constrainScrollVel(); // massages into realistic values - - // if there is non-zero velocity, and an animation loop hasn't already started, then START - if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) { - this.scrollIntervalId = setInterval( - $.proxy(this, 'scrollIntervalFunc'), // scope to `this` - this.scrollIntervalMs - ); - } - }, - - - // Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way - constrainScrollVel: function() { - var el = this.scrollEl; - - if (this.scrollTopVel < 0) { // scrolling up? - if (el.scrollTop() <= 0) { // already scrolled all the way up? - this.scrollTopVel = 0; - } - } - else if (this.scrollTopVel > 0) { // scrolling down? - if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) { // already scrolled all the way down? - this.scrollTopVel = 0; - } - } - - if (this.scrollLeftVel < 0) { // scrolling left? - if (el.scrollLeft() <= 0) { // already scrolled all the left? - this.scrollLeftVel = 0; - } - } - else if (this.scrollLeftVel > 0) { // scrolling right? - if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) { // already scrolled all the way right? - this.scrollLeftVel = 0; - } - } - }, - - - // This function gets called during every iteration of the scrolling animation loop - scrollIntervalFunc: function() { - var el = this.scrollEl; - var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by - - // change the value of scrollEl's scroll - if (this.scrollTopVel) { - el.scrollTop(el.scrollTop() + this.scrollTopVel * frac); - } - if (this.scrollLeftVel) { - el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac); - } - - this.constrainScrollVel(); // since the scroll values changed, recompute the velocities - - // if scrolled all the way, which causes the vels to be zero, stop the animation loop - if (!this.scrollTopVel && !this.scrollLeftVel) { - this.stopScrolling(); - } - }, - - - // Kills any existing scrolling animation loop - stopScrolling: function() { - if (this.scrollIntervalId) { - clearInterval(this.scrollIntervalId); - this.scrollIntervalId = null; - - // when all done with scrolling, recompute positions since they probably changed - this.computeCoords(); - } - }, - - - // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce) - scrollHandler: function() { - // recompute all coordinates, but *only* if this is *not* part of our scrolling animation - if (!this.scrollIntervalId) { - this.computeCoords(); - } - } - -}; - - -// Returns `true` if the cells are identically equal. `false` otherwise. -// They must have the same row, col, and be from the same grid. -// Two null values will be considered equal, as two "out of the grid" states are the same. -function isCellsEqual(cell1, cell2) { - - if (!cell1 && !cell2) { - return true; - } - - if (cell1 && cell2) { - return cell1.grid === cell2.grid && - cell1.row === cell2.row && - cell1.col === cell2.col; - } - - return false; -} - -;; - -/* Creates a clone of an element and lets it track the mouse as it moves -----------------------------------------------------------------------------------------------------------------------*/ - -function MouseFollower(sourceEl, options) { - this.options = options = options || {}; - this.sourceEl = sourceEl; - this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent -} - - -MouseFollower.prototype = { - - options: null, - - sourceEl: null, // the element that will be cloned and made to look like it is dragging - el: null, // the clone of `sourceEl` that will track the mouse - parentEl: null, // the element that `el` (the clone) will be attached to - - // the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl - top0: null, - left0: null, - - // the initial position of the mouse - mouseY0: null, - mouseX0: null, - - // the number of pixels the mouse has moved from its initial position - topDelta: null, - leftDelta: null, - - mousemoveProxy: null, // document mousemove handler, bound to the MouseFollower's `this` - - isFollowing: false, - isHidden: false, - isAnimating: false, // doing the revert animation? - - - // Causes the element to start following the mouse - start: function(ev) { - if (!this.isFollowing) { - this.isFollowing = true; - - this.mouseY0 = ev.pageY; - this.mouseX0 = ev.pageX; - this.topDelta = 0; - this.leftDelta = 0; - - if (!this.isHidden) { - this.updatePosition(); - } - - $(document).on('mousemove', this.mousemoveProxy = $.proxy(this, 'mousemove')); - } - }, - - - // Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position. - // `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately. - stop: function(shouldRevert, callback) { - var _this = this; - var revertDuration = this.options.revertDuration; - - function complete() { - this.isAnimating = false; - _this.destroyEl(); - - this.top0 = this.left0 = null; // reset state for future updatePosition calls - - if (callback) { - callback(); - } - } - - if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time - this.isFollowing = false; - - $(document).off('mousemove', this.mousemoveProxy); - - if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation? - this.isAnimating = true; - this.el.animate({ - top: this.top0, - left: this.left0 - }, { - duration: revertDuration, - complete: complete - }); - } - else { - complete(); - } - } - }, - - - // Gets the tracking element. Create it if necessary - getEl: function() { - var el = this.el; - - if (!el) { - this.sourceEl.width(); // hack to force IE8 to compute correct bounding box - el = this.el = this.sourceEl.clone() - .css({ - position: 'absolute', - visibility: '', // in case original element was hidden (commonly through hideEvents()) - display: this.isHidden ? 'none' : '', // for when initially hidden - margin: 0, - right: 'auto', // erase and set width instead - bottom: 'auto', // erase and set height instead - width: this.sourceEl.width(), // explicit height in case there was a 'right' value - height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value - opacity: this.options.opacity || '', - zIndex: this.options.zIndex - }) - .appendTo(this.parentEl); - } - - return el; - }, - - - // Removes the tracking element if it has already been created - destroyEl: function() { - if (this.el) { - this.el.remove(); - this.el = null; - } - }, - - - // Update the CSS position of the tracking element - updatePosition: function() { - var sourceOffset; - var origin; - - this.getEl(); // ensure this.el - - // make sure origin info was computed - if (this.top0 === null) { - this.sourceEl.width(); // hack to force IE8 to compute correct bounding box - sourceOffset = this.sourceEl.offset(); - origin = this.el.offsetParent().offset(); - this.top0 = sourceOffset.top - origin.top; - this.left0 = sourceOffset.left - origin.left; - } - - this.el.css({ - top: this.top0 + this.topDelta, - left: this.left0 + this.leftDelta - }); - }, - - - // Gets called when the user moves the mouse - mousemove: function(ev) { - this.topDelta = ev.pageY - this.mouseY0; - this.leftDelta = ev.pageX - this.mouseX0; - - if (!this.isHidden) { - this.updatePosition(); - } - }, - - - // Temporarily makes the tracking element invisible. Can be called before following starts - hide: function() { - if (!this.isHidden) { - this.isHidden = true; - if (this.el) { - this.el.hide(); - } - } - }, - - - // Show the tracking element after it has been temporarily hidden - show: function() { - if (this.isHidden) { - this.isHidden = false; - this.updatePosition(); - this.getEl().show(); - } - } - -}; - -;; - -/* A utility class for rendering
', // plain default HTML used for a cell when no other is available - - - // Renders the HTML for a row, leveraging custom cell-HTML-renderers based on the `rowType`. - // Also applies the "intro" and "outro" cells, which are specified by the subclass and views. - // `row` is an optional row number. - rowHtml: function(rowType, row) { - var view = this.view; - var renderCell = this.getHtmlRenderer('cell', rowType); - var cellHtml = ''; - var col; - var date; - - row = row || 0; - - for (col = 0; col < view.colCnt; col++) { - date = view.cellToDate(row, col); - cellHtml += renderCell(row, col, date); - } - - cellHtml = this.bookendCells(cellHtml, rowType, row); // apply intro and outro - - return '
's or a jQuery
' + - '' + - this.rowHtml('head') + // leverages RowRenderer - '' + - '
' + - '
'; - }, - - - // Used by the `headHtml` method, via RowRenderer, for rendering the HTML of a day-of-week header cell - headCellHtml: function(row, col, date) { - var view = this.view; - var calendar = view.calendar; - var colFormat = view.opt('columnFormat'); - - return '' + - '' + - htmlEscape(calendar.formatDate(date, colFormat)) + - ''; - }, - - - // Renders the HTML for a single-day background cell - bgCellHtml: function(row, col, date) { - var view = this.view; - var classes = this.getDayClasses(date); - - classes.unshift('fc-day', view.widgetContentClass); - - return ''; - }, - - - // Computes HTML classNames for a single-day cell - getDayClasses: function(date) { - var view = this.view; - var today = view.calendar.getNow().stripTime(); - var classes = [ 'fc-' + dayIDs[date.day()] ]; - - if ( - view.name === 'month' && - date.month() != view.intervalStart.month() - ) { - classes.push('fc-other-month'); - } - - if (date.isSame(today, 'day')) { - classes.push( - 'fc-today', - view.highlightStateClass - ); - } - else if (date < today) { - classes.push('fc-past'); - } - else { - classes.push('fc-future'); - } - - return classes; - } - -}); - -;; - -/* Event-rendering and event-interaction methods for the abstract Grid class -----------------------------------------------------------------------------------------------------------------------*/ - -$.extend(Grid.prototype, { - - mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing - isDraggingSeg: false, // is a segment being dragged? boolean - isResizingSeg: false, // is a segment being resized? boolean - - - // Renders the given events onto the grid - renderEvents: function(events) { - // subclasses must implement - }, - - - // Retrieves all rendered segment objects in this grid - getSegs: function() { - // subclasses must implement - }, - - - // Unrenders all events. Subclasses should implement, calling this super-method first. - destroyEvents: function() { - this.triggerSegMouseout(); // trigger an eventMouseout if user's mouse is over an event - }, - - - // Renders a `el` property for each seg, and only returns segments that successfully rendered - renderSegs: function(segs, disableResizing) { - var view = this.view; - var html = ''; - var renderedSegs = []; - var i; - - // build a large concatenation of event segment HTML - for (i = 0; i < segs.length; i++) { - html += this.renderSegHtml(segs[i], disableResizing); - } - - // Grab individual elements from the combined HTML string. Use each as the default rendering. - // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false. - $(html).each(function(i, node) { - var seg = segs[i]; - var el = view.resolveEventEl(seg.event, $(node)); - if (el) { - el.data('fc-seg', seg); // used by handlers - seg.el = el; - renderedSegs.push(seg); - } - }); - - return renderedSegs; - }, - - - // Generates the HTML for the default rendering of a segment - renderSegHtml: function(seg, disableResizing) { - // subclasses must implement - }, - - - // Converts an array of event objects into an array of segment objects - eventsToSegs: function(events, intervalStart, intervalEnd) { - var _this = this; - - return $.map(events, function(event) { - return _this.eventToSegs(event, intervalStart, intervalEnd); // $.map flattens all returned arrays together - }); - }, - - - // Slices a single event into an array of event segments. - // When `intervalStart` and `intervalEnd` are specified, intersect the events with that interval. - // Otherwise, let the subclass decide how it wants to slice the segments over the grid. - eventToSegs: function(event, intervalStart, intervalEnd) { - var eventStart = event.start.clone().stripZone(); // normalize - var eventEnd = this.view.calendar.getEventEnd(event).stripZone(); // compute (if necessary) and normalize - var segs; - var i, seg; - - if (intervalStart && intervalEnd) { - seg = intersectionToSeg(eventStart, eventEnd, intervalStart, intervalEnd); - segs = seg ? [ seg ] : []; - } - else { - segs = this.rangeToSegs(eventStart, eventEnd); // defined by the subclass - } - - // assign extra event-related properties to the segment objects - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - seg.event = event; - seg.eventStartMS = +eventStart; - seg.eventDurationMS = eventEnd - eventStart; - } - - return segs; - }, - - - /* Handlers - ------------------------------------------------------------------------------------------------------------------*/ - - - // Attaches event-element-related handlers to the container element and leverage bubbling - bindSegHandlers: function() { - var _this = this; - var view = this.view; - - $.each( - { - mouseenter: function(seg, ev) { - _this.triggerSegMouseover(seg, ev); - }, - mouseleave: function(seg, ev) { - _this.triggerSegMouseout(seg, ev); - }, - click: function(seg, ev) { - return view.trigger('eventClick', this, seg.event, ev); // can return `false` to cancel - }, - mousedown: function(seg, ev) { - if ($(ev.target).is('.fc-resizer') && view.isEventResizable(seg.event)) { - _this.segResizeMousedown(seg, ev); - } - else if (view.isEventDraggable(seg.event)) { - _this.segDragMousedown(seg, ev); - } - } - }, - function(name, func) { - // attach the handler to the container element and only listen for real event elements via bubbling - _this.el.on(name, '.fc-event-container > *', function(ev) { - var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents - - // only call the handlers if there is not a drag/resize in progress - if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) { - return func.call(this, seg, ev); // `this` will be the event element - } - }); - } - ); - }, - - - // Updates internal state and triggers handlers for when an event element is moused over - triggerSegMouseover: function(seg, ev) { - if (!this.mousedOverSeg) { - this.mousedOverSeg = seg; - this.view.trigger('eventMouseover', seg.el[0], seg.event, ev); - } - }, - - - // Updates internal state and triggers handlers for when an event element is moused out. - // Can be given no arguments, in which case it will mouseout the segment that was previously moused over. - triggerSegMouseout: function(seg, ev) { - ev = ev || {}; // if given no args, make a mock mouse event - - if (this.mousedOverSeg) { - seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment - this.mousedOverSeg = null; - this.view.trigger('eventMouseout', seg.el[0], seg.event, ev); - } - }, - - - /* Dragging - ------------------------------------------------------------------------------------------------------------------*/ - - - // Called when the user does a mousedown on an event, which might lead to dragging. - // Generic enough to work with any type of Grid. - segDragMousedown: function(seg, ev) { - var _this = this; - var view = this.view; - var el = seg.el; - var event = seg.event; - var newStart, newEnd; - - // A clone of the original element that will move with the mouse - var mouseFollower = new MouseFollower(seg.el, { - parentEl: view.el, - opacity: view.opt('dragOpacity'), - revertDuration: view.opt('dragRevertDuration'), - zIndex: 2 // one above the .fc-view - }); - - // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents - // of the view. - var dragListener = new DragListener(view.coordMap, { - distance: 5, - scroll: view.opt('dragScroll'), - listenStart: function(ev) { - mouseFollower.hide(); // don't show until we know this is a real drag - mouseFollower.start(ev); - }, - dragStart: function(ev) { - _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported - _this.isDraggingSeg = true; - view.hideEvent(event); // hide all event segments. our mouseFollower will take over - view.trigger('eventDragStart', el[0], event, ev, {}); // last argument is jqui dummy - }, - cellOver: function(cell, date) { - var origDate = seg.cellDate || dragListener.origDate; - var res = _this.computeDraggedEventDates(seg, origDate, date); - newStart = res.start; - newEnd = res.end; - - if (view.renderDrag(newStart, newEnd, seg)) { // have the view render a visual indication - mouseFollower.hide(); // if the view is already using a mock event "helper", hide our own - } - else { - mouseFollower.show(); - } - }, - cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells - newStart = null; - view.destroyDrag(); // unrender whatever was done in view.renderDrag - mouseFollower.show(); // show in case we are moving out of all cells - }, - dragStop: function(ev) { - var hasChanged = newStart && !newStart.isSame(event.start); - - // do revert animation if hasn't changed. calls a callback when finished (whether animation or not) - mouseFollower.stop(!hasChanged, function() { - _this.isDraggingSeg = false; - view.destroyDrag(); - view.showEvent(event); - view.trigger('eventDragStop', el[0], event, ev, {}); // last argument is jqui dummy - - if (hasChanged) { - view.eventDrop(el[0], event, newStart, ev); // will rerender all events... - } - }); - }, - listenStop: function() { - mouseFollower.stop(); // put in listenStop in case there was a mousedown but the drag never started - } - }); - - dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart - }, - - - // Given a segment, the dates where a drag began and ended, calculates the Event Object's new start and end dates - computeDraggedEventDates: function(seg, dragStartDate, dropDate) { - var view = this.view; - var event = seg.event; - var start = event.start; - var end = view.calendar.getEventEnd(event); - var delta; - var newStart; - var newEnd; - - if (dropDate.hasTime() === dragStartDate.hasTime()) { - delta = dayishDiff(dropDate, dragStartDate); - newStart = start.clone().add(delta); - if (event.end === null) { // do we need to compute an end? - newEnd = null; - } - else { - newEnd = end.clone().add(delta); - } - } - else { - // if switching from day <-> timed, start should be reset to the dropped date, and the end cleared - newStart = dropDate; - newEnd = null; // end should be cleared - } - - return { start: newStart, end: newEnd }; - }, - - - /* Resizing - ------------------------------------------------------------------------------------------------------------------*/ - - - // Called when the user does a mousedown on an event's resizer, which might lead to resizing. - // Generic enough to work with any type of Grid. - segResizeMousedown: function(seg, ev) { - var _this = this; - var view = this.view; - var el = seg.el; - var event = seg.event; - var start = event.start; - var end = view.calendar.getEventEnd(event); - var newEnd = null; - var dragListener; - - function destroy() { // resets the rendering - _this.destroyResize(); - view.showEvent(event); - } - - // Tracks mouse movement over the *grid's* coordinate map - dragListener = new DragListener(this.coordMap, { - distance: 5, - scroll: view.opt('dragScroll'), - dragStart: function(ev) { - _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported - _this.isResizingSeg = true; - view.trigger('eventResizeStart', el[0], event, ev, {}); // last argument is jqui dummy - }, - cellOver: function(cell, date) { - // compute the new end. don't allow it to go before the event's start - if (date.isBefore(start)) { // allows comparing ambig to non-ambig - date = start; - } - newEnd = date.clone().add(_this.cellDuration); // make it an exclusive end - - if (newEnd.isSame(end)) { - newEnd = null; - destroy(); - } - else { - _this.renderResize(start, newEnd, seg); - view.hideEvent(event); - } - }, - cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells - newEnd = null; - destroy(); - }, - dragStop: function(ev) { - _this.isResizingSeg = false; - destroy(); - view.trigger('eventResizeStop', el[0], event, ev, {}); // last argument is jqui dummy - - if (newEnd) { - view.eventResize(el[0], event, newEnd, ev); // will rerender all events... - } - } - }); - - dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart - }, - - - /* Rendering Utils - ------------------------------------------------------------------------------------------------------------------*/ - - - // Generic utility for generating the HTML classNames for an event segment's element - getSegClasses: function(seg, isDraggable, isResizable) { - var event = seg.event; - var classes = [ - 'fc-event', - seg.isStart ? 'fc-start' : 'fc-not-start', - seg.isEnd ? 'fc-end' : 'fc-not-end' - ].concat( - event.className, - event.source ? event.source.className : [] - ); - - if (isDraggable) { - classes.push('fc-draggable'); - } - if (isResizable) { - classes.push('fc-resizable'); - } - - return classes; - }, - - - // Utility for generating a CSS string with all the event skin-related properties - getEventSkinCss: function(event) { - var view = this.view; - var source = event.source || {}; - var eventColor = event.color; - var sourceColor = source.color; - var optionColor = view.opt('eventColor'); - var backgroundColor = - event.backgroundColor || - eventColor || - source.backgroundColor || - sourceColor || - view.opt('eventBackgroundColor') || - optionColor; - var borderColor = - event.borderColor || - eventColor || - source.borderColor || - sourceColor || - view.opt('eventBorderColor') || - optionColor; - var textColor = - event.textColor || - source.textColor || - view.opt('eventTextColor'); - var statements = []; - if (backgroundColor) { - statements.push('background-color:' + backgroundColor); - } - if (borderColor) { - statements.push('border-color:' + borderColor); - } - if (textColor) { - statements.push('color:' + textColor); - } - return statements.join(';'); - } - -}); - - -/* Event Segment Utilities -----------------------------------------------------------------------------------------------------------------------*/ - - -// A cmp function for determining which segments should take visual priority -function compareSegs(seg1, seg2) { - return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first - seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first - seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1) - (seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title -} - - -;; - -/* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week. -----------------------------------------------------------------------------------------------------------------------*/ - -function DayGrid(view) { - Grid.call(this, view); // call the super-constructor -} - - -DayGrid.prototype = createObject(Grid.prototype); // declare the super-class -$.extend(DayGrid.prototype, { - - numbersVisible: false, // should render a row for day/week numbers? manually set by the view - cellDuration: moment.duration({ days: 1 }), // required for Grid.event.js. Each cell is always a single day - bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid - - rowEls: null, // set of fake row elements - dayEls: null, // set of whole-day elements comprising the row's background - helperEls: null, // set of cell skeleton elements for rendering the mock event "helper" - highlightEls: null, // set of cell skeleton elements for rendering the highlight - - - // Renders the rows and columns into the component's `this.el`, which should already be assigned. - // isRigid determins whether the individual rows should ignore the contents and be a constant height. - // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient. - render: function(isRigid) { - var view = this.view; - var html = ''; - var row; - - for (row = 0; row < view.rowCnt; row++) { - html += this.dayRowHtml(row, isRigid); - } - this.el.html(html); - - this.rowEls = this.el.find('.fc-row'); - this.dayEls = this.el.find('.fc-day'); - - // run all the day cells through the dayRender callback - this.dayEls.each(function(i, node) { - var date = view.cellToDate(Math.floor(i / view.colCnt), i % view.colCnt); - view.trigger('dayRender', null, date, $(node)); - }); - - Grid.prototype.render.call(this); // call the super-method - }, - - - destroy: function() { - this.destroySegPopover(); - }, - - - // Generates the HTML for a single row. `row` is the row number. - dayRowHtml: function(row, isRigid) { - var view = this.view; - var classes = [ 'fc-row', 'fc-week', view.widgetContentClass ]; - - if (isRigid) { - classes.push('fc-rigid'); - } - - return '' + - '
' + - '
' + - '' + - this.rowHtml('day', row) + // leverages RowRenderer. calls dayCellHtml() - '
' + - '
' + - '
' + - '' + - (this.numbersVisible ? - '' + - this.rowHtml('number', row) + // leverages RowRenderer. View will define render method - '' : - '' - ) + - '
' + - '
' + - '
'; - }, - - - // Renders the HTML for a whole-day cell. Will eventually end up in the day-row's background. - // We go through a 'day' row type instead of just doing a 'bg' row type so that the View can do custom rendering - // specifically for whole-day rows, whereas a 'bg' might also be used for other purposes (TimeGrid bg for example). - dayCellHtml: function(row, col, date) { - return this.bgCellHtml(row, col, date); - }, - - - /* Coordinates & Cells - ------------------------------------------------------------------------------------------------------------------*/ - - - // Populates the empty `rows` and `cols` arrays with coordinates of the cells. For CoordGrid. - buildCoords: function(rows, cols) { - var colCnt = this.view.colCnt; - var e, n, p; - - this.dayEls.slice(0, colCnt).each(function(i, _e) { // iterate the first row of day elements - e = $(_e); - n = e.offset().left; - if (i) { - p[1] = n; - } - p = [ n ]; - cols[i] = p; - }); - p[1] = n + e.outerWidth(); - - this.rowEls.each(function(i, _e) { - e = $(_e); - n = e.offset().top; - if (i) { - p[1] = n; - } - p = [ n ]; - rows[i] = p; - }); - p[1] = n + e.outerHeight() + this.bottomCoordPadding; // hack to extend hit area of last row - }, - - - // Converts a cell to a date - getCellDate: function(cell) { - return this.view.cellToDate(cell); // leverages the View's cell system - }, - - - // Gets the whole-day element associated with the cell - getCellDayEl: function(cell) { - return this.dayEls.eq(cell.row * this.view.colCnt + cell.col); - }, - - - // Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects - rangeToSegs: function(start, end) { - return this.view.rangeToSegments(start, end); // leverages the View's cell system - }, - - - /* Event Drag Visualization - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event hovering over the given date(s). - // `end` can be null, as well as `seg`. See View's documentation on renderDrag for more info. - // A returned value of `true` signals that a mock "helper" event has been rendered. - renderDrag: function(start, end, seg) { - var opacity; - - // always render a highlight underneath - this.renderHighlight( - start, - end || this.view.calendar.getDefaultEventEnd(true, start) - ); - - // if a segment from the same calendar but another component is being dragged, render a helper event - if (seg && !seg.el.closest(this.el).length) { - - this.renderRangeHelper(start, end, seg); - - opacity = this.view.opt('dragOpacity'); - if (opacity !== undefined) { - this.helperEls.css('opacity', opacity); - } - - return true; // a helper has been rendered - } - }, - - - // Unrenders any visual indication of a hovering event - destroyDrag: function() { - this.destroyHighlight(); - this.destroyHelper(); - }, - - - /* Event Resize Visualization - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event being resized - renderResize: function(start, end, seg) { - this.renderHighlight(start, end); - this.renderRangeHelper(start, end, seg); - }, - - - // Unrenders a visual indication of an event being resized - destroyResize: function() { - this.destroyHighlight(); - this.destroyHelper(); - }, - - - /* Event Helper - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null. - renderHelper: function(event, sourceSeg) { - var helperNodes = []; - var rowStructs = this.renderEventRows([ event ]); - - // inject each new event skeleton into each associated row - this.rowEls.each(function(row, rowNode) { - var rowEl = $(rowNode); // the .fc-row - var skeletonEl = $('
'); // will be absolutely positioned - var skeletonTop; - - // If there is an original segment, match the top position. Otherwise, put it at the row's top level - if (sourceSeg && sourceSeg.row === row) { - skeletonTop = sourceSeg.el.position().top; - } - else { - skeletonTop = rowEl.find('.fc-content-skeleton tbody').position().top; - } - - skeletonEl.css('top', skeletonTop) - .find('table') - .append(rowStructs[row].tbodyEl); - - rowEl.append(skeletonEl); - helperNodes.push(skeletonEl[0]); - }); - - this.helperEls = $(helperNodes); // array -> jQuery set - }, - - - // Unrenders any visual indication of a mock helper event - destroyHelper: function() { - if (this.helperEls) { - this.helperEls.remove(); - this.helperEls = null; - } - }, - - - /* Highlighting - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders an emphasis on the given date range. `start` is an inclusive, `end` is exclusive. - renderHighlight: function(start, end) { - var segs = this.rangeToSegs(start, end); - var highlightNodes = []; - var i, seg; - var el; - - // build an event skeleton for each row that needs it - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - el = $( - this.highlightSkeletonHtml(seg.leftCol, seg.rightCol + 1) // make end exclusive - ); - el.appendTo(this.rowEls[seg.row]); - highlightNodes.push(el[0]); - } - - this.highlightEls = $(highlightNodes); // array -> jQuery set - }, - - - // Unrenders any visual emphasis on a date range - destroyHighlight: function() { - if (this.highlightEls) { - this.highlightEls.remove(); - this.highlightEls = null; - } - }, - - - // Generates the HTML used to build a single-row "highlight skeleton", a table that frames highlight cells - highlightSkeletonHtml: function(startCol, endCol) { - var colCnt = this.view.colCnt; - var cellHtml = ''; - - if (startCol > 0) { - cellHtml += ' elements that should be appended to each row's content skeleton. - // Returns an array of rowStruct objects (see the bottom of `renderEventRow`). - renderEventRows: function(events) { - var segs = this.eventsToSegs(events); - var rowStructs = []; - var segRows; - var row; - - segs = this.renderSegs(segs); // returns a new array with only visible segments - segRows = this.groupSegRows(segs); // group into nested arrays - - // iterate each row of segment groupings - for (row = 0; row < segRows.length; row++) { - rowStructs.push( - this.renderEventRow(row, segRows[row]) - ); - } - - return rowStructs; - }, - - - // Builds the HTML to be used for the default element for an individual segment - renderSegHtml: function(seg, disableResizing) { - var view = this.view; - var isRTL = view.opt('isRTL'); - var event = seg.event; - var isDraggable = view.isEventDraggable(event); - var isResizable = !disableResizing && event.allDay && seg.isEnd && view.isEventResizable(event); - var classes = this.getSegClasses(seg, isDraggable, isResizable); - var skinCss = this.getEventSkinCss(event); - var timeHtml = ''; - var titleHtml; - - classes.unshift('fc-day-grid-event'); - - // Only display a timed events time if it is the starting segment - if (!event.allDay && seg.isStart) { - timeHtml = '' + htmlEscape(view.getEventTimeText(event)) + ''; - } - - titleHtml = - '' + - (htmlEscape(event.title || '') || ' ') + // we always want one line of height - ''; - - return '' + - '
' + - (isRTL ? - titleHtml + ' ' + timeHtml : // put a natural space in between - timeHtml + ' ' + titleHtml // - ) + - '
' + - (isResizable ? - '
' : - '' - ) + - ''; - }, - - - // Given a row # and an array of segments all in the same row, render a
element, a skeleton that contains - // the segments. Returns object with a bunch of internal data about how the render was calculated. - renderEventRow: function(row, rowSegs) { - var view = this.view; - var colCnt = view.colCnt; - var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels - var levelCnt = Math.max(1, segLevels.length); // ensure at least one level - var tbody = $(''); - var segMatrix = []; // lookup for which segments are rendered into which level+col cells - var cellMatrix = []; // lookup for all '); - - segMatrix.push([]); - cellMatrix.push([]); - loneCellMatrix.push([]); - - // levelCnt might be 1 even though there are no actual levels. protect against this. - // this single empty row is useful for styling. - if (levelSegs) { - for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level - seg = levelSegs[j]; - - emptyCellsUntil(seg.leftCol); - - // create a container that occupies or more columns. append the event element. - td = $(' at a time and stop when we find one out of bounds - for (i = 0; i < trEls.length; i++) { - trEl = trEls.eq(i).removeClass('fc-limited'); // get and reveal - if (trEl.position().top + trEl.outerHeight() > rowHeight) { - return i; - } - } - - return false; // should not limit at all - }, - - - // Limits the given grid row to the maximum number of levels and injects "more" links if necessary. - // `row` is the row number. - // `levelLimit` is a number for the maximum (inclusive) number of levels allowed. - limitRow: function(row, levelLimit) { - var _this = this; - var view = this.view; - var rowStruct = this.rowStructs[row]; - var moreNodes = []; // array of "more" links and and segment elements past the limit - .addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array - - // iterate though segments in the last allowable level - for (i = 0; i < levelSegs.length; i++) { - seg = levelSegs[i]; - emptyCellsUntil(seg.leftCol); // process empty cells before the segment - - // determine *all* segments below `seg` that occupy the same columns - colSegsBelow = []; - totalSegsBelow = 0; - while (col <= seg.rightCol) { - cell = { row: row, col: col }; - segsBelow = this.getCellSegs(cell, levelLimit); - colSegsBelow.push(segsBelow); - totalSegsBelow += segsBelow.length; - col++; - } - - if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links? - td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell - rowspan = td.attr('rowspan') || 1; - segMoreNodes = []; - - // make a replacement '; - - html += - '' + - (!isRTL ? axisHtml : '') + - '"; - - slotTime.add(this.slotDuration); - } - - return html; - }, - - - // Parses various options into properties of this object - processOptions: function() { - var view = this.view; - var slotDuration = view.opt('slotDuration'); - var snapDuration = view.opt('snapDuration'); - - slotDuration = moment.duration(slotDuration); - snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration; - - this.slotDuration = slotDuration; - this.snapDuration = snapDuration; - this.cellDuration = snapDuration; // important to assign this for Grid.events.js - - this.minTime = moment.duration(view.opt('minTime')); - this.maxTime = moment.duration(view.opt('maxTime')); - }, - - - // Slices up a date range into a segment for each column - rangeToSegs: function(rangeStart, rangeEnd) { - var view = this.view; - var segs = []; - var seg; - var col; - var cellDate; - var colStart, colEnd; - - // normalize - rangeStart = rangeStart.clone().stripZone(); - rangeEnd = rangeEnd.clone().stripZone(); - - for (col = 0; col < view.colCnt; col++) { - cellDate = view.cellToDate(0, col); // use the View's cell system for this - colStart = cellDate.clone().time(this.minTime); - colEnd = cellDate.clone().time(this.maxTime); - seg = intersectionToSeg(rangeStart, rangeEnd, colStart, colEnd); - if (seg) { - seg.col = col; - segs.push(seg); - } - } - - return segs; - }, - - - /* Coordinates - ------------------------------------------------------------------------------------------------------------------*/ - - - // Called when there is a window resize/zoom and we need to recalculate coordinates for the grid - resize: function() { - this.computeSlatTops(); - this.updateSegVerticals(); - }, - - - // Populates the given empty `rows` and `cols` arrays with offset positions of the "snap" cells. - // "Snap" cells are different the slots because they might have finer granularity. - buildCoords: function(rows, cols) { - var colCnt = this.view.colCnt; - var originTop = this.el.offset().top; - var snapTime = moment.duration(+this.minTime); - var p = null; - var e, n; - - this.dayEls.slice(0, colCnt).each(function(i, _e) { - e = $(_e); - n = e.offset().left; - if (p) { - p[1] = n; - } - p = [ n ]; - cols[i] = p; - }); - p[1] = n + e.outerWidth(); - - p = null; - while (snapTime < this.maxTime) { - n = originTop + this.computeTimeTop(snapTime); - if (p) { - p[1] = n; - } - p = [ n ]; - rows.push(p); - snapTime.add(this.snapDuration); - } - p[1] = originTop + this.computeTimeTop(snapTime); // the position of the exclusive end - }, - - - // Gets the datetime for the given slot cell - getCellDate: function(cell) { - var view = this.view; - var calendar = view.calendar; - - return calendar.rezoneDate( // since we are adding a time, it needs to be in the calendar's timezone - view.cellToDate(0, cell.col) // View's coord system only accounts for start-of-day for column - .time(this.minTime + this.snapDuration * cell.row) - ); - }, - - - // Gets the element that represents the whole-day the cell resides on - getCellDayEl: function(cell) { - return this.dayEls.eq(cell.col); - }, - - - // Computes the top coordinate, relative to the bounds of the grid, of the given date. - // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight. - computeDateTop: function(date, startOfDayDate) { - return this.computeTimeTop( - moment.duration( - date.clone().stripZone() - startOfDayDate.clone().stripTime() - ) - ); - }, - - - // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration). - computeTimeTop: function(time) { - var slatCoverage = (time - this.minTime) / this.slotDuration; // floating-point value of # of slots covered - var slatIndex; - var slatRemainder; - var slatTop; - var slatBottom; - - // constrain. because minTime/maxTime might be customized - slatCoverage = Math.max(0, slatCoverage); - slatCoverage = Math.min(this.slatEls.length, slatCoverage); - - slatIndex = Math.floor(slatCoverage); // an integer index of the furthest whole slot - slatRemainder = slatCoverage - slatIndex; - slatTop = this.slatTops[slatIndex]; // the top position of the furthest whole slot - - if (slatRemainder) { // time spans part-way into the slot - slatBottom = this.slatTops[slatIndex + 1]; - return slatTop + (slatBottom - slatTop) * slatRemainder; // part-way between slots - } - else { - return slatTop; - } - }, - - - // Queries each `slatEl` for its position relative to the grid's container and stores it in `slatTops`. - // Includes the the bottom of the last slat as the last item in the array. - computeSlatTops: function() { - var tops = []; - var top; - - this.slatEls.each(function(i, node) { - top = $(node).position().top; - tops.push(top); - }); - - tops.push(top + this.slatEls.last().outerHeight()); // bottom of the last slat - - this.slatTops = tops; - }, - - - /* Event Drag Visualization - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event being dragged over the specified date(s). - // `end` and `seg` can be null. See View's documentation on renderDrag for more info. - renderDrag: function(start, end, seg) { - var opacity; - - if (seg) { // if there is event information for this drag, render a helper event - this.renderRangeHelper(start, end, seg); - - opacity = this.view.opt('dragOpacity'); - if (opacity !== undefined) { - this.helperEl.css('opacity', opacity); - } - - return true; // signal that a helper has been rendered - } - else { - // otherwise, just render a highlight - this.renderHighlight( - start, - end || this.view.calendar.getDefaultEventEnd(false, start) - ); - } - }, - - - // Unrenders any visual indication of an event being dragged - destroyDrag: function() { - this.destroyHelper(); - this.destroyHighlight(); - }, - - - /* Event Resize Visualization - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event being resized - renderResize: function(start, end, seg) { - this.renderRangeHelper(start, end, seg); - }, - - - // Unrenders any visual indication of an event being resized - destroyResize: function() { - this.destroyHelper(); - }, - - - /* Event Helper - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag) - renderHelper: function(event, sourceSeg) { - var res = this.renderEventTable([ event ]); - var tableEl = res.tableEl; - var segs = res.segs; - var i, seg; - var sourceEl; - - // Try to make the segment that is in the same row as sourceSeg look the same - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - if (sourceSeg && sourceSeg.col === seg.col) { - sourceEl = sourceSeg.el; - seg.el.css({ - left: sourceEl.css('left'), - right: sourceEl.css('right'), - 'margin-left': sourceEl.css('margin-left'), - 'margin-right': sourceEl.css('margin-right') - }); - } - } - - this.helperEl = $('
') - .append(tableEl) - .appendTo(this.el); - }, - - - // Unrenders any mock helper event - destroyHelper: function() { - if (this.helperEl) { - this.helperEl.remove(); - this.helperEl = null; - } - }, - - - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight. - renderSelection: function(start, end) { - if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered - this.renderRangeHelper(start, end); - } - else { - this.renderHighlight(start, end); - } - }, - - - // Unrenders any visual indication of a selection - destroySelection: function() { - this.destroyHelper(); - this.destroyHighlight(); - }, - - - /* Highlight - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders an emphasis on the given date range. `start` is inclusive. `end` is exclusive. - renderHighlight: function(start, end) { - this.highlightEl = $( - this.highlightSkeletonHtml(start, end) - ).appendTo(this.el); - }, - - - // Unrenders the emphasis on a date range - destroyHighlight: function() { - if (this.highlightEl) { - this.highlightEl.remove(); - this.highlightEl = null; - } - }, - - - // Generates HTML for a table element with containers in each column, responsible for absolutely positioning the - // highlight elements to cover the highlighted slots. - highlightSkeletonHtml: function(start, end) { - var view = this.view; - var segs = this.rangeToSegs(start, end); - var cellHtml = ''; - var col = 0; - var i, seg; - var dayDate; - var top, bottom; - - for (i = 0; i < segs.length; i++) { // loop through the segments. one per column - seg = segs[i]; - - // need empty cells beforehand? - if (col < seg.col) { - cellHtml += '
'; - - col++; - } - - // need empty cells after the last segment? - if (col < view.colCnt) { - cellHtml += ''; - } - }, - - - // Generates the HTML that will go before content-skeleton cells that display the day/week numbers. - // Queried by the DayGrid subcomponent. Ordering depends on isRTL. - numberIntroHtml: function(row) { - if (this.weekNumbersVisible) { - return '' + - ''; - } - }, - - - // Generates the HTML that goes before the day bg cells for each day-row. - // Queried by the DayGrid subcomponent. Ordering depends on isRTL. - dayIntroHtml: function() { - if (this.weekNumbersVisible) { - return ''; - } - }, - - - // Generates the HTML that goes before every other type of row generated by DayGrid. Ordering depends on isRTL. - // Affects helper-skeleton and highlight-skeleton rows. - introHtml: function() { - if (this.weekNumbersVisible) { - return ''; - } - }, - - - // Generates the HTML for the '; - }, - - - // Generates an HTML attribute string for setting the width of the week number column, if it is known - weekNumberStyleAttr: function() { - if (this.weekNumberWidth !== null) { - return 'style="width:' + this.weekNumberWidth + 'px"'; - } - return ''; - }, - - - // Determines whether each row should have a constant height - hasRigidRows: function() { - var eventLimit = this.opt('eventLimit'); - return eventLimit && typeof eventLimit !== 'number'; - }, - - - /* Dimensions - ------------------------------------------------------------------------------------------------------------------*/ - - - // Refreshes the horizontal dimensions of the view - updateWidth: function() { - if (this.weekNumbersVisible) { - // Make sure all week number cells running down the side have the same width. - // Record the width for cells created later. - this.weekNumberWidth = matchCellWidths( - this.el.find('.fc-week-number') - ); - } - }, - - - // Adjusts the vertical dimensions of the view to the specified values - setHeight: function(totalHeight, isAuto) { - var eventLimit = this.opt('eventLimit'); - var scrollerHeight; - - // reset all heights to be natural - unsetScroller(this.scrollerEl); - uncompensateScroll(this.headRowEl); - - this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed - - // is the event limit a constant level number? - if (eventLimit && typeof eventLimit === 'number') { - this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after - } - - scrollerHeight = this.computeScrollerHeight(totalHeight); - this.setGridHeight(scrollerHeight, isAuto); - - // is the event limit dynamically calculated? - if (eventLimit && typeof eventLimit !== 'number') { - this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set - } - - if (!isAuto && setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars? - - compensateScroll(this.headRowEl, getScrollbarWidths(this.scrollerEl)); - - // doing the scrollbar compensation might have created text overflow which created more height. redo - scrollerHeight = this.computeScrollerHeight(totalHeight); - this.scrollerEl.height(scrollerHeight); - - this.restoreScroll(); - } - }, - - - // Sets the height of just the DayGrid component in this view - setGridHeight: function(height, isAuto) { - if (isAuto) { - undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding - } - else { - distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows - } - }, - - - /* Events - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders the given events onto the view and populates the segments array - renderEvents: function(events) { - this.dayGrid.renderEvents(events); - - this.updateHeight(); // must compensate for events that overflow the row - - View.prototype.renderEvents.call(this, events); // call the super-method - }, - - - // Retrieves all segment objects that are rendered in the view - getSegs: function() { - return this.dayGrid.getSegs(); - }, - - - // Unrenders all event elements and clears internal segment data - destroyEvents: function() { - View.prototype.destroyEvents.call(this); // do this before dayGrid's segs have been cleared - - this.recordScroll(); // removing events will reduce height and mess with the scroll, so record beforehand - this.dayGrid.destroyEvents(); - - // we DON'T need to call updateHeight() because: - // A) a renderEvents() call always happens after this, which will eventually call updateHeight() - // B) in IE8, this causes a flash whenever events are rerendered - }, - - - /* Event Dragging - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event being dragged over the view. - // A returned value of `true` signals that a mock "helper" event has been rendered. - renderDrag: function(start, end, seg) { - return this.dayGrid.renderDrag(start, end, seg); - }, - - - // Unrenders the visual indication of an event being dragged over the view - destroyDrag: function() { - this.dayGrid.destroyDrag(); - }, - - - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of a selection - renderSelection: function(start, end) { - this.dayGrid.renderSelection(start, end); - }, - - - // Unrenders a visual indications of a selection - destroySelection: function() { - this.dayGrid.destroySelection(); - } - -}); - -;; - -/* A month view with day cells running in rows (one-per-week) and columns -----------------------------------------------------------------------------------------------------------------------*/ - -setDefaults({ - fixedWeekCount: true -}); - -fcViews.month = MonthView; // register the view - -function MonthView(calendar) { - BasicView.call(this, calendar); // call the super-constructor -} - - -MonthView.prototype = createObject(BasicView.prototype); // define the super-class -$.extend(MonthView.prototype, { - - name: 'month', - - - incrementDate: function(date, delta) { - return date.clone().stripTime().add(delta, 'months').startOf('month'); - }, - - - render: function(date) { - var rowCnt; - - this.intervalStart = date.clone().stripTime().startOf('month'); - this.intervalEnd = this.intervalStart.clone().add(1, 'months'); - - this.start = this.intervalStart.clone(); - this.start = this.skipHiddenDays(this.start); // move past the first week if no visible days - this.start.startOf('week'); - this.start = this.skipHiddenDays(this.start); // move past the first invisible days of the week - - this.end = this.intervalEnd.clone(); - this.end = this.skipHiddenDays(this.end, -1, true); // move in from the last week if no visible days - this.end.add((7 - this.end.weekday()) % 7, 'days'); // move to end of week if not already - this.end = this.skipHiddenDays(this.end, -1, true); // move in from the last invisible days of the week - - rowCnt = Math.ceil( // need to ceil in case there are hidden days - this.end.diff(this.start, 'weeks', true) // returnfloat=true - ); - if (this.isFixedWeeks()) { - this.end.add(6 - rowCnt, 'weeks'); - rowCnt = 6; - } - - this.title = this.calendar.formatDate(this.intervalStart, this.opt('titleFormat')); - - BasicView.prototype.render.call(this, rowCnt, this.getCellsPerWeek(), true); // call the super-method - }, - - - // Overrides the default BasicView behavior to have special multi-week auto-height logic - setGridHeight: function(height, isAuto) { - - isAuto = isAuto || this.opt('weekMode') === 'variable'; // LEGACY: weekMode is deprecated - - // if auto, make the height of each row the height that it would be if there were 6 weeks - if (isAuto) { - height *= this.rowCnt / 6; - } - - distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows - }, - - - isFixedWeeks: function() { - var weekMode = this.opt('weekMode'); // LEGACY: weekMode is deprecated - if (weekMode) { - return weekMode === 'fixed'; // if any other type of weekMode, assume NOT fixed - } - - return this.opt('fixedWeekCount'); - } - -}); - -;; - -/* A week view with simple day cells running horizontally -----------------------------------------------------------------------------------------------------------------------*/ -// TODO: a WeekView mixin for calculating dates and titles - -fcViews.basicWeek = BasicWeekView; // register this view - -function BasicWeekView(calendar) { - BasicView.call(this, calendar); // call the super-constructor -} - - -BasicWeekView.prototype = createObject(BasicView.prototype); // define the super-class -$.extend(BasicWeekView.prototype, { - - name: 'basicWeek', - - - incrementDate: function(date, delta) { - return date.clone().stripTime().add(delta, 'weeks').startOf('week'); - }, - - - render: function(date) { - - this.intervalStart = date.clone().stripTime().startOf('week'); - this.intervalEnd = this.intervalStart.clone().add(1, 'weeks'); - - this.start = this.skipHiddenDays(this.intervalStart); - this.end = this.skipHiddenDays(this.intervalEnd, -1, true); - - this.title = this.calendar.formatRange( - this.start, - this.end.clone().subtract(1), // make inclusive by subtracting 1 ms - this.opt('titleFormat'), - ' \u2014 ' // emphasized dash - ); - - BasicView.prototype.render.call(this, 1, this.getCellsPerWeek(), false); // call the super-method - } - -}); -;; - -/* A view with a single simple day cell -----------------------------------------------------------------------------------------------------------------------*/ - -fcViews.basicDay = BasicDayView; // register this view - -function BasicDayView(calendar) { - BasicView.call(this, calendar); // call the super-constructor -} - - -BasicDayView.prototype = createObject(BasicView.prototype); // define the super-class -$.extend(BasicDayView.prototype, { - - name: 'basicDay', - - - incrementDate: function(date, delta) { - var out = date.clone().stripTime().add(delta, 'days'); - out = this.skipHiddenDays(out, delta < 0 ? -1 : 1); - return out; - }, - - - render: function(date) { - - this.start = this.intervalStart = date.clone().stripTime(); - this.end = this.intervalEnd = this.start.clone().add(1, 'days'); - - this.title = this.calendar.formatDate(this.start, this.opt('titleFormat')); - - BasicView.prototype.render.call(this, 1, 1, false); // call the super-method - } - -}); -;; - -/* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically. -----------------------------------------------------------------------------------------------------------------------*/ -// Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on). -// Responsible for managing width/height. - -setDefaults({ - allDaySlot: true, - allDayText: 'all-day', - - scrollTime: '06:00:00', - - slotDuration: '00:30:00', - - axisFormat: generateAgendaAxisFormat, - timeFormat: { - agenda: generateAgendaTimeFormat - }, - - minTime: '00:00:00', - maxTime: '24:00:00', - slotEventOverlap: true -}); - -var AGENDA_ALL_DAY_EVENT_LIMIT = 5; - - -function generateAgendaAxisFormat(options, langData) { - return langData.longDateFormat('LT') - .replace(':mm', '(:mm)') - .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs - .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand -} - - -function generateAgendaTimeFormat(options, langData) { - return langData.longDateFormat('LT') - .replace(/\s*a$/i, ''); // remove trailing AM/PM -} - - -function AgendaView(calendar) { - View.call(this, calendar); // call the super-constructor - - this.timeGrid = new TimeGrid(this); - - if (this.opt('allDaySlot')) { // should we display the "all-day" area? - this.dayGrid = new DayGrid(this); // the all-day subcomponent of this view - - // the coordinate grid will be a combination of both subcomponents' grids - this.coordMap = new ComboCoordMap([ - this.dayGrid.coordMap, - this.timeGrid.coordMap - ]); - } - else { - this.coordMap = this.timeGrid.coordMap; - } -} - - -AgendaView.prototype = createObject(View.prototype); // define the super-class -$.extend(AgendaView.prototype, { - - timeGrid: null, // the main time-grid subcomponent of this view - dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null - - axisWidth: null, // the width of the time axis running down the side - - noScrollRowEls: null, // set of fake row elements that must compensate when scrollerEl has scrollbars - - // when the time-grid isn't tall enough to occupy the given height, we render an
underneath - bottomRuleEl: null, - bottomRuleHeight: null, - - - /* Rendering - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders the view into `this.el`, which has already been assigned. - // `colCnt` has been calculated by a subclass and passed here. - render: function(colCnt) { - - // needed for cell-to-date and date-to-cell calculations in View - this.rowCnt = 1; - this.colCnt = colCnt; - - this.el.addClass('fc-agenda-view').html(this.renderHtml()); - - // the element that wraps the time-grid that will probably scroll - this.scrollerEl = this.el.find('.fc-time-grid-container'); - this.timeGrid.coordMap.containerEl = this.scrollerEl; // don't accept clicks/etc outside of this - - this.timeGrid.el = this.el.find('.fc-time-grid'); - this.timeGrid.render(); - - // the
that sometimes displays under the time-grid - this.bottomRuleEl = $('
') - .appendTo(this.timeGrid.el); // inject it into the time-grid - - if (this.dayGrid) { - this.dayGrid.el = this.el.find('.fc-day-grid'); - this.dayGrid.render(); - - // have the day-grid extend it's coordinate area over the
dividing the two grids - this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight(); - } - - this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller - - View.prototype.render.call(this); // call the super-method - - this.resetScroll(); // do this after sizes have been set - }, - - - // Make subcomponents ready for cleanup - destroy: function() { - this.timeGrid.destroy(); - if (this.dayGrid) { - this.dayGrid.destroy(); - } - View.prototype.destroy.call(this); // call the super-method - }, - - - // Builds the HTML skeleton for the view. - // The day-grid and time-grid components will render inside containers defined by this HTML. - renderHtml: function() { - return '' + - '
'; - } - if (endCol > startCol) { - cellHtml += ''; - } - if (colCnt > endCol) { - cellHtml += ''; - } - - cellHtml = this.bookendCells(cellHtml, 'highlight'); - - return '' + - '
' + - '' + - '' + - cellHtml + - '' + - '
' + - '
'; - } - -}); - -;; - -/* Event-rendering methods for the DayGrid class -----------------------------------------------------------------------------------------------------------------------*/ - -$.extend(DayGrid.prototype, { - - segs: null, - rowStructs: null, // an array of objects, each holding information about a row's event-rendering - - - // Render the given events onto the Grid and return the rendered segments - renderEvents: function(events) { - var rowStructs = this.rowStructs = this.renderEventRows(events); - var segs = []; - - // append to each row's content skeleton - this.rowEls.each(function(i, rowNode) { - $(rowNode).find('.fc-content-skeleton > table').append( - rowStructs[i].tbodyEl - ); - segs.push.apply(segs, rowStructs[i].segs); - }); - - this.segs = segs; - }, - - - // Retrieves all segment objects that have been rendered - getSegs: function() { - return (this.segs || []).concat( - this.popoverSegs || [] // segs rendered in the "more" events popover - ); - }, - - - // Removes all rendered event elements - destroyEvents: function() { - var rowStructs; - var rowStruct; - - Grid.prototype.destroyEvents.call(this); // call the super-method - - rowStructs = this.rowStructs || []; - while ((rowStruct = rowStructs.pop())) { - rowStruct.tbodyEl.remove(); - } - - this.segs = null; - this.destroySegPopover(); // removes the "more.." events popover - }, - - - // Uses the given events array to generate
elements of the level+col matrix - var loneCellMatrix = []; // lookup for elements that only take up a single column - var i, levelSegs; - var col; - var tr; - var j, seg; - var td; - - // populates empty cells from the current column (`col`) to `endCol` - function emptyCellsUntil(endCol) { - while (col < endCol) { - // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell - td = (loneCellMatrix[i - 1] || [])[col]; - if (td) { - td.attr( - 'rowspan', - parseInt(td.attr('rowspan') || 1, 10) + 1 - ); - } - else { - td = $(''); - tr.append(td); - } - cellMatrix[i][col] = td; - loneCellMatrix[i][col] = td; - col++; - } - } - - for (i = 0; i < levelCnt; i++) { // iterate through all levels - levelSegs = segLevels[i]; - col = 0; - tr = $('
').append(seg.el); - if (seg.leftCol != seg.rightCol) { - td.attr('colspan', seg.rightCol - seg.leftCol + 1); - } - else { // a single-column segment - loneCellMatrix[i][col] = td; - } - - while (col <= seg.rightCol) { - cellMatrix[i][col] = td; - segMatrix[i][col] = seg; - col++; - } - - tr.append(td); - } - } - - emptyCellsUntil(colCnt); // finish off the row - this.bookendCells(tr, 'eventSkeleton'); - tbody.append(tr); - } - - return { // a "rowStruct" - row: row, // the row number - tbodyEl: tbody, - cellMatrix: cellMatrix, - segMatrix: segMatrix, - segLevels: segLevels, - segs: rowSegs - }; - }, - - - // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels. - buildSegLevels: function(segs) { - var levels = []; - var i, seg; - var j; - - // Give preference to elements with certain criteria, so they have - // a chance to be closer to the top. - segs.sort(compareSegs); - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - - // loop through levels, starting with the topmost, until the segment doesn't collide with other segments - for (j = 0; j < levels.length; j++) { - if (!isDaySegCollision(seg, levels[j])) { - break; - } - } - // `j` now holds the desired subrow index - seg.level = j; - - // create new level array if needed and append segment - (levels[j] || (levels[j] = [])).push(seg); - } - - // order segments left-to-right. very important if calendar is RTL - for (j = 0; j < levels.length; j++) { - levels[j].sort(compareDaySegCols); - } - - return levels; - }, - - - // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row - groupSegRows: function(segs) { - var view = this.view; - var segRows = []; - var i; - - for (i = 0; i < view.rowCnt; i++) { - segRows.push([]); - } - - for (i = 0; i < segs.length; i++) { - segRows[segs[i].row].push(segs[i]); - } - - return segRows; - } - -}); - - -// Computes whether two segments' columns collide. They are assumed to be in the same row. -function isDaySegCollision(seg, otherSegs) { - var i, otherSeg; - - for (i = 0; i < otherSegs.length; i++) { - otherSeg = otherSegs[i]; - - if ( - otherSeg.leftCol <= seg.rightCol && - otherSeg.rightCol >= seg.leftCol - ) { - return true; - } - } - - return false; -} - - -// A cmp function for determining the leftmost event -function compareDaySegCols(a, b) { - return a.leftCol - b.leftCol; -} - -;; - -/* Methods relate to limiting the number events for a given day on a DayGrid -----------------------------------------------------------------------------------------------------------------------*/ - -$.extend(DayGrid.prototype, { - - - segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible - popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible - - - destroySegPopover: function() { - if (this.segPopover) { - this.segPopover.hide(); // will trigger destruction of `segPopover` and `popoverSegs` - } - }, - - - // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid. - // `levelLimit` can be false (don't limit), a number, or true (should be computed). - limitRows: function(levelLimit) { - var rowStructs = this.rowStructs || []; - var row; // row # - var rowLevelLimit; - - for (row = 0; row < rowStructs.length; row++) { - this.unlimitRow(row); - - if (!levelLimit) { - rowLevelLimit = false; - } - else if (typeof levelLimit === 'number') { - rowLevelLimit = levelLimit; - } - else { - rowLevelLimit = this.computeRowLevelLimit(row); - } - - if (rowLevelLimit !== false) { - this.limitRow(row, rowLevelLimit); - } - } - }, - - - // Computes the number of levels a row will accomodate without going outside its bounds. - // Assumes the row is "rigid" (maintains a constant height regardless of what is inside). - // `row` is the row number. - computeRowLevelLimit: function(row) { - var rowEl = this.rowEls.eq(row); // the containing "fake" row div - var rowHeight = rowEl.height(); // TODO: cache somehow? - var trEls = this.rowStructs[row].tbodyEl.children(); - var i, trEl; - - // Reveal one level
DOM nodes - var col = 0; // col # - var cell; - var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right - var cellMatrix; // a matrix (by level, then column) of all jQuery elements in the row - var limitedNodes; // array of temporarily hidden level
DOM nodes - var i, seg; - var segsBelow; // array of segment objects below `seg` in the current `col` - var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies - var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column) - var td, rowspan; - var segMoreNodes; // array of "more" cells that will stand-in for the current seg's cell - var j; - var moreTd, moreWrap, moreLink; - - // Iterates through empty level cells and places "more" links inside if need be - function emptyCellsUntil(endCol) { // goes from current `col` to `endCol` - while (col < endCol) { - cell = { row: row, col: col }; - segsBelow = _this.getCellSegs(cell, levelLimit); - if (segsBelow.length) { - td = cellMatrix[levelLimit - 1][col]; - moreLink = _this.renderMoreLink(cell, segsBelow); - moreWrap = $('
').append(moreLink); - td.append(moreWrap); - moreNodes.push(moreWrap[0]); - } - col++; - } - } - - if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit? - levelSegs = rowStruct.segLevels[levelLimit - 1]; - cellMatrix = rowStruct.cellMatrix; - - limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level
for each column the segment occupies. will be one for each colspan - for (j = 0; j < colSegsBelow.length; j++) { - moreTd = $('').attr('rowspan', rowspan); - segsBelow = colSegsBelow[j]; - cell = { row: row, col: seg.leftCol + j }; - moreLink = this.renderMoreLink(cell, [ seg ].concat(segsBelow)); // count seg as hidden too - moreWrap = $('
').append(moreLink); - moreTd.append(moreWrap); - segMoreNodes.push(moreTd[0]); - moreNodes.push(moreTd[0]); - } - - td.addClass('fc-limited').after($(segMoreNodes)); // hide original
and inject replacements - limitedNodes.push(td[0]); - } - } - - emptyCellsUntil(view.colCnt); // finish off the level - rowStruct.moreEls = $(moreNodes); // for easy undoing later - rowStruct.limitedEls = $(limitedNodes); // for easy undoing later - } - }, - - - // Reveals all levels and removes all "more"-related elements for a grid's row. - // `row` is a row number. - unlimitRow: function(row) { - var rowStruct = this.rowStructs[row]; - - if (rowStruct.moreEls) { - rowStruct.moreEls.remove(); - rowStruct.moreEls = null; - } - - if (rowStruct.limitedEls) { - rowStruct.limitedEls.removeClass('fc-limited'); - rowStruct.limitedEls = null; - } - }, - - - // Renders an element that represents hidden event element for a cell. - // Responsible for attaching click handler as well. - renderMoreLink: function(cell, hiddenSegs) { - var _this = this; - var view = this.view; - - return $('') - .text( - this.getMoreLinkText(hiddenSegs.length) - ) - .on('click', function(ev) { - var clickOption = view.opt('eventLimitClick'); - var date = view.cellToDate(cell); - var moreEl = $(this); - var dayEl = _this.getCellDayEl(cell); - var allSegs = _this.getCellSegs(cell); - - // rescope the segments to be within the cell's date - var reslicedAllSegs = _this.resliceDaySegs(allSegs, date); - var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date); - - if (typeof clickOption === 'function') { - // the returned value can be an atomic option - clickOption = view.trigger('eventLimitClick', null, { - date: date, - dayEl: dayEl, - moreEl: moreEl, - segs: reslicedAllSegs, - hiddenSegs: reslicedHiddenSegs - }, ev); - } - - if (clickOption === 'popover') { - _this.showSegPopover(date, cell, moreEl, reslicedAllSegs); - } - else if (typeof clickOption === 'string') { // a view name - view.calendar.zoomTo(date, clickOption); - } - }); - }, - - - // Reveals the popover that displays all events within a cell - showSegPopover: function(date, cell, moreLink, segs) { - var _this = this; - var view = this.view; - var moreWrap = moreLink.parent(); // the
wrapper around the - var topEl; // the element we want to match the top coordinate of - var options; - - if (view.rowCnt == 1) { - topEl = this.view.el; // will cause the popover to cover any sort of header - } - else { - topEl = this.rowEls.eq(cell.row); // will align with top of row - } - - options = { - className: 'fc-more-popover', - content: this.renderSegPopoverContent(date, segs), - parentEl: this.el, - top: topEl.offset().top, - autoHide: true, // when the user clicks elsewhere, hide the popover - viewportConstrain: view.opt('popoverViewportConstrain'), - hide: function() { - // destroy everything when the popover is hidden - _this.segPopover.destroy(); - _this.segPopover = null; - _this.popoverSegs = null; - } - }; - - // Determine horizontal coordinate. - // We use the moreWrap instead of the
to avoid border confusion. - if (view.opt('isRTL')) { - options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border - } - else { - options.left = moreWrap.offset().left - 1; // -1 to be over cell border - } - - this.segPopover = new Popover(options); - this.segPopover.show(); - }, - - - // Builds the inner DOM contents of the segment popover - renderSegPopoverContent: function(date, segs) { - var view = this.view; - var isTheme = view.opt('theme'); - var title = date.format(view.opt('dayPopoverFormat')); - var content = $( - '
' + - '' + - '' + - htmlEscape(title) + - '' + - '
' + - '
' + - '
' + - '
' + - '
' - ); - var segContainer = content.find('.fc-event-container'); - var i; - - // render each seg's `el` and only return the visible segs - segs = this.renderSegs(segs, true); // disableResizing=true - this.popoverSegs = segs; - - for (i = 0; i < segs.length; i++) { - - // because segments in the popover are not part of a grid coordinate system, provide a hint to any - // grids that want to do drag-n-drop about which cell it came from - segs[i].cellDate = date; - - segContainer.append(segs[i].el); - } - - return content; - }, - - - // Given the events within an array of segment objects, reslice them to be in a single day - resliceDaySegs: function(segs, dayDate) { - var events = $.map(segs, function(seg) { - return seg.event; - }); - var dayStart = dayDate.clone().stripTime(); - var dayEnd = dayStart.clone().add(1, 'days'); - - return this.eventsToSegs(events, dayStart, dayEnd); - }, - - - // Generates the text that should be inside a "more" link, given the number of events it represents - getMoreLinkText: function(num) { - var view = this.view; - var opt = view.opt('eventLimitText'); - - if (typeof opt === 'function') { - return opt(num); - } - else { - return '+' + num + ' ' + opt; - } - }, - - - // Returns segments within a given cell. - // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs. - getCellSegs: function(cell, startLevel) { - var segMatrix = this.rowStructs[cell.row].segMatrix; - var level = startLevel || 0; - var segs = []; - var seg; - - while (level < segMatrix.length) { - seg = segMatrix[level][cell.col]; - if (seg) { - segs.push(seg); - } - level++; - } - - return segs; - } - -}); - -;; - -/* A component that renders one or more columns of vertical time slots -----------------------------------------------------------------------------------------------------------------------*/ - -function TimeGrid(view) { - Grid.call(this, view); // call the super-constructor -} - - -TimeGrid.prototype = createObject(Grid.prototype); // define the super-class -$.extend(TimeGrid.prototype, { - - slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines - snapDuration: null, // granularity of time for dragging and selecting - - minTime: null, // Duration object that denotes the first visible time of any given day - maxTime: null, // Duration object that denotes the exclusive visible end time of any given day - - dayEls: null, // cells elements in the day-row background - slatEls: null, // elements running horizontally across all columns - - slatTops: null, // an array of top positions, relative to the container. last item holds bottom of last slot - - highlightEl: null, // cell skeleton element for rendering the highlight - helperEl: null, // cell skeleton element for rendering the mock event "helper" - - - // Renders the time grid into `this.el`, which should already be assigned. - // Relies on the view's colCnt. In the future, this component should probably be self-sufficient. - render: function() { - this.processOptions(); - - this.el.html(this.renderHtml()); - - this.dayEls = this.el.find('.fc-day'); - this.slatEls = this.el.find('.fc-slats tr'); - - this.computeSlatTops(); - - Grid.prototype.render.call(this); // call the super-method - }, - - - // Renders the basic HTML skeleton for the grid - renderHtml: function() { - return '' + - '
' + - '' + - this.rowHtml('slotBg') + // leverages RowRenderer, which will call slotBgCellHtml - '
' + - '
' + - '
' + - '' + - this.slatRowHtml() + - '
' + - '
'; - }, - - - // Renders the HTML for a vertical background cell behind the slots. - // This method is distinct from 'bg' because we wanted a new `rowType` so the View could customize the rendering. - slotBgCellHtml: function(row, col, date) { - return this.bgCellHtml(row, col, date); - }, - - - // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL. - slatRowHtml: function() { - var view = this.view; - var calendar = view.calendar; - var isRTL = view.opt('isRTL'); - var html = ''; - var slotNormal = this.slotDuration.asMinutes() % 15 === 0; - var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations - var slotDate; // will be on the view's first day, but we only care about its time - var minutes; - var axisHtml; - - // Calculate the time for each slot - while (slotTime < this.maxTime) { - slotDate = view.start.clone().time(slotTime); // will be in UTC but that's good. to avoid DST issues - minutes = slotDate.minutes(); - - axisHtml = - '
' + - ((!slotNormal || !minutes) ? // if irregular slot duration, or on the hour, then display the time - '' + // for matchCellWidths - htmlEscape(calendar.formatDate(slotDate, view.opt('axisFormat'))) + - '' : - '' - ) + - '
' + - (isRTL ? axisHtml : '') + - "
'; - col = seg.col; - } - - // compute vertical position - dayDate = view.cellToDate(0, col); - top = this.computeDateTop(seg.start, dayDate); - bottom = this.computeDateTop(seg.end, dayDate); // the y position of the bottom edge - - // generate the cell HTML. bottom becomes negative because it needs to be a CSS value relative to the - // bottom edge of the zero-height container. - cellHtml += - '' + - '
' + - '
' + - '
' + - '
'; - } - - cellHtml = this.bookendCells(cellHtml, 'highlight'); - - return '' + - '
' + - '' + - '' + - cellHtml + - '' + - '
' + - '
'; - } - -}); - -;; - -/* Event-rendering methods for the TimeGrid class -----------------------------------------------------------------------------------------------------------------------*/ - -$.extend(TimeGrid.prototype, { - - segs: null, // segment objects rendered in the component. null of events haven't been rendered yet - eventSkeletonEl: null, // has cells with event-containers, which contain absolutely positioned event elements - - - // Renders the events onto the grid and returns an array of segments that have been rendered - renderEvents: function(events) { - var res = this.renderEventTable(events); - - this.eventSkeletonEl = $('
').append(res.tableEl); - this.el.append(this.eventSkeletonEl); - - this.segs = res.segs; - }, - - - // Retrieves rendered segment objects - getSegs: function() { - return this.segs || []; - }, - - - // Removes all event segment elements from the view - destroyEvents: function() { - Grid.prototype.destroyEvents.call(this); // call the super-method - - if (this.eventSkeletonEl) { - this.eventSkeletonEl.remove(); - this.eventSkeletonEl = null; - } - - this.segs = null; - }, - - - // Renders and returns the portion of the event-skeleton. - // Returns an object with properties 'tbodyEl' and 'segs'. - renderEventTable: function(events) { - var tableEl = $('
'); - var trEl = tableEl.find('tr'); - var segs = this.eventsToSegs(events); - var segCols; - var i, seg; - var col, colSegs; - var containerEl; - - segs = this.renderSegs(segs); // returns only the visible segs - segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg - - this.computeSegVerticals(segs); // compute and assign top/bottom - - for (col = 0; col < segCols.length; col++) { // iterate each column grouping - colSegs = segCols[col]; - placeSlotSegs(colSegs); // compute horizontal coordinates, z-index's, and reorder the array - - containerEl = $('
'); - - // assign positioning CSS and insert into container - for (i = 0; i < colSegs.length; i++) { - seg = colSegs[i]; - seg.el.css(this.generateSegPositionCss(seg)); - - // if the height is short, add a className for alternate styling - if (seg.bottom - seg.top < 30) { - seg.el.addClass('fc-short'); - } - - containerEl.append(seg.el); - } - - trEl.append($('
').append(containerEl)); - } - - this.bookendCells(trEl, 'eventSkeleton'); - - return { - tableEl: tableEl, - segs: segs - }; - }, - - - // Refreshes the CSS top/bottom coordinates for each segment element. Probably after a window resize/zoom. - updateSegVerticals: function() { - var segs = this.segs; - var i; - - if (segs) { - this.computeSegVerticals(segs); - - for (i = 0; i < segs.length; i++) { - segs[i].el.css( - this.generateSegVerticalCss(segs[i]) - ); - } - } - }, - - - // For each segment in an array, computes and assigns its top and bottom properties - computeSegVerticals: function(segs) { - var i, seg; - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - seg.top = this.computeDateTop(seg.start, seg.start); - seg.bottom = this.computeDateTop(seg.end, seg.start); - } - }, - - - // Renders the HTML for a single event segment's default rendering - renderSegHtml: function(seg, disableResizing) { - var view = this.view; - var event = seg.event; - var isDraggable = view.isEventDraggable(event); - var isResizable = !disableResizing && seg.isEnd && view.isEventResizable(event); - var classes = this.getSegClasses(seg, isDraggable, isResizable); - var skinCss = this.getEventSkinCss(event); - var timeText; - var fullTimeText; // more verbose time text. for the print stylesheet - var startTimeText; // just the start time text - - classes.unshift('fc-time-grid-event'); - - if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day... - // Don't display time text on segments that run entirely through a day. - // That would appear as midnight-midnight and would look dumb. - // Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am) - if (seg.isStart || seg.isEnd) { - timeText = view.getEventTimeText(seg.start, seg.end); - fullTimeText = view.getEventTimeText(seg.start, seg.end, 'LT'); - startTimeText = view.getEventTimeText(seg.start, null); - } - } else { - // Display the normal time text for the *event's* times - timeText = view.getEventTimeText(event); - fullTimeText = view.getEventTimeText(event, 'LT'); - startTimeText = view.getEventTimeText(event.start, null); - } - - return '' + - '
' + - (timeText ? - '
' + - '' + htmlEscape(timeText) + '' + - '
' : - '' - ) + - (event.title ? - '
' + - htmlEscape(event.title) + - '
' : - '' - ) + - '
' + - '
' + - (isResizable ? - '
' : - '' - ) + - ''; - }, - - - // Generates an object with CSS properties/values that should be applied to an event segment element. - // Contains important positioning-related properties that should be applied to any event element, customized or not. - generateSegPositionCss: function(seg) { - var view = this.view; - var isRTL = view.opt('isRTL'); - var shouldOverlap = view.opt('slotEventOverlap'); - var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point - var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point - var props = this.generateSegVerticalCss(seg); // get top/bottom first - var left; // amount of space from left edge, a fraction of the total width - var right; // amount of space from right edge, a fraction of the total width - - if (shouldOverlap) { - // double the width, but don't go beyond the maximum forward coordinate (1.0) - forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2); - } - - if (isRTL) { - left = 1 - forwardCoord; - right = backwardCoord; - } - else { - left = backwardCoord; - right = 1 - forwardCoord; - } - - props.zIndex = seg.level + 1; // convert from 0-base to 1-based - props.left = left * 100 + '%'; - props.right = right * 100 + '%'; - - if (shouldOverlap && seg.forwardPressure) { - // add padding to the edge so that forward stacked events don't cover the resizer's icon - props[isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width - } - - return props; - }, - - - // Generates an object with CSS properties for the top/bottom coordinates of a segment element - generateSegVerticalCss: function(seg) { - return { - top: seg.top, - bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container - }; - }, - - - // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col - groupSegCols: function(segs) { - var view = this.view; - var segCols = []; - var i; - - for (i = 0; i < view.colCnt; i++) { - segCols.push([]); - } - - for (i = 0; i < segs.length; i++) { - segCols[segs[i].col].push(segs[i]); - } - - return segCols; - } - -}); - - -// Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each. -// Also reorders the given array by date! -function placeSlotSegs(segs) { - var levels; - var level0; - var i; - - segs.sort(compareSegs); // order by date - levels = buildSlotSegLevels(segs); - computeForwardSlotSegs(levels); - - if ((level0 = levels[0])) { - - for (i = 0; i < level0.length; i++) { - computeSlotSegPressures(level0[i]); - } - - for (i = 0; i < level0.length; i++) { - computeSlotSegCoords(level0[i], 0, 0); - } - } -} - - -// Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is -// left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date. -function buildSlotSegLevels(segs) { - var levels = []; - var i, seg; - var j; - - for (i=0; i seg2.top && seg1.top < seg2.bottom; -} - - -// A cmp function for determining which forward segment to rely on more when computing coordinates. -function compareForwardSlotSegs(seg1, seg2) { - // put higher-pressure first - return seg2.forwardPressure - seg1.forwardPressure || - // put segments that are closer to initial edge first (and favor ones with no coords yet) - (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) || - // do normal sorting... - compareSegs(seg1, seg2); -} - -;; - -/* An abstract class from which other views inherit from -----------------------------------------------------------------------------------------------------------------------*/ -// Newer methods should be written as prototype methods, not in the monster `View` function at the bottom. - -View.prototype = { - - calendar: null, // owner Calendar object - coordMap: null, // a CoordMap object for converting pixel regions to dates - el: null, // the view's containing element. set by Calendar - - // important Moments - start: null, // the date of the very first cell - end: null, // the date after the very last cell - intervalStart: null, // the start of the interval of time the view represents (1st of month for month view) - intervalEnd: null, // the exclusive end of the interval of time the view represents - - // used for cell-to-date and date-to-cell calculations - rowCnt: null, // # of weeks - colCnt: null, // # of days displayed in a week - - isSelected: false, // boolean whether cells are user-selected or not - - // subclasses can optionally use a scroll container - scrollerEl: null, // the element that will most likely scroll when content is too tall - scrollTop: null, // cached vertical scroll value - - // classNames styled by jqui themes - widgetHeaderClass: null, - widgetContentClass: null, - highlightStateClass: null, - - // document handlers, bound to `this` object - documentMousedownProxy: null, - documentDragStartProxy: null, - - - // Serves as a "constructor" to suppliment the monster `View` constructor below - init: function() { - var tm = this.opt('theme') ? 'ui' : 'fc'; - - this.widgetHeaderClass = tm + '-widget-header'; - this.widgetContentClass = tm + '-widget-content'; - this.highlightStateClass = tm + '-state-highlight'; - - // save references to `this`-bound handlers - this.documentMousedownProxy = $.proxy(this, 'documentMousedown'); - this.documentDragStartProxy = $.proxy(this, 'documentDragStart'); - }, - - - // Renders the view inside an already-defined `this.el`. - // Subclasses should override this and then call the super method afterwards. - render: function() { - this.updateSize(); - this.trigger('viewRender', this, this, this.el); - - // attach handlers to document. do it here to allow for destroy/rerender - $(document) - .on('mousedown', this.documentMousedownProxy) - .on('dragstart', this.documentDragStartProxy); // jqui drag - }, - - - // Clears all view rendering, event elements, and unregisters handlers - destroy: function() { - this.unselect(); - this.trigger('viewDestroy', this, this, this.el); - this.destroyEvents(); - this.el.empty(); // removes inner contents but leaves the element intact - - $(document) - .off('mousedown', this.documentMousedownProxy) - .off('dragstart', this.documentDragStartProxy); - }, - - - // Used to determine what happens when the users clicks next/prev. Given -1 for prev, 1 for next. - // Should apply the delta to `date` (a Moment) and return it. - incrementDate: function(date, delta) { - // subclasses should implement - }, - - - /* Dimensions - ------------------------------------------------------------------------------------------------------------------*/ - - - // Refreshes anything dependant upon sizing of the container element of the grid - updateSize: function(isResize) { - if (isResize) { - this.recordScroll(); - } - this.updateHeight(); - this.updateWidth(); - }, - - - // Refreshes the horizontal dimensions of the calendar - updateWidth: function() { - // subclasses should implement - }, - - - // Refreshes the vertical dimensions of the calendar - updateHeight: function() { - var calendar = this.calendar; // we poll the calendar for height information - - this.setHeight( - calendar.getSuggestedViewHeight(), - calendar.isHeightAuto() - ); - }, - - - // Updates the vertical dimensions of the calendar to the specified height. - // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height. - setHeight: function(height, isAuto) { - // subclasses should implement - }, - - - // Given the total height of the view, return the number of pixels that should be used for the scroller. - // Utility for subclasses. - computeScrollerHeight: function(totalHeight) { - var both = this.el.add(this.scrollerEl); - var otherHeight; // cumulative height of everything that is not the scrollerEl in the view (header+borders) - - // fuckin IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked - both.css({ - position: 'relative', // cause a reflow, which will force fresh dimension recalculation - left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll - }); - otherHeight = this.el.outerHeight() - this.scrollerEl.height(); // grab the dimensions - both.css({ position: '', left: '' }); // undo hack - - return totalHeight - otherHeight; - }, - - - // Called for remembering the current scroll value of the scroller. - // Should be called before there is a destructive operation (like removing DOM elements) that might inadvertently - // change the scroll of the container. - recordScroll: function() { - if (this.scrollerEl) { - this.scrollTop = this.scrollerEl.scrollTop(); - } - }, - - - // Set the scroll value of the scroller to the previously recorded value. - // Should be called after we know the view's dimensions have been restored following some type of destructive - // operation (like temporarily removing DOM elements). - restoreScroll: function() { - if (this.scrollTop !== null) { - this.scrollerEl.scrollTop(this.scrollTop); - } - }, - - - /* Events - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders the events onto the view. - // Should be overriden by subclasses. Subclasses should call the super-method afterwards. - renderEvents: function(events) { - this.segEach(function(seg) { - this.trigger('eventAfterRender', seg.event, seg.event, seg.el); - }); - this.trigger('eventAfterAllRender'); - }, - - - // Removes event elements from the view. - // Should be overridden by subclasses. Should call this super-method FIRST, then subclass DOM destruction. - destroyEvents: function() { - this.segEach(function(seg) { - this.trigger('eventDestroy', seg.event, seg.event, seg.el); - }); - }, - - - // Given an event and the default element used for rendering, returns the element that should actually be used. - // Basically runs events and elements through the eventRender hook. - resolveEventEl: function(event, el) { - var custom = this.trigger('eventRender', event, event, el); - - if (custom === false) { // means don't render at all - el = null; - } - else if (custom && custom !== true) { - el = $(custom); - } - - return el; - }, - - - // Hides all rendered event segments linked to the given event - showEvent: function(event) { - this.segEach(function(seg) { - seg.el.css('visibility', ''); - }, event); - }, - - - // Shows all rendered event segments linked to the given event - hideEvent: function(event) { - this.segEach(function(seg) { - seg.el.css('visibility', 'hidden'); - }, event); - }, - - - // Iterates through event segments. Goes through all by default. - // If the optional `event` argument is specified, only iterates through segments linked to that event. - // The `this` value of the callback function will be the view. - segEach: function(func, event) { - var segs = this.getSegs(); - var i; - - for (i = 0; i < segs.length; i++) { - if (!event || segs[i].event._id === event._id) { - func.call(this, segs[i]); - } - } - }, - - - // Retrieves all the rendered segment objects for the view - getSegs: function() { - // subclasses must implement - }, - - - /* Event Drag Visualization - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event hovering over the specified date. - // `end` is a Moment and might be null. - // `seg` might be null. if specified, it is the segment object of the event being dragged. - // otherwise, an external event from outside the calendar is being dragged. - renderDrag: function(start, end, seg) { - // subclasses should implement - }, - - - // Unrenders a visual indication of event hovering - destroyDrag: function() { - // subclasses should implement - }, - - - // Handler for accepting externally dragged events being dropped in the view. - // Gets called when jqui's 'dragstart' is fired. - documentDragStart: function(ev, ui) { - var _this = this; - var dropDate = null; - var dragListener; - - if (this.opt('droppable')) { // only listen if this setting is on - - // listener that tracks mouse movement over date-associated pixel regions - dragListener = new DragListener(this.coordMap, { - cellOver: function(cell, date) { - dropDate = date; - _this.renderDrag(date); - }, - cellOut: function() { - dropDate = null; - _this.destroyDrag(); - } - }); - - // gets called, only once, when jqui drag is finished - $(document).one('dragstop', function(ev, ui) { - _this.destroyDrag(); - if (dropDate) { - _this.trigger('drop', ev.target, dropDate, ev, ui); - } - }); - - dragListener.startDrag(ev); // start listening immediately - } - }, - - - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - // Selects a date range on the view. `start` and `end` are both Moments. - // `ev` is the native mouse event that begin the interaction. - select: function(start, end, ev) { - this.unselect(ev); - this.renderSelection(start, end); - this.reportSelection(start, end, ev); - }, - - - // Renders a visual indication of the selection - renderSelection: function(start, end) { - // subclasses should implement - }, - - - // Called when a new selection is made. Updates internal state and triggers handlers. - reportSelection: function(start, end, ev) { - this.isSelected = true; - this.trigger('select', null, start, end, ev); - }, - - - // Undoes a selection. updates in the internal state and triggers handlers. - // `ev` is the native mouse event that began the interaction. - unselect: function(ev) { - if (this.isSelected) { - this.isSelected = false; - this.destroySelection(); - this.trigger('unselect', null, ev); - } - }, - - - // Unrenders a visual indication of selection - destroySelection: function() { - // subclasses should implement - }, - - - // Handler for unselecting when the user clicks something and the 'unselectAuto' setting is on - documentMousedown: function(ev) { - var ignore; - - // is there a selection, and has the user made a proper left click? - if (this.isSelected && this.opt('unselectAuto') && isPrimaryMouseButton(ev)) { - - // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element - ignore = this.opt('unselectCancel'); - if (!ignore || !$(ev.target).closest(ignore).length) { - this.unselect(ev); - } - } - } - -}; - - -// We are mixing JavaScript OOP design patterns here by putting methods and member variables in the closed scope of the -// constructor. Going forward, methods should be part of the prototype. -function View(calendar) { - var t = this; - - // exports - t.calendar = calendar; - t.opt = opt; - t.trigger = trigger; - t.isEventDraggable = isEventDraggable; - t.isEventResizable = isEventResizable; - t.eventDrop = eventDrop; - t.eventResize = eventResize; - - // imports - var reportEventChange = calendar.reportEventChange; - - // locals - var options = calendar.options; - var nextDayThreshold = moment.duration(options.nextDayThreshold); - - - t.init(); // the "constructor" that concerns the prototype methods - - - function opt(name) { - var v = options[name]; - if ($.isPlainObject(v) && !isForcedAtomicOption(name)) { - return smartProperty(v, t.name); - } - return v; - } - - - function trigger(name, thisObj) { - return calendar.trigger.apply( - calendar, - [name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t]) - ); - } - - - - /* Event Editable Boolean Calculations - ------------------------------------------------------------------------------*/ - - - function isEventDraggable(event) { - var source = event.source || {}; - - return firstDefined( - event.startEditable, - source.startEditable, - opt('eventStartEditable'), - event.editable, - source.editable, - opt('editable') - ); - } - - - function isEventResizable(event) { - var source = event.source || {}; - - return firstDefined( - event.durationEditable, - source.durationEditable, - opt('eventDurationEditable'), - event.editable, - source.editable, - opt('editable') - ); - } - - - - /* Event Elements - ------------------------------------------------------------------------------*/ - - - // Compute the text that should be displayed on an event's element. - // Based off the settings of the view. Possible signatures: - // .getEventTimeText(event, formatStr) - // .getEventTimeText(startMoment, endMoment, formatStr) - // .getEventTimeText(startMoment, null, formatStr) - // `timeFormat` is used but the `formatStr` argument can be used to override. - t.getEventTimeText = function(event, formatStr) { - var start; - var end; - - if (typeof event === 'object' && typeof formatStr === 'object') { - // first two arguments are actually moments (or null). shift arguments. - start = event; - end = formatStr; - formatStr = arguments[2]; - } - else { - // otherwise, an event object was the first argument - start = event.start; - end = event.end; - } - - formatStr = formatStr || opt('timeFormat'); - - if (end && opt('displayEventEnd')) { - return calendar.formatRange(start, end, formatStr); - } - else { - return calendar.formatDate(start, formatStr); - } - }; - - - - /* Event Modification Reporting - ---------------------------------------------------------------------------------*/ - - - function eventDrop(el, event, newStart, ev) { - var mutateResult = calendar.mutateEvent(event, newStart, null); - - trigger( - 'eventDrop', - el, - event, - mutateResult.dateDelta, - function() { - mutateResult.undo(); - reportEventChange(); - }, - ev, - {} // jqui dummy - ); - - reportEventChange(); - } - - - function eventResize(el, event, newEnd, ev) { - var mutateResult = calendar.mutateEvent(event, null, newEnd); - - trigger( - 'eventResize', - el, - event, - mutateResult.durationDelta, - function() { - mutateResult.undo(); - reportEventChange(); - }, - ev, - {} // jqui dummy - ); - - reportEventChange(); - } - - - // ==================================================================================================== - // Utilities for day "cells" - // ==================================================================================================== - // The "basic" views are completely made up of day cells. - // The "agenda" views have day cells at the top "all day" slot. - // This was the obvious common place to put these utilities, but they should be abstracted out into - // a more meaningful class (like DayEventRenderer). - // ==================================================================================================== - - - // For determining how a given "cell" translates into a "date": - // - // 1. Convert the "cell" (row and column) into a "cell offset" (the # of the cell, cronologically from the first). - // Keep in mind that column indices are inverted with isRTL. This is taken into account. - // - // 2. Convert the "cell offset" to a "day offset" (the # of days since the first visible day in the view). - // - // 3. Convert the "day offset" into a "date" (a Moment). - // - // The reverse transformation happens when transforming a date into a cell. - - - // exports - t.isHiddenDay = isHiddenDay; - t.skipHiddenDays = skipHiddenDays; - t.getCellsPerWeek = getCellsPerWeek; - t.dateToCell = dateToCell; - t.dateToDayOffset = dateToDayOffset; - t.dayOffsetToCellOffset = dayOffsetToCellOffset; - t.cellOffsetToCell = cellOffsetToCell; - t.cellToDate = cellToDate; - t.cellToCellOffset = cellToCellOffset; - t.cellOffsetToDayOffset = cellOffsetToDayOffset; - t.dayOffsetToDate = dayOffsetToDate; - t.rangeToSegments = rangeToSegments; - t.isMultiDayEvent = isMultiDayEvent; - - - // internals - var hiddenDays = opt('hiddenDays') || []; // array of day-of-week indices that are hidden - var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool) - var cellsPerWeek; - var dayToCellMap = []; // hash from dayIndex -> cellIndex, for one week - var cellToDayMap = []; // hash from cellIndex -> dayIndex, for one week - var isRTL = opt('isRTL'); - - - // initialize important internal variables - (function() { - - if (opt('weekends') === false) { - hiddenDays.push(0, 6); // 0=sunday, 6=saturday - } - - // Loop through a hypothetical week and determine which - // days-of-week are hidden. Record in both hashes (one is the reverse of the other). - for (var dayIndex=0, cellIndex=0; dayIndex<7; dayIndex++) { - dayToCellMap[dayIndex] = cellIndex; - isHiddenDayHash[dayIndex] = $.inArray(dayIndex, hiddenDays) != -1; - if (!isHiddenDayHash[dayIndex]) { - cellToDayMap[cellIndex] = dayIndex; - cellIndex++; - } - } - - cellsPerWeek = cellIndex; - if (!cellsPerWeek) { - throw 'invalid hiddenDays'; // all days were hidden? bad. - } - - })(); - - - // Is the current day hidden? - // `day` is a day-of-week index (0-6), or a Moment - function isHiddenDay(day) { - if (moment.isMoment(day)) { - day = day.day(); - } - return isHiddenDayHash[day]; - } - - - function getCellsPerWeek() { - return cellsPerWeek; - } - - - // Incrementing the current day until it is no longer a hidden day, returning a copy. - // If the initial value of `date` is not a hidden day, don't do anything. - // Pass `isExclusive` as `true` if you are dealing with an end date. - // `inc` defaults to `1` (increment one day forward each time) - function skipHiddenDays(date, inc, isExclusive) { - var out = date.clone(); - inc = inc || 1; - while ( - isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7] - ) { - out.add(inc, 'days'); - } - return out; - } - - - // - // TRANSFORMATIONS: cell -> cell offset -> day offset -> date - // - - // cell -> date (combines all transformations) - // Possible arguments: - // - row, col - // - { row:#, col: # } - function cellToDate() { - var cellOffset = cellToCellOffset.apply(null, arguments); - var dayOffset = cellOffsetToDayOffset(cellOffset); - var date = dayOffsetToDate(dayOffset); - return date; - } - - // cell -> cell offset - // Possible arguments: - // - row, col - // - { row:#, col:# } - function cellToCellOffset(row, col) { - var colCnt = t.colCnt; - - // rtl variables. wish we could pre-populate these. but where? - var dis = isRTL ? -1 : 1; - var dit = isRTL ? colCnt - 1 : 0; - - if (typeof row == 'object') { - col = row.col; - row = row.row; - } - var cellOffset = row * colCnt + (col * dis + dit); // column, adjusted for RTL (dis & dit) - - return cellOffset; - } - - // cell offset -> day offset - function cellOffsetToDayOffset(cellOffset) { - var day0 = t.start.day(); // first date's day of week - cellOffset += dayToCellMap[day0]; // normlize cellOffset to beginning-of-week - return Math.floor(cellOffset / cellsPerWeek) * 7 + // # of days from full weeks - cellToDayMap[ // # of days from partial last week - (cellOffset % cellsPerWeek + cellsPerWeek) % cellsPerWeek // crazy math to handle negative cellOffsets - ] - - day0; // adjustment for beginning-of-week normalization - } - - // day offset -> date - function dayOffsetToDate(dayOffset) { - return t.start.clone().add(dayOffset, 'days'); - } - - - // - // TRANSFORMATIONS: date -> day offset -> cell offset -> cell - // - - // date -> cell (combines all transformations) - function dateToCell(date) { - var dayOffset = dateToDayOffset(date); - var cellOffset = dayOffsetToCellOffset(dayOffset); - var cell = cellOffsetToCell(cellOffset); - return cell; - } - - // date -> day offset - function dateToDayOffset(date) { - return date.clone().stripTime().diff(t.start, 'days'); - } - - // day offset -> cell offset - function dayOffsetToCellOffset(dayOffset) { - var day0 = t.start.day(); // first date's day of week - dayOffset += day0; // normalize dayOffset to beginning-of-week - return Math.floor(dayOffset / 7) * cellsPerWeek + // # of cells from full weeks - dayToCellMap[ // # of cells from partial last week - (dayOffset % 7 + 7) % 7 // crazy math to handle negative dayOffsets - ] - - dayToCellMap[day0]; // adjustment for beginning-of-week normalization - } - - // cell offset -> cell (object with row & col keys) - function cellOffsetToCell(cellOffset) { - var colCnt = t.colCnt; - - // rtl variables. wish we could pre-populate these. but where? - var dis = isRTL ? -1 : 1; - var dit = isRTL ? colCnt - 1 : 0; - - var row = Math.floor(cellOffset / colCnt); - var col = ((cellOffset % colCnt + colCnt) % colCnt) * dis + dit; // column, adjusted for RTL (dis & dit) - return { - row: row, - col: col - }; - } - - - // - // Converts a date range into an array of segment objects. - // "Segments" are horizontal stretches of time, sliced up by row. - // A segment object has the following properties: - // - row - // - cols - // - isStart - // - isEnd - // - function rangeToSegments(start, end) { - - var rowCnt = t.rowCnt; - var colCnt = t.colCnt; - var segments = []; // array of segments to return - - // day offset for given date range - var dayRange = computeDayRange(start, end); // convert to a whole-day range - var rangeDayOffsetStart = dateToDayOffset(dayRange.start); - var rangeDayOffsetEnd = dateToDayOffset(dayRange.end); // an exclusive value - - // first and last cell offset for the given date range - // "last" implies inclusivity - var rangeCellOffsetFirst = dayOffsetToCellOffset(rangeDayOffsetStart); - var rangeCellOffsetLast = dayOffsetToCellOffset(rangeDayOffsetEnd) - 1; - - // loop through all the rows in the view - for (var row=0; row
' + - '' + // needed for matchCellWidths - htmlEscape(this.opt('weekNumberTitle')) + - '' + - '' + - '' + // needed for matchCellWidths - this.calendar.calculateWeekNumber(this.cellToDate(row, 0)) + - '' + - 's of the "number" row in the DayGrid's content skeleton. - // The number row will only exist if either day numbers or week numbers are turned on. - numberCellHtml: function(row, col, date) { - var classes; - - if (!this.dayNumbersVisible) { // if there are week numbers but not day numbers - return ''; // will create an empty space above events :( - } - - classes = this.dayGrid.getDayClasses(date); - classes.unshift('fc-day-number'); - - return '' + - '' + - date.date() + - '
' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '
' + - this.timeGrid.headHtml() + // render the day-of-week headers - '
' + - (this.dayGrid ? - '
' + - '
' : - '' - ) + - '
' + - '
' + - '
' + - '
'; - }, - - - // Generates the HTML that will go before the day-of week header cells. - // Queried by the TimeGrid subcomponent when generating rows. Ordering depends on isRTL. - headIntroHtml: function() { - var date; - var weekNumber; - var weekTitle; - var weekText; - - if (this.opt('weekNumbers')) { - date = this.cellToDate(0, 0); - weekNumber = this.calendar.calculateWeekNumber(date); - weekTitle = this.opt('weekNumberTitle'); - - if (this.opt('isRTL')) { - weekText = weekNumber + weekTitle; - } - else { - weekText = weekTitle + weekNumber; - } - - return '' + - '' + - '' + // needed for matchCellWidths - htmlEscape(weekText) + - '' + - ''; - } - else { - return ''; - } - }, - - - // Generates the HTML that goes before the all-day cells. - // Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL. - dayIntroHtml: function() { - return '' + - '' + - '' + // needed for matchCellWidths - (this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'))) + - '' + - ''; - }, - - - // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column. - slotBgIntroHtml: function() { - return ''; - }, - - - // Generates the HTML that goes before all other types of cells. - // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid. - // Queried by the TimeGrid and DayGrid subcomponents when generating rows. Ordering depends on isRTL. - introHtml: function() { - return ''; - }, - - - // Generates an HTML attribute string for setting the width of the axis, if it is known - axisStyleAttr: function() { - if (this.axisWidth !== null) { - return 'style="width:' + this.axisWidth + 'px"'; - } - return ''; - }, - - - /* Dimensions - ------------------------------------------------------------------------------------------------------------------*/ - - updateSize: function(isResize) { - if (isResize) { - this.timeGrid.resize(); - } - View.prototype.updateSize.call(this, isResize); - }, - - - // Refreshes the horizontal dimensions of the view - updateWidth: function() { - // make all axis cells line up, and record the width so newly created axis cells will have it - this.axisWidth = matchCellWidths(this.el.find('.fc-axis')); - }, - - - // Adjusts the vertical dimensions of the view to the specified values - setHeight: function(totalHeight, isAuto) { - var eventLimit; - var scrollerHeight; - - if (this.bottomRuleHeight === null) { - // calculate the height of the rule the very first time - this.bottomRuleHeight = this.bottomRuleEl.outerHeight(); - } - this.bottomRuleEl.hide(); // .show() will be called later if this
is necessary - - // reset all dimensions back to the original state - this.scrollerEl.css('overflow', ''); - unsetScroller(this.scrollerEl); - uncompensateScroll(this.noScrollRowEls); - - // limit number of events in the all-day area - if (this.dayGrid) { - this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed - - eventLimit = this.opt('eventLimit'); - if (eventLimit && typeof eventLimit !== 'number') { - eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number - } - if (eventLimit) { - this.dayGrid.limitRows(eventLimit); - } - } - - if (!isAuto) { // should we force dimensions of the scroll container, or let the contents be natural height? - - scrollerHeight = this.computeScrollerHeight(totalHeight); - if (setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars? - - // make the all-day and header rows lines up - compensateScroll(this.noScrollRowEls, getScrollbarWidths(this.scrollerEl)); - - // the scrollbar compensation might have changed text flow, which might affect height, so recalculate - // and reapply the desired height to the scroller. - scrollerHeight = this.computeScrollerHeight(totalHeight); - this.scrollerEl.height(scrollerHeight); - - this.restoreScroll(); - } - else { // no scrollbars - // still, force a height and display the bottom rule (marks the end of day) - this.scrollerEl.height(scrollerHeight).css('overflow', 'hidden'); // in case
goes outside - this.bottomRuleEl.show(); - } - } - }, - - - // Sets the scroll value of the scroller to the intial pre-configured state prior to allowing the user to change it. - resetScroll: function() { - var _this = this; - var scrollTime = moment.duration(this.opt('scrollTime')); - var top = this.timeGrid.computeTimeTop(scrollTime); - - // zoom can give weird floating-point values. rather scroll a little bit further - top = Math.ceil(top); - - if (top) { - top++; // to overcome top border that slots beyond the first have. looks better - } - - function scroll() { - _this.scrollerEl.scrollTop(top); - } - - scroll(); - setTimeout(scroll, 0); // overrides any previous scroll state made by the browser - }, - - - /* Events - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders events onto the view and populates the View's segment array - renderEvents: function(events) { - var dayEvents = []; - var timedEvents = []; - var daySegs = []; - var timedSegs; - var i; - - // separate the events into all-day and timed - for (i = 0; i < events.length; i++) { - if (events[i].allDay) { - dayEvents.push(events[i]); - } - else { - timedEvents.push(events[i]); - } - } - - // render the events in the subcomponents - timedSegs = this.timeGrid.renderEvents(timedEvents); - if (this.dayGrid) { - daySegs = this.dayGrid.renderEvents(dayEvents); - } - - // the all-day area is flexible and might have a lot of events, so shift the height - this.updateHeight(); - - View.prototype.renderEvents.call(this, events); // call the super-method - }, - - - // Retrieves all segment objects that are rendered in the view - getSegs: function() { - return this.timeGrid.getSegs().concat( - this.dayGrid ? this.dayGrid.getSegs() : [] - ); - }, - - - // Unrenders all event elements and clears internal segment data - destroyEvents: function() { - View.prototype.destroyEvents.call(this); // do this before the grids' segs have been cleared - - // if destroyEvents is being called as part of an event rerender, renderEvents will be called shortly - // after, so remember what the scroll value was so we can restore it. - this.recordScroll(); - - // destroy the events in the subcomponents - this.timeGrid.destroyEvents(); - if (this.dayGrid) { - this.dayGrid.destroyEvents(); - } - - // we DON'T need to call updateHeight() because: - // A) a renderEvents() call always happens after this, which will eventually call updateHeight() - // B) in IE8, this causes a flash whenever events are rerendered - }, - - - /* Event Dragging - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event being dragged over the view. - // A returned value of `true` signals that a mock "helper" event has been rendered. - renderDrag: function(start, end, seg) { - if (start.hasTime()) { - return this.timeGrid.renderDrag(start, end, seg); - } - else if (this.dayGrid) { - return this.dayGrid.renderDrag(start, end, seg); - } - }, - - - // Unrenders a visual indications of an event being dragged over the view - destroyDrag: function() { - this.timeGrid.destroyDrag(); - if (this.dayGrid) { - this.dayGrid.destroyDrag(); - } - }, - - - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of a selection - renderSelection: function(start, end) { - if (start.hasTime() || end.hasTime()) { - this.timeGrid.renderSelection(start, end); - } - else if (this.dayGrid) { - this.dayGrid.renderSelection(start, end); - } - }, - - - // Unrenders a visual indications of a selection - destroySelection: function() { - this.timeGrid.destroySelection(); - if (this.dayGrid) { - this.dayGrid.destroySelection(); - } - } - -}); - -;; - -/* A week view with an all-day cell area at the top, and a time grid below -----------------------------------------------------------------------------------------------------------------------*/ -// TODO: a WeekView mixin for calculating dates and titles - -fcViews.agendaWeek = AgendaWeekView; // register the view - -function AgendaWeekView(calendar) { - AgendaView.call(this, calendar); // call the super-constructor -} - - -AgendaWeekView.prototype = createObject(AgendaView.prototype); // define the super-class -$.extend(AgendaWeekView.prototype, { - - name: 'agendaWeek', - - - incrementDate: function(date, delta) { - return date.clone().stripTime().add(delta, 'weeks').startOf('week'); - }, - - - render: function(date) { - - this.intervalStart = date.clone().stripTime().startOf('week'); - this.intervalEnd = this.intervalStart.clone().add(1, 'weeks'); - - this.start = this.skipHiddenDays(this.intervalStart); - this.end = this.skipHiddenDays(this.intervalEnd, -1, true); - - this.title = this.calendar.formatRange( - this.start, - this.end.clone().subtract(1), // make inclusive by subtracting 1 ms - this.opt('titleFormat'), - ' \u2014 ' // emphasized dash - ); - - AgendaView.prototype.render.call(this, this.getCellsPerWeek()); // call the super-method - } - -}); - -;; - -/* A day view with an all-day cell area at the top, and a time grid below -----------------------------------------------------------------------------------------------------------------------*/ - -fcViews.agendaDay = AgendaDayView; // register the view - -function AgendaDayView(calendar) { - AgendaView.call(this, calendar); // call the super-constructor -} - - -AgendaDayView.prototype = createObject(AgendaView.prototype); // define the super-class -$.extend(AgendaDayView.prototype, { - - name: 'agendaDay', - - - incrementDate: function(date, delta) { - var out = date.clone().stripTime().add(delta, 'days'); - out = this.skipHiddenDays(out, delta < 0 ? -1 : 1); - return out; - }, - - - render: function(date) { - - this.start = this.intervalStart = date.clone().stripTime(); - this.end = this.intervalEnd = this.start.clone().add(1, 'days'); - - this.title = this.calendar.formatDate(this.start, this.opt('titleFormat')); - - AgendaView.prototype.render.call(this, 1); // call the super-method - } - -}); - -;; - +/*! + * FullCalendar v2.1.1 + * Docs & License: http://arshaw.com/fullcalendar/ + * (c) 2013 Adam Shaw + */ + +(function(factory) { + if (typeof define === 'function' && define.amd) { + define([ 'jquery', 'moment' ], factory); + } + else { + factory(jQuery, moment); + } +})(function($, moment) { + +;; + +var defaults = { + + lang: 'en', + + defaultTimedEventDuration: '02:00:00', + defaultAllDayEventDuration: { days: 1 }, + forceEventDuration: false, + nextDayThreshold: '09:00:00', // 9am + + // display + defaultView: 'month', + aspectRatio: 1.35, + header: { + left: 'title', + center: '', + right: 'today prev,next' + }, + weekends: true, + weekNumbers: false, + + weekNumberTitle: 'W', + weekNumberCalculation: 'local', + + //editable: false, + + // event ajax + lazyFetching: true, + startParam: 'start', + endParam: 'end', + timezoneParam: 'timezone', + + timezone: false, + + //allDayDefault: undefined, + + // time formats + titleFormat: { + month: 'MMMM YYYY', // like "September 1986". each language will override this + week: 'll', // like "Sep 4 1986" + day: 'LL' // like "September 4 1986" + }, + columnFormat: { + month: 'ddd', // like "Sat" + week: generateWeekColumnFormat, + day: 'dddd' // like "Saturday" + }, + timeFormat: { // for event elements + 'default': generateShortTimeFormat + }, + + displayEventEnd: { + month: false, + basicWeek: false, + 'default': true + }, + + // locale + isRTL: false, + defaultButtonText: { + prev: "prev", + next: "next", + prevYear: "prev year", + nextYear: "next year", + today: 'today', + month: 'month', + week: 'week', + day: 'day' + }, + + buttonIcons: { + prev: 'left-single-arrow', + next: 'right-single-arrow', + prevYear: 'left-double-arrow', + nextYear: 'right-double-arrow' + }, + + // jquery-ui theming + theme: false, + themeButtonIcons: { + prev: 'circle-triangle-w', + next: 'circle-triangle-e', + prevYear: 'seek-prev', + nextYear: 'seek-next' + }, + + dragOpacity: .75, + dragRevertDuration: 500, + dragScroll: true, + + //selectable: false, + unselectAuto: true, + + dropAccept: '*', + + eventLimit: false, + eventLimitText: 'more', + eventLimitClick: 'popover', + dayPopoverFormat: 'LL', + + handleWindowResize: true, + windowResizeDelay: 200 // milliseconds before a rerender happens + +}; + + +function generateShortTimeFormat(options, langData) { + return langData.longDateFormat('LT') + .replace(':mm', '(:mm)') + .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs + .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand +} + + +function generateWeekColumnFormat(options, langData) { + var format = langData.longDateFormat('L'); // for the format like "MM/DD/YYYY" + format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, ''); // strip the year off the edge, as well as other misc non-whitespace chars + if (options.isRTL) { + format += ' ddd'; // for RTL, add day-of-week to end + } + else { + format = 'ddd ' + format; // for LTR, add day-of-week to beginning + } + return format; +} + + +var langOptionHash = { + en: { + columnFormat: { + week: 'ddd M/D' // override for english. different from the generated default, which is MM/DD + }, + dayPopoverFormat: 'dddd, MMMM D' + } +}; + + +// right-to-left defaults +var rtlDefaults = { + header: { + left: 'next,prev today', + center: '', + right: 'title' + }, + buttonIcons: { + prev: 'right-single-arrow', + next: 'left-single-arrow', + prevYear: 'right-double-arrow', + nextYear: 'left-double-arrow' + }, + themeButtonIcons: { + prev: 'circle-triangle-e', + next: 'circle-triangle-w', + nextYear: 'seek-prev', + prevYear: 'seek-next' + } +}; + +;; + +var fc = $.fullCalendar = { version: "2.1.1" }; +var fcViews = fc.views = {}; + + +$.fn.fullCalendar = function(options) { + var args = Array.prototype.slice.call(arguments, 1); // for a possible method call + var res = this; // what this function will return (this jQuery object by default) + + this.each(function(i, _element) { // loop each DOM element involved + var element = $(_element); + var calendar = element.data('fullCalendar'); // get the existing calendar object (if any) + var singleRes; // the returned value of this single method call + + // a method call + if (typeof options === 'string') { + if (calendar && $.isFunction(calendar[options])) { + singleRes = calendar[options].apply(calendar, args); + if (!i) { + res = singleRes; // record the first method call result + } + if (options === 'destroy') { // for the destroy method, must remove Calendar object data + element.removeData('fullCalendar'); + } + } + } + // a new calendar initialization + else if (!calendar) { // don't initialize twice + calendar = new Calendar(element, options); + element.data('fullCalendar', calendar); + calendar.render(); + } + }); + + return res; +}; + + +// function for adding/overriding defaults +function setDefaults(d) { + mergeOptions(defaults, d); +} + + +// Recursively combines option hash-objects. +// Better than `$.extend(true, ...)` because arrays are not traversed/copied. +// +// called like: +// mergeOptions(target, obj1, obj2, ...) +// +function mergeOptions(target) { + + function mergeIntoTarget(name, value) { + if ($.isPlainObject(value) && $.isPlainObject(target[name]) && !isForcedAtomicOption(name)) { + // merge into a new object to avoid destruction + target[name] = mergeOptions({}, target[name], value); // combine. `value` object takes precedence + } + else if (value !== undefined) { // only use values that are set and not undefined + target[name] = value; + } + } + + for (var i=1; i").prependTo(element); + + header = new Header(t, options); + headerElement = header.render(); + if (headerElement) { + element.prepend(headerElement); + } + + changeView(options.defaultView); + + if (options.handleWindowResize) { + windowResizeProxy = debounce(windowResize, options.windowResizeDelay); // prevents rapid calls + $(window).resize(windowResizeProxy); + } + } + + + function destroy() { + + if (currentView) { + currentView.destroy(); + } + + header.destroy(); + content.remove(); + element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget'); + + $(window).unbind('resize', windowResizeProxy); + } + + + function elementVisible() { + return element.is(':visible'); + } + + + + // View Rendering + // ----------------------------------------------------------------------------------- + + + function changeView(viewName) { + renderView(0, viewName); + } + + + // Renders a view because of a date change, view-type change, or for the first time + function renderView(delta, viewName) { + ignoreWindowResize++; + + // if viewName is changing, destroy the old view + if (currentView && viewName && currentView.name !== viewName) { + header.deactivateButton(currentView.name); + freezeContentHeight(); // prevent a scroll jump when view element is removed + if (currentView.start) { // rendered before? + currentView.destroy(); + } + currentView.el.remove(); + currentView = null; + } + + // if viewName changed, or the view was never created, create a fresh view + if (!currentView && viewName) { + currentView = new fcViews[viewName](t); + currentView.el = $("
").appendTo(content); + header.activateButton(viewName); + } + + if (currentView) { + + // let the view determine what the delta means + if (delta) { + date = currentView.incrementDate(date, delta); + } + + // render or rerender the view + if ( + !currentView.start || // never rendered before + delta || // explicit date window change + !date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change + ) { + if (elementVisible()) { + + freezeContentHeight(); + if (currentView.start) { // rendered before? + currentView.destroy(); + } + currentView.render(date); + unfreezeContentHeight(); + + // need to do this after View::render, so dates are calculated + updateTitle(); + updateTodayButton(); + + getAndRenderEvents(); + } + } + } + + unfreezeContentHeight(); // undo any lone freezeContentHeight calls + ignoreWindowResize--; + } + + + + // Resizing + // ----------------------------------------------------------------------------------- + + + t.getSuggestedViewHeight = function() { + if (suggestedViewHeight === undefined) { + calcSize(); + } + return suggestedViewHeight; + }; + + + t.isHeightAuto = function() { + return options.contentHeight === 'auto' || options.height === 'auto'; + }; + + + function updateSize(shouldRecalc) { + if (elementVisible()) { + + if (shouldRecalc) { + _calcSize(); + } + + ignoreWindowResize++; + currentView.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto() + ignoreWindowResize--; + + return true; // signal success + } + } + + + function calcSize() { + if (elementVisible()) { + _calcSize(); + } + } + + + function _calcSize() { // assumes elementVisible + if (typeof options.contentHeight === 'number') { // exists and not 'auto' + suggestedViewHeight = options.contentHeight; + } + else if (typeof options.height === 'number') { // exists and not 'auto' + suggestedViewHeight = options.height - (headerElement ? headerElement.outerHeight(true) : 0); + } + else { + suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5)); + } + } + + + function windowResize(ev) { + if ( + !ignoreWindowResize && + ev.target === window && // so we don't process jqui "resize" events that have bubbled up + currentView.start // view has already been rendered + ) { + if (updateSize(true)) { + currentView.trigger('windowResize', _element); + } + } + } + + + + /* Event Fetching/Rendering + -----------------------------------------------------------------------------*/ + // TODO: going forward, most of this stuff should be directly handled by the view + + + function refetchEvents() { // can be called as an API method + destroyEvents(); // so that events are cleared before user starts waiting for AJAX + fetchAndRenderEvents(); + } + + + function renderEvents() { // destroys old events if previously rendered + if (elementVisible()) { + freezeContentHeight(); + currentView.destroyEvents(); // no performance cost if never rendered + currentView.renderEvents(events); + unfreezeContentHeight(); + } + } + + + function destroyEvents() { + freezeContentHeight(); + currentView.destroyEvents(); + unfreezeContentHeight(); + } + + + function getAndRenderEvents() { + if (!options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) { + fetchAndRenderEvents(); + } + else { + renderEvents(); + } + } + + + function fetchAndRenderEvents() { + fetchEvents(currentView.start, currentView.end); + // ... will call reportEvents + // ... which will call renderEvents + } + + + // called when event data arrives + function reportEvents(_events) { + events = _events; + renderEvents(); + } + + + // called when a single event's data has been changed + function reportEventChange() { + renderEvents(); + } + + + + /* Header Updating + -----------------------------------------------------------------------------*/ + + + function updateTitle() { + header.updateTitle(currentView.title); + } + + + function updateTodayButton() { + var now = t.getNow(); + if (now.isWithin(currentView.intervalStart, currentView.intervalEnd)) { + header.disableButton('today'); + } + else { + header.enableButton('today'); + } + } + + + + /* Selection + -----------------------------------------------------------------------------*/ + + + function select(start, end) { + + start = t.moment(start); + if (end) { + end = t.moment(end); + } + else if (start.hasTime()) { + end = start.clone().add(t.defaultTimedEventDuration); + } + else { + end = start.clone().add(t.defaultAllDayEventDuration); + } + + currentView.select(start, end); + } + + + function unselect() { // safe to be called before renderView + if (currentView) { + currentView.unselect(); + } + } + + + + /* Date + -----------------------------------------------------------------------------*/ + + + function prev() { + renderView(-1); + } + + + function next() { + renderView(1); + } + + + function prevYear() { + date.add(-1, 'years'); + renderView(); + } + + + function nextYear() { + date.add(1, 'years'); + renderView(); + } + + + function today() { + date = t.getNow(); + renderView(); + } + + + function gotoDate(dateInput) { + date = t.moment(dateInput); + renderView(); + } + + + function incrementDate(delta) { + date.add(moment.duration(delta)); + renderView(); + } + + + // Forces navigation to a view for the given date. + // `viewName` can be a specific view name or a generic one like "week" or "day". + function zoomTo(newDate, viewName) { + var viewStr; + var match; + + if (!viewName || fcViews[viewName] === undefined) { // a general view name, or "auto" + viewName = viewName || 'day'; + viewStr = header.getViewsWithButtons().join(' '); // space-separated string of all the views in the header + + // try to match a general view name, like "week", against a specific one, like "agendaWeek" + match = viewStr.match(new RegExp('\\w+' + capitaliseFirstLetter(viewName))); + + // fall back to the day view being used in the header + if (!match) { + match = viewStr.match(/\w+Day/); + } + + viewName = match ? match[0] : 'agendaDay'; // fall back to agendaDay + } + + date = newDate; + changeView(viewName); + } + + + function getDate() { + return date.clone(); + } + + + + /* Height "Freezing" + -----------------------------------------------------------------------------*/ + + + function freezeContentHeight() { + content.css({ + width: '100%', + height: content.height(), + overflow: 'hidden' + }); + } + + + function unfreezeContentHeight() { + content.css({ + width: '', + height: '', + overflow: '' + }); + } + + + + /* Misc + -----------------------------------------------------------------------------*/ + + + function getCalendar() { + return t; + } + + + function getView() { + return currentView; + } + + + function option(name, value) { + if (value === undefined) { + return options[name]; + } + if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') { + options[name] = value; + updateSize(true); // true = allow recalculation of height + } + } + + + function trigger(name, thisObj) { + if (options[name]) { + return options[name].apply( + thisObj || _element, + Array.prototype.slice.call(arguments, 2) + ); + } + } + +} + +;; + +/* Top toolbar area with buttons and title +----------------------------------------------------------------------------------------------------------------------*/ +// TODO: rename all header-related things to "toolbar" + +function Header(calendar, options) { + var t = this; + + // exports + t.render = render; + t.destroy = destroy; + t.updateTitle = updateTitle; + t.activateButton = activateButton; + t.deactivateButton = deactivateButton; + t.disableButton = disableButton; + t.enableButton = enableButton; + t.getViewsWithButtons = getViewsWithButtons; + + // locals + var el = $(); + var viewsWithButtons = []; + var tm; + + + function render() { + var sections = options.header; + + tm = options.theme ? 'ui' : 'fc'; + + if (sections) { + el = $("
") + .append(renderSection('left')) + .append(renderSection('right')) + .append(renderSection('center')) + .append('
'); + + return el; + } + } + + + function destroy() { + el.remove(); + } + + + function renderSection(position) { + var sectionEl = $('
'); + var buttonStr = options.header[position]; + + if (buttonStr) { + $.each(buttonStr.split(' '), function(i) { + var groupChildren = $(); + var isOnlyButtons = true; + var groupEl; + + $.each(this.split(','), function(j, buttonName) { + var buttonClick; + var themeIcon; + var normalIcon; + var defaultText; + var customText; + var innerHtml; + var classes; + var button; + + if (buttonName == 'title') { + groupChildren = groupChildren.add($('

 

')); // we always want it to take up height + isOnlyButtons = false; + } + else { + if (calendar[buttonName]) { // a calendar method + buttonClick = function() { + calendar[buttonName](); + }; + } + else if (fcViews[buttonName]) { // a view name + buttonClick = function() { + calendar.changeView(buttonName); + }; + viewsWithButtons.push(buttonName); + } + if (buttonClick) { + + // smartProperty allows different text per view button (ex: "Agenda Week" vs "Basic Week") + themeIcon = smartProperty(options.themeButtonIcons, buttonName); + normalIcon = smartProperty(options.buttonIcons, buttonName); + defaultText = smartProperty(options.defaultButtonText, buttonName); + customText = smartProperty(options.buttonText, buttonName); + + if (customText) { + innerHtml = htmlEscape(customText); + } + else if (themeIcon && options.theme) { + innerHtml = ""; + } + else if (normalIcon && !options.theme) { + innerHtml = ""; + } + else { + innerHtml = htmlEscape(defaultText || buttonName); + } + + classes = [ + 'fc-' + buttonName + '-button', + tm + '-button', + tm + '-state-default' + ]; + + button = $( // type="button" so that it doesn't submit a form + '' + ) + .click(function() { + // don't process clicks for disabled buttons + if (!button.hasClass(tm + '-state-disabled')) { + + buttonClick(); + + // after the click action, if the button becomes the "active" tab, or disabled, + // it should never have a hover class, so remove it now. + if ( + button.hasClass(tm + '-state-active') || + button.hasClass(tm + '-state-disabled') + ) { + button.removeClass(tm + '-state-hover'); + } + } + }) + .mousedown(function() { + // the *down* effect (mouse pressed in). + // only on buttons that are not the "active" tab, or disabled + button + .not('.' + tm + '-state-active') + .not('.' + tm + '-state-disabled') + .addClass(tm + '-state-down'); + }) + .mouseup(function() { + // undo the *down* effect + button.removeClass(tm + '-state-down'); + }) + .hover( + function() { + // the *hover* effect. + // only on buttons that are not the "active" tab, or disabled + button + .not('.' + tm + '-state-active') + .not('.' + tm + '-state-disabled') + .addClass(tm + '-state-hover'); + }, + function() { + // undo the *hover* effect + button + .removeClass(tm + '-state-hover') + .removeClass(tm + '-state-down'); // if mouseleave happens before mouseup + } + ); + + groupChildren = groupChildren.add(button); + } + } + }); + + if (isOnlyButtons) { + groupChildren + .first().addClass(tm + '-corner-left').end() + .last().addClass(tm + '-corner-right').end(); + } + + if (groupChildren.length > 1) { + groupEl = $('
'); + if (isOnlyButtons) { + groupEl.addClass('fc-button-group'); + } + groupEl.append(groupChildren); + sectionEl.append(groupEl); + } + else { + sectionEl.append(groupChildren); // 1 or 0 children + } + }); + } + + return sectionEl; + } + + + function updateTitle(text) { + el.find('h2').text(text); + } + + + function activateButton(buttonName) { + el.find('.fc-' + buttonName + '-button') + .addClass(tm + '-state-active'); + } + + + function deactivateButton(buttonName) { + el.find('.fc-' + buttonName + '-button') + .removeClass(tm + '-state-active'); + } + + + function disableButton(buttonName) { + el.find('.fc-' + buttonName + '-button') + .attr('disabled', 'disabled') + .addClass(tm + '-state-disabled'); + } + + + function enableButton(buttonName) { + el.find('.fc-' + buttonName + '-button') + .removeAttr('disabled') + .removeClass(tm + '-state-disabled'); + } + + + function getViewsWithButtons() { + return viewsWithButtons; + } + +} + +;; + +fc.sourceNormalizers = []; +fc.sourceFetchers = []; + +var ajaxDefaults = { + dataType: 'json', + cache: false +}; + +var eventGUID = 1; + + +function EventManager(options) { // assumed to be a calendar + var t = this; + + + // exports + t.isFetchNeeded = isFetchNeeded; + t.fetchEvents = fetchEvents; + t.addEventSource = addEventSource; + t.removeEventSource = removeEventSource; + t.updateEvent = updateEvent; + t.renderEvent = renderEvent; + t.removeEvents = removeEvents; + t.clientEvents = clientEvents; + t.mutateEvent = mutateEvent; + + + // imports + var trigger = t.trigger; + var getView = t.getView; + var reportEvents = t.reportEvents; + var getEventEnd = t.getEventEnd; + + + // locals + var stickySource = { events: [] }; + var sources = [ stickySource ]; + var rangeStart, rangeEnd; + var currentFetchID = 0; + var pendingSourceCnt = 0; + var loadingLevel = 0; + var cache = []; + + + $.each( + (options.events ? [ options.events ] : []).concat(options.eventSources || []), + function(i, sourceInput) { + var source = buildEventSource(sourceInput); + if (source) { + sources.push(source); + } + } + ); + + + + /* Fetching + -----------------------------------------------------------------------------*/ + + + function isFetchNeeded(start, end) { + return !rangeStart || // nothing has been fetched yet? + // or, a part of the new range is outside of the old range? (after normalizing) + start.clone().stripZone() < rangeStart.clone().stripZone() || + end.clone().stripZone() > rangeEnd.clone().stripZone(); + } + + + function fetchEvents(start, end) { + rangeStart = start; + rangeEnd = end; + cache = []; + var fetchID = ++currentFetchID; + var len = sources.length; + pendingSourceCnt = len; + for (var i=0; i cells, find the cell with the largest natural width and set the widths of all the +// cells to be that width. +// PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline +function matchCellWidths(els) { + var maxInnerWidth = 0; + + els.find('> *').each(function(i, innerEl) { + var innerWidth = $(innerEl).outerWidth(); + if (innerWidth > maxInnerWidth) { + maxInnerWidth = innerWidth; + } + }); + + maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance + + els.width(maxInnerWidth); + + return maxInnerWidth; +} + + +// Turns a container element into a scroller if its contents is taller than the allotted height. +// Returns true if the element is now a scroller, false otherwise. +// NOTE: this method is best because it takes weird zooming dimensions into account +function setPotentialScroller(containerEl, height) { + containerEl.height(height).addClass('fc-scroller'); + + // are scrollbars needed? + if (containerEl[0].scrollHeight - 1 > containerEl[0].clientHeight) { // !!! -1 because IE is often off-by-one :( + return true; + } + + unsetScroller(containerEl); // undo + return false; +} + + +// Takes an element that might have been a scroller, and turns it back into a normal element. +function unsetScroller(containerEl) { + containerEl.height('').removeClass('fc-scroller'); +} + + +/* General DOM Utilities +----------------------------------------------------------------------------------------------------------------------*/ + + +// borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51 +function getScrollParent(el) { + var position = el.css('position'), + scrollParent = el.parents().filter(function() { + var parent = $(this); + return (/(auto|scroll)/).test( + parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x') + ); + }).eq(0); + + return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent; +} + + +// Given a container element, return an object with the pixel values of the left/right scrollbars. +// Left scrollbars might occur on RTL browsers (IE maybe?) but I have not tested. +// PREREQUISITE: container element must have a single child with display:block +function getScrollbarWidths(container) { + var containerLeft = container.offset().left; + var containerRight = containerLeft + container.width(); + var inner = container.children(); + var innerLeft = inner.offset().left; + var innerRight = innerLeft + inner.outerWidth(); + + return { + left: innerLeft - containerLeft, + right: containerRight - innerRight + }; +} + + +// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac) +function isPrimaryMouseButton(ev) { + return ev.which == 1 && !ev.ctrlKey; +} + + +/* FullCalendar-specific Misc Utilities +----------------------------------------------------------------------------------------------------------------------*/ + + +// Creates a basic segment with the intersection of the two ranges. Returns undefined if no intersection. +// Expects all dates to be normalized to the same timezone beforehand. +function intersectionToSeg(subjectStart, subjectEnd, intervalStart, intervalEnd) { + var segStart, segEnd; + var isStart, isEnd; + + if (subjectEnd > intervalStart && subjectStart < intervalEnd) { // in bounds at all? + + if (subjectStart >= intervalStart) { + segStart = subjectStart.clone(); + isStart = true; + } + else { + segStart = intervalStart.clone(); + isStart = false; + } + + if (subjectEnd <= intervalEnd) { + segEnd = subjectEnd.clone(); + isEnd = true; + } + else { + segEnd = intervalEnd.clone(); + isEnd = false; + } + + return { + start: segStart, + end: segEnd, + isStart: isStart, + isEnd: isEnd + }; + } +} + + +function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object + obj = obj || {}; + if (obj[name] !== undefined) { + return obj[name]; + } + var parts = name.split(/(?=[A-Z])/), + i = parts.length - 1, res; + for (; i>=0; i--) { + res = obj[parts[i].toLowerCase()]; + if (res !== undefined) { + return res; + } + } + return obj['default']; +} + + +/* Date Utilities +----------------------------------------------------------------------------------------------------------------------*/ + +var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ]; + + +// Diffs the two moments into a Duration where full-days are recorded first, then the remaining time. +// Moments will have their timezones normalized. +function dayishDiff(a, b) { + return moment.duration({ + days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'), + ms: a.time() - b.time() + }); +} + + +function isNativeDate(input) { + return Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date; +} + + +function dateCompare(a, b) { // works with Moments and native Dates + return a - b; +} + + +/* General Utilities +----------------------------------------------------------------------------------------------------------------------*/ + +fc.applyAll = applyAll; // export + + +// Create an object that has the given prototype. Just like Object.create +function createObject(proto) { + var f = function() {}; + f.prototype = proto; + return new f(); +} + + +// Copies specifically-owned (non-protoype) properties of `b` onto `a`. +// FYI, $.extend would copy *all* properties of `b` onto `a`. +function extend(a, b) { + for (var i in b) { + if (b.hasOwnProperty(i)) { + a[i] = b[i]; + } + } +} + + +function applyAll(functions, thisObj, args) { + if ($.isFunction(functions)) { + functions = [ functions ]; + } + if (functions) { + var i; + var ret; + for (i=0; i/g, '>') + .replace(/'/g, ''') + .replace(/"/g, '"') + .replace(/\n/g, '
'); +} + + +function stripHtmlEntities(text) { + return text.replace(/&.*?;/g, ''); +} + + +function capitaliseFirstLetter(str) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + + +// Returns a function, that, as long as it continues to be invoked, will not +// be triggered. The function will be called after it stops being called for +// N milliseconds. +// https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714 +function debounce(func, wait) { + var timeoutId; + var args; + var context; + var timestamp; // of most recent call + var later = function() { + var last = +new Date() - timestamp; + if (last < wait && last > 0) { + timeoutId = setTimeout(later, wait - last); + } + else { + timeoutId = null; + func.apply(context, args); + if (!timeoutId) { + context = args = null; + } + } + }; + + return function() { + context = this; + args = arguments; + timestamp = +new Date(); + if (!timeoutId) { + timeoutId = setTimeout(later, wait); + } + }; +} + +;; + +var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/; +var ambigTimeOrZoneRegex = + /^\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+)?)?)?)?)?$/; + + +// Creating +// ------------------------------------------------------------------------------------------------- + +// Creates a new moment, similar to the vanilla moment(...) constructor, but with +// extra features (ambiguous time, enhanced formatting). When gived an existing moment, +// it will function as a clone (and retain the zone of the moment). Anything else will +// result in a moment in the local zone. +fc.moment = function() { + return makeMoment(arguments); +}; + +// Sames as fc.moment, but forces the resulting moment to be in the UTC timezone. +fc.moment.utc = function() { + var mom = makeMoment(arguments, true); + + // Force it into UTC because makeMoment doesn't guarantee it. + if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone + mom.utc(); + } + + return mom; +}; + +// Same as fc.moment, but when given an ISO8601 string, the timezone offset is preserved. +// ISO8601 strings with no timezone offset will become ambiguously zoned. +fc.moment.parseZone = function() { + return makeMoment(arguments, true, true); +}; + +// Builds an FCMoment from args. When given an existing moment, it clones. When given a native +// Date, or called with no arguments (the current time), the resulting moment will be local. +// Anything else needs to be "parsed" (a string or an array), and will be affected by: +// parseAsUTC - if there is no zone information, should we parse the input in UTC? +// parseZone - if there is zone information, should we force the zone of the moment? +function makeMoment(args, parseAsUTC, parseZone) { + var input = args[0]; + var isSingleString = args.length == 1 && typeof input === 'string'; + var isAmbigTime; + var isAmbigZone; + var ambigMatch; + var output; // an object with fields for the new FCMoment object + + if (moment.isMoment(input)) { + output = moment.apply(null, args); // clone it + + // the ambig properties have not been preserved in the clone, so reassign them + if (input._ambigTime) { + output._ambigTime = true; + } + if (input._ambigZone) { + output._ambigZone = true; + } + } + else if (isNativeDate(input) || input === undefined) { + output = moment.apply(null, args); // will be local + } + else { // "parsing" is required + isAmbigTime = false; + isAmbigZone = false; + + if (isSingleString) { + if (ambigDateOfMonthRegex.test(input)) { + // accept strings like '2014-05', but convert to the first of the month + input += '-01'; + args = [ input ]; // for when we pass it on to moment's constructor + isAmbigTime = true; + isAmbigZone = true; + } + else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) { + isAmbigTime = !ambigMatch[5]; // no time part? + isAmbigZone = true; + } + } + else if ($.isArray(input)) { + // arrays have no timezone information, so assume ambiguous zone + isAmbigZone = true; + } + // otherwise, probably a string with a format + + if (parseAsUTC) { + output = moment.utc.apply(moment, args); + } + else { + output = moment.apply(null, args); + } + + if (isAmbigTime) { + output._ambigTime = true; + output._ambigZone = true; // ambiguous time always means ambiguous zone + } + else if (parseZone) { // let's record the inputted zone somehow + if (isAmbigZone) { + output._ambigZone = true; + } + else if (isSingleString) { + output.zone(input); // if not a valid zone, will assign UTC + } + } + } + + return new FCMoment(output); +} + +// Our subclass of Moment. +// Accepts an object with the internal Moment properties that should be copied over to +// `this` object (most likely another Moment object). The values in this data must not +// be referenced by anything else (two moments sharing a Date object for example). +function FCMoment(internalData) { + extend(this, internalData); +} + +// Chain the prototype to Moment's +FCMoment.prototype = createObject(moment.fn); + +// We need this because Moment's implementation won't create an FCMoment, +// nor will it copy over the ambig flags. +FCMoment.prototype.clone = function() { + return makeMoment([ this ]); +}; + + +// Time-of-day +// ------------------------------------------------------------------------------------------------- + +// GETTER +// Returns a Duration with the hours/minutes/seconds/ms values of the moment. +// If the moment has an ambiguous time, a duration of 00:00 will be returned. +// +// SETTER +// You can supply a Duration, a Moment, or a Duration-like argument. +// When setting the time, and the moment has an ambiguous time, it then becomes unambiguous. +FCMoment.prototype.time = function(time) { + if (time == null) { // getter + return moment.duration({ + hours: this.hours(), + minutes: this.minutes(), + seconds: this.seconds(), + milliseconds: this.milliseconds() + }); + } + else { // setter + + delete this._ambigTime; // mark that the moment now has a time + + if (!moment.isDuration(time) && !moment.isMoment(time)) { + time = moment.duration(time); + } + + // The day value should cause overflow (so 24 hours becomes 00:00:00 of next day). + // Only for Duration times, not Moment times. + var dayHours = 0; + if (moment.isDuration(time)) { + dayHours = Math.floor(time.asDays()) * 24; + } + + // We need to set the individual fields. + // Can't use startOf('day') then add duration. In case of DST at start of day. + return this.hours(dayHours + time.hours()) + .minutes(time.minutes()) + .seconds(time.seconds()) + .milliseconds(time.milliseconds()); + } +}; + +// Converts the moment to UTC, stripping out its time-of-day and timezone offset, +// but preserving its YMD. A moment with a stripped time will display no time +// nor timezone offset when .format() is called. +FCMoment.prototype.stripTime = function() { + var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array + + // set the internal UTC flag + moment.fn.utc.call(this); // call the original method, because we don't want to affect _ambigZone + + this.year(a[0]) // TODO: find a way to do this in one shot + .month(a[1]) + .date(a[2]) + .hours(0) + .minutes(0) + .seconds(0) + .milliseconds(0); + + // Mark the time as ambiguous. This needs to happen after the .utc() call, which calls .zone(), which + // clears all ambig flags. Same concept with the .year/month/date calls in the case of moment-timezone. + this._ambigTime = true; + this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset + + return this; // for chaining +}; + +// Returns if the moment has a non-ambiguous time (boolean) +FCMoment.prototype.hasTime = function() { + return !this._ambigTime; +}; + + +// Timezone +// ------------------------------------------------------------------------------------------------- + +// Converts the moment to UTC, stripping out its timezone offset, but preserving its +// YMD and time-of-day. A moment with a stripped timezone offset will display no +// timezone offset when .format() is called. +FCMoment.prototype.stripZone = function() { + var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array + var wasAmbigTime = this._ambigTime; + + moment.fn.utc.call(this); // set the internal UTC flag + + this.year(a[0]) // TODO: find a way to do this in one shot + .month(a[1]) + .date(a[2]) + .hours(a[3]) + .minutes(a[4]) + .seconds(a[5]) + .milliseconds(a[6]); + + if (wasAmbigTime) { + // the above call to .utc()/.zone() unfortunately clears the ambig flags, so reassign + this._ambigTime = true; + } + + // Mark the zone as ambiguous. This needs to happen after the .utc() call, which calls .zone(), which + // clears all ambig flags. Same concept with the .year/month/date calls in the case of moment-timezone. + this._ambigZone = true; + + return this; // for chaining +}; + +// Returns of the moment has a non-ambiguous timezone offset (boolean) +FCMoment.prototype.hasZone = function() { + return !this._ambigZone; +}; + +// this method implicitly marks a zone +FCMoment.prototype.zone = function(tzo) { + + if (tzo != null) { + // FYI, the delete statements need to be before the .zone() call or else chaos ensues + // for reasons I don't understand. + delete this._ambigTime; + delete this._ambigZone; + } + + return moment.fn.zone.apply(this, arguments); +}; + +// this method implicitly marks a zone +FCMoment.prototype.local = function() { + var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array + var wasAmbigZone = this._ambigZone; + + // will happen anyway via .local()/.zone(), but don't want to rely on internal implementation + delete this._ambigTime; + delete this._ambigZone; + + moment.fn.local.apply(this, arguments); + + if (wasAmbigZone) { + // If the moment was ambiguously zoned, the date fields were stored as UTC. + // We want to preserve these, but in local time. + this.year(a[0]) // TODO: find a way to do this in one shot + .month(a[1]) + .date(a[2]) + .hours(a[3]) + .minutes(a[4]) + .seconds(a[5]) + .milliseconds(a[6]); + } + + return this; // for chaining +}; + +// this method implicitly marks a zone +FCMoment.prototype.utc = function() { + + // will happen anyway via .local()/.zone(), but don't want to rely on internal implementation + delete this._ambigTime; + delete this._ambigZone; + + return moment.fn.utc.apply(this, arguments); +}; + + +// Formatting +// ------------------------------------------------------------------------------------------------- + +FCMoment.prototype.format = function() { + if (arguments[0]) { + return formatDate(this, arguments[0]); // our extended formatting + } + if (this._ambigTime) { + return momentFormat(this, 'YYYY-MM-DD'); + } + if (this._ambigZone) { + return momentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss'); + } + return momentFormat(this); // default moment original formatting +}; + +FCMoment.prototype.toISOString = function() { + if (this._ambigTime) { + return momentFormat(this, 'YYYY-MM-DD'); + } + if (this._ambigZone) { + return momentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss'); + } + return moment.fn.toISOString.apply(this, arguments); +}; + + +// Querying +// ------------------------------------------------------------------------------------------------- + +// Is the moment within the specified range? `end` is exclusive. +FCMoment.prototype.isWithin = function(start, end) { + var a = commonlyAmbiguate([ this, start, end ]); + return a[0] >= a[1] && a[0] < a[2]; +}; + +// When isSame is called with units, timezone ambiguity is normalized before the comparison happens. +// If no units are specified, the two moments must be identically the same, with matching ambig flags. +FCMoment.prototype.isSame = function(input, units) { + var a; + + if (units) { + a = commonlyAmbiguate([ this, input ], true); // normalize timezones but don't erase times + return moment.fn.isSame.call(a[0], a[1], units); + } + else { + input = fc.moment.parseZone(input); // normalize input + return moment.fn.isSame.call(this, input) && + Boolean(this._ambigTime) === Boolean(input._ambigTime) && + Boolean(this._ambigZone) === Boolean(input._ambigZone); + } +}; + +// Make these query methods work with ambiguous moments +$.each([ + 'isBefore', + 'isAfter' +], function(i, methodName) { + FCMoment.prototype[methodName] = function(input, units) { + var a = commonlyAmbiguate([ this, input ]); + return moment.fn[methodName].call(a[0], a[1], units); + }; +}); + + +// Misc Internals +// ------------------------------------------------------------------------------------------------- + +// given an array of moment-like inputs, return a parallel array w/ moments similarly ambiguated. +// for example, of one moment has ambig time, but not others, all moments will have their time stripped. +// set `preserveTime` to `true` to keep times, but only normalize zone ambiguity. +function commonlyAmbiguate(inputs, preserveTime) { + var outputs = []; + var anyAmbigTime = false; + var anyAmbigZone = false; + var i; + + for (i=0; i "MMMM D YYYY" + formatStr = localeData.longDateFormat(formatStr) || formatStr; + // BTW, this is not important for `formatDate` because it is impossible to put custom tokens + // or non-zero areas in Moment's localized format strings. + + separator = separator || ' - '; + + return formatRangeWithChunks( + date1, + date2, + getFormatStringChunks(formatStr), + separator, + isRTL + ); +} +fc.formatRange = formatRange; // expose + + +function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) { + var chunkStr; // the rendering of the chunk + var leftI; + var leftStr = ''; + var rightI; + var rightStr = ''; + var middleI; + var middleStr1 = ''; + var middleStr2 = ''; + var middleStr = ''; + + // Start at the leftmost side of the formatting string and continue until you hit a token + // that is not the same between dates. + for (leftI=0; leftIleftI; rightI--) { + chunkStr = formatSimilarChunk(date1, date2, chunks[rightI]); + if (chunkStr === false) { + break; + } + rightStr = chunkStr + rightStr; + } + + // The area in the middle is different for both of the dates. + // Collect them distinctly so we can jam them together later. + for (middleI=leftI; middleI<=rightI; middleI++) { + middleStr1 += formatDateWithChunk(date1, chunks[middleI]); + middleStr2 += formatDateWithChunk(date2, chunks[middleI]); + } + + if (middleStr1 || middleStr2) { + if (isRTL) { + middleStr = middleStr2 + separator + middleStr1; + } + else { + middleStr = middleStr1 + separator + middleStr2; + } + } + + return leftStr + middleStr + rightStr; +} + + +var similarUnitMap = { + Y: 'year', + M: 'month', + D: 'day', // day of month + d: 'day', // day of week + // prevents a separator between anything time-related... + A: 'second', // AM/PM + a: 'second', // am/pm + T: 'second', // A/P + t: 'second', // a/p + H: 'second', // hour (24) + h: 'second', // hour (12) + m: 'second', // minute + s: 'second' // second +}; +// TODO: week maybe? + + +// Given a formatting chunk, and given that both dates are similar in the regard the +// formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`. +function formatSimilarChunk(date1, date2, chunk) { + var token; + var unit; + + if (typeof chunk === 'string') { // a literal string + return chunk; + } + else if ((token = chunk.token)) { + unit = similarUnitMap[token.charAt(0)]; + // are the dates the same for this unit of measurement? + if (unit && date1.isSame(date2, unit)) { + return momentFormat(date1, token); // would be the same if we used `date2` + // BTW, don't support custom tokens + } + } + + return false; // the chunk is NOT the same for the two dates + // BTW, don't support splitting on non-zero areas +} + + +// Chunking Utils +// ------------------------------------------------------------------------------------------------- + + +var formatStringChunkCache = {}; + + +function getFormatStringChunks(formatStr) { + if (formatStr in formatStringChunkCache) { + return formatStringChunkCache[formatStr]; + } + return (formatStringChunkCache[formatStr] = chunkFormatString(formatStr)); +} + + +// Break the formatting string into an array of chunks +function chunkFormatString(formatStr) { + var chunks = []; + var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LT|(\w)\4*o?)|([^\w\[\(]+)/g; // TODO: more descrimination + var match; + + while ((match = chunker.exec(formatStr))) { + if (match[1]) { // a literal string inside [ ... ] + chunks.push(match[1]); + } + else if (match[2]) { // non-zero formatting inside ( ... ) + chunks.push({ maybe: chunkFormatString(match[2]) }); + } + else if (match[3]) { // a formatting token + chunks.push({ token: match[3] }); + } + else if (match[5]) { // an unenclosed literal string + chunks.push(match[5]); + } + } + + return chunks; +} + +;; + +/* A rectangular panel that is absolutely positioned over other content +------------------------------------------------------------------------------------------------------------------------ +Options: + - className (string) + - content (HTML string or jQuery element set) + - parentEl + - top + - left + - right (the x coord of where the right edge should be. not a "CSS" right) + - autoHide (boolean) + - show (callback) + - hide (callback) +*/ + +function Popover(options) { + this.options = options || {}; +} + + +Popover.prototype = { + + isHidden: true, + options: null, + el: null, // the container element for the popover. generated by this object + documentMousedownProxy: null, // document mousedown handler bound to `this` + margin: 10, // the space required between the popover and the edges of the scroll container + + + // Shows the popover on the specified position. Renders it if not already + show: function() { + if (this.isHidden) { + if (!this.el) { + this.render(); + } + this.el.show(); + this.position(); + this.isHidden = false; + this.trigger('show'); + } + }, + + + // Hides the popover, through CSS, but does not remove it from the DOM + hide: function() { + if (!this.isHidden) { + this.el.hide(); + this.isHidden = true; + this.trigger('hide'); + } + }, + + + // Creates `this.el` and renders content inside of it + render: function() { + var _this = this; + var options = this.options; + + this.el = $('
') + .addClass(options.className || '') + .css({ + // position initially to the top left to avoid creating scrollbars + top: 0, + left: 0 + }) + .append(options.content) + .appendTo(options.parentEl); + + // when a click happens on anything inside with a 'fc-close' className, hide the popover + this.el.on('click', '.fc-close', function() { + _this.hide(); + }); + + if (options.autoHide) { + $(document).on('mousedown', this.documentMousedownProxy = $.proxy(this, 'documentMousedown')); + } + }, + + + // Triggered when the user clicks *anywhere* in the document, for the autoHide feature + documentMousedown: function(ev) { + // only hide the popover if the click happened outside the popover + if (this.el && !$(ev.target).closest(this.el).length) { + this.hide(); + } + }, + + + // Hides and unregisters any handlers + destroy: function() { + this.hide(); + + if (this.el) { + this.el.remove(); + this.el = null; + } + + $(document).off('mousedown', this.documentMousedownProxy); + }, + + + // Positions the popover optimally, using the top/left/right options + position: function() { + var options = this.options; + var origin = this.el.offsetParent().offset(); + var width = this.el.outerWidth(); + var height = this.el.outerHeight(); + var windowEl = $(window); + var viewportEl = getScrollParent(this.el); + var viewportTop; + var viewportLeft; + var viewportOffset; + var top; // the "position" (not "offset") values for the popover + var left; // + + // compute top and left + top = options.top || 0; + if (options.left !== undefined) { + left = options.left; + } + else if (options.right !== undefined) { + left = options.right - width; // derive the left value from the right value + } + else { + left = 0; + } + + if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result + viewportEl = windowEl; + viewportTop = 0; // the window is always at the top left + viewportLeft = 0; // (and .offset() won't work if called here) + } + else { + viewportOffset = viewportEl.offset(); + viewportTop = viewportOffset.top; + viewportLeft = viewportOffset.left; + } + + // if the window is scrolled, it causes the visible area to be further down + viewportTop += windowEl.scrollTop(); + viewportLeft += windowEl.scrollLeft(); + + // constrain to the view port. if constrained by two edges, give precedence to top/left + if (options.viewportConstrain !== false) { + top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin); + top = Math.max(top, viewportTop + this.margin); + left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin); + left = Math.max(left, viewportLeft + this.margin); + } + + this.el.css({ + top: top - origin.top, + left: left - origin.left + }); + }, + + + // Triggers a callback. Calls a function in the option hash of the same name. + // Arguments beyond the first `name` are forwarded on. + // TODO: better code reuse for this. Repeat code + trigger: function(name) { + if (this.options[name]) { + this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); + } + } + +}; + +;; + +/* A "coordinate map" converts pixel coordinates into an associated cell, which has an associated date +------------------------------------------------------------------------------------------------------------------------ +Common interface: + + CoordMap.prototype = { + build: function() {}, + getCell: function(x, y) {} + }; + +*/ + +/* Coordinate map for a grid component +----------------------------------------------------------------------------------------------------------------------*/ + +function GridCoordMap(grid) { + this.grid = grid; +} + + +GridCoordMap.prototype = { + + grid: null, // reference to the Grid + rows: null, // the top-to-bottom y coordinates. including the bottom of the last item + cols: null, // the left-to-right x coordinates. including the right of the last item + + containerEl: null, // container element that all coordinates are constrained to. optionally assigned + minX: null, + maxX: null, // exclusive + minY: null, + maxY: null, // exclusive + + + // Queries the grid for the coordinates of all the cells + build: function() { + this.grid.buildCoords( + this.rows = [], + this.cols = [] + ); + this.computeBounds(); + }, + + + // Given a coordinate of the document, gets the associated cell. If no cell is underneath, returns null + getCell: function(x, y) { + var cell = null; + var rows = this.rows; + var cols = this.cols; + var r = -1; + var c = -1; + var i; + + if (this.inBounds(x, y)) { + + for (i = 0; i < rows.length; i++) { + if (y >= rows[i][0] && y < rows[i][1]) { + r = i; + break; + } + } + + for (i = 0; i < cols.length; i++) { + if (x >= cols[i][0] && x < cols[i][1]) { + c = i; + break; + } + } + + if (r >= 0 && c >= 0) { + cell = { row: r, col: c }; + cell.grid = this.grid; + cell.date = this.grid.getCellDate(cell); + } + } + + return cell; + }, + + + // If there is a containerEl, compute the bounds into min/max values + computeBounds: function() { + var containerOffset; + + if (this.containerEl) { + containerOffset = this.containerEl.offset(); + this.minX = containerOffset.left; + this.maxX = containerOffset.left + this.containerEl.outerWidth(); + this.minY = containerOffset.top; + this.maxY = containerOffset.top + this.containerEl.outerHeight(); + } + }, + + + // Determines if the given coordinates are in bounds. If no `containerEl`, always true + inBounds: function(x, y) { + if (this.containerEl) { + return x >= this.minX && x < this.maxX && y >= this.minY && y < this.maxY; + } + return true; + } + +}; + + +/* Coordinate map that is a combination of multiple other coordinate maps +----------------------------------------------------------------------------------------------------------------------*/ + +function ComboCoordMap(coordMaps) { + this.coordMaps = coordMaps; +} + + +ComboCoordMap.prototype = { + + coordMaps: null, // an array of CoordMaps + + + // Builds all coordMaps + build: function() { + var coordMaps = this.coordMaps; + var i; + + for (i = 0; i < coordMaps.length; i++) { + coordMaps[i].build(); + } + }, + + + // Queries all coordMaps for the cell underneath the given coordinates, returning the first result + getCell: function(x, y) { + var coordMaps = this.coordMaps; + var cell = null; + var i; + + for (i = 0; i < coordMaps.length && !cell; i++) { + cell = coordMaps[i].getCell(x, y); + } + + return cell; + } + +}; +;; + +/* Tracks mouse movements over a CoordMap and raises events about which cell the mouse is over. +----------------------------------------------------------------------------------------------------------------------*/ +// TODO: implement scrolling + +function DragListener(coordMap, options) { + this.coordMap = coordMap; + this.options = options || {}; +} + + +DragListener.prototype = { + + coordMap: null, + options: null, + + isListening: false, + isDragging: false, + + // the cell/date the mouse was over when listening started + origCell: null, + origDate: null, + + // the cell/date the mouse is over + cell: null, + date: null, + + // coordinates of the initial mousedown + mouseX0: null, + mouseY0: null, + + // handler attached to the document, bound to the DragListener's `this` + mousemoveProxy: null, + mouseupProxy: null, + + scrollEl: null, + scrollBounds: null, // { top, bottom, left, right } + scrollTopVel: null, // pixels per second + scrollLeftVel: null, // pixels per second + scrollIntervalId: null, // ID of setTimeout for scrolling animation loop + scrollHandlerProxy: null, // this-scoped function for handling when scrollEl is scrolled + + scrollSensitivity: 30, // pixels from edge for scrolling to start + scrollSpeed: 200, // pixels per second, at maximum speed + scrollIntervalMs: 50, // millisecond wait between scroll increment + + + // Call this when the user does a mousedown. Will probably lead to startListening + mousedown: function(ev) { + if (isPrimaryMouseButton(ev)) { + + ev.preventDefault(); // prevents native selection in most browsers + + this.startListening(ev); + + // start the drag immediately if there is no minimum distance for a drag start + if (!this.options.distance) { + this.startDrag(ev); + } + } + }, + + + // Call this to start tracking mouse movements + startListening: function(ev) { + var scrollParent; + var cell; + + if (!this.isListening) { + + // grab scroll container and attach handler + if (ev && this.options.scroll) { + scrollParent = getScrollParent($(ev.target)); + if (!scrollParent.is(window) && !scrollParent.is(document)) { + this.scrollEl = scrollParent; + + // scope to `this`, and use `debounce` to make sure rapid calls don't happen + this.scrollHandlerProxy = debounce($.proxy(this, 'scrollHandler'), 100); + this.scrollEl.on('scroll', this.scrollHandlerProxy); + } + } + + this.computeCoords(); // relies on `scrollEl` + + // get info on the initial cell, date, and coordinates + if (ev) { + cell = this.getCell(ev); + this.origCell = cell; + this.origDate = cell ? cell.date : null; + + this.mouseX0 = ev.pageX; + this.mouseY0 = ev.pageY; + } + + $(document) + .on('mousemove', this.mousemoveProxy = $.proxy(this, 'mousemove')) + .on('mouseup', this.mouseupProxy = $.proxy(this, 'mouseup')) + .on('selectstart', this.preventDefault); // prevents native selection in IE<=8 + + this.isListening = true; + this.trigger('listenStart', ev); + } + }, + + + // Recomputes the drag-critical positions of elements + computeCoords: function() { + this.coordMap.build(); + this.computeScrollBounds(); + }, + + + // Called when the user moves the mouse + mousemove: function(ev) { + var minDistance; + var distanceSq; // current distance from mouseX0/mouseY0, squared + + if (!this.isDragging) { // if not already dragging... + // then start the drag if the minimum distance criteria is met + minDistance = this.options.distance || 1; + distanceSq = Math.pow(ev.pageX - this.mouseX0, 2) + Math.pow(ev.pageY - this.mouseY0, 2); + if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem + this.startDrag(ev); + } + } + + if (this.isDragging) { + this.drag(ev); // report a drag, even if this mousemove initiated the drag + } + }, + + + // Call this to initiate a legitimate drag. + // This function is called internally from this class, but can also be called explicitly from outside + startDrag: function(ev) { + var cell; + + if (!this.isListening) { // startDrag must have manually initiated + this.startListening(); + } + + if (!this.isDragging) { + this.isDragging = true; + this.trigger('dragStart', ev); + + // report the initial cell the mouse is over + cell = this.getCell(ev); + if (cell) { + this.cellOver(cell, true); + } + } + }, + + + // Called while the mouse is being moved and when we know a legitimate drag is taking place + drag: function(ev) { + var cell; + + if (this.isDragging) { + cell = this.getCell(ev); + + if (!isCellsEqual(cell, this.cell)) { // a different cell than before? + if (this.cell) { + this.cellOut(); + } + if (cell) { + this.cellOver(cell); + } + } + + this.dragScroll(ev); // will possibly cause scrolling + } + }, + + + // Called when a the mouse has just moved over a new cell + cellOver: function(cell) { + this.cell = cell; + this.date = cell.date; + this.trigger('cellOver', cell, cell.date); + }, + + + // Called when the mouse has just moved out of a cell + cellOut: function() { + if (this.cell) { + this.trigger('cellOut', this.cell); + this.cell = null; + this.date = null; + } + }, + + + // Called when the user does a mouseup + mouseup: function(ev) { + this.stopDrag(ev); + this.stopListening(ev); + }, + + + // Called when the drag is over. Will not cause listening to stop however. + // A concluding 'cellOut' event will NOT be triggered. + stopDrag: function(ev) { + if (this.isDragging) { + this.stopScrolling(); + this.trigger('dragStop', ev); + this.isDragging = false; + } + }, + + + // Call this to stop listening to the user's mouse events + stopListening: function(ev) { + if (this.isListening) { + + // remove the scroll handler if there is a scrollEl + if (this.scrollEl) { + this.scrollEl.off('scroll', this.scrollHandlerProxy); + this.scrollHandlerProxy = null; + } + + $(document) + .off('mousemove', this.mousemoveProxy) + .off('mouseup', this.mouseupProxy) + .off('selectstart', this.preventDefault); + + this.mousemoveProxy = null; + this.mouseupProxy = null; + + this.isListening = false; + this.trigger('listenStop', ev); + + this.origCell = this.cell = null; + this.origDate = this.date = null; + } + }, + + + // Gets the cell underneath the coordinates for the given mouse event + getCell: function(ev) { + return this.coordMap.getCell(ev.pageX, ev.pageY); + }, + + + // Triggers a callback. Calls a function in the option hash of the same name. + // Arguments beyond the first `name` are forwarded on. + trigger: function(name) { + if (this.options[name]) { + this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); + } + }, + + + // Stops a given mouse event from doing it's native browser action. In our case, text selection. + preventDefault: function(ev) { + ev.preventDefault(); + }, + + + /* Scrolling + ------------------------------------------------------------------------------------------------------------------*/ + + + // Computes and stores the bounding rectangle of scrollEl + computeScrollBounds: function() { + var el = this.scrollEl; + var offset; + + if (el) { + offset = el.offset(); + this.scrollBounds = { + top: offset.top, + left: offset.left, + bottom: offset.top + el.outerHeight(), + right: offset.left + el.outerWidth() + }; + } + }, + + + // Called when the dragging is in progress and scrolling should be updated + dragScroll: function(ev) { + var sensitivity = this.scrollSensitivity; + var bounds = this.scrollBounds; + var topCloseness, bottomCloseness; + var leftCloseness, rightCloseness; + var topVel = 0; + var leftVel = 0; + + if (bounds) { // only scroll if scrollEl exists + + // compute closeness to edges. valid range is from 0.0 - 1.0 + topCloseness = (sensitivity - (ev.pageY - bounds.top)) / sensitivity; + bottomCloseness = (sensitivity - (bounds.bottom - ev.pageY)) / sensitivity; + leftCloseness = (sensitivity - (ev.pageX - bounds.left)) / sensitivity; + rightCloseness = (sensitivity - (bounds.right - ev.pageX)) / sensitivity; + + // translate vertical closeness into velocity. + // mouse must be completely in bounds for velocity to happen. + if (topCloseness >= 0 && topCloseness <= 1) { + topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up + } + else if (bottomCloseness >= 0 && bottomCloseness <= 1) { + topVel = bottomCloseness * this.scrollSpeed; + } + + // translate horizontal closeness into velocity + if (leftCloseness >= 0 && leftCloseness <= 1) { + leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left + } + else if (rightCloseness >= 0 && rightCloseness <= 1) { + leftVel = rightCloseness * this.scrollSpeed; + } + } + + this.setScrollVel(topVel, leftVel); + }, + + + // Sets the speed-of-scrolling for the scrollEl + setScrollVel: function(topVel, leftVel) { + + this.scrollTopVel = topVel; + this.scrollLeftVel = leftVel; + + this.constrainScrollVel(); // massages into realistic values + + // if there is non-zero velocity, and an animation loop hasn't already started, then START + if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) { + this.scrollIntervalId = setInterval( + $.proxy(this, 'scrollIntervalFunc'), // scope to `this` + this.scrollIntervalMs + ); + } + }, + + + // Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way + constrainScrollVel: function() { + var el = this.scrollEl; + + if (this.scrollTopVel < 0) { // scrolling up? + if (el.scrollTop() <= 0) { // already scrolled all the way up? + this.scrollTopVel = 0; + } + } + else if (this.scrollTopVel > 0) { // scrolling down? + if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) { // already scrolled all the way down? + this.scrollTopVel = 0; + } + } + + if (this.scrollLeftVel < 0) { // scrolling left? + if (el.scrollLeft() <= 0) { // already scrolled all the left? + this.scrollLeftVel = 0; + } + } + else if (this.scrollLeftVel > 0) { // scrolling right? + if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) { // already scrolled all the way right? + this.scrollLeftVel = 0; + } + } + }, + + + // This function gets called during every iteration of the scrolling animation loop + scrollIntervalFunc: function() { + var el = this.scrollEl; + var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by + + // change the value of scrollEl's scroll + if (this.scrollTopVel) { + el.scrollTop(el.scrollTop() + this.scrollTopVel * frac); + } + if (this.scrollLeftVel) { + el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac); + } + + this.constrainScrollVel(); // since the scroll values changed, recompute the velocities + + // if scrolled all the way, which causes the vels to be zero, stop the animation loop + if (!this.scrollTopVel && !this.scrollLeftVel) { + this.stopScrolling(); + } + }, + + + // Kills any existing scrolling animation loop + stopScrolling: function() { + if (this.scrollIntervalId) { + clearInterval(this.scrollIntervalId); + this.scrollIntervalId = null; + + // when all done with scrolling, recompute positions since they probably changed + this.computeCoords(); + } + }, + + + // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce) + scrollHandler: function() { + // recompute all coordinates, but *only* if this is *not* part of our scrolling animation + if (!this.scrollIntervalId) { + this.computeCoords(); + } + } + +}; + + +// Returns `true` if the cells are identically equal. `false` otherwise. +// They must have the same row, col, and be from the same grid. +// Two null values will be considered equal, as two "out of the grid" states are the same. +function isCellsEqual(cell1, cell2) { + + if (!cell1 && !cell2) { + return true; + } + + if (cell1 && cell2) { + return cell1.grid === cell2.grid && + cell1.row === cell2.row && + cell1.col === cell2.col; + } + + return false; +} + +;; + +/* Creates a clone of an element and lets it track the mouse as it moves +----------------------------------------------------------------------------------------------------------------------*/ + +function MouseFollower(sourceEl, options) { + this.options = options = options || {}; + this.sourceEl = sourceEl; + this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent +} + + +MouseFollower.prototype = { + + options: null, + + sourceEl: null, // the element that will be cloned and made to look like it is dragging + el: null, // the clone of `sourceEl` that will track the mouse + parentEl: null, // the element that `el` (the clone) will be attached to + + // the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl + top0: null, + left0: null, + + // the initial position of the mouse + mouseY0: null, + mouseX0: null, + + // the number of pixels the mouse has moved from its initial position + topDelta: null, + leftDelta: null, + + mousemoveProxy: null, // document mousemove handler, bound to the MouseFollower's `this` + + isFollowing: false, + isHidden: false, + isAnimating: false, // doing the revert animation? + + + // Causes the element to start following the mouse + start: function(ev) { + if (!this.isFollowing) { + this.isFollowing = true; + + this.mouseY0 = ev.pageY; + this.mouseX0 = ev.pageX; + this.topDelta = 0; + this.leftDelta = 0; + + if (!this.isHidden) { + this.updatePosition(); + } + + $(document).on('mousemove', this.mousemoveProxy = $.proxy(this, 'mousemove')); + } + }, + + + // Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position. + // `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately. + stop: function(shouldRevert, callback) { + var _this = this; + var revertDuration = this.options.revertDuration; + + function complete() { + this.isAnimating = false; + _this.destroyEl(); + + this.top0 = this.left0 = null; // reset state for future updatePosition calls + + if (callback) { + callback(); + } + } + + if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time + this.isFollowing = false; + + $(document).off('mousemove', this.mousemoveProxy); + + if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation? + this.isAnimating = true; + this.el.animate({ + top: this.top0, + left: this.left0 + }, { + duration: revertDuration, + complete: complete + }); + } + else { + complete(); + } + } + }, + + + // Gets the tracking element. Create it if necessary + getEl: function() { + var el = this.el; + + if (!el) { + this.sourceEl.width(); // hack to force IE8 to compute correct bounding box + el = this.el = this.sourceEl.clone() + .css({ + position: 'absolute', + visibility: '', // in case original element was hidden (commonly through hideEvents()) + display: this.isHidden ? 'none' : '', // for when initially hidden + margin: 0, + right: 'auto', // erase and set width instead + bottom: 'auto', // erase and set height instead + width: this.sourceEl.width(), // explicit height in case there was a 'right' value + height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value + opacity: this.options.opacity || '', + zIndex: this.options.zIndex + }) + .appendTo(this.parentEl); + } + + return el; + }, + + + // Removes the tracking element if it has already been created + destroyEl: function() { + if (this.el) { + this.el.remove(); + this.el = null; + } + }, + + + // Update the CSS position of the tracking element + updatePosition: function() { + var sourceOffset; + var origin; + + this.getEl(); // ensure this.el + + // make sure origin info was computed + if (this.top0 === null) { + this.sourceEl.width(); // hack to force IE8 to compute correct bounding box + sourceOffset = this.sourceEl.offset(); + origin = this.el.offsetParent().offset(); + this.top0 = sourceOffset.top - origin.top; + this.left0 = sourceOffset.left - origin.left; + } + + this.el.css({ + top: this.top0 + this.topDelta, + left: this.left0 + this.leftDelta + }); + }, + + + // Gets called when the user moves the mouse + mousemove: function(ev) { + this.topDelta = ev.pageY - this.mouseY0; + this.leftDelta = ev.pageX - this.mouseX0; + + if (!this.isHidden) { + this.updatePosition(); + } + }, + + + // Temporarily makes the tracking element invisible. Can be called before following starts + hide: function() { + if (!this.isHidden) { + this.isHidden = true; + if (this.el) { + this.el.hide(); + } + } + }, + + + // Show the tracking element after it has been temporarily hidden + show: function() { + if (this.isHidden) { + this.isHidden = false; + this.updatePosition(); + this.getEl().show(); + } + } + +}; + +;; + +/* A utility class for rendering rows. +----------------------------------------------------------------------------------------------------------------------*/ +// It leverages methods of the subclass and the View to determine custom rendering behavior for each row "type" +// (such as highlight rows, day rows, helper rows, etc). + +function RowRenderer(view) { + this.view = view; +} + + +RowRenderer.prototype = { + + view: null, // a View object + cellHtml: '', // plain default HTML used for a cell when no other is available + + + // Renders the HTML for a row, leveraging custom cell-HTML-renderers based on the `rowType`. + // Also applies the "intro" and "outro" cells, which are specified by the subclass and views. + // `row` is an optional row number. + rowHtml: function(rowType, row) { + var view = this.view; + var renderCell = this.getHtmlRenderer('cell', rowType); + var cellHtml = ''; + var col; + var date; + + row = row || 0; + + for (col = 0; col < view.colCnt; col++) { + date = view.cellToDate(row, col); + cellHtml += renderCell(row, col, date); + } + + cellHtml = this.bookendCells(cellHtml, rowType, row); // apply intro and outro + + return '' + cellHtml + ''; + }, + + + // Applies the "intro" and "outro" HTML to the given cells. + // Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro. + // `cells` can be an HTML string of 's or a jQuery element + // `row` is an optional row number. + bookendCells: function(cells, rowType, row) { + var view = this.view; + var intro = this.getHtmlRenderer('intro', rowType)(row || 0); + var outro = this.getHtmlRenderer('outro', rowType)(row || 0); + var isRTL = view.opt('isRTL'); + var prependHtml = isRTL ? outro : intro; + var appendHtml = isRTL ? intro : outro; + + if (typeof cells === 'string') { + return prependHtml + cells + appendHtml; + } + else { // a jQuery element + return cells.prepend(prependHtml).append(appendHtml); + } + }, + + + // Returns an HTML-rendering function given a specific `rendererName` (like cell, intro, or outro) and a specific + // `rowType` (like day, eventSkeleton, helperSkeleton), which is optional. + // If a renderer for the specific rowType doesn't exist, it will fall back to a generic renderer. + // We will query the View object first for any custom rendering functions, then the methods of the subclass. + getHtmlRenderer: function(rendererName, rowType) { + var view = this.view; + var generalName; // like "cellHtml" + var specificName; // like "dayCellHtml". based on rowType + var provider; // either the View or the RowRenderer subclass, whichever provided the method + var renderer; + + generalName = rendererName + 'Html'; + if (rowType) { + specificName = rowType + capitaliseFirstLetter(rendererName) + 'Html'; + } + + if (specificName && (renderer = view[specificName])) { + provider = view; + } + else if (specificName && (renderer = this[specificName])) { + provider = this; + } + else if ((renderer = view[generalName])) { + provider = view; + } + else if ((renderer = this[generalName])) { + provider = this; + } + + if (typeof renderer === 'function') { + return function(row) { + return renderer.apply(provider, arguments) || ''; // use correct `this` and always return a string + }; + } + + // the rendered can be a plain string as well. if not specified, always an empty string. + return function() { + return renderer || ''; + }; + } + +}; + +;; + +/* An abstract class comprised of a "grid" of cells that each represent a specific datetime +----------------------------------------------------------------------------------------------------------------------*/ + +function Grid(view) { + RowRenderer.call(this, view); // call the super-constructor + this.coordMap = new GridCoordMap(this); +} + + +Grid.prototype = createObject(RowRenderer.prototype); // declare the super-class +$.extend(Grid.prototype, { + + el: null, // the containing element + coordMap: null, // a GridCoordMap that converts pixel values to datetimes + cellDuration: null, // a cell's duration. subclasses must assign this ASAP + + + // Renders the grid into the `el` element. + // Subclasses should override and call this super-method when done. + render: function() { + this.bindHandlers(); + }, + + + // Called when the grid's resources need to be cleaned up + destroy: function() { + // subclasses can implement + }, + + + /* Coordinates & Cells + ------------------------------------------------------------------------------------------------------------------*/ + + + // Populates the given empty arrays with the y and x coordinates of the cells + buildCoords: function(rows, cols) { + // subclasses must implement + }, + + + // Given a cell object, returns the date for that cell + getCellDate: function(cell) { + // subclasses must implement + }, + + + // Given a cell object, returns the element that represents the cell's whole-day + getCellDayEl: function(cell) { + // subclasses must implement + }, + + + // Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects + rangeToSegs: function(start, end) { + // subclasses must implement + }, + + + /* Handlers + ------------------------------------------------------------------------------------------------------------------*/ + + + // Attach handlers to `this.el`, using bubbling to listen to all ancestors. + // We don't need to undo any of this in a "destroy" method, because the view will simply remove `this.el` from the + // DOM and jQuery will be smart enough to garbage collect the handlers. + bindHandlers: function() { + var _this = this; + + this.el.on('mousedown', function(ev) { + if ( + !$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link + !$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one) + ) { + _this.dayMousedown(ev); + } + }); + + this.bindSegHandlers(); // attach event-element-related handlers. in Grid.events.js + }, + + + // Process a mousedown on an element that represents a day. For day clicking and selecting. + dayMousedown: function(ev) { + var _this = this; + var view = this.view; + var isSelectable = view.opt('selectable'); + var dates = null; // the inclusive dates of the selection. will be null if no selection + var start; // the inclusive start of the selection + var end; // the *exclusive* end of the selection + var dayEl; + + // this listener tracks a mousedown on a day element, and a subsequent drag. + // if the drag ends on the same day, it is a 'dayClick'. + // if 'selectable' is enabled, this listener also detects selections. + var dragListener = new DragListener(this.coordMap, { + //distance: 5, // needs more work if we want dayClick to fire correctly + scroll: view.opt('dragScroll'), + dragStart: function() { + view.unselect(); // since we could be rendering a new selection, we want to clear any old one + }, + cellOver: function(cell, date) { + if (dragListener.origDate) { // click needs to have started on a cell + + dayEl = _this.getCellDayEl(cell); + + dates = [ date, dragListener.origDate ].sort(dateCompare); + start = dates[0]; + end = dates[1].clone().add(_this.cellDuration); + + if (isSelectable) { + _this.renderSelection(start, end); + } + } + }, + cellOut: function(cell, date) { + dates = null; + _this.destroySelection(); + }, + listenStop: function(ev) { + if (dates) { // started and ended on a cell? + if (dates[0].isSame(dates[1])) { + view.trigger('dayClick', dayEl[0], start, ev); + } + if (isSelectable) { + // the selection will already have been rendered. just report it + view.reportSelection(start, end, ev); + } + } + } + }); + + dragListener.mousedown(ev); // start listening, which will eventually initiate a dragStart + }, + + + /* Event Dragging + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of a event being dragged over the given date(s). + // `end` can be null, as well as `seg`. See View's documentation on renderDrag for more info. + // A returned value of `true` signals that a mock "helper" event has been rendered. + renderDrag: function(start, end, seg) { + // subclasses must implement + }, + + + // Unrenders a visual indication of an event being dragged + destroyDrag: function() { + // subclasses must implement + }, + + + /* Event Resizing + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of an event being resized. + // `start` and `end` are the updated dates of the event. `seg` is the original segment object involved in the drag. + renderResize: function(start, end, seg) { + // subclasses must implement + }, + + + // Unrenders a visual indication of an event being resized. + destroyResize: function() { + // subclasses must implement + }, + + + /* Event Helper + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a mock event over the given date(s). + // `end` can be null, in which case the mock event that is rendered will have a null end time. + // `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging. + renderRangeHelper: function(start, end, sourceSeg) { + var view = this.view; + var fakeEvent; + + // compute the end time if forced to do so (this is what EventManager does) + if (!end && view.opt('forceEventDuration')) { + end = view.calendar.getDefaultEventEnd(!start.hasTime(), start); + } + + fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible + fakeEvent.start = start; + fakeEvent.end = end; + fakeEvent.allDay = !(start.hasTime() || (end && end.hasTime())); // freshly compute allDay + + // this extra className will be useful for differentiating real events from mock events in CSS + fakeEvent.className = (fakeEvent.className || []).concat('fc-helper'); + + // if something external is being dragged in, don't render a resizer + if (!sourceSeg) { + fakeEvent.editable = false; + } + + this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering + }, + + + // Renders a mock event + renderHelper: function(event, sourceSeg) { + // subclasses must implement + }, + + + // Unrenders a mock event + destroyHelper: function() { + // subclasses must implement + }, + + + /* Selection + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses. + renderSelection: function(start, end) { + this.renderHighlight(start, end); + }, + + + // Unrenders any visual indications of a selection. Will unrender a highlight by default. + destroySelection: function() { + this.destroyHighlight(); + }, + + + /* Highlight + ------------------------------------------------------------------------------------------------------------------*/ + + + // Puts visual emphasis on a certain date range + renderHighlight: function(start, end) { + // subclasses should implement + }, + + + // Removes visual emphasis on a date range + destroyHighlight: function() { + // subclasses should implement + }, + + + + /* Generic rendering utilities for subclasses + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a day-of-week header row + headHtml: function() { + return '' + + '
' + + '' + + '' + + this.rowHtml('head') + // leverages RowRenderer + '' + + '
' + + '
'; + }, + + + // Used by the `headHtml` method, via RowRenderer, for rendering the HTML of a day-of-week header cell + headCellHtml: function(row, col, date) { + var view = this.view; + var calendar = view.calendar; + var colFormat = view.opt('columnFormat'); + + return '' + + '' + + htmlEscape(calendar.formatDate(date, colFormat)) + + ''; + }, + + + // Renders the HTML for a single-day background cell + bgCellHtml: function(row, col, date) { + var view = this.view; + var classes = this.getDayClasses(date); + + classes.unshift('fc-day', view.widgetContentClass); + + return ''; + }, + + + // Computes HTML classNames for a single-day cell + getDayClasses: function(date) { + var view = this.view; + var today = view.calendar.getNow().stripTime(); + var classes = [ 'fc-' + dayIDs[date.day()] ]; + + if ( + view.name === 'month' && + date.month() != view.intervalStart.month() + ) { + classes.push('fc-other-month'); + } + + if (date.isSame(today, 'day')) { + classes.push( + 'fc-today', + view.highlightStateClass + ); + } + else if (date < today) { + classes.push('fc-past'); + } + else { + classes.push('fc-future'); + } + + return classes; + } + +}); + +;; + +/* Event-rendering and event-interaction methods for the abstract Grid class +----------------------------------------------------------------------------------------------------------------------*/ + +$.extend(Grid.prototype, { + + mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing + isDraggingSeg: false, // is a segment being dragged? boolean + isResizingSeg: false, // is a segment being resized? boolean + + + // Renders the given events onto the grid + renderEvents: function(events) { + // subclasses must implement + }, + + + // Retrieves all rendered segment objects in this grid + getSegs: function() { + // subclasses must implement + }, + + + // Unrenders all events. Subclasses should implement, calling this super-method first. + destroyEvents: function() { + this.triggerSegMouseout(); // trigger an eventMouseout if user's mouse is over an event + }, + + + // Renders a `el` property for each seg, and only returns segments that successfully rendered + renderSegs: function(segs, disableResizing) { + var view = this.view; + var html = ''; + var renderedSegs = []; + var i; + + // build a large concatenation of event segment HTML + for (i = 0; i < segs.length; i++) { + html += this.renderSegHtml(segs[i], disableResizing); + } + + // Grab individual elements from the combined HTML string. Use each as the default rendering. + // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false. + $(html).each(function(i, node) { + var seg = segs[i]; + var el = view.resolveEventEl(seg.event, $(node)); + if (el) { + el.data('fc-seg', seg); // used by handlers + seg.el = el; + renderedSegs.push(seg); + } + }); + + return renderedSegs; + }, + + + // Generates the HTML for the default rendering of a segment + renderSegHtml: function(seg, disableResizing) { + // subclasses must implement + }, + + + // Converts an array of event objects into an array of segment objects + eventsToSegs: function(events, intervalStart, intervalEnd) { + var _this = this; + + return $.map(events, function(event) { + return _this.eventToSegs(event, intervalStart, intervalEnd); // $.map flattens all returned arrays together + }); + }, + + + // Slices a single event into an array of event segments. + // When `intervalStart` and `intervalEnd` are specified, intersect the events with that interval. + // Otherwise, let the subclass decide how it wants to slice the segments over the grid. + eventToSegs: function(event, intervalStart, intervalEnd) { + var eventStart = event.start.clone().stripZone(); // normalize + var eventEnd = this.view.calendar.getEventEnd(event).stripZone(); // compute (if necessary) and normalize + var segs; + var i, seg; + + if (intervalStart && intervalEnd) { + seg = intersectionToSeg(eventStart, eventEnd, intervalStart, intervalEnd); + segs = seg ? [ seg ] : []; + } + else { + segs = this.rangeToSegs(eventStart, eventEnd); // defined by the subclass + } + + // assign extra event-related properties to the segment objects + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + seg.event = event; + seg.eventStartMS = +eventStart; + seg.eventDurationMS = eventEnd - eventStart; + } + + return segs; + }, + + + /* Handlers + ------------------------------------------------------------------------------------------------------------------*/ + + + // Attaches event-element-related handlers to the container element and leverage bubbling + bindSegHandlers: function() { + var _this = this; + var view = this.view; + + $.each( + { + mouseenter: function(seg, ev) { + _this.triggerSegMouseover(seg, ev); + }, + mouseleave: function(seg, ev) { + _this.triggerSegMouseout(seg, ev); + }, + click: function(seg, ev) { + return view.trigger('eventClick', this, seg.event, ev); // can return `false` to cancel + }, + mousedown: function(seg, ev) { + if ($(ev.target).is('.fc-resizer') && view.isEventResizable(seg.event)) { + _this.segResizeMousedown(seg, ev); + } + else if (view.isEventDraggable(seg.event)) { + _this.segDragMousedown(seg, ev); + } + } + }, + function(name, func) { + // attach the handler to the container element and only listen for real event elements via bubbling + _this.el.on(name, '.fc-event-container > *', function(ev) { + var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents + + // only call the handlers if there is not a drag/resize in progress + if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) { + return func.call(this, seg, ev); // `this` will be the event element + } + }); + } + ); + }, + + + // Updates internal state and triggers handlers for when an event element is moused over + triggerSegMouseover: function(seg, ev) { + if (!this.mousedOverSeg) { + this.mousedOverSeg = seg; + this.view.trigger('eventMouseover', seg.el[0], seg.event, ev); + } + }, + + + // Updates internal state and triggers handlers for when an event element is moused out. + // Can be given no arguments, in which case it will mouseout the segment that was previously moused over. + triggerSegMouseout: function(seg, ev) { + ev = ev || {}; // if given no args, make a mock mouse event + + if (this.mousedOverSeg) { + seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment + this.mousedOverSeg = null; + this.view.trigger('eventMouseout', seg.el[0], seg.event, ev); + } + }, + + + /* Dragging + ------------------------------------------------------------------------------------------------------------------*/ + + + // Called when the user does a mousedown on an event, which might lead to dragging. + // Generic enough to work with any type of Grid. + segDragMousedown: function(seg, ev) { + var _this = this; + var view = this.view; + var el = seg.el; + var event = seg.event; + var newStart, newEnd; + + // A clone of the original element that will move with the mouse + var mouseFollower = new MouseFollower(seg.el, { + parentEl: view.el, + opacity: view.opt('dragOpacity'), + revertDuration: view.opt('dragRevertDuration'), + zIndex: 2 // one above the .fc-view + }); + + // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents + // of the view. + var dragListener = new DragListener(view.coordMap, { + distance: 5, + scroll: view.opt('dragScroll'), + listenStart: function(ev) { + mouseFollower.hide(); // don't show until we know this is a real drag + mouseFollower.start(ev); + }, + dragStart: function(ev) { + _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported + _this.isDraggingSeg = true; + view.hideEvent(event); // hide all event segments. our mouseFollower will take over + view.trigger('eventDragStart', el[0], event, ev, {}); // last argument is jqui dummy + }, + cellOver: function(cell, date) { + var origDate = seg.cellDate || dragListener.origDate; + var res = _this.computeDraggedEventDates(seg, origDate, date); + newStart = res.start; + newEnd = res.end; + + if (view.renderDrag(newStart, newEnd, seg)) { // have the view render a visual indication + mouseFollower.hide(); // if the view is already using a mock event "helper", hide our own + } + else { + mouseFollower.show(); + } + }, + cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells + newStart = null; + view.destroyDrag(); // unrender whatever was done in view.renderDrag + mouseFollower.show(); // show in case we are moving out of all cells + }, + dragStop: function(ev) { + var hasChanged = newStart && !newStart.isSame(event.start); + + // do revert animation if hasn't changed. calls a callback when finished (whether animation or not) + mouseFollower.stop(!hasChanged, function() { + _this.isDraggingSeg = false; + view.destroyDrag(); + view.showEvent(event); + view.trigger('eventDragStop', el[0], event, ev, {}); // last argument is jqui dummy + + if (hasChanged) { + view.eventDrop(el[0], event, newStart, ev); // will rerender all events... + } + }); + }, + listenStop: function() { + mouseFollower.stop(); // put in listenStop in case there was a mousedown but the drag never started + } + }); + + dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart + }, + + + // Given a segment, the dates where a drag began and ended, calculates the Event Object's new start and end dates + computeDraggedEventDates: function(seg, dragStartDate, dropDate) { + var view = this.view; + var event = seg.event; + var start = event.start; + var end = view.calendar.getEventEnd(event); + var delta; + var newStart; + var newEnd; + + if (dropDate.hasTime() === dragStartDate.hasTime()) { + delta = dayishDiff(dropDate, dragStartDate); + newStart = start.clone().add(delta); + if (event.end === null) { // do we need to compute an end? + newEnd = null; + } + else { + newEnd = end.clone().add(delta); + } + } + else { + // if switching from day <-> timed, start should be reset to the dropped date, and the end cleared + newStart = dropDate; + newEnd = null; // end should be cleared + } + + return { start: newStart, end: newEnd }; + }, + + + /* Resizing + ------------------------------------------------------------------------------------------------------------------*/ + + + // Called when the user does a mousedown on an event's resizer, which might lead to resizing. + // Generic enough to work with any type of Grid. + segResizeMousedown: function(seg, ev) { + var _this = this; + var view = this.view; + var el = seg.el; + var event = seg.event; + var start = event.start; + var end = view.calendar.getEventEnd(event); + var newEnd = null; + var dragListener; + + function destroy() { // resets the rendering + _this.destroyResize(); + view.showEvent(event); + } + + // Tracks mouse movement over the *grid's* coordinate map + dragListener = new DragListener(this.coordMap, { + distance: 5, + scroll: view.opt('dragScroll'), + dragStart: function(ev) { + _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported + _this.isResizingSeg = true; + view.trigger('eventResizeStart', el[0], event, ev, {}); // last argument is jqui dummy + }, + cellOver: function(cell, date) { + // compute the new end. don't allow it to go before the event's start + if (date.isBefore(start)) { // allows comparing ambig to non-ambig + date = start; + } + newEnd = date.clone().add(_this.cellDuration); // make it an exclusive end + + if (newEnd.isSame(end)) { + newEnd = null; + destroy(); + } + else { + _this.renderResize(start, newEnd, seg); + view.hideEvent(event); + } + }, + cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells + newEnd = null; + destroy(); + }, + dragStop: function(ev) { + _this.isResizingSeg = false; + destroy(); + view.trigger('eventResizeStop', el[0], event, ev, {}); // last argument is jqui dummy + + if (newEnd) { + view.eventResize(el[0], event, newEnd, ev); // will rerender all events... + } + } + }); + + dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart + }, + + + /* Rendering Utils + ------------------------------------------------------------------------------------------------------------------*/ + + + // Generic utility for generating the HTML classNames for an event segment's element + getSegClasses: function(seg, isDraggable, isResizable) { + var event = seg.event; + var classes = [ + 'fc-event', + seg.isStart ? 'fc-start' : 'fc-not-start', + seg.isEnd ? 'fc-end' : 'fc-not-end' + ].concat( + event.className, + event.source ? event.source.className : [] + ); + + if (isDraggable) { + classes.push('fc-draggable'); + } + if (isResizable) { + classes.push('fc-resizable'); + } + + return classes; + }, + + + // Utility for generating a CSS string with all the event skin-related properties + getEventSkinCss: function(event) { + var view = this.view; + var source = event.source || {}; + var eventColor = event.color; + var sourceColor = source.color; + var optionColor = view.opt('eventColor'); + var backgroundColor = + event.backgroundColor || + eventColor || + source.backgroundColor || + sourceColor || + view.opt('eventBackgroundColor') || + optionColor; + var borderColor = + event.borderColor || + eventColor || + source.borderColor || + sourceColor || + view.opt('eventBorderColor') || + optionColor; + var textColor = + event.textColor || + source.textColor || + view.opt('eventTextColor'); + var statements = []; + if (backgroundColor) { + statements.push('background-color:' + backgroundColor); + } + if (borderColor) { + statements.push('border-color:' + borderColor); + } + if (textColor) { + statements.push('color:' + textColor); + } + return statements.join(';'); + } + +}); + + +/* Event Segment Utilities +----------------------------------------------------------------------------------------------------------------------*/ + + +// A cmp function for determining which segments should take visual priority +function compareSegs(seg1, seg2) { + return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first + seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first + seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1) + (seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title +} + + +;; + +/* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week. +----------------------------------------------------------------------------------------------------------------------*/ + +function DayGrid(view) { + Grid.call(this, view); // call the super-constructor +} + + +DayGrid.prototype = createObject(Grid.prototype); // declare the super-class +$.extend(DayGrid.prototype, { + + numbersVisible: false, // should render a row for day/week numbers? manually set by the view + cellDuration: moment.duration({ days: 1 }), // required for Grid.event.js. Each cell is always a single day + bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid + + rowEls: null, // set of fake row elements + dayEls: null, // set of whole-day elements comprising the row's background + helperEls: null, // set of cell skeleton elements for rendering the mock event "helper" + highlightEls: null, // set of cell skeleton elements for rendering the highlight + + + // Renders the rows and columns into the component's `this.el`, which should already be assigned. + // isRigid determins whether the individual rows should ignore the contents and be a constant height. + // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient. + render: function(isRigid) { + var view = this.view; + var html = ''; + var row; + + for (row = 0; row < view.rowCnt; row++) { + html += this.dayRowHtml(row, isRigid); + } + this.el.html(html); + + this.rowEls = this.el.find('.fc-row'); + this.dayEls = this.el.find('.fc-day'); + + // run all the day cells through the dayRender callback + this.dayEls.each(function(i, node) { + var date = view.cellToDate(Math.floor(i / view.colCnt), i % view.colCnt); + view.trigger('dayRender', null, date, $(node)); + }); + + Grid.prototype.render.call(this); // call the super-method + }, + + + destroy: function() { + this.destroySegPopover(); + }, + + + // Generates the HTML for a single row. `row` is the row number. + dayRowHtml: function(row, isRigid) { + var view = this.view; + var classes = [ 'fc-row', 'fc-week', view.widgetContentClass ]; + + if (isRigid) { + classes.push('fc-rigid'); + } + + return '' + + '
' + + '
' + + '' + + this.rowHtml('day', row) + // leverages RowRenderer. calls dayCellHtml() + '
' + + '
' + + '
' + + '' + + (this.numbersVisible ? + '' + + this.rowHtml('number', row) + // leverages RowRenderer. View will define render method + '' : + '' + ) + + '
' + + '
' + + '
'; + }, + + + // Renders the HTML for a whole-day cell. Will eventually end up in the day-row's background. + // We go through a 'day' row type instead of just doing a 'bg' row type so that the View can do custom rendering + // specifically for whole-day rows, whereas a 'bg' might also be used for other purposes (TimeGrid bg for example). + dayCellHtml: function(row, col, date) { + return this.bgCellHtml(row, col, date); + }, + + + /* Coordinates & Cells + ------------------------------------------------------------------------------------------------------------------*/ + + + // Populates the empty `rows` and `cols` arrays with coordinates of the cells. For CoordGrid. + buildCoords: function(rows, cols) { + var colCnt = this.view.colCnt; + var e, n, p; + + this.dayEls.slice(0, colCnt).each(function(i, _e) { // iterate the first row of day elements + e = $(_e); + n = e.offset().left; + if (i) { + p[1] = n; + } + p = [ n ]; + cols[i] = p; + }); + p[1] = n + e.outerWidth(); + + this.rowEls.each(function(i, _e) { + e = $(_e); + n = e.offset().top; + if (i) { + p[1] = n; + } + p = [ n ]; + rows[i] = p; + }); + p[1] = n + e.outerHeight() + this.bottomCoordPadding; // hack to extend hit area of last row + }, + + + // Converts a cell to a date + getCellDate: function(cell) { + return this.view.cellToDate(cell); // leverages the View's cell system + }, + + + // Gets the whole-day element associated with the cell + getCellDayEl: function(cell) { + return this.dayEls.eq(cell.row * this.view.colCnt + cell.col); + }, + + + // Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects + rangeToSegs: function(start, end) { + return this.view.rangeToSegments(start, end); // leverages the View's cell system + }, + + + /* Event Drag Visualization + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of an event hovering over the given date(s). + // `end` can be null, as well as `seg`. See View's documentation on renderDrag for more info. + // A returned value of `true` signals that a mock "helper" event has been rendered. + renderDrag: function(start, end, seg) { + var opacity; + + // always render a highlight underneath + this.renderHighlight( + start, + end || this.view.calendar.getDefaultEventEnd(true, start) + ); + + // if a segment from the same calendar but another component is being dragged, render a helper event + if (seg && !seg.el.closest(this.el).length) { + + this.renderRangeHelper(start, end, seg); + + opacity = this.view.opt('dragOpacity'); + if (opacity !== undefined) { + this.helperEls.css('opacity', opacity); + } + + return true; // a helper has been rendered + } + }, + + + // Unrenders any visual indication of a hovering event + destroyDrag: function() { + this.destroyHighlight(); + this.destroyHelper(); + }, + + + /* Event Resize Visualization + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of an event being resized + renderResize: function(start, end, seg) { + this.renderHighlight(start, end); + this.renderRangeHelper(start, end, seg); + }, + + + // Unrenders a visual indication of an event being resized + destroyResize: function() { + this.destroyHighlight(); + this.destroyHelper(); + }, + + + /* Event Helper + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null. + renderHelper: function(event, sourceSeg) { + var helperNodes = []; + var rowStructs = this.renderEventRows([ event ]); + + // inject each new event skeleton into each associated row + this.rowEls.each(function(row, rowNode) { + var rowEl = $(rowNode); // the .fc-row + var skeletonEl = $('
'); // will be absolutely positioned + var skeletonTop; + + // If there is an original segment, match the top position. Otherwise, put it at the row's top level + if (sourceSeg && sourceSeg.row === row) { + skeletonTop = sourceSeg.el.position().top; + } + else { + skeletonTop = rowEl.find('.fc-content-skeleton tbody').position().top; + } + + skeletonEl.css('top', skeletonTop) + .find('table') + .append(rowStructs[row].tbodyEl); + + rowEl.append(skeletonEl); + helperNodes.push(skeletonEl[0]); + }); + + this.helperEls = $(helperNodes); // array -> jQuery set + }, + + + // Unrenders any visual indication of a mock helper event + destroyHelper: function() { + if (this.helperEls) { + this.helperEls.remove(); + this.helperEls = null; + } + }, + + + /* Highlighting + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders an emphasis on the given date range. `start` is an inclusive, `end` is exclusive. + renderHighlight: function(start, end) { + var segs = this.rangeToSegs(start, end); + var highlightNodes = []; + var i, seg; + var el; + + // build an event skeleton for each row that needs it + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + el = $( + this.highlightSkeletonHtml(seg.leftCol, seg.rightCol + 1) // make end exclusive + ); + el.appendTo(this.rowEls[seg.row]); + highlightNodes.push(el[0]); + } + + this.highlightEls = $(highlightNodes); // array -> jQuery set + }, + + + // Unrenders any visual emphasis on a date range + destroyHighlight: function() { + if (this.highlightEls) { + this.highlightEls.remove(); + this.highlightEls = null; + } + }, + + + // Generates the HTML used to build a single-row "highlight skeleton", a table that frames highlight cells + highlightSkeletonHtml: function(startCol, endCol) { + var colCnt = this.view.colCnt; + var cellHtml = ''; + + if (startCol > 0) { + cellHtml += ' elements that should be appended to each row's content skeleton. + // Returns an array of rowStruct objects (see the bottom of `renderEventRow`). + renderEventRows: function(events) { + var segs = this.eventsToSegs(events); + var rowStructs = []; + var segRows; + var row; + + segs = this.renderSegs(segs); // returns a new array with only visible segments + segRows = this.groupSegRows(segs); // group into nested arrays + + // iterate each row of segment groupings + for (row = 0; row < segRows.length; row++) { + rowStructs.push( + this.renderEventRow(row, segRows[row]) + ); + } + + return rowStructs; + }, + + + // Builds the HTML to be used for the default element for an individual segment + renderSegHtml: function(seg, disableResizing) { + var view = this.view; + var isRTL = view.opt('isRTL'); + var event = seg.event; + var isDraggable = view.isEventDraggable(event); + var isResizable = !disableResizing && event.allDay && seg.isEnd && view.isEventResizable(event); + var classes = this.getSegClasses(seg, isDraggable, isResizable); + var skinCss = this.getEventSkinCss(event); + var timeHtml = ''; + var titleHtml; + + classes.unshift('fc-day-grid-event'); + + // Only display a timed events time if it is the starting segment + if (!event.allDay && seg.isStart) { + timeHtml = '' + htmlEscape(view.getEventTimeText(event)) + ''; + } + + titleHtml = + '' + + (htmlEscape(event.title || '') || ' ') + // we always want one line of height + ''; + + return '' + + '
' + + (isRTL ? + titleHtml + ' ' + timeHtml : // put a natural space in between + timeHtml + ' ' + titleHtml // + ) + + '
' + + (isResizable ? + '
' : + '' + ) + + ''; + }, + + + // Given a row # and an array of segments all in the same row, render a
element, a skeleton that contains + // the segments. Returns object with a bunch of internal data about how the render was calculated. + renderEventRow: function(row, rowSegs) { + var view = this.view; + var colCnt = view.colCnt; + var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels + var levelCnt = Math.max(1, segLevels.length); // ensure at least one level + var tbody = $(''); + var segMatrix = []; // lookup for which segments are rendered into which level+col cells + var cellMatrix = []; // lookup for all '); + + segMatrix.push([]); + cellMatrix.push([]); + loneCellMatrix.push([]); + + // levelCnt might be 1 even though there are no actual levels. protect against this. + // this single empty row is useful for styling. + if (levelSegs) { + for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level + seg = levelSegs[j]; + + emptyCellsUntil(seg.leftCol); + + // create a container that occupies or more columns. append the event element. + td = $(' at a time and stop when we find one out of bounds + for (i = 0; i < trEls.length; i++) { + trEl = trEls.eq(i).removeClass('fc-limited'); // get and reveal + if (trEl.position().top + trEl.outerHeight() > rowHeight) { + return i; + } + } + + return false; // should not limit at all + }, + + + // Limits the given grid row to the maximum number of levels and injects "more" links if necessary. + // `row` is the row number. + // `levelLimit` is a number for the maximum (inclusive) number of levels allowed. + limitRow: function(row, levelLimit) { + var _this = this; + var view = this.view; + var rowStruct = this.rowStructs[row]; + var moreNodes = []; // array of "more" links and and segment elements past the limit + .addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array + + // iterate though segments in the last allowable level + for (i = 0; i < levelSegs.length; i++) { + seg = levelSegs[i]; + emptyCellsUntil(seg.leftCol); // process empty cells before the segment + + // determine *all* segments below `seg` that occupy the same columns + colSegsBelow = []; + totalSegsBelow = 0; + while (col <= seg.rightCol) { + cell = { row: row, col: col }; + segsBelow = this.getCellSegs(cell, levelLimit); + colSegsBelow.push(segsBelow); + totalSegsBelow += segsBelow.length; + col++; + } + + if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links? + td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell + rowspan = td.attr('rowspan') || 1; + segMoreNodes = []; + + // make a replacement '; + + html += + '' + + (!isRTL ? axisHtml : '') + + '"; + + slotTime.add(this.slotDuration); + } + + return html; + }, + + + // Parses various options into properties of this object + processOptions: function() { + var view = this.view; + var slotDuration = view.opt('slotDuration'); + var snapDuration = view.opt('snapDuration'); + + slotDuration = moment.duration(slotDuration); + snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration; + + this.slotDuration = slotDuration; + this.snapDuration = snapDuration; + this.cellDuration = snapDuration; // important to assign this for Grid.events.js + + this.minTime = moment.duration(view.opt('minTime')); + this.maxTime = moment.duration(view.opt('maxTime')); + }, + + + // Slices up a date range into a segment for each column + rangeToSegs: function(rangeStart, rangeEnd) { + var view = this.view; + var segs = []; + var seg; + var col; + var cellDate; + var colStart, colEnd; + + // normalize + rangeStart = rangeStart.clone().stripZone(); + rangeEnd = rangeEnd.clone().stripZone(); + + for (col = 0; col < view.colCnt; col++) { + cellDate = view.cellToDate(0, col); // use the View's cell system for this + colStart = cellDate.clone().time(this.minTime); + colEnd = cellDate.clone().time(this.maxTime); + seg = intersectionToSeg(rangeStart, rangeEnd, colStart, colEnd); + if (seg) { + seg.col = col; + segs.push(seg); + } + } + + return segs; + }, + + + /* Coordinates + ------------------------------------------------------------------------------------------------------------------*/ + + + // Called when there is a window resize/zoom and we need to recalculate coordinates for the grid + resize: function() { + this.computeSlatTops(); + this.updateSegVerticals(); + }, + + + // Populates the given empty `rows` and `cols` arrays with offset positions of the "snap" cells. + // "Snap" cells are different the slots because they might have finer granularity. + buildCoords: function(rows, cols) { + var colCnt = this.view.colCnt; + var originTop = this.el.offset().top; + var snapTime = moment.duration(+this.minTime); + var p = null; + var e, n; + + this.dayEls.slice(0, colCnt).each(function(i, _e) { + e = $(_e); + n = e.offset().left; + if (p) { + p[1] = n; + } + p = [ n ]; + cols[i] = p; + }); + p[1] = n + e.outerWidth(); + + p = null; + while (snapTime < this.maxTime) { + n = originTop + this.computeTimeTop(snapTime); + if (p) { + p[1] = n; + } + p = [ n ]; + rows.push(p); + snapTime.add(this.snapDuration); + } + p[1] = originTop + this.computeTimeTop(snapTime); // the position of the exclusive end + }, + + + // Gets the datetime for the given slot cell + getCellDate: function(cell) { + var view = this.view; + var calendar = view.calendar; + + return calendar.rezoneDate( // since we are adding a time, it needs to be in the calendar's timezone + view.cellToDate(0, cell.col) // View's coord system only accounts for start-of-day for column + .time(this.minTime + this.snapDuration * cell.row) + ); + }, + + + // Gets the element that represents the whole-day the cell resides on + getCellDayEl: function(cell) { + return this.dayEls.eq(cell.col); + }, + + + // Computes the top coordinate, relative to the bounds of the grid, of the given date. + // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight. + computeDateTop: function(date, startOfDayDate) { + return this.computeTimeTop( + moment.duration( + date.clone().stripZone() - startOfDayDate.clone().stripTime() + ) + ); + }, + + + // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration). + computeTimeTop: function(time) { + var slatCoverage = (time - this.minTime) / this.slotDuration; // floating-point value of # of slots covered + var slatIndex; + var slatRemainder; + var slatTop; + var slatBottom; + + // constrain. because minTime/maxTime might be customized + slatCoverage = Math.max(0, slatCoverage); + slatCoverage = Math.min(this.slatEls.length, slatCoverage); + + slatIndex = Math.floor(slatCoverage); // an integer index of the furthest whole slot + slatRemainder = slatCoverage - slatIndex; + slatTop = this.slatTops[slatIndex]; // the top position of the furthest whole slot + + if (slatRemainder) { // time spans part-way into the slot + slatBottom = this.slatTops[slatIndex + 1]; + return slatTop + (slatBottom - slatTop) * slatRemainder; // part-way between slots + } + else { + return slatTop; + } + }, + + + // Queries each `slatEl` for its position relative to the grid's container and stores it in `slatTops`. + // Includes the the bottom of the last slat as the last item in the array. + computeSlatTops: function() { + var tops = []; + var top; + + this.slatEls.each(function(i, node) { + top = $(node).position().top; + tops.push(top); + }); + + tops.push(top + this.slatEls.last().outerHeight()); // bottom of the last slat + + this.slatTops = tops; + }, + + + /* Event Drag Visualization + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of an event being dragged over the specified date(s). + // `end` and `seg` can be null. See View's documentation on renderDrag for more info. + renderDrag: function(start, end, seg) { + var opacity; + + if (seg) { // if there is event information for this drag, render a helper event + this.renderRangeHelper(start, end, seg); + + opacity = this.view.opt('dragOpacity'); + if (opacity !== undefined) { + this.helperEl.css('opacity', opacity); + } + + return true; // signal that a helper has been rendered + } + else { + // otherwise, just render a highlight + this.renderHighlight( + start, + end || this.view.calendar.getDefaultEventEnd(false, start) + ); + } + }, + + + // Unrenders any visual indication of an event being dragged + destroyDrag: function() { + this.destroyHelper(); + this.destroyHighlight(); + }, + + + /* Event Resize Visualization + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of an event being resized + renderResize: function(start, end, seg) { + this.renderRangeHelper(start, end, seg); + }, + + + // Unrenders any visual indication of an event being resized + destroyResize: function() { + this.destroyHelper(); + }, + + + /* Event Helper + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag) + renderHelper: function(event, sourceSeg) { + var res = this.renderEventTable([ event ]); + var tableEl = res.tableEl; + var segs = res.segs; + var i, seg; + var sourceEl; + + // Try to make the segment that is in the same row as sourceSeg look the same + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + if (sourceSeg && sourceSeg.col === seg.col) { + sourceEl = sourceSeg.el; + seg.el.css({ + left: sourceEl.css('left'), + right: sourceEl.css('right'), + 'margin-left': sourceEl.css('margin-left'), + 'margin-right': sourceEl.css('margin-right') + }); + } + } + + this.helperEl = $('
') + .append(tableEl) + .appendTo(this.el); + }, + + + // Unrenders any mock helper event + destroyHelper: function() { + if (this.helperEl) { + this.helperEl.remove(); + this.helperEl = null; + } + }, + + + /* Selection + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight. + renderSelection: function(start, end) { + if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered + this.renderRangeHelper(start, end); + } + else { + this.renderHighlight(start, end); + } + }, + + + // Unrenders any visual indication of a selection + destroySelection: function() { + this.destroyHelper(); + this.destroyHighlight(); + }, + + + /* Highlight + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders an emphasis on the given date range. `start` is inclusive. `end` is exclusive. + renderHighlight: function(start, end) { + this.highlightEl = $( + this.highlightSkeletonHtml(start, end) + ).appendTo(this.el); + }, + + + // Unrenders the emphasis on a date range + destroyHighlight: function() { + if (this.highlightEl) { + this.highlightEl.remove(); + this.highlightEl = null; + } + }, + + + // Generates HTML for a table element with containers in each column, responsible for absolutely positioning the + // highlight elements to cover the highlighted slots. + highlightSkeletonHtml: function(start, end) { + var view = this.view; + var segs = this.rangeToSegs(start, end); + var cellHtml = ''; + var col = 0; + var i, seg; + var dayDate; + var top, bottom; + + for (i = 0; i < segs.length; i++) { // loop through the segments. one per column + seg = segs[i]; + + // need empty cells beforehand? + if (col < seg.col) { + cellHtml += '
'; + + col++; + } + + // need empty cells after the last segment? + if (col < view.colCnt) { + cellHtml += ''; + } + }, + + + // Generates the HTML that will go before content-skeleton cells that display the day/week numbers. + // Queried by the DayGrid subcomponent. Ordering depends on isRTL. + numberIntroHtml: function(row) { + if (this.weekNumbersVisible) { + return '' + + ''; + } + }, + + + // Generates the HTML that goes before the day bg cells for each day-row. + // Queried by the DayGrid subcomponent. Ordering depends on isRTL. + dayIntroHtml: function() { + if (this.weekNumbersVisible) { + return ''; + } + }, + + + // Generates the HTML that goes before every other type of row generated by DayGrid. Ordering depends on isRTL. + // Affects helper-skeleton and highlight-skeleton rows. + introHtml: function() { + if (this.weekNumbersVisible) { + return ''; + } + }, + + + // Generates the HTML for the '; + }, + + + // Generates an HTML attribute string for setting the width of the week number column, if it is known + weekNumberStyleAttr: function() { + if (this.weekNumberWidth !== null) { + return 'style="width:' + this.weekNumberWidth + 'px"'; + } + return ''; + }, + + + // Determines whether each row should have a constant height + hasRigidRows: function() { + var eventLimit = this.opt('eventLimit'); + return eventLimit && typeof eventLimit !== 'number'; + }, + + + /* Dimensions + ------------------------------------------------------------------------------------------------------------------*/ + + + // Refreshes the horizontal dimensions of the view + updateWidth: function() { + if (this.weekNumbersVisible) { + // Make sure all week number cells running down the side have the same width. + // Record the width for cells created later. + this.weekNumberWidth = matchCellWidths( + this.el.find('.fc-week-number') + ); + } + }, + + + // Adjusts the vertical dimensions of the view to the specified values + setHeight: function(totalHeight, isAuto) { + var eventLimit = this.opt('eventLimit'); + var scrollerHeight; + + // reset all heights to be natural + unsetScroller(this.scrollerEl); + uncompensateScroll(this.headRowEl); + + this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed + + // is the event limit a constant level number? + if (eventLimit && typeof eventLimit === 'number') { + this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after + } + + scrollerHeight = this.computeScrollerHeight(totalHeight); + this.setGridHeight(scrollerHeight, isAuto); + + // is the event limit dynamically calculated? + if (eventLimit && typeof eventLimit !== 'number') { + this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set + } + + if (!isAuto && setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars? + + compensateScroll(this.headRowEl, getScrollbarWidths(this.scrollerEl)); + + // doing the scrollbar compensation might have created text overflow which created more height. redo + scrollerHeight = this.computeScrollerHeight(totalHeight); + this.scrollerEl.height(scrollerHeight); + + this.restoreScroll(); + } + }, + + + // Sets the height of just the DayGrid component in this view + setGridHeight: function(height, isAuto) { + if (isAuto) { + undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding + } + else { + distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows + } + }, + + + /* Events + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders the given events onto the view and populates the segments array + renderEvents: function(events) { + this.dayGrid.renderEvents(events); + + this.updateHeight(); // must compensate for events that overflow the row + + View.prototype.renderEvents.call(this, events); // call the super-method + }, + + + // Retrieves all segment objects that are rendered in the view + getSegs: function() { + return this.dayGrid.getSegs(); + }, + + + // Unrenders all event elements and clears internal segment data + destroyEvents: function() { + View.prototype.destroyEvents.call(this); // do this before dayGrid's segs have been cleared + + this.recordScroll(); // removing events will reduce height and mess with the scroll, so record beforehand + this.dayGrid.destroyEvents(); + + // we DON'T need to call updateHeight() because: + // A) a renderEvents() call always happens after this, which will eventually call updateHeight() + // B) in IE8, this causes a flash whenever events are rerendered + }, + + + /* Event Dragging + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of an event being dragged over the view. + // A returned value of `true` signals that a mock "helper" event has been rendered. + renderDrag: function(start, end, seg) { + return this.dayGrid.renderDrag(start, end, seg); + }, + + + // Unrenders the visual indication of an event being dragged over the view + destroyDrag: function() { + this.dayGrid.destroyDrag(); + }, + + + /* Selection + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of a selection + renderSelection: function(start, end) { + this.dayGrid.renderSelection(start, end); + }, + + + // Unrenders a visual indications of a selection + destroySelection: function() { + this.dayGrid.destroySelection(); + } + +}); + +;; + +/* A month view with day cells running in rows (one-per-week) and columns +----------------------------------------------------------------------------------------------------------------------*/ + +setDefaults({ + fixedWeekCount: true +}); + +fcViews.month = MonthView; // register the view + +function MonthView(calendar) { + BasicView.call(this, calendar); // call the super-constructor +} + + +MonthView.prototype = createObject(BasicView.prototype); // define the super-class +$.extend(MonthView.prototype, { + + name: 'month', + + + incrementDate: function(date, delta) { + return date.clone().stripTime().add(delta, 'months').startOf('month'); + }, + + + render: function(date) { + var rowCnt; + + this.intervalStart = date.clone().stripTime().startOf('month'); + this.intervalEnd = this.intervalStart.clone().add(1, 'months'); + + this.start = this.intervalStart.clone(); + this.start = this.skipHiddenDays(this.start); // move past the first week if no visible days + this.start.startOf('week'); + this.start = this.skipHiddenDays(this.start); // move past the first invisible days of the week + + this.end = this.intervalEnd.clone(); + this.end = this.skipHiddenDays(this.end, -1, true); // move in from the last week if no visible days + this.end.add((7 - this.end.weekday()) % 7, 'days'); // move to end of week if not already + this.end = this.skipHiddenDays(this.end, -1, true); // move in from the last invisible days of the week + + rowCnt = Math.ceil( // need to ceil in case there are hidden days + this.end.diff(this.start, 'weeks', true) // returnfloat=true + ); + if (this.isFixedWeeks()) { + this.end.add(6 - rowCnt, 'weeks'); + rowCnt = 6; + } + + this.title = this.calendar.formatDate(this.intervalStart, this.opt('titleFormat')); + + BasicView.prototype.render.call(this, rowCnt, this.getCellsPerWeek(), true); // call the super-method + }, + + + // Overrides the default BasicView behavior to have special multi-week auto-height logic + setGridHeight: function(height, isAuto) { + + isAuto = isAuto || this.opt('weekMode') === 'variable'; // LEGACY: weekMode is deprecated + + // if auto, make the height of each row the height that it would be if there were 6 weeks + if (isAuto) { + height *= this.rowCnt / 6; + } + + distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows + }, + + + isFixedWeeks: function() { + var weekMode = this.opt('weekMode'); // LEGACY: weekMode is deprecated + if (weekMode) { + return weekMode === 'fixed'; // if any other type of weekMode, assume NOT fixed + } + + return this.opt('fixedWeekCount'); + } + +}); + +;; + +/* A week view with simple day cells running horizontally +----------------------------------------------------------------------------------------------------------------------*/ +// TODO: a WeekView mixin for calculating dates and titles + +fcViews.basicWeek = BasicWeekView; // register this view + +function BasicWeekView(calendar) { + BasicView.call(this, calendar); // call the super-constructor +} + + +BasicWeekView.prototype = createObject(BasicView.prototype); // define the super-class +$.extend(BasicWeekView.prototype, { + + name: 'basicWeek', + + + incrementDate: function(date, delta) { + return date.clone().stripTime().add(delta, 'weeks').startOf('week'); + }, + + + render: function(date) { + + this.intervalStart = date.clone().stripTime().startOf('week'); + this.intervalEnd = this.intervalStart.clone().add(1, 'weeks'); + + this.start = this.skipHiddenDays(this.intervalStart); + this.end = this.skipHiddenDays(this.intervalEnd, -1, true); + + this.title = this.calendar.formatRange( + this.start, + this.end.clone().subtract(1), // make inclusive by subtracting 1 ms + this.opt('titleFormat'), + ' \u2014 ' // emphasized dash + ); + + BasicView.prototype.render.call(this, 1, this.getCellsPerWeek(), false); // call the super-method + } + +}); +;; + +/* A view with a single simple day cell +----------------------------------------------------------------------------------------------------------------------*/ + +fcViews.basicDay = BasicDayView; // register this view + +function BasicDayView(calendar) { + BasicView.call(this, calendar); // call the super-constructor +} + + +BasicDayView.prototype = createObject(BasicView.prototype); // define the super-class +$.extend(BasicDayView.prototype, { + + name: 'basicDay', + + + incrementDate: function(date, delta) { + var out = date.clone().stripTime().add(delta, 'days'); + out = this.skipHiddenDays(out, delta < 0 ? -1 : 1); + return out; + }, + + + render: function(date) { + + this.start = this.intervalStart = date.clone().stripTime(); + this.end = this.intervalEnd = this.start.clone().add(1, 'days'); + + this.title = this.calendar.formatDate(this.start, this.opt('titleFormat')); + + BasicView.prototype.render.call(this, 1, 1, false); // call the super-method + } + +}); +;; + +/* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically. +----------------------------------------------------------------------------------------------------------------------*/ +// Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on). +// Responsible for managing width/height. + +setDefaults({ + allDaySlot: true, + allDayText: 'all-day', + + scrollTime: '06:00:00', + + slotDuration: '00:30:00', + + axisFormat: generateAgendaAxisFormat, + timeFormat: { + agenda: generateAgendaTimeFormat + }, + + minTime: '00:00:00', + maxTime: '24:00:00', + slotEventOverlap: true +}); + +var AGENDA_ALL_DAY_EVENT_LIMIT = 5; + + +function generateAgendaAxisFormat(options, langData) { + return langData.longDateFormat('LT') + .replace(':mm', '(:mm)') + .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs + .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand +} + + +function generateAgendaTimeFormat(options, langData) { + return langData.longDateFormat('LT') + .replace(/\s*a$/i, ''); // remove trailing AM/PM +} + + +function AgendaView(calendar) { + View.call(this, calendar); // call the super-constructor + + this.timeGrid = new TimeGrid(this); + + if (this.opt('allDaySlot')) { // should we display the "all-day" area? + this.dayGrid = new DayGrid(this); // the all-day subcomponent of this view + + // the coordinate grid will be a combination of both subcomponents' grids + this.coordMap = new ComboCoordMap([ + this.dayGrid.coordMap, + this.timeGrid.coordMap + ]); + } + else { + this.coordMap = this.timeGrid.coordMap; + } +} + + +AgendaView.prototype = createObject(View.prototype); // define the super-class +$.extend(AgendaView.prototype, { + + timeGrid: null, // the main time-grid subcomponent of this view + dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null + + axisWidth: null, // the width of the time axis running down the side + + noScrollRowEls: null, // set of fake row elements that must compensate when scrollerEl has scrollbars + + // when the time-grid isn't tall enough to occupy the given height, we render an
underneath + bottomRuleEl: null, + bottomRuleHeight: null, + + + /* Rendering + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders the view into `this.el`, which has already been assigned. + // `colCnt` has been calculated by a subclass and passed here. + render: function(colCnt) { + + // needed for cell-to-date and date-to-cell calculations in View + this.rowCnt = 1; + this.colCnt = colCnt; + + this.el.addClass('fc-agenda-view').html(this.renderHtml()); + + // the element that wraps the time-grid that will probably scroll + this.scrollerEl = this.el.find('.fc-time-grid-container'); + this.timeGrid.coordMap.containerEl = this.scrollerEl; // don't accept clicks/etc outside of this + + this.timeGrid.el = this.el.find('.fc-time-grid'); + this.timeGrid.render(); + + // the
that sometimes displays under the time-grid + this.bottomRuleEl = $('
') + .appendTo(this.timeGrid.el); // inject it into the time-grid + + if (this.dayGrid) { + this.dayGrid.el = this.el.find('.fc-day-grid'); + this.dayGrid.render(); + + // have the day-grid extend it's coordinate area over the
dividing the two grids + this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight(); + } + + this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller + + View.prototype.render.call(this); // call the super-method + + this.resetScroll(); // do this after sizes have been set + }, + + + // Make subcomponents ready for cleanup + destroy: function() { + this.timeGrid.destroy(); + if (this.dayGrid) { + this.dayGrid.destroy(); + } + View.prototype.destroy.call(this); // call the super-method + }, + + + // Builds the HTML skeleton for the view. + // The day-grid and time-grid components will render inside containers defined by this HTML. + renderHtml: function() { + return '' + + '
'; + } + if (endCol > startCol) { + cellHtml += ''; + } + if (colCnt > endCol) { + cellHtml += ''; + } + + cellHtml = this.bookendCells(cellHtml, 'highlight'); + + return '' + + '
' + + '' + + '' + + cellHtml + + '' + + '
' + + '
'; + } + +}); + +;; + +/* Event-rendering methods for the DayGrid class +----------------------------------------------------------------------------------------------------------------------*/ + +$.extend(DayGrid.prototype, { + + segs: null, + rowStructs: null, // an array of objects, each holding information about a row's event-rendering + + + // Render the given events onto the Grid and return the rendered segments + renderEvents: function(events) { + var rowStructs = this.rowStructs = this.renderEventRows(events); + var segs = []; + + // append to each row's content skeleton + this.rowEls.each(function(i, rowNode) { + $(rowNode).find('.fc-content-skeleton > table').append( + rowStructs[i].tbodyEl + ); + segs.push.apply(segs, rowStructs[i].segs); + }); + + this.segs = segs; + }, + + + // Retrieves all segment objects that have been rendered + getSegs: function() { + return (this.segs || []).concat( + this.popoverSegs || [] // segs rendered in the "more" events popover + ); + }, + + + // Removes all rendered event elements + destroyEvents: function() { + var rowStructs; + var rowStruct; + + Grid.prototype.destroyEvents.call(this); // call the super-method + + rowStructs = this.rowStructs || []; + while ((rowStruct = rowStructs.pop())) { + rowStruct.tbodyEl.remove(); + } + + this.segs = null; + this.destroySegPopover(); // removes the "more.." events popover + }, + + + // Uses the given events array to generate
elements of the level+col matrix + var loneCellMatrix = []; // lookup for elements that only take up a single column + var i, levelSegs; + var col; + var tr; + var j, seg; + var td; + + // populates empty cells from the current column (`col`) to `endCol` + function emptyCellsUntil(endCol) { + while (col < endCol) { + // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell + td = (loneCellMatrix[i - 1] || [])[col]; + if (td) { + td.attr( + 'rowspan', + parseInt(td.attr('rowspan') || 1, 10) + 1 + ); + } + else { + td = $(''); + tr.append(td); + } + cellMatrix[i][col] = td; + loneCellMatrix[i][col] = td; + col++; + } + } + + for (i = 0; i < levelCnt; i++) { // iterate through all levels + levelSegs = segLevels[i]; + col = 0; + tr = $('
').append(seg.el); + if (seg.leftCol != seg.rightCol) { + td.attr('colspan', seg.rightCol - seg.leftCol + 1); + } + else { // a single-column segment + loneCellMatrix[i][col] = td; + } + + while (col <= seg.rightCol) { + cellMatrix[i][col] = td; + segMatrix[i][col] = seg; + col++; + } + + tr.append(td); + } + } + + emptyCellsUntil(colCnt); // finish off the row + this.bookendCells(tr, 'eventSkeleton'); + tbody.append(tr); + } + + return { // a "rowStruct" + row: row, // the row number + tbodyEl: tbody, + cellMatrix: cellMatrix, + segMatrix: segMatrix, + segLevels: segLevels, + segs: rowSegs + }; + }, + + + // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels. + buildSegLevels: function(segs) { + var levels = []; + var i, seg; + var j; + + // Give preference to elements with certain criteria, so they have + // a chance to be closer to the top. + segs.sort(compareSegs); + + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + + // loop through levels, starting with the topmost, until the segment doesn't collide with other segments + for (j = 0; j < levels.length; j++) { + if (!isDaySegCollision(seg, levels[j])) { + break; + } + } + // `j` now holds the desired subrow index + seg.level = j; + + // create new level array if needed and append segment + (levels[j] || (levels[j] = [])).push(seg); + } + + // order segments left-to-right. very important if calendar is RTL + for (j = 0; j < levels.length; j++) { + levels[j].sort(compareDaySegCols); + } + + return levels; + }, + + + // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row + groupSegRows: function(segs) { + var view = this.view; + var segRows = []; + var i; + + for (i = 0; i < view.rowCnt; i++) { + segRows.push([]); + } + + for (i = 0; i < segs.length; i++) { + segRows[segs[i].row].push(segs[i]); + } + + return segRows; + } + +}); + + +// Computes whether two segments' columns collide. They are assumed to be in the same row. +function isDaySegCollision(seg, otherSegs) { + var i, otherSeg; + + for (i = 0; i < otherSegs.length; i++) { + otherSeg = otherSegs[i]; + + if ( + otherSeg.leftCol <= seg.rightCol && + otherSeg.rightCol >= seg.leftCol + ) { + return true; + } + } + + return false; +} + + +// A cmp function for determining the leftmost event +function compareDaySegCols(a, b) { + return a.leftCol - b.leftCol; +} + +;; + +/* Methods relate to limiting the number events for a given day on a DayGrid +----------------------------------------------------------------------------------------------------------------------*/ + +$.extend(DayGrid.prototype, { + + + segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible + popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible + + + destroySegPopover: function() { + if (this.segPopover) { + this.segPopover.hide(); // will trigger destruction of `segPopover` and `popoverSegs` + } + }, + + + // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid. + // `levelLimit` can be false (don't limit), a number, or true (should be computed). + limitRows: function(levelLimit) { + var rowStructs = this.rowStructs || []; + var row; // row # + var rowLevelLimit; + + for (row = 0; row < rowStructs.length; row++) { + this.unlimitRow(row); + + if (!levelLimit) { + rowLevelLimit = false; + } + else if (typeof levelLimit === 'number') { + rowLevelLimit = levelLimit; + } + else { + rowLevelLimit = this.computeRowLevelLimit(row); + } + + if (rowLevelLimit !== false) { + this.limitRow(row, rowLevelLimit); + } + } + }, + + + // Computes the number of levels a row will accomodate without going outside its bounds. + // Assumes the row is "rigid" (maintains a constant height regardless of what is inside). + // `row` is the row number. + computeRowLevelLimit: function(row) { + var rowEl = this.rowEls.eq(row); // the containing "fake" row div + var rowHeight = rowEl.height(); // TODO: cache somehow? + var trEls = this.rowStructs[row].tbodyEl.children(); + var i, trEl; + + // Reveal one level
DOM nodes + var col = 0; // col # + var cell; + var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right + var cellMatrix; // a matrix (by level, then column) of all jQuery elements in the row + var limitedNodes; // array of temporarily hidden level
DOM nodes + var i, seg; + var segsBelow; // array of segment objects below `seg` in the current `col` + var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies + var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column) + var td, rowspan; + var segMoreNodes; // array of "more" cells that will stand-in for the current seg's cell + var j; + var moreTd, moreWrap, moreLink; + + // Iterates through empty level cells and places "more" links inside if need be + function emptyCellsUntil(endCol) { // goes from current `col` to `endCol` + while (col < endCol) { + cell = { row: row, col: col }; + segsBelow = _this.getCellSegs(cell, levelLimit); + if (segsBelow.length) { + td = cellMatrix[levelLimit - 1][col]; + moreLink = _this.renderMoreLink(cell, segsBelow); + moreWrap = $('
').append(moreLink); + td.append(moreWrap); + moreNodes.push(moreWrap[0]); + } + col++; + } + } + + if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit? + levelSegs = rowStruct.segLevels[levelLimit - 1]; + cellMatrix = rowStruct.cellMatrix; + + limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level
for each column the segment occupies. will be one for each colspan + for (j = 0; j < colSegsBelow.length; j++) { + moreTd = $('').attr('rowspan', rowspan); + segsBelow = colSegsBelow[j]; + cell = { row: row, col: seg.leftCol + j }; + moreLink = this.renderMoreLink(cell, [ seg ].concat(segsBelow)); // count seg as hidden too + moreWrap = $('
').append(moreLink); + moreTd.append(moreWrap); + segMoreNodes.push(moreTd[0]); + moreNodes.push(moreTd[0]); + } + + td.addClass('fc-limited').after($(segMoreNodes)); // hide original
and inject replacements + limitedNodes.push(td[0]); + } + } + + emptyCellsUntil(view.colCnt); // finish off the level + rowStruct.moreEls = $(moreNodes); // for easy undoing later + rowStruct.limitedEls = $(limitedNodes); // for easy undoing later + } + }, + + + // Reveals all levels and removes all "more"-related elements for a grid's row. + // `row` is a row number. + unlimitRow: function(row) { + var rowStruct = this.rowStructs[row]; + + if (rowStruct.moreEls) { + rowStruct.moreEls.remove(); + rowStruct.moreEls = null; + } + + if (rowStruct.limitedEls) { + rowStruct.limitedEls.removeClass('fc-limited'); + rowStruct.limitedEls = null; + } + }, + + + // Renders an element that represents hidden event element for a cell. + // Responsible for attaching click handler as well. + renderMoreLink: function(cell, hiddenSegs) { + var _this = this; + var view = this.view; + + return $('') + .text( + this.getMoreLinkText(hiddenSegs.length) + ) + .on('click', function(ev) { + var clickOption = view.opt('eventLimitClick'); + var date = view.cellToDate(cell); + var moreEl = $(this); + var dayEl = _this.getCellDayEl(cell); + var allSegs = _this.getCellSegs(cell); + + // rescope the segments to be within the cell's date + var reslicedAllSegs = _this.resliceDaySegs(allSegs, date); + var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date); + + if (typeof clickOption === 'function') { + // the returned value can be an atomic option + clickOption = view.trigger('eventLimitClick', null, { + date: date, + dayEl: dayEl, + moreEl: moreEl, + segs: reslicedAllSegs, + hiddenSegs: reslicedHiddenSegs + }, ev); + } + + if (clickOption === 'popover') { + _this.showSegPopover(date, cell, moreEl, reslicedAllSegs); + } + else if (typeof clickOption === 'string') { // a view name + view.calendar.zoomTo(date, clickOption); + } + }); + }, + + + // Reveals the popover that displays all events within a cell + showSegPopover: function(date, cell, moreLink, segs) { + var _this = this; + var view = this.view; + var moreWrap = moreLink.parent(); // the
wrapper around the + var topEl; // the element we want to match the top coordinate of + var options; + + if (view.rowCnt == 1) { + topEl = this.view.el; // will cause the popover to cover any sort of header + } + else { + topEl = this.rowEls.eq(cell.row); // will align with top of row + } + + options = { + className: 'fc-more-popover', + content: this.renderSegPopoverContent(date, segs), + parentEl: this.el, + top: topEl.offset().top, + autoHide: true, // when the user clicks elsewhere, hide the popover + viewportConstrain: view.opt('popoverViewportConstrain'), + hide: function() { + // destroy everything when the popover is hidden + _this.segPopover.destroy(); + _this.segPopover = null; + _this.popoverSegs = null; + } + }; + + // Determine horizontal coordinate. + // We use the moreWrap instead of the
to avoid border confusion. + if (view.opt('isRTL')) { + options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border + } + else { + options.left = moreWrap.offset().left - 1; // -1 to be over cell border + } + + this.segPopover = new Popover(options); + this.segPopover.show(); + }, + + + // Builds the inner DOM contents of the segment popover + renderSegPopoverContent: function(date, segs) { + var view = this.view; + var isTheme = view.opt('theme'); + var title = date.format(view.opt('dayPopoverFormat')); + var content = $( + '
' + + '' + + '' + + htmlEscape(title) + + '' + + '
' + + '
' + + '
' + + '
' + + '
' + ); + var segContainer = content.find('.fc-event-container'); + var i; + + // render each seg's `el` and only return the visible segs + segs = this.renderSegs(segs, true); // disableResizing=true + this.popoverSegs = segs; + + for (i = 0; i < segs.length; i++) { + + // because segments in the popover are not part of a grid coordinate system, provide a hint to any + // grids that want to do drag-n-drop about which cell it came from + segs[i].cellDate = date; + + segContainer.append(segs[i].el); + } + + return content; + }, + + + // Given the events within an array of segment objects, reslice them to be in a single day + resliceDaySegs: function(segs, dayDate) { + var events = $.map(segs, function(seg) { + return seg.event; + }); + var dayStart = dayDate.clone().stripTime(); + var dayEnd = dayStart.clone().add(1, 'days'); + + return this.eventsToSegs(events, dayStart, dayEnd); + }, + + + // Generates the text that should be inside a "more" link, given the number of events it represents + getMoreLinkText: function(num) { + var view = this.view; + var opt = view.opt('eventLimitText'); + + if (typeof opt === 'function') { + return opt(num); + } + else { + return '+' + num + ' ' + opt; + } + }, + + + // Returns segments within a given cell. + // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs. + getCellSegs: function(cell, startLevel) { + var segMatrix = this.rowStructs[cell.row].segMatrix; + var level = startLevel || 0; + var segs = []; + var seg; + + while (level < segMatrix.length) { + seg = segMatrix[level][cell.col]; + if (seg) { + segs.push(seg); + } + level++; + } + + return segs; + } + +}); + +;; + +/* A component that renders one or more columns of vertical time slots +----------------------------------------------------------------------------------------------------------------------*/ + +function TimeGrid(view) { + Grid.call(this, view); // call the super-constructor +} + + +TimeGrid.prototype = createObject(Grid.prototype); // define the super-class +$.extend(TimeGrid.prototype, { + + slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines + snapDuration: null, // granularity of time for dragging and selecting + + minTime: null, // Duration object that denotes the first visible time of any given day + maxTime: null, // Duration object that denotes the exclusive visible end time of any given day + + dayEls: null, // cells elements in the day-row background + slatEls: null, // elements running horizontally across all columns + + slatTops: null, // an array of top positions, relative to the container. last item holds bottom of last slot + + highlightEl: null, // cell skeleton element for rendering the highlight + helperEl: null, // cell skeleton element for rendering the mock event "helper" + + + // Renders the time grid into `this.el`, which should already be assigned. + // Relies on the view's colCnt. In the future, this component should probably be self-sufficient. + render: function() { + this.processOptions(); + + this.el.html(this.renderHtml()); + + this.dayEls = this.el.find('.fc-day'); + this.slatEls = this.el.find('.fc-slats tr'); + + this.computeSlatTops(); + + Grid.prototype.render.call(this); // call the super-method + }, + + + // Renders the basic HTML skeleton for the grid + renderHtml: function() { + return '' + + '
' + + '' + + this.rowHtml('slotBg') + // leverages RowRenderer, which will call slotBgCellHtml + '
' + + '
' + + '
' + + '' + + this.slatRowHtml() + + '
' + + '
'; + }, + + + // Renders the HTML for a vertical background cell behind the slots. + // This method is distinct from 'bg' because we wanted a new `rowType` so the View could customize the rendering. + slotBgCellHtml: function(row, col, date) { + return this.bgCellHtml(row, col, date); + }, + + + // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL. + slatRowHtml: function() { + var view = this.view; + var calendar = view.calendar; + var isRTL = view.opt('isRTL'); + var html = ''; + var slotNormal = this.slotDuration.asMinutes() % 15 === 0; + var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations + var slotDate; // will be on the view's first day, but we only care about its time + var minutes; + var axisHtml; + + // Calculate the time for each slot + while (slotTime < this.maxTime) { + slotDate = view.start.clone().time(slotTime); // will be in UTC but that's good. to avoid DST issues + minutes = slotDate.minutes(); + + axisHtml = + '
' + + ((!slotNormal || !minutes) ? // if irregular slot duration, or on the hour, then display the time + '' + // for matchCellWidths + htmlEscape(calendar.formatDate(slotDate, view.opt('axisFormat'))) + + '' : + '' + ) + + '
' + + (isRTL ? axisHtml : '') + + "
'; + col = seg.col; + } + + // compute vertical position + dayDate = view.cellToDate(0, col); + top = this.computeDateTop(seg.start, dayDate); + bottom = this.computeDateTop(seg.end, dayDate); // the y position of the bottom edge + + // generate the cell HTML. bottom becomes negative because it needs to be a CSS value relative to the + // bottom edge of the zero-height container. + cellHtml += + '' + + '
' + + '
' + + '
' + + '
'; + } + + cellHtml = this.bookendCells(cellHtml, 'highlight'); + + return '' + + '
' + + '' + + '' + + cellHtml + + '' + + '
' + + '
'; + } + +}); + +;; + +/* Event-rendering methods for the TimeGrid class +----------------------------------------------------------------------------------------------------------------------*/ + +$.extend(TimeGrid.prototype, { + + segs: null, // segment objects rendered in the component. null of events haven't been rendered yet + eventSkeletonEl: null, // has cells with event-containers, which contain absolutely positioned event elements + + + // Renders the events onto the grid and returns an array of segments that have been rendered + renderEvents: function(events) { + var res = this.renderEventTable(events); + + this.eventSkeletonEl = $('
').append(res.tableEl); + this.el.append(this.eventSkeletonEl); + + this.segs = res.segs; + }, + + + // Retrieves rendered segment objects + getSegs: function() { + return this.segs || []; + }, + + + // Removes all event segment elements from the view + destroyEvents: function() { + Grid.prototype.destroyEvents.call(this); // call the super-method + + if (this.eventSkeletonEl) { + this.eventSkeletonEl.remove(); + this.eventSkeletonEl = null; + } + + this.segs = null; + }, + + + // Renders and returns the portion of the event-skeleton. + // Returns an object with properties 'tbodyEl' and 'segs'. + renderEventTable: function(events) { + var tableEl = $('
'); + var trEl = tableEl.find('tr'); + var segs = this.eventsToSegs(events); + var segCols; + var i, seg; + var col, colSegs; + var containerEl; + + segs = this.renderSegs(segs); // returns only the visible segs + segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg + + this.computeSegVerticals(segs); // compute and assign top/bottom + + for (col = 0; col < segCols.length; col++) { // iterate each column grouping + colSegs = segCols[col]; + placeSlotSegs(colSegs); // compute horizontal coordinates, z-index's, and reorder the array + + containerEl = $('
'); + + // assign positioning CSS and insert into container + for (i = 0; i < colSegs.length; i++) { + seg = colSegs[i]; + seg.el.css(this.generateSegPositionCss(seg)); + + // if the height is short, add a className for alternate styling + if (seg.bottom - seg.top < 30) { + seg.el.addClass('fc-short'); + } + + containerEl.append(seg.el); + } + + trEl.append($('
').append(containerEl)); + } + + this.bookendCells(trEl, 'eventSkeleton'); + + return { + tableEl: tableEl, + segs: segs + }; + }, + + + // Refreshes the CSS top/bottom coordinates for each segment element. Probably after a window resize/zoom. + updateSegVerticals: function() { + var segs = this.segs; + var i; + + if (segs) { + this.computeSegVerticals(segs); + + for (i = 0; i < segs.length; i++) { + segs[i].el.css( + this.generateSegVerticalCss(segs[i]) + ); + } + } + }, + + + // For each segment in an array, computes and assigns its top and bottom properties + computeSegVerticals: function(segs) { + var i, seg; + + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + seg.top = this.computeDateTop(seg.start, seg.start); + seg.bottom = this.computeDateTop(seg.end, seg.start); + } + }, + + + // Renders the HTML for a single event segment's default rendering + renderSegHtml: function(seg, disableResizing) { + var view = this.view; + var event = seg.event; + var isDraggable = view.isEventDraggable(event); + var isResizable = !disableResizing && seg.isEnd && view.isEventResizable(event); + var classes = this.getSegClasses(seg, isDraggable, isResizable); + var skinCss = this.getEventSkinCss(event); + var timeText; + var fullTimeText; // more verbose time text. for the print stylesheet + var startTimeText; // just the start time text + + classes.unshift('fc-time-grid-event'); + + if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day... + // Don't display time text on segments that run entirely through a day. + // That would appear as midnight-midnight and would look dumb. + // Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am) + if (seg.isStart || seg.isEnd) { + timeText = view.getEventTimeText(seg.start, seg.end); + fullTimeText = view.getEventTimeText(seg.start, seg.end, 'LT'); + startTimeText = view.getEventTimeText(seg.start, null); + } + } else { + // Display the normal time text for the *event's* times + timeText = view.getEventTimeText(event); + fullTimeText = view.getEventTimeText(event, 'LT'); + startTimeText = view.getEventTimeText(event.start, null); + } + + return '' + + '
' + + (timeText ? + '
' + + '' + htmlEscape(timeText) + '' + + '
' : + '' + ) + + (event.title ? + '
' + + htmlEscape(event.title) + + '
' : + '' + ) + + '
' + + '
' + + (isResizable ? + '
' : + '' + ) + + ''; + }, + + + // Generates an object with CSS properties/values that should be applied to an event segment element. + // Contains important positioning-related properties that should be applied to any event element, customized or not. + generateSegPositionCss: function(seg) { + var view = this.view; + var isRTL = view.opt('isRTL'); + var shouldOverlap = view.opt('slotEventOverlap'); + var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point + var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point + var props = this.generateSegVerticalCss(seg); // get top/bottom first + var left; // amount of space from left edge, a fraction of the total width + var right; // amount of space from right edge, a fraction of the total width + + if (shouldOverlap) { + // double the width, but don't go beyond the maximum forward coordinate (1.0) + forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2); + } + + if (isRTL) { + left = 1 - forwardCoord; + right = backwardCoord; + } + else { + left = backwardCoord; + right = 1 - forwardCoord; + } + + props.zIndex = seg.level + 1; // convert from 0-base to 1-based + props.left = left * 100 + '%'; + props.right = right * 100 + '%'; + + if (shouldOverlap && seg.forwardPressure) { + // add padding to the edge so that forward stacked events don't cover the resizer's icon + props[isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width + } + + return props; + }, + + + // Generates an object with CSS properties for the top/bottom coordinates of a segment element + generateSegVerticalCss: function(seg) { + return { + top: seg.top, + bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container + }; + }, + + + // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col + groupSegCols: function(segs) { + var view = this.view; + var segCols = []; + var i; + + for (i = 0; i < view.colCnt; i++) { + segCols.push([]); + } + + for (i = 0; i < segs.length; i++) { + segCols[segs[i].col].push(segs[i]); + } + + return segCols; + } + +}); + + +// Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each. +// Also reorders the given array by date! +function placeSlotSegs(segs) { + var levels; + var level0; + var i; + + segs.sort(compareSegs); // order by date + levels = buildSlotSegLevels(segs); + computeForwardSlotSegs(levels); + + if ((level0 = levels[0])) { + + for (i = 0; i < level0.length; i++) { + computeSlotSegPressures(level0[i]); + } + + for (i = 0; i < level0.length; i++) { + computeSlotSegCoords(level0[i], 0, 0); + } + } +} + + +// Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is +// left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date. +function buildSlotSegLevels(segs) { + var levels = []; + var i, seg; + var j; + + for (i=0; i seg2.top && seg1.top < seg2.bottom; +} + + +// A cmp function for determining which forward segment to rely on more when computing coordinates. +function compareForwardSlotSegs(seg1, seg2) { + // put higher-pressure first + return seg2.forwardPressure - seg1.forwardPressure || + // put segments that are closer to initial edge first (and favor ones with no coords yet) + (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) || + // do normal sorting... + compareSegs(seg1, seg2); +} + +;; + +/* An abstract class from which other views inherit from +----------------------------------------------------------------------------------------------------------------------*/ +// Newer methods should be written as prototype methods, not in the monster `View` function at the bottom. + +View.prototype = { + + calendar: null, // owner Calendar object + coordMap: null, // a CoordMap object for converting pixel regions to dates + el: null, // the view's containing element. set by Calendar + + // important Moments + start: null, // the date of the very first cell + end: null, // the date after the very last cell + intervalStart: null, // the start of the interval of time the view represents (1st of month for month view) + intervalEnd: null, // the exclusive end of the interval of time the view represents + + // used for cell-to-date and date-to-cell calculations + rowCnt: null, // # of weeks + colCnt: null, // # of days displayed in a week + + isSelected: false, // boolean whether cells are user-selected or not + + // subclasses can optionally use a scroll container + scrollerEl: null, // the element that will most likely scroll when content is too tall + scrollTop: null, // cached vertical scroll value + + // classNames styled by jqui themes + widgetHeaderClass: null, + widgetContentClass: null, + highlightStateClass: null, + + // document handlers, bound to `this` object + documentMousedownProxy: null, + documentDragStartProxy: null, + + + // Serves as a "constructor" to suppliment the monster `View` constructor below + init: function() { + var tm = this.opt('theme') ? 'ui' : 'fc'; + + this.widgetHeaderClass = tm + '-widget-header'; + this.widgetContentClass = tm + '-widget-content'; + this.highlightStateClass = tm + '-state-highlight'; + + // save references to `this`-bound handlers + this.documentMousedownProxy = $.proxy(this, 'documentMousedown'); + this.documentDragStartProxy = $.proxy(this, 'documentDragStart'); + }, + + + // Renders the view inside an already-defined `this.el`. + // Subclasses should override this and then call the super method afterwards. + render: function() { + this.updateSize(); + this.trigger('viewRender', this, this, this.el); + + // attach handlers to document. do it here to allow for destroy/rerender + $(document) + .on('mousedown', this.documentMousedownProxy) + .on('dragstart', this.documentDragStartProxy); // jqui drag + }, + + + // Clears all view rendering, event elements, and unregisters handlers + destroy: function() { + this.unselect(); + this.trigger('viewDestroy', this, this, this.el); + this.destroyEvents(); + this.el.empty(); // removes inner contents but leaves the element intact + + $(document) + .off('mousedown', this.documentMousedownProxy) + .off('dragstart', this.documentDragStartProxy); + }, + + + // Used to determine what happens when the users clicks next/prev. Given -1 for prev, 1 for next. + // Should apply the delta to `date` (a Moment) and return it. + incrementDate: function(date, delta) { + // subclasses should implement + }, + + + /* Dimensions + ------------------------------------------------------------------------------------------------------------------*/ + + + // Refreshes anything dependant upon sizing of the container element of the grid + updateSize: function(isResize) { + if (isResize) { + this.recordScroll(); + } + this.updateHeight(); + this.updateWidth(); + }, + + + // Refreshes the horizontal dimensions of the calendar + updateWidth: function() { + // subclasses should implement + }, + + + // Refreshes the vertical dimensions of the calendar + updateHeight: function() { + var calendar = this.calendar; // we poll the calendar for height information + + this.setHeight( + calendar.getSuggestedViewHeight(), + calendar.isHeightAuto() + ); + }, + + + // Updates the vertical dimensions of the calendar to the specified height. + // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height. + setHeight: function(height, isAuto) { + // subclasses should implement + }, + + + // Given the total height of the view, return the number of pixels that should be used for the scroller. + // Utility for subclasses. + computeScrollerHeight: function(totalHeight) { + var both = this.el.add(this.scrollerEl); + var otherHeight; // cumulative height of everything that is not the scrollerEl in the view (header+borders) + + // fuckin IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked + both.css({ + position: 'relative', // cause a reflow, which will force fresh dimension recalculation + left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll + }); + otherHeight = this.el.outerHeight() - this.scrollerEl.height(); // grab the dimensions + both.css({ position: '', left: '' }); // undo hack + + return totalHeight - otherHeight; + }, + + + // Called for remembering the current scroll value of the scroller. + // Should be called before there is a destructive operation (like removing DOM elements) that might inadvertently + // change the scroll of the container. + recordScroll: function() { + if (this.scrollerEl) { + this.scrollTop = this.scrollerEl.scrollTop(); + } + }, + + + // Set the scroll value of the scroller to the previously recorded value. + // Should be called after we know the view's dimensions have been restored following some type of destructive + // operation (like temporarily removing DOM elements). + restoreScroll: function() { + if (this.scrollTop !== null) { + this.scrollerEl.scrollTop(this.scrollTop); + } + }, + + + /* Events + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders the events onto the view. + // Should be overriden by subclasses. Subclasses should call the super-method afterwards. + renderEvents: function(events) { + this.segEach(function(seg) { + this.trigger('eventAfterRender', seg.event, seg.event, seg.el); + }); + this.trigger('eventAfterAllRender'); + }, + + + // Removes event elements from the view. + // Should be overridden by subclasses. Should call this super-method FIRST, then subclass DOM destruction. + destroyEvents: function() { + this.segEach(function(seg) { + this.trigger('eventDestroy', seg.event, seg.event, seg.el); + }); + }, + + + // Given an event and the default element used for rendering, returns the element that should actually be used. + // Basically runs events and elements through the eventRender hook. + resolveEventEl: function(event, el) { + var custom = this.trigger('eventRender', event, event, el); + + if (custom === false) { // means don't render at all + el = null; + } + else if (custom && custom !== true) { + el = $(custom); + } + + return el; + }, + + + // Hides all rendered event segments linked to the given event + showEvent: function(event) { + this.segEach(function(seg) { + seg.el.css('visibility', ''); + }, event); + }, + + + // Shows all rendered event segments linked to the given event + hideEvent: function(event) { + this.segEach(function(seg) { + seg.el.css('visibility', 'hidden'); + }, event); + }, + + + // Iterates through event segments. Goes through all by default. + // If the optional `event` argument is specified, only iterates through segments linked to that event. + // The `this` value of the callback function will be the view. + segEach: function(func, event) { + var segs = this.getSegs(); + var i; + + for (i = 0; i < segs.length; i++) { + if (!event || segs[i].event._id === event._id) { + func.call(this, segs[i]); + } + } + }, + + + // Retrieves all the rendered segment objects for the view + getSegs: function() { + // subclasses must implement + }, + + + /* Event Drag Visualization + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of an event hovering over the specified date. + // `end` is a Moment and might be null. + // `seg` might be null. if specified, it is the segment object of the event being dragged. + // otherwise, an external event from outside the calendar is being dragged. + renderDrag: function(start, end, seg) { + // subclasses should implement + }, + + + // Unrenders a visual indication of event hovering + destroyDrag: function() { + // subclasses should implement + }, + + + // Handler for accepting externally dragged events being dropped in the view. + // Gets called when jqui's 'dragstart' is fired. + documentDragStart: function(ev, ui) { + var _this = this; + var dropDate = null; + var dragListener; + + if (this.opt('droppable')) { // only listen if this setting is on + + // listener that tracks mouse movement over date-associated pixel regions + dragListener = new DragListener(this.coordMap, { + cellOver: function(cell, date) { + dropDate = date; + _this.renderDrag(date); + }, + cellOut: function() { + dropDate = null; + _this.destroyDrag(); + } + }); + + // gets called, only once, when jqui drag is finished + $(document).one('dragstop', function(ev, ui) { + _this.destroyDrag(); + if (dropDate) { + _this.trigger('drop', ev.target, dropDate, ev, ui); + } + }); + + dragListener.startDrag(ev); // start listening immediately + } + }, + + + /* Selection + ------------------------------------------------------------------------------------------------------------------*/ + + + // Selects a date range on the view. `start` and `end` are both Moments. + // `ev` is the native mouse event that begin the interaction. + select: function(start, end, ev) { + this.unselect(ev); + this.renderSelection(start, end); + this.reportSelection(start, end, ev); + }, + + + // Renders a visual indication of the selection + renderSelection: function(start, end) { + // subclasses should implement + }, + + + // Called when a new selection is made. Updates internal state and triggers handlers. + reportSelection: function(start, end, ev) { + this.isSelected = true; + this.trigger('select', null, start, end, ev); + }, + + + // Undoes a selection. updates in the internal state and triggers handlers. + // `ev` is the native mouse event that began the interaction. + unselect: function(ev) { + if (this.isSelected) { + this.isSelected = false; + this.destroySelection(); + this.trigger('unselect', null, ev); + } + }, + + + // Unrenders a visual indication of selection + destroySelection: function() { + // subclasses should implement + }, + + + // Handler for unselecting when the user clicks something and the 'unselectAuto' setting is on + documentMousedown: function(ev) { + var ignore; + + // is there a selection, and has the user made a proper left click? + if (this.isSelected && this.opt('unselectAuto') && isPrimaryMouseButton(ev)) { + + // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element + ignore = this.opt('unselectCancel'); + if (!ignore || !$(ev.target).closest(ignore).length) { + this.unselect(ev); + } + } + } + +}; + + +// We are mixing JavaScript OOP design patterns here by putting methods and member variables in the closed scope of the +// constructor. Going forward, methods should be part of the prototype. +function View(calendar) { + var t = this; + + // exports + t.calendar = calendar; + t.opt = opt; + t.trigger = trigger; + t.isEventDraggable = isEventDraggable; + t.isEventResizable = isEventResizable; + t.eventDrop = eventDrop; + t.eventResize = eventResize; + + // imports + var reportEventChange = calendar.reportEventChange; + + // locals + var options = calendar.options; + var nextDayThreshold = moment.duration(options.nextDayThreshold); + + + t.init(); // the "constructor" that concerns the prototype methods + + + function opt(name) { + var v = options[name]; + if ($.isPlainObject(v) && !isForcedAtomicOption(name)) { + return smartProperty(v, t.name); + } + return v; + } + + + function trigger(name, thisObj) { + return calendar.trigger.apply( + calendar, + [name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t]) + ); + } + + + + /* Event Editable Boolean Calculations + ------------------------------------------------------------------------------*/ + + + function isEventDraggable(event) { + var source = event.source || {}; + + return firstDefined( + event.startEditable, + source.startEditable, + opt('eventStartEditable'), + event.editable, + source.editable, + opt('editable') + ); + } + + + function isEventResizable(event) { + var source = event.source || {}; + + return firstDefined( + event.durationEditable, + source.durationEditable, + opt('eventDurationEditable'), + event.editable, + source.editable, + opt('editable') + ); + } + + + + /* Event Elements + ------------------------------------------------------------------------------*/ + + + // Compute the text that should be displayed on an event's element. + // Based off the settings of the view. Possible signatures: + // .getEventTimeText(event, formatStr) + // .getEventTimeText(startMoment, endMoment, formatStr) + // .getEventTimeText(startMoment, null, formatStr) + // `timeFormat` is used but the `formatStr` argument can be used to override. + t.getEventTimeText = function(event, formatStr) { + var start; + var end; + + if (typeof event === 'object' && typeof formatStr === 'object') { + // first two arguments are actually moments (or null). shift arguments. + start = event; + end = formatStr; + formatStr = arguments[2]; + } + else { + // otherwise, an event object was the first argument + start = event.start; + end = event.end; + } + + formatStr = formatStr || opt('timeFormat'); + + if (end && opt('displayEventEnd')) { + return calendar.formatRange(start, end, formatStr); + } + else { + return calendar.formatDate(start, formatStr); + } + }; + + + + /* Event Modification Reporting + ---------------------------------------------------------------------------------*/ + + + function eventDrop(el, event, newStart, ev) { + var mutateResult = calendar.mutateEvent(event, newStart, null); + + trigger( + 'eventDrop', + el, + event, + mutateResult.dateDelta, + function() { + mutateResult.undo(); + reportEventChange(); + }, + ev, + {} // jqui dummy + ); + + reportEventChange(); + } + + + function eventResize(el, event, newEnd, ev) { + var mutateResult = calendar.mutateEvent(event, null, newEnd); + + trigger( + 'eventResize', + el, + event, + mutateResult.durationDelta, + function() { + mutateResult.undo(); + reportEventChange(); + }, + ev, + {} // jqui dummy + ); + + reportEventChange(); + } + + + // ==================================================================================================== + // Utilities for day "cells" + // ==================================================================================================== + // The "basic" views are completely made up of day cells. + // The "agenda" views have day cells at the top "all day" slot. + // This was the obvious common place to put these utilities, but they should be abstracted out into + // a more meaningful class (like DayEventRenderer). + // ==================================================================================================== + + + // For determining how a given "cell" translates into a "date": + // + // 1. Convert the "cell" (row and column) into a "cell offset" (the # of the cell, cronologically from the first). + // Keep in mind that column indices are inverted with isRTL. This is taken into account. + // + // 2. Convert the "cell offset" to a "day offset" (the # of days since the first visible day in the view). + // + // 3. Convert the "day offset" into a "date" (a Moment). + // + // The reverse transformation happens when transforming a date into a cell. + + + // exports + t.isHiddenDay = isHiddenDay; + t.skipHiddenDays = skipHiddenDays; + t.getCellsPerWeek = getCellsPerWeek; + t.dateToCell = dateToCell; + t.dateToDayOffset = dateToDayOffset; + t.dayOffsetToCellOffset = dayOffsetToCellOffset; + t.cellOffsetToCell = cellOffsetToCell; + t.cellToDate = cellToDate; + t.cellToCellOffset = cellToCellOffset; + t.cellOffsetToDayOffset = cellOffsetToDayOffset; + t.dayOffsetToDate = dayOffsetToDate; + t.rangeToSegments = rangeToSegments; + t.isMultiDayEvent = isMultiDayEvent; + + + // internals + var hiddenDays = opt('hiddenDays') || []; // array of day-of-week indices that are hidden + var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool) + var cellsPerWeek; + var dayToCellMap = []; // hash from dayIndex -> cellIndex, for one week + var cellToDayMap = []; // hash from cellIndex -> dayIndex, for one week + var isRTL = opt('isRTL'); + + + // initialize important internal variables + (function() { + + if (opt('weekends') === false) { + hiddenDays.push(0, 6); // 0=sunday, 6=saturday + } + + // Loop through a hypothetical week and determine which + // days-of-week are hidden. Record in both hashes (one is the reverse of the other). + for (var dayIndex=0, cellIndex=0; dayIndex<7; dayIndex++) { + dayToCellMap[dayIndex] = cellIndex; + isHiddenDayHash[dayIndex] = $.inArray(dayIndex, hiddenDays) != -1; + if (!isHiddenDayHash[dayIndex]) { + cellToDayMap[cellIndex] = dayIndex; + cellIndex++; + } + } + + cellsPerWeek = cellIndex; + if (!cellsPerWeek) { + throw 'invalid hiddenDays'; // all days were hidden? bad. + } + + })(); + + + // Is the current day hidden? + // `day` is a day-of-week index (0-6), or a Moment + function isHiddenDay(day) { + if (moment.isMoment(day)) { + day = day.day(); + } + return isHiddenDayHash[day]; + } + + + function getCellsPerWeek() { + return cellsPerWeek; + } + + + // Incrementing the current day until it is no longer a hidden day, returning a copy. + // If the initial value of `date` is not a hidden day, don't do anything. + // Pass `isExclusive` as `true` if you are dealing with an end date. + // `inc` defaults to `1` (increment one day forward each time) + function skipHiddenDays(date, inc, isExclusive) { + var out = date.clone(); + inc = inc || 1; + while ( + isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7] + ) { + out.add(inc, 'days'); + } + return out; + } + + + // + // TRANSFORMATIONS: cell -> cell offset -> day offset -> date + // + + // cell -> date (combines all transformations) + // Possible arguments: + // - row, col + // - { row:#, col: # } + function cellToDate() { + var cellOffset = cellToCellOffset.apply(null, arguments); + var dayOffset = cellOffsetToDayOffset(cellOffset); + var date = dayOffsetToDate(dayOffset); + return date; + } + + // cell -> cell offset + // Possible arguments: + // - row, col + // - { row:#, col:# } + function cellToCellOffset(row, col) { + var colCnt = t.colCnt; + + // rtl variables. wish we could pre-populate these. but where? + var dis = isRTL ? -1 : 1; + var dit = isRTL ? colCnt - 1 : 0; + + if (typeof row == 'object') { + col = row.col; + row = row.row; + } + var cellOffset = row * colCnt + (col * dis + dit); // column, adjusted for RTL (dis & dit) + + return cellOffset; + } + + // cell offset -> day offset + function cellOffsetToDayOffset(cellOffset) { + var day0 = t.start.day(); // first date's day of week + cellOffset += dayToCellMap[day0]; // normlize cellOffset to beginning-of-week + return Math.floor(cellOffset / cellsPerWeek) * 7 + // # of days from full weeks + cellToDayMap[ // # of days from partial last week + (cellOffset % cellsPerWeek + cellsPerWeek) % cellsPerWeek // crazy math to handle negative cellOffsets + ] - + day0; // adjustment for beginning-of-week normalization + } + + // day offset -> date + function dayOffsetToDate(dayOffset) { + return t.start.clone().add(dayOffset, 'days'); + } + + + // + // TRANSFORMATIONS: date -> day offset -> cell offset -> cell + // + + // date -> cell (combines all transformations) + function dateToCell(date) { + var dayOffset = dateToDayOffset(date); + var cellOffset = dayOffsetToCellOffset(dayOffset); + var cell = cellOffsetToCell(cellOffset); + return cell; + } + + // date -> day offset + function dateToDayOffset(date) { + return date.clone().stripTime().diff(t.start, 'days'); + } + + // day offset -> cell offset + function dayOffsetToCellOffset(dayOffset) { + var day0 = t.start.day(); // first date's day of week + dayOffset += day0; // normalize dayOffset to beginning-of-week + return Math.floor(dayOffset / 7) * cellsPerWeek + // # of cells from full weeks + dayToCellMap[ // # of cells from partial last week + (dayOffset % 7 + 7) % 7 // crazy math to handle negative dayOffsets + ] - + dayToCellMap[day0]; // adjustment for beginning-of-week normalization + } + + // cell offset -> cell (object with row & col keys) + function cellOffsetToCell(cellOffset) { + var colCnt = t.colCnt; + + // rtl variables. wish we could pre-populate these. but where? + var dis = isRTL ? -1 : 1; + var dit = isRTL ? colCnt - 1 : 0; + + var row = Math.floor(cellOffset / colCnt); + var col = ((cellOffset % colCnt + colCnt) % colCnt) * dis + dit; // column, adjusted for RTL (dis & dit) + return { + row: row, + col: col + }; + } + + + // + // Converts a date range into an array of segment objects. + // "Segments" are horizontal stretches of time, sliced up by row. + // A segment object has the following properties: + // - row + // - cols + // - isStart + // - isEnd + // + function rangeToSegments(start, end) { + + var rowCnt = t.rowCnt; + var colCnt = t.colCnt; + var segments = []; // array of segments to return + + // day offset for given date range + var dayRange = computeDayRange(start, end); // convert to a whole-day range + var rangeDayOffsetStart = dateToDayOffset(dayRange.start); + var rangeDayOffsetEnd = dateToDayOffset(dayRange.end); // an exclusive value + + // first and last cell offset for the given date range + // "last" implies inclusivity + var rangeCellOffsetFirst = dayOffsetToCellOffset(rangeDayOffsetStart); + var rangeCellOffsetLast = dayOffsetToCellOffset(rangeDayOffsetEnd) - 1; + + // loop through all the rows in the view + for (var row=0; row
' + + '' + // needed for matchCellWidths + htmlEscape(this.opt('weekNumberTitle')) + + '' + + '' + + '' + // needed for matchCellWidths + this.calendar.calculateWeekNumber(this.cellToDate(row, 0)) + + '' + + 's of the "number" row in the DayGrid's content skeleton. + // The number row will only exist if either day numbers or week numbers are turned on. + numberCellHtml: function(row, col, date) { + var classes; + + if (!this.dayNumbersVisible) { // if there are week numbers but not day numbers + return ''; // will create an empty space above events :( + } + + classes = this.dayGrid.getDayClasses(date); + classes.unshift('fc-day-number'); + + return '' + + '' + + date.date() + + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + this.timeGrid.headHtml() + // render the day-of-week headers + '
' + + (this.dayGrid ? + '
' + + '
' : + '' + ) + + '
' + + '
' + + '
' + + '
'; + }, + + + // Generates the HTML that will go before the day-of week header cells. + // Queried by the TimeGrid subcomponent when generating rows. Ordering depends on isRTL. + headIntroHtml: function() { + var date; + var weekNumber; + var weekTitle; + var weekText; + + if (this.opt('weekNumbers')) { + date = this.cellToDate(0, 0); + weekNumber = this.calendar.calculateWeekNumber(date); + weekTitle = this.opt('weekNumberTitle'); + + if (this.opt('isRTL')) { + weekText = weekNumber + weekTitle; + } + else { + weekText = weekTitle + weekNumber; + } + + return '' + + '' + + '' + // needed for matchCellWidths + htmlEscape(weekText) + + '' + + ''; + } + else { + return ''; + } + }, + + + // Generates the HTML that goes before the all-day cells. + // Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL. + dayIntroHtml: function() { + return '' + + '' + + '' + // needed for matchCellWidths + (this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'))) + + '' + + ''; + }, + + + // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column. + slotBgIntroHtml: function() { + return ''; + }, + + + // Generates the HTML that goes before all other types of cells. + // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid. + // Queried by the TimeGrid and DayGrid subcomponents when generating rows. Ordering depends on isRTL. + introHtml: function() { + return ''; + }, + + + // Generates an HTML attribute string for setting the width of the axis, if it is known + axisStyleAttr: function() { + if (this.axisWidth !== null) { + return 'style="width:' + this.axisWidth + 'px"'; + } + return ''; + }, + + + /* Dimensions + ------------------------------------------------------------------------------------------------------------------*/ + + updateSize: function(isResize) { + if (isResize) { + this.timeGrid.resize(); + } + View.prototype.updateSize.call(this, isResize); + }, + + + // Refreshes the horizontal dimensions of the view + updateWidth: function() { + // make all axis cells line up, and record the width so newly created axis cells will have it + this.axisWidth = matchCellWidths(this.el.find('.fc-axis')); + }, + + + // Adjusts the vertical dimensions of the view to the specified values + setHeight: function(totalHeight, isAuto) { + var eventLimit; + var scrollerHeight; + + if (this.bottomRuleHeight === null) { + // calculate the height of the rule the very first time + this.bottomRuleHeight = this.bottomRuleEl.outerHeight(); + } + this.bottomRuleEl.hide(); // .show() will be called later if this
is necessary + + // reset all dimensions back to the original state + this.scrollerEl.css('overflow', ''); + unsetScroller(this.scrollerEl); + uncompensateScroll(this.noScrollRowEls); + + // limit number of events in the all-day area + if (this.dayGrid) { + this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed + + eventLimit = this.opt('eventLimit'); + if (eventLimit && typeof eventLimit !== 'number') { + eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number + } + if (eventLimit) { + this.dayGrid.limitRows(eventLimit); + } + } + + if (!isAuto) { // should we force dimensions of the scroll container, or let the contents be natural height? + + scrollerHeight = this.computeScrollerHeight(totalHeight); + if (setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars? + + // make the all-day and header rows lines up + compensateScroll(this.noScrollRowEls, getScrollbarWidths(this.scrollerEl)); + + // the scrollbar compensation might have changed text flow, which might affect height, so recalculate + // and reapply the desired height to the scroller. + scrollerHeight = this.computeScrollerHeight(totalHeight); + this.scrollerEl.height(scrollerHeight); + + this.restoreScroll(); + } + else { // no scrollbars + // still, force a height and display the bottom rule (marks the end of day) + this.scrollerEl.height(scrollerHeight).css('overflow', 'hidden'); // in case
goes outside + this.bottomRuleEl.show(); + } + } + }, + + + // Sets the scroll value of the scroller to the intial pre-configured state prior to allowing the user to change it. + resetScroll: function() { + var _this = this; + var scrollTime = moment.duration(this.opt('scrollTime')); + var top = this.timeGrid.computeTimeTop(scrollTime); + + // zoom can give weird floating-point values. rather scroll a little bit further + top = Math.ceil(top); + + if (top) { + top++; // to overcome top border that slots beyond the first have. looks better + } + + function scroll() { + _this.scrollerEl.scrollTop(top); + } + + scroll(); + setTimeout(scroll, 0); // overrides any previous scroll state made by the browser + }, + + + /* Events + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders events onto the view and populates the View's segment array + renderEvents: function(events) { + var dayEvents = []; + var timedEvents = []; + var daySegs = []; + var timedSegs; + var i; + + // separate the events into all-day and timed + for (i = 0; i < events.length; i++) { + if (events[i].allDay) { + dayEvents.push(events[i]); + } + else { + timedEvents.push(events[i]); + } + } + + // render the events in the subcomponents + timedSegs = this.timeGrid.renderEvents(timedEvents); + if (this.dayGrid) { + daySegs = this.dayGrid.renderEvents(dayEvents); + } + + // the all-day area is flexible and might have a lot of events, so shift the height + this.updateHeight(); + + // /View.prototype.renderEvents.call(this, events); // call the super-method + }, + + + // Retrieves all segment objects that are rendered in the view + getSegs: function() { + return this.timeGrid.getSegs().concat( + this.dayGrid ? this.dayGrid.getSegs() : [] + ); + }, + + + // Unrenders all event elements and clears internal segment data + destroyEvents: function() { + View.prototype.destroyEvents.call(this); // do this before the grids' segs have been cleared + + // if destroyEvents is being called as part of an event rerender, renderEvents will be called shortly + // after, so remember what the scroll value was so we can restore it. + this.recordScroll(); + + // destroy the events in the subcomponents + this.timeGrid.destroyEvents(); + if (this.dayGrid) { + this.dayGrid.destroyEvents(); + } + + // we DON'T need to call updateHeight() because: + // A) a renderEvents() call always happens after this, which will eventually call updateHeight() + // B) in IE8, this causes a flash whenever events are rerendered + }, + + + /* Event Dragging + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of an event being dragged over the view. + // A returned value of `true` signals that a mock "helper" event has been rendered. + renderDrag: function(start, end, seg) { + if (start.hasTime()) { + return this.timeGrid.renderDrag(start, end, seg); + } + else if (this.dayGrid) { + return this.dayGrid.renderDrag(start, end, seg); + } + }, + + + // Unrenders a visual indications of an event being dragged over the view + destroyDrag: function() { + this.timeGrid.destroyDrag(); + if (this.dayGrid) { + this.dayGrid.destroyDrag(); + } + }, + + + /* Selection + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of a selection + renderSelection: function(start, end) { + if (start.hasTime() || end.hasTime()) { + this.timeGrid.renderSelection(start, end); + } + else if (this.dayGrid) { + this.dayGrid.renderSelection(start, end); + } + }, + + + // Unrenders a visual indications of a selection + destroySelection: function() { + this.timeGrid.destroySelection(); + if (this.dayGrid) { + this.dayGrid.destroySelection(); + } + } + +}); + +;; + +/* A week view with an all-day cell area at the top, and a time grid below +----------------------------------------------------------------------------------------------------------------------*/ +// TODO: a WeekView mixin for calculating dates and titles + +fcViews.agendaWeek = AgendaWeekView; // register the view + +function AgendaWeekView(calendar) { + AgendaView.call(this, calendar); // call the super-constructor +} + + +AgendaWeekView.prototype = createObject(AgendaView.prototype); // define the super-class +$.extend(AgendaWeekView.prototype, { + + name: 'agendaWeek', + + + incrementDate: function(date, delta) { + return date.clone().stripTime().add(delta, 'weeks').startOf('week'); + }, + + + render: function(date) { + + this.intervalStart = date.clone().stripTime().startOf('week'); + this.intervalEnd = this.intervalStart.clone().add(1, 'weeks'); + + this.start = this.skipHiddenDays(this.intervalStart); + this.end = this.skipHiddenDays(this.intervalEnd, -1, true); + + this.title = this.calendar.formatRange( + this.start, + this.end.clone().subtract(1), // make inclusive by subtracting 1 ms + this.opt('titleFormat'), + ' \u2014 ' // emphasized dash + ); + + AgendaView.prototype.render.call(this, this.getCellsPerWeek()); // call the super-method + } + +}); + +;; + +/* A day view with an all-day cell area at the top, and a time grid below +----------------------------------------------------------------------------------------------------------------------*/ + +fcViews.agendaDay = AgendaDayView; // register the view + +function AgendaDayView(calendar) { + AgendaView.call(this, calendar); // call the super-constructor +} + + +AgendaDayView.prototype = createObject(AgendaView.prototype); // define the super-class +$.extend(AgendaDayView.prototype, { + + name: 'agendaDay', + + + incrementDate: function(date, delta) { + var out = date.clone().stripTime().add(delta, 'days'); + out = this.skipHiddenDays(out, delta < 0 ? -1 : 1); + return out; + }, + + + render: function(date) { + + this.start = this.intervalStart = date.clone().stripTime(); + this.end = this.intervalEnd = this.start.clone().add(1, 'days'); + + this.title = this.calendar.formatDate(this.start, this.opt('titleFormat')); + + AgendaView.prototype.render.call(this, 1); // call the super-method + } + +}); + +;; + +/* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week. +----------------------------------------------------------------------------------------------------------------------*/ + +function ResourceDayGrid(view) { + DayGrid.call(this, view); // call the super-constructor +} + + +ResourceDayGrid.prototype = createObject(DayGrid.prototype); // declare the super-class +$.extend(ResourceDayGrid.prototype, { + rangeToSegs: function(rangeStart, rangeEnd, resourceIds) { + var col, segments = []; + + var currentDate = this.view.calendar.getDate(); + if((rangeStart <= currentDate) && (currentDate < rangeEnd)) { + var view = this.view; + var resources = view.calendar.fetchResources(); + + for (col = 0; col < view.colCnt; col++) { + var resource = resources[col]; + if (resourceIds.indexOf(resource.id) > -1){ + segments.push({ + row: 0, + leftCol: col, + rightCol: col, + isStart: true, + isEnd: true + }); + } + } + } + + return segments; + } +}); +;; + +/* Event-rendering methods for the DayGrid class +----------------------------------------------------------------------------------------------------------------------*/ + +$.extend(ResourceDayGrid.prototype, { + eventToSegs: function(event, intervalStart, intervalEnd) { + var eventStart = event.start.clone().stripZone(); // normalize + var eventEnd = this.view.calendar.getEventEnd(event).stripZone(); // compute (if necessary) and normalize + var segs; + var i, seg; + + if (intervalStart && intervalEnd) { + seg = intersectionToSeg(eventStart, eventEnd, intervalStart, intervalEnd); + segs = seg ? [ seg ] : []; + } + else { + segs = this.rangeToSegs(eventStart, eventEnd, event.resources); // defined by the subclass + } + + // assign extra event-related properties to the segment objects + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + seg.event = event; + seg.eventStartMS = +eventStart; + seg.eventDurationMS = eventEnd - eventStart; + } + + return segs; + } +}); +;; + +/* A component that renders one or more columns of vertical time slots +----------------------------------------------------------------------------------------------------------------------*/ + +function ResourceTimeGrid(view) { + TimeGrid.call(this, view); // call the super-constructor +} + + +ResourceTimeGrid.prototype = createObject(TimeGrid.prototype); // define the super-class +$.extend(ResourceTimeGrid.prototype, { +// Slices up a date range into a segment for each column +// Each column represents a resource. An event can be assigned to multiple resources +// so we need to build segs accordingly + rangeToSegs: function(rangeStart, rangeEnd, resourceIds) { + var view = this.view; + var segs = []; + var seg; + var col; + var cellDate; + var colStart, colEnd; + + var resources = view.calendar.fetchResources(); + + // normalize + rangeStart = rangeStart.clone().stripZone(); + rangeEnd = rangeEnd.clone().stripZone(); + + for (col = 0; col < view.colCnt; col++) { + var resource = resources[col]; + if (resourceIds.indexOf(resource.id) > -1){ + cellDate = view.cellToDate(0, col); // use the View's cell system for this + colStart = cellDate.clone().time(this.minTime); + colEnd = cellDate.clone().time(this.maxTime); + seg = intersectionToSeg(rangeStart, rangeEnd, colStart, colEnd); + if (seg) { + seg.col = col; + segs.push(seg); + } + } + } + + return segs; + } + +}); + +;; + +/* Event-rendering methods for the TimeGrid class +----------------------------------------------------------------------------------------------------------------------*/ + +$.extend(ResourceTimeGrid.prototype, { +// Slices a single event into an array of event segments. + // When `intervalStart` and `intervalEnd` are specified, intersect the events with that interval. + // Otherwise, let the subclass decide how it wants to slice the segments over the grid. + eventToSegs: function(event, intervalStart, intervalEnd) { + var eventStart = event.start.clone().stripZone(); // normalize + var eventEnd = this.view.calendar.getEventEnd(event).stripZone(); // compute (if necessary) and normalize + var segs; + var i, seg; + + if (intervalStart && intervalEnd) { + seg = intersectionToSeg(eventStart, eventEnd, intervalStart, intervalEnd); + segs = seg ? [ seg ] : []; + } + else { + segs = this.rangeToSegs(eventStart, eventEnd, event.resources); // defined by the subclass + } + + // assign extra event-related properties to the segment objects + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + seg.event = event; + seg.eventStartMS = +eventStart; + seg.eventDurationMS = eventEnd - eventStart; + } + + return segs; + } +}); + +;; + +/* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically. +----------------------------------------------------------------------------------------------------------------------*/ +// Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on). +// Responsible for managing width/height. + +setDefaults({ + allDaySlot: true, + allDayText: 'all-day', + + scrollTime: '06:00:00', + + slotDuration: '00:30:00', + + axisFormat: generateAgendaAxisFormat, + timeFormat: { + agenda: generateAgendaTimeFormat + }, + + minTime: '00:00:00', + maxTime: '24:00:00', + slotEventOverlap: true +}); + +function ResourceView(calendar) { + View.call(this, calendar); // call the super-constructor + + // overrides - the view.js should expose these on the prototype then we wouldn't have to do this. + this.cellToDate = ResourceView.prototype.cellToDate; + + this.timeGrid = new ResourceTimeGrid(this); + + if (this.opt('allDaySlot')) { // should we display the "all-day" area? + this.dayGrid = new ResourceDayGrid(this); // the all-day subcomponent of this view + + // the coordinate grid will be a combination of both subcomponents' grids + this.coordMap = new ComboCoordMap([ + this.dayGrid.coordMap, + this.timeGrid.coordMap + ]); + } + else { + this.coordMap = this.timeGrid.coordMap; + } +} + + +ResourceView.prototype = createObject(AgendaView.prototype); // define the super-class +$.extend(ResourceView.prototype, { + + cellToDate: function() { + return this.start.clone(); + }, + + // fix the classes, etc. + headCellHtml: function(row, col, date) { + var view = this; + var calendar = view.calendar; + var resource = calendar.fetchResources()[col]; + return '' + + '' + + htmlEscape(resource.name) + + ''; + } + +}); + +;; + +/* A day view with an all-day cell area at the top, and a time grid below by resource +----------------------------------------------------------------------------------------------------------------------*/ + +fcViews.resourceDay = ResourceDayView; + +function ResourceDayView(calendar) { // TODO: make a ResourceView mixin + ResourceView.call(this, calendar); // call the super-constructor +} + +ResourceDayView.prototype = createObject(ResourceView.prototype); // define the super-class +$.extend(ResourceDayView.prototype, { + + name: 'resourceDay', + + + incrementDate: function(date, delta) { + var out = date.clone().stripTime().add(delta, 'days'); + out = this.skipHiddenDays(out, delta < 0 ? -1 : 1); + return out; + }, + + + render: function(date) { + this.start = this.intervalStart = date.clone().stripTime(); + this.end = this.intervalEnd = this.start.clone().add(1, 'days'); + + this.title = this.calendar.formatDate(this.start, this.opt('titleFormat')); + + ResourceView.prototype.render.call(this, this.calendar.fetchResources().length); // call the super-method + }, + + // Computes HTML classNames for a single-day cell + getDayClasses: function(date) { + var view = this.view; + var today = view.calendar.getNow().stripTime(); + var classes = [ 'fc-' + dayIDs[date.day()] ]; + + if ( + view.name === 'month' && + date.month() != view.intervalStart.month() + ) { + classes.push('fc-other-month'); + } + + if (date.isSame(today, 'day')) { + classes.push( + 'fc-todaysss', + view.highlightStateClass + ); + } + else if (date < today) { + classes.push('fc-past'); + } + else { + classes.push('fc-future'); + } + + return classes; + } + +}); +;; + }); \ No newline at end of file diff --git a/dist/fullcalendar.min.css b/dist/fullcalendar.min.css index 2858443..31aaeb4 100644 --- a/dist/fullcalendar.min.css +++ b/dist/fullcalendar.min.css @@ -1,5 +1,5 @@ -/*! - * FullCalendar v2.1.1 Stylesheet - * Docs & License: http://arshaw.com/fullcalendar/ - * (c) 2013 Adam Shaw +/*! + * FullCalendar v2.1.1 Stylesheet + * Docs & License: http://arshaw.com/fullcalendar/ + * (c) 2013 Adam Shaw */.fc{direction:ltr;text-align:left}.fc-rtl{text-align:right}body .fc{font-size:1em}.fc-unthemed .fc-popover,.fc-unthemed .fc-row,.fc-unthemed hr,.fc-unthemed tbody,.fc-unthemed td,.fc-unthemed th,.fc-unthemed thead{border-color:#ddd}.fc-unthemed .fc-popover{background-color:#fff}.fc-unthemed .fc-popover .fc-header,.fc-unthemed hr{background:#eee}.fc-unthemed .fc-popover .fc-header .fc-close{color:#666}.fc-unthemed .fc-today{background:#fcf8e3}.fc-highlight{background:#bce8f1;opacity:.3;filter:alpha(opacity=30)}.fc-icon{display:inline-block;font-size:2em;line-height:.5em;height:.5em;font-family:"Courier New",Courier,monospace}.fc-icon-left-single-arrow:after{content:"\02039";font-weight:700}.fc-icon-right-single-arrow:after{content:"\0203A";font-weight:700}.fc-icon-left-double-arrow:after{content:"\000AB"}.fc-icon-right-double-arrow:after{content:"\000BB"}.fc-icon-x:after{content:"\000D7"}.fc button{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;height:2.1em;padding:0 .6em;font-size:1em;white-space:nowrap;cursor:pointer}.fc button::-moz-focus-inner{margin:0;padding:0}.fc-state-default{border:1px solid}.fc-state-default.fc-corner-left{border-top-left-radius:4px;border-bottom-left-radius:4px}.fc-state-default.fc-corner-right{border-top-right-radius:4px;border-bottom-right-radius:4px}.fc button .fc-icon{position:relative;top:.05em;margin:0 .1em}.fc-state-default{background-color:#f5f5f5;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);color:#333;text-shadow:0 1px 1px rgba(255,255,255,.75);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05)}.fc-state-active,.fc-state-disabled,.fc-state-down,.fc-state-hover{color:#333;background-color:#e6e6e6}.fc-state-hover{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.fc-state-active,.fc-state-down{background-color:#ccc;background-image:none;box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05)}.fc-state-disabled{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);box-shadow:none}.fc-button-group{display:inline-block}.fc .fc-button-group>*{float:left;margin:0 0 0 -1px}.fc .fc-button-group>:first-child{margin-left:0}.fc-popover{position:absolute;box-shadow:0 2px 6px rgba(0,0,0,.15)}.fc-popover .fc-header{padding:2px 4px}.fc-popover .fc-header .fc-title{margin:0 2px}.fc-popover .fc-header .fc-close{cursor:pointer}.fc-ltr .fc-popover .fc-header .fc-title,.fc-rtl .fc-popover .fc-header .fc-close{float:left}.fc-ltr .fc-popover .fc-header .fc-close,.fc-rtl .fc-popover .fc-header .fc-title{float:right}.fc-unthemed .fc-popover{border-width:1px;border-style:solid}.fc-unthemed .fc-popover .fc-header .fc-close{font-size:25px;margin-top:4px}.fc-popover>.ui-widget-header+.ui-widget-content{border-top:0}.fc hr{height:0;margin:0;padding:0 0 2px;border-style:solid;border-width:1px 0}.fc-clear{clear:both}.fc-bg,.fc-helper-skeleton,.fc-highlight-skeleton{position:absolute;top:0;left:0;right:0}.fc-bg{bottom:0}.fc-bg table{height:100%}.fc table{width:100%;table-layout:fixed;border-collapse:collapse;border-spacing:0;font-size:1em}.fc th{text-align:center}.fc td,.fc th{border-style:solid;border-width:1px;padding:0;vertical-align:top}.fc td.fc-today{border-style:double}.fc .fc-row{border-style:solid;border-width:0}.fc-row table{border-left:0 hidden transparent;border-right:0 hidden transparent;border-bottom:0 hidden transparent}.fc-row:first-child table{border-top:0 hidden transparent}.fc-row{position:relative}.fc-row .fc-bg{z-index:1}.fc-row .fc-highlight-skeleton{z-index:2;bottom:0}.fc-row .fc-highlight-skeleton table{height:100%}.fc-row .fc-highlight-skeleton td{border-color:transparent}.fc-row .fc-content-skeleton{position:relative;z-index:3;padding-bottom:2px}.fc-row .fc-helper-skeleton{z-index:4}.fc-row .fc-content-skeleton td,.fc-row .fc-helper-skeleton td{background:0 0;border-color:transparent;border-bottom:0}.fc-row .fc-content-skeleton tbody td,.fc-row .fc-helper-skeleton tbody td{border-top:0}.fc-scroller{overflow-y:scroll;overflow-x:hidden}.fc-scroller>*{position:relative;width:100%;overflow:hidden}.fc-event{position:relative;display:block;font-size:.85em;line-height:1.3;border-radius:3px;border:1px solid #3a87ad;background-color:#3a87ad;font-weight:400}.fc-event,.fc-event:hover,.ui-widget .fc-event{color:#fff;text-decoration:none}.fc-event.fc-draggable,.fc-event[href]{cursor:pointer}.fc-day-grid-event{margin:1px 2px 0;padding:0 1px}.fc-ltr .fc-day-grid-event.fc-not-start,.fc-rtl .fc-day-grid-event.fc-not-end{margin-left:0;border-left-width:0;padding-left:1px;border-top-left-radius:0;border-bottom-left-radius:0}.fc-ltr .fc-day-grid-event.fc-not-end,.fc-rtl .fc-day-grid-event.fc-not-start{margin-right:0;border-right-width:0;padding-right:1px;border-top-right-radius:0;border-bottom-right-radius:0}.fc-day-grid-event>.fc-content{white-space:nowrap;overflow:hidden}.fc-day-grid-event .fc-time{font-weight:700}.fc-day-grid-event .fc-resizer{position:absolute;top:0;bottom:0;width:7px}.fc-ltr .fc-day-grid-event .fc-resizer{right:-3px;cursor:e-resize}.fc-rtl .fc-day-grid-event .fc-resizer{left:-3px;cursor:w-resize}a.fc-more{margin:1px 3px;font-size:.85em;cursor:pointer;text-decoration:none}a.fc-more:hover{text-decoration:underline}.fc-limited{display:none}.fc-day-grid .fc-row{z-index:1}.fc-more-popover{z-index:2;width:220px}.fc-more-popover .fc-event-container{padding:10px}.fc-toolbar{text-align:center;margin-bottom:1em}.fc-toolbar .fc-left{float:left}.fc-toolbar .fc-right{float:right}.fc-toolbar .fc-center{display:inline-block}.fc .fc-toolbar>*>*{float:left;margin-left:.75em}.fc .fc-toolbar>*>:first-child{margin-left:0}.fc-toolbar h2{margin:0}.fc-toolbar button{position:relative}.fc-toolbar .fc-state-hover,.fc-toolbar .ui-state-hover{z-index:2}.fc-toolbar .fc-state-down{z-index:3}.fc-toolbar .fc-state-active,.fc-toolbar .ui-state-active{z-index:4}.fc-toolbar button:focus{z-index:5}.fc-view-container *,.fc-view-container :after,.fc-view-container :before{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}.fc-view,.fc-view>table{position:relative;z-index:1}.fc-basicDay-view .fc-content-skeleton,.fc-basicWeek-view .fc-content-skeleton{padding-top:1px;padding-bottom:1em}.fc-basic-view tbody .fc-row{min-height:4em}.fc-row.fc-rigid{overflow:hidden}.fc-row.fc-rigid .fc-content-skeleton{position:absolute;top:0;left:0;right:0}.fc-basic-view .fc-day-number,.fc-basic-view .fc-week-number{padding:0 2px}.fc-basic-view td.fc-day-number,.fc-basic-view td.fc-week-number span{padding-top:2px;padding-bottom:2px}.fc-basic-view .fc-week-number{text-align:center}.fc-basic-view .fc-week-number span{display:inline-block;min-width:1.25em}.fc-ltr .fc-basic-view .fc-day-number{text-align:right}.fc-rtl .fc-basic-view .fc-day-number{text-align:left}.fc-day-number.fc-other-month{opacity:.3;filter:alpha(opacity=30)}.fc-agenda-view .fc-day-grid{position:relative;z-index:2}.fc-agenda-view .fc-day-grid .fc-row{min-height:3em}.fc-agenda-view .fc-day-grid .fc-row .fc-content-skeleton{padding-top:1px;padding-bottom:1em}.fc .fc-axis{vertical-align:middle;padding:0 4px;white-space:nowrap}.fc-ltr .fc-axis{text-align:right}.fc-rtl .fc-axis{text-align:left}.ui-widget td.fc-axis{font-weight:400}.fc-time-grid,.fc-time-grid-container{position:relative;z-index:1}.fc-time-grid{min-height:100%}.fc-time-grid table{border:0 hidden transparent}.fc-time-grid>.fc-bg{z-index:1}.fc-time-grid .fc-slats,.fc-time-grid>hr{position:relative;z-index:2}.fc-time-grid .fc-highlight-skeleton{z-index:3}.fc-time-grid .fc-content-skeleton{position:absolute;z-index:4;top:0;left:0;right:0}.fc-time-grid>.fc-helper-skeleton{z-index:5}.fc-slats td{height:1.5em;border-bottom:0}.fc-slats .fc-minor td{border-top-style:dotted}.fc-slats .ui-widget-content{background:0 0}.fc-time-grid .fc-highlight-container{position:relative}.fc-time-grid .fc-highlight{position:absolute;left:0;right:0}.fc-time-grid .fc-event-container{position:relative}.fc-ltr .fc-time-grid .fc-event-container{margin:0 2.5% 0 2px}.fc-rtl .fc-time-grid .fc-event-container{margin:0 2px 0 2.5%}.fc-time-grid .fc-event{position:absolute;z-index:1}.fc-time-grid-event.fc-not-start{border-top-width:0;padding-top:1px;border-top-left-radius:0;border-top-right-radius:0}.fc-time-grid-event.fc-not-end{border-bottom-width:0;padding-bottom:1px;border-bottom-left-radius:0;border-bottom-right-radius:0}.fc-time-grid-event{overflow:hidden}.fc-time-grid-event>.fc-content{position:relative;z-index:2}.fc-time-grid-event .fc-time,.fc-time-grid-event .fc-title{padding:0 1px}.fc-time-grid-event .fc-time{font-size:.85em;white-space:nowrap}.fc-time-grid-event .fc-bg{z-index:1;background:#fff;opacity:.25;filter:alpha(opacity=25)}.fc-time-grid-event.fc-short .fc-content{white-space:nowrap}.fc-time-grid-event.fc-short .fc-time,.fc-time-grid-event.fc-short .fc-title{display:inline-block;vertical-align:top}.fc-time-grid-event.fc-short .fc-time span{display:none}.fc-time-grid-event.fc-short .fc-time:before{content:attr(data-start)}.fc-time-grid-event.fc-short .fc-time:after{content:"\000A0-\000A0"}.fc-time-grid-event.fc-short .fc-title{font-size:.85em;padding:0}.fc-time-grid-event .fc-resizer{position:absolute;z-index:3;left:0;right:0;bottom:0;height:8px;overflow:hidden;line-height:8px;font-size:11px;font-family:monospace;text-align:center;cursor:s-resize}.fc-time-grid-event .fc-resizer:after{content:"="} \ No newline at end of file diff --git a/dist/fullcalendar.min.js b/dist/fullcalendar.min.js index aa83c57..93bbc04 100644 --- a/dist/fullcalendar.min.js +++ b/dist/fullcalendar.min.js @@ -3,6 +3,6 @@ * Docs & License: http://arshaw.com/fullcalendar/ * (c) 2013 Adam Shaw */ -(function(t){"function"==typeof define&&define.amd?define(["jquery","moment"],t):t(jQuery,moment)})(function(t,e){function i(t,e){return e.longDateFormat("LT").replace(":mm","(:mm)").replace(/(\Wmm)$/,"($1)").replace(/\s*a$/i,"t")}function n(t,e){var i=e.longDateFormat("L");return i=i.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g,""),t.isRTL?i+=" ddd":i="ddd "+i,i}function r(t){o(De,t)}function o(e){function i(i,n){t.isPlainObject(n)&&t.isPlainObject(e[i])&&!s(i)?e[i]=o({},e[i],n):void 0!==n&&(e[i]=n)}for(var n=1;arguments.length>n;n++)t.each(arguments[n],i);return e}function s(t){return/(Time|Duration)$/.test(t)}function l(i,n){function r(t){var i=e.localeData||e.langData;return i.call(e,t)||i.call(e,"en")}function s(t){ie?h()&&(p(),f(t)):l()}function l(){ne=K.theme?"ui":"fc",i.addClass("fc"),K.isRTL?i.addClass("fc-rtl"):i.addClass("fc-ltr"),K.theme?i.addClass("ui-widget"):i.addClass("fc-unthemed"),ie=t("
").prependTo(i),te=new a(q,K),ee=te.render(),ee&&i.prepend(ee),u(K.defaultView),K.handleWindowResize&&(se=L(v,K.windowResizeDelay),t(window).resize(se))}function d(){re&&re.destroy(),te.destroy(),ie.remove(),i.removeClass("fc fc-ltr fc-rtl fc-unthemed ui-widget"),t(window).unbind("resize",se)}function h(){return i.is(":visible")}function u(t){f(0,t)}function f(e,i){he++,re&&i&&re.name!==i&&(te.deactivateButton(re.name),I(),re.start&&re.destroy(),re.el.remove(),re=null),!re&&i&&(re=new xe[i](q),re.el=t("
").appendTo(ie),te.activateButton(i)),re&&(e&&(le=re.incrementDate(le,e)),re.start&&!e&&le.isWithin(re.intervalStart,re.intervalEnd)||h()&&(I(),re.start&&re.destroy(),re.render(le),Z(),C(),x(),b())),Z(),he--}function g(t){return h()?(t&&m(),he++,re.updateSize(!0),he--,!0):void 0}function p(){h()&&m()}function m(){oe="number"==typeof K.contentHeight?K.contentHeight:"number"==typeof K.height?K.height-(ee?ee.outerHeight(!0):0):Math.round(ie.width()/Math.max(K.aspectRatio,.5))}function v(t){!he&&t.target===window&&re.start&&g(!0)&&re.trigger("windowResize",de)}function y(){E(),S()}function w(){h()&&(I(),re.destroyEvents(),re.renderEvents(ue),Z())}function E(){I(),re.destroyEvents(),Z()}function b(){!K.lazyFetching||ae(re.start,re.end)?S():w()}function S(){ce(re.start,re.end)}function D(t){ue=t,w()}function T(){w()}function C(){te.updateTitle(re.title)}function x(){var t=q.getNow();t.isWithin(re.intervalStart,re.intervalEnd)?te.disableButton("today"):te.enableButton("today")}function k(t,e){t=q.moment(t),e=e?q.moment(e):t.hasTime()?t.clone().add(q.defaultTimedEventDuration):t.clone().add(q.defaultAllDayEventDuration),re.select(t,e)}function M(){re&&re.unselect()}function R(){f(-1)}function P(){f(1)}function G(){le.add(-1,"years"),f()}function N(){le.add(1,"years"),f()}function Y(){le=q.getNow(),f()}function A(t){le=q.moment(t),f()}function _(t){le.add(e.duration(t)),f()}function O(t,e){var i,n;e&&void 0!==xe[e]||(e=e||"day",i=te.getViewsWithButtons().join(" "),n=i.match(RegExp("\\w+"+z(e))),n||(n=i.match(/\w+Day/)),e=n?n[0]:"agendaDay"),le=t,u(e)}function F(){return le.clone()}function I(){ie.css({width:"100%",height:ie.height(),overflow:"hidden"})}function Z(){ie.css({width:"",height:"",overflow:""})}function B(){return q}function j(){return re}function X(t,e){return void 0===e?K[t]:(("height"==t||"contentHeight"==t||"aspectRatio"==t)&&(K[t]=e,g(!0)),void 0)}function $(t,e){return K[t]?K[t].apply(e||de,Array.prototype.slice.call(arguments,2)):void 0}var q=this;n=n||{};var U,K=o({},De,n);U=K.lang in Te?Te[K.lang]:Te[De.lang],U&&(K=o({},De,U,n)),K.isRTL&&(K=o({},De,Ce,U||{},n)),q.options=K,q.render=s,q.destroy=d,q.refetchEvents=y,q.reportEvents=D,q.reportEventChange=T,q.rerenderEvents=w,q.changeView=u,q.select=k,q.unselect=M,q.prev=R,q.next=P,q.prevYear=G,q.nextYear=N,q.today=Y,q.gotoDate=A,q.incrementDate=_,q.zoomTo=O,q.getDate=F,q.getCalendar=B,q.getView=j,q.option=X,q.trigger=$;var Q=H(r(K.lang));if(K.monthNames&&(Q._months=K.monthNames),K.monthNamesShort&&(Q._monthsShort=K.monthNamesShort),K.dayNames&&(Q._weekdays=K.dayNames),K.dayNamesShort&&(Q._weekdaysShort=K.dayNamesShort),null!=K.firstDay){var J=H(Q._week);J.dow=K.firstDay,Q._week=J}q.defaultAllDayEventDuration=e.duration(K.defaultAllDayEventDuration),q.defaultTimedEventDuration=e.duration(K.defaultTimedEventDuration),q.moment=function(){var t;return"local"===K.timezone?(t=He.moment.apply(null,arguments),t.hasTime()&&t.local()):t="UTC"===K.timezone?He.moment.utc.apply(null,arguments):He.moment.parseZone.apply(null,arguments),"_locale"in t?t._locale=Q:t._lang=Q,t},q.getIsAmbigTimezone=function(){return"local"!==K.timezone&&"UTC"!==K.timezone},q.rezoneDate=function(t){return q.moment(t.toArray())},q.getNow=function(){var t=K.now;return"function"==typeof t&&(t=t()),q.moment(t)},q.calculateWeekNumber=function(t){var e=K.weekNumberCalculation;return"function"==typeof e?e(t):"local"===e?t.week():"ISO"===e.toUpperCase()?t.isoWeek():void 0},q.getEventEnd=function(t){return t.end?t.end.clone():q.getDefaultEventEnd(t.allDay,t.start)},q.getDefaultEventEnd=function(t,e){var i=e.clone();return t?i.stripTime().add(q.defaultAllDayEventDuration):i.add(q.defaultTimedEventDuration),q.getIsAmbigTimezone()&&i.stripZone(),i},q.formatRange=function(t,e,i){return"function"==typeof i&&(i=i.call(q,K,Q)),W(t,e,i,null,K.isRTL)},q.formatDate=function(t,e){return"function"==typeof e&&(e=e.call(q,K,Q)),V(t,e)},c.call(q,K);var te,ee,ie,ne,re,oe,se,le,ae=q.isFetchNeeded,ce=q.fetchEvents,de=i[0],he=0,ue=[];le=null!=K.defaultDate?q.moment(K.defaultDate):q.getNow(),q.getSuggestedViewHeight=function(){return void 0===oe&&p(),oe},q.isHeightAuto=function(){return"auto"===K.contentHeight||"auto"===K.height}}function a(e,i){function n(){var e=i.header;return f=i.theme?"ui":"fc",e?g=t("
").append(o("left")).append(o("right")).append(o("center")).append('
'):void 0}function r(){g.remove()}function o(n){var r=t('
'),o=i.header[n];return o&&t.each(o.split(" "),function(){var n,o=t(),s=!0;t.each(this.split(","),function(n,r){var l,a,c,d,h,u,g,m;"title"==r?(o=o.add(t("

 

")),s=!1):(e[r]?l=function(){e[r]()}:xe[r]&&(l=function(){e.changeView(r)},p.push(r)),l&&(a=S(i.themeButtonIcons,r),c=S(i.buttonIcons,r),d=S(i.defaultButtonText,r),h=S(i.buttonText,r),u=h?R(h):a&&i.theme?"":c&&!i.theme?"":R(d||r),g=["fc-"+r+"-button",f+"-button",f+"-state-default"],m=t('").click(function(){m.hasClass(f+"-state-disabled")||(l(),(m.hasClass(f+"-state-active")||m.hasClass(f+"-state-disabled"))&&m.removeClass(f+"-state-hover"))}).mousedown(function(){m.not("."+f+"-state-active").not("."+f+"-state-disabled").addClass(f+"-state-down")}).mouseup(function(){m.removeClass(f+"-state-down")}).hover(function(){m.not("."+f+"-state-active").not("."+f+"-state-disabled").addClass(f+"-state-hover")},function(){m.removeClass(f+"-state-hover").removeClass(f+"-state-down")}),o=o.add(m)))}),s&&o.first().addClass(f+"-corner-left").end().last().addClass(f+"-corner-right").end(),o.length>1?(n=t("
"),s&&n.addClass("fc-button-group"),n.append(o),r.append(n)):r.append(o)}),r}function s(t){g.find("h2").text(t)}function l(t){g.find(".fc-"+t+"-button").addClass(f+"-state-active")}function a(t){g.find(".fc-"+t+"-button").removeClass(f+"-state-active")}function c(t){g.find(".fc-"+t+"-button").attr("disabled","disabled").addClass(f+"-state-disabled")}function d(t){g.find(".fc-"+t+"-button").removeAttr("disabled").removeClass(f+"-state-disabled")}function h(){return p}var u=this;u.render=n,u.destroy=r,u.updateTitle=s,u.activateButton=l,u.deactivateButton=a,u.disableButton=c,u.enableButton=d,u.getViewsWithButtons=h;var f,g=t(),p=[]}function c(e){function i(t,e){return!T||t.clone().stripZone()C.clone().stripZone()}function n(t,e){T=t,C=e,A=[];var i=++G,n=L.length;N=n;for(var o=0;n>o;o++)r(L[o],i)}function r(e,i){o(e,function(n){var r,o,s=t.isArray(e.events);if(i==G){if(n)for(r=0;n.length>r;r++)o=n[r],s||(o=w(o,e)),o&&A.push(o);N--,N||R(A)}})}function o(i,n){var r,s,l=He.sourceFetchers;for(r=0;l.length>r;r++){if(s=l[r].call(S,i,T.clone(),C.clone(),e.timezone,n),s===!0)return;if("object"==typeof s)return o(s,n),void 0}var a=i.events;if(a)t.isFunction(a)?(v(),a.call(S,T.clone(),C.clone(),e.timezone,function(t){n(t),y()})):t.isArray(a)?n(a):n();else{var c=i.url;if(c){var d,h=i.success,u=i.error,f=i.complete;d=t.isFunction(i.data)?i.data():i.data;var g=t.extend({},d||{}),p=M(i.startParam,e.startParam),m=M(i.endParam,e.endParam),w=M(i.timezoneParam,e.timezoneParam);p&&(g[p]=T.format()),m&&(g[m]=C.format()),e.timezone&&"local"!=e.timezone&&(g[w]=e.timezone),v(),t.ajax(t.extend({},ke,i,{data:g,success:function(e){e=e||[];var i=k(h,this,arguments);t.isArray(i)&&(e=i),n(e)},error:function(){k(u,this,arguments),n()},complete:function(){k(f,this,arguments),y()}}))}else n()}}function s(t){var e=l(t);e&&(L.push(e),N++,r(e,G))}function l(e){var i,n,r=He.sourceNormalizers;if(t.isFunction(e)||t.isArray(e)?i={events:e}:"string"==typeof e?i={url:e}:"object"==typeof e&&(i=t.extend({},e)),i){for(i.className?"string"==typeof i.className&&(i.className=i.className.split(/\s+/)):i.className=[],t.isArray(i.events)&&(i.origArray=i.events,i.events=t.map(i.events,function(t){return w(t,i)})),n=0;r.length>n;n++)r[n].call(S,i);return i}}function a(e){L=t.grep(L,function(t){return!c(t,e)}),A=t.grep(A,function(t){return!c(t.source,e)}),R(A)}function c(t,e){return t&&e&&h(t)==h(e)}function h(t){return("object"==typeof t?t.origArray||t.url||t.events:null)||t}function u(t){t.start=S.moment(t.start),t.end&&(t.end=S.moment(t.end)),E(t),f(t),R(A)}function f(t){var e,i,n,r;for(e=0;A.length>e;e++)if(i=A[e],i._id==t._id&&i!==t)for(n=0;V.length>n;n++)r=V[n],void 0!==t[r]&&(i[r]=t[r])}function g(t,e){var i=w(t);i&&(i.source||(e&&(z.events.push(i),i.source=z),A.push(i)),R(A))}function p(e){var i,n;for(null==e?e=function(){return!0}:t.isFunction(e)||(i=e+"",e=function(t){return t._id==i}),A=t.grep(A,e,!0),n=0;L.length>n;n++)t.isArray(L[n].events)&&(L[n].events=t.grep(L[n].events,e,!0));R(A)}function m(e){return t.isFunction(e)?t.grep(A,e):null!=e?(e+="",t.grep(A,function(t){return t._id==e})):A}function v(){Y++||H("loading",null,!0,x())}function y(){--Y||H("loading",null,!1,x())}function w(i,n){var r,o,s,l,a={};return e.eventDataTransform&&(i=e.eventDataTransform(i)),n&&n.eventDataTransform&&(i=n.eventDataTransform(i)),r=S.moment(i.start||i.date),r.isValid()&&(o=null,!i.end||(o=S.moment(i.end),o.isValid()))?(s=i.allDay,void 0===s&&(l=M(n?n.allDayDefault:void 0,e.allDayDefault),s=void 0!==l?l:!(r.hasTime()||o&&o.hasTime())),s?(r.hasTime()&&r.stripTime(),o&&o.hasTime()&&o.stripTime()):(r.hasTime()||(r=S.rezoneDate(r)),o&&!o.hasTime()&&(o=S.rezoneDate(o))),t.extend(a,i),n&&(a.source=n),a._id=i._id||(void 0===i.id?"_fc"+Me++:i.id+""),a.className=i.className?"string"==typeof i.className?i.className.split(/\s+/):i.className:[],a.resources?"string"==typeof a.resources&&(a.resources=a.resources.split(/\s+/)):a.resources=[],a.allDay=s,a.start=r,a.end=o,e.forceEventDuration&&!a.end&&(a.end=P(a)),d(a),a):void 0}function E(t,e,i){var n,r,o,s,l=t._allDay,a=t._start,c=t._end,d=!1;return e||i||(e=t.start,i=t.end),n=t.allDay!=l?t.allDay:!(e||i).hasTime(),n&&(e&&(e=e.clone().stripTime()),i&&(i=i.clone().stripTime())),e&&(r=n?D(e,a.clone().stripTime()):D(e,a)),n!=l?d=!0:i&&(o=D(i||S.getDefaultEventEnd(n,e||a),e||a).subtract(D(c||S.getDefaultEventEnd(l,a),a))),s=b(m(t._id),d,n,r,o),{dateDelta:r,durationDelta:o,undo:s}}function b(i,n,r,o,s){var l=S.getIsAmbigTimezone(),a=[];return t.each(i,function(t,i){var c=i._allDay,h=i._start,u=i._end,f=null!=r?r:c,g=h.clone(),p=!n&&u?u.clone():null;f?(g.stripTime(),p&&p.stripTime()):(g.hasTime()||(g=S.rezoneDate(g)),p&&!p.hasTime()&&(p=S.rezoneDate(p))),p||!e.forceEventDuration&&!+s||(p=S.getDefaultEventEnd(f,g)),g.add(o),p&&p.add(o).add(s),l&&(+o||+s)&&(g.stripZone(),p&&p.stripZone()),i.allDay=f,i.start=g,i.end=p,d(i),a.push(function(){i.allDay=c,i.start=h,i.end=u,d(i)})}),function(){for(var t=0;a.length>t;t++)a[t]()}}var S=this;S.isFetchNeeded=i,S.fetchEvents=n,S.addEventSource=s,S.removeEventSource=a,S.updateEvent=u,S.renderEvent=g,S.removeEvents=p,S.clientEvents=m,S.mutateEvent=E;var T,C,H=S.trigger,x=S.getView,R=S.reportEvents,P=S.getEventEnd,z={events:[]},L=[z],G=0,N=0,Y=0,A=[];t.each((e.events?[e.events]:[]).concat(e.eventSources||[]),function(t,e){var i=l(e);i&&L.push(i)});var V=["title","url","allDay","className","editable","color","backgroundColor","borderColor","textColor"]}function d(t){t._allDay=t.allDay,t._start=t.start.clone(),t._end=t.end?t.end.clone():null}function h(t,e){e.left&&t.css({"border-left-width":1,"margin-left":e.left-1}),e.right&&t.css({"border-right-width":1,"margin-right":e.right-1})}function u(t){t.css({"margin-left":"","margin-right":"","border-left-width":"","border-right-width":""})}function f(e,i,n){var r=Math.floor(i/e.length),o=Math.floor(i-r*(e.length-1)),s=[],l=[],a=[],c=0;g(e),e.each(function(i,n){var d=i===e.length-1?o:r,h=t(n).outerHeight(!0);d>h?(s.push(n),l.push(h),a.push(t(n).height())):c+=h}),n&&(i-=c,r=Math.floor(i/s.length),o=Math.floor(i-r*(s.length-1))),t(s).each(function(e,i){var n=e===s.length-1?o:r,c=l[e],d=a[e],h=n-(c-d);n>c&&t(i).height(h)})}function g(t){t.height("")}function p(e){var i=0;return e.find("> *").each(function(e,n){var r=t(n).outerWidth();r>i&&(i=r)}),i++,e.width(i),i}function m(t,e){return t.height(e).addClass("fc-scroller"),t[0].scrollHeight-1>t[0].clientHeight?!0:(v(t),!1)}function v(t){t.height("").removeClass("fc-scroller")}function y(e){var i=e.css("position"),n=e.parents().filter(function(){var e=t(this);return/(auto|scroll)/.test(e.css("overflow")+e.css("overflow-y")+e.css("overflow-x"))}).eq(0);return"fixed"!==i&&n.length?n:t(e[0].ownerDocument||document)}function w(t){var e=t.offset().left,i=e+t.width(),n=t.children(),r=n.offset().left,o=r+n.outerWidth();return{left:r-e,right:i-o}}function E(t){return 1==t.which&&!t.ctrlKey}function b(t,e,i,n){var r,o,s,l;return e>i&&n>t?(t>=i?(r=t.clone(),s=!0):(r=i.clone(),s=!1),n>=e?(o=e.clone(),l=!0):(o=n.clone(),l=!1),{start:r,end:o,isStart:s,isEnd:l}):void 0}function S(t,e){if(t=t||{},void 0!==t[e])return t[e];for(var i,n=e.split(/(?=[A-Z])/),r=n.length-1;r>=0;r--)if(i=t[n[r].toLowerCase()],void 0!==i)return i;return t["default"]}function D(t,i){return e.duration({days:t.clone().stripTime().diff(i.clone().stripTime(),"days"),ms:t.time()-i.time()})}function T(t){return"[object Date]"===Object.prototype.toString.call(t)||t instanceof Date}function C(t,e){return t-e}function H(t){var e=function(){};return e.prototype=t,new e}function x(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i])}function k(e,i,n){if(t.isFunction(e)&&(e=[e]),e){var r,o;for(r=0;e.length>r;r++)o=e[r].apply(i,n)||o;return o}}function M(){for(var t=0;arguments.length>t;t++)if(void 0!==arguments[t])return arguments[t]}function R(t){return(t+"").replace(/&/g,"&").replace(//g,">").replace(/'/g,"'").replace(/"/g,""").replace(/\n/g,"
")}function P(t){return t.replace(/&.*?;/g,"")}function z(t){return t.charAt(0).toUpperCase()+t.slice(1)}function L(t,e){var i,n,r,o,s=function(){var l=+new Date-o;e>l&&l>0?i=setTimeout(s,e-l):(i=null,t.apply(r,n),i||(r=n=null))};return function(){r=this,n=arguments,o=+new Date,i||(i=setTimeout(s,e))}}function G(i,n,r){var o,s,l,a,c=i[0],d=1==i.length&&"string"==typeof c;return e.isMoment(c)?(a=e.apply(null,i),c._ambigTime&&(a._ambigTime=!0),c._ambigZone&&(a._ambigZone=!0)):T(c)||void 0===c?a=e.apply(null,i):(o=!1,s=!1,d?Pe.test(c)?(c+="-01",i=[c],o=!0,s=!0):(l=ze.exec(c))&&(o=!l[5],s=!0):t.isArray(c)&&(s=!0),a=n?e.utc.apply(e,i):e.apply(null,i),o?(a._ambigTime=!0,a._ambigZone=!0):r&&(s?a._ambigZone=!0:d&&a.zone(c))),new N(a)}function N(t){x(this,t)}function Y(t,e){var i,n=[],r=!1,o=!1;for(i=0;t.length>i;i++)n.push(He.moment.parseZone(t[i])),r=r||n[i]._ambigTime,o=o||n[i]._ambigZone;for(i=0;n.length>i;i++)r&&!e?n[i].stripTime():o&&n[i].stripZone();return n}function A(t,i){return e.fn.format.call(t,i)}function V(t,e){return _(t,Z(e))}function _(t,e){var i,n="";for(i=0;e.length>i;i++)n+=O(t,e[i]);return n}function O(t,e){var i,n;return"string"==typeof e?e:(i=e.token)?Le[i]?Le[i](t):A(t,i):e.maybe&&(n=_(t,e.maybe),n.match(/[1-9]/))?n:""}function W(t,e,i,n,r){var o;return t=He.moment.parseZone(t),e=He.moment.parseZone(e),o=(t.localeData||t.lang).call(t),i=o.longDateFormat(i)||i,n=n||" - ",F(t,e,Z(i),n,r)}function F(t,e,i,n,r){var o,s,l,a,c="",d="",h="",u="",f="";for(s=0;i.length>s&&(o=I(t,e,i[s]),o!==!1);s++)c+=o;for(l=i.length-1;l>s&&(o=I(t,e,i[l]),o!==!1);l--)d=o+d;for(a=s;l>=a;a++)h+=O(t,i[a]),u+=O(e,i[a]);return(h||u)&&(f=r?u+n+h:h+n+u),c+f+d}function I(t,e,i){var n,r;return"string"==typeof i?i:(n=i.token)&&(r=Ge[n.charAt(0)],r&&t.isSame(e,r))?A(t,n):!1}function Z(t){return t in Ne?Ne[t]:Ne[t]=B(t)}function B(t){for(var e,i=[],n=/\[([^\]]*)\]|\(([^\)]*)\)|(LT|(\w)\4*o?)|([^\w\[\(]+)/g;e=n.exec(t);)e[1]?i.push(e[1]):e[2]?i.push({maybe:B(e[2])}):e[3]?i.push({token:e[3]}):e[5]&&i.push(e[5]);return i}function j(t){this.options=t||{}}function X(t){this.grid=t}function $(t){this.coordMaps=t}function q(t,e){this.coordMap=t,this.options=e||{}}function U(t,e){return t||e?t&&e?t.grid===e.grid&&t.row===e.row&&t.col===e.col:!1:!0}function K(e,i){this.options=i=i||{},this.sourceEl=e,this.parentEl=i.parentEl?t(i.parentEl):e.parent()}function Q(t){this.view=t}function J(t){Q.call(this,t),this.coordMap=new X(this)}function te(t,e){return t.eventStartMS-e.eventStartMS||e.eventDurationMS-t.eventDurationMS||e.event.allDay-t.event.allDay||(t.event.title||"").localeCompare(e.event.title)}function ee(t){J.call(this,t)}function ie(t,e){var i,n;for(i=0;e.length>i;i++)if(n=e[i],n.leftCol<=t.rightCol&&n.rightCol>=t.leftCol)return!0;return!1}function ne(t,e){return t.leftCol-e.leftCol}function re(t){J.call(this,t)}function oe(t){var e,i,n;if(t.sort(te),e=se(t),le(e),i=e[0]){for(n=0;i.length>n;n++)ae(i[n]);for(n=0;i.length>n;n++)ce(i[n],0,0)}}function se(t){var e,i,n,r=[];for(e=0;t.length>e;e++){for(i=t[e],n=0;r.length>n&&de(i,r[n]).length;n++);i.level=n,(r[n]||(r[n]=[])).push(i)}return r}function le(t){var e,i,n,r,o;for(e=0;t.length>e;e++)for(i=t[e],n=0;i.length>n;n++)for(r=i[n],r.forwardSegs=[],o=e+1;t.length>o;o++)de(r,t[o],r.forwardSegs)}function ae(t){var e,i,n=t.forwardSegs,r=0;if(void 0===t.forwardPressure){for(e=0;n.length>e;e++)i=n[e],ae(i),r=Math.max(r,1+i.forwardPressure);t.forwardPressure=r}}function ce(t,e,i){var n,r=t.forwardSegs;if(void 0===t.forwardCoord)for(r.length?(r.sort(ue),ce(r[0],e+1,i),t.forwardCoord=r[0].backwardCoord):t.forwardCoord=1,t.backwardCoord=t.forwardCoord-(t.forwardCoord-i)/(e+1),n=0;r.length>n;n++)ce(r[n],0,t.forwardCoord)}function de(t,e,i){i=i||[];for(var n=0;e.length>n;n++)he(t,e[n])&&i.push(e[n]);return i}function he(t,e){return t.bottom>e.top&&t.topd;d++){var h=d*n,u=h+n-1,f=Math.max(a,h),g=Math.min(c,u);if(g>=f){var m=E(f),v=E(g),b=[m.col,v.col].sort(),S=p(f)==s,T=p(g)+1==l;r.push({row:d,leftCol:b[0],rightCol:b[1],isStart:S,isEnd:T})}}return r}function D(t,e){var i,n,r=t.clone().stripTime();return e&&(i=e.clone().stripTime(),n=+e.time(),n&&n>=k&&i.add(1,"days")),(!e||r>=i)&&(i=r.clone().add(1,"days")),{start:r,end:i}}function T(t){var e=D(t.start,t.end);return e.end.diff(e.start,"days")>1}var C=this;C.calendar=i,C.opt=n,C.trigger=r,C.isEventDraggable=o,C.isEventResizable=l,C.eventDrop=a,C.eventResize=c;var H=i.reportEventChange,x=i.options,k=e.duration(x.nextDayThreshold);C.init(),C.getEventTimeText=function(t,e){var r,o;return"object"==typeof t&&"object"==typeof e?(r=t,o=e,e=arguments[2]):(r=t.start,o=t.end),e=e||n("timeFormat"),o&&n("displayEventEnd")?i.formatRange(r,o,e):i.formatDate(r,e)},C.isHiddenDay=d,C.skipHiddenDays=u,C.getCellsPerWeek=h,C.dateToCell=v,C.dateToDayOffset=y,C.dayOffsetToCellOffset=w,C.cellOffsetToCell=E,C.cellToDate=f,C.cellToCellOffset=g,C.cellOffsetToDayOffset=p,C.dayOffsetToDate=m,C.rangeToSegments=b,C.isMultiDayEvent=T;var R,P=n("hiddenDays")||[],z=[],L=[],G=[],N=n("isRTL");(function(){n("weekends")===!1&&P.push(0,6);for(var e=0,i=0;7>e;e++)L[e]=i,z[e]=-1!=t.inArray(e,P),z[e]||(G[i]=e,i++);if(R=i,!R)throw"invalid hiddenDays"})()}function ge(t){fe.call(this,t),this.dayGrid=new ee(this),this.coordMap=this.dayGrid.coordMap}function pe(t){ge.call(this,t)}function me(t){ge.call(this,t)}function ve(t){ge.call(this,t)}function ye(t,e){return e.longDateFormat("LT").replace(":mm","(:mm)").replace(/(\Wmm)$/,"($1)").replace(/\s*a$/i,"a")}function we(t,e){return e.longDateFormat("LT").replace(/\s*a$/i,"")}function Ee(t){fe.call(this,t),this.timeGrid=new re(this),this.opt("allDaySlot")?(this.dayGrid=new ee(this),this.coordMap=new $([this.dayGrid.coordMap,this.timeGrid.coordMap])):this.coordMap=this.timeGrid.coordMap}function be(t){Ee.call(this,t)}function Se(t){Ee.call(this,t)}var De={lang:"en",defaultTimedEventDuration:"02:00:00",defaultAllDayEventDuration:{days:1},forceEventDuration:!1,nextDayThreshold:"09:00:00",defaultView:"month",aspectRatio:1.35,header:{left:"title",center:"",right:"today prev,next"},weekends:!0,weekNumbers:!1,weekNumberTitle:"W",weekNumberCalculation:"local",lazyFetching:!0,startParam:"start",endParam:"end",timezoneParam:"timezone",timezone:!1,titleFormat:{month:"MMMM YYYY",week:"ll",day:"LL"},columnFormat:{month:"ddd",week:n,day:"dddd"},timeFormat:{"default":i},displayEventEnd:{month:!1,basicWeek:!1,"default":!0},isRTL:!1,defaultButtonText:{prev:"prev",next:"next",prevYear:"prev year",nextYear:"next year",today:"today",month:"month",week:"week",day:"day"},buttonIcons:{prev:"left-single-arrow",next:"right-single-arrow",prevYear:"left-double-arrow",nextYear:"right-double-arrow"},theme:!1,themeButtonIcons:{prev:"circle-triangle-w",next:"circle-triangle-e",prevYear:"seek-prev",nextYear:"seek-next"},dragOpacity:.75,dragRevertDuration:500,dragScroll:!0,unselectAuto:!0,dropAccept:"*",eventLimit:!1,eventLimitText:"more",eventLimitClick:"popover",dayPopoverFormat:"LL",handleWindowResize:!0,windowResizeDelay:200},Te={en:{columnFormat:{week:"ddd M/D"},dayPopoverFormat:"dddd, MMMM D"}},Ce={header:{left:"next,prev today",center:"",right:"title"},buttonIcons:{prev:"right-single-arrow",next:"left-single-arrow",prevYear:"right-double-arrow",nextYear:"left-double-arrow"},themeButtonIcons:{prev:"circle-triangle-e",next:"circle-triangle-w",nextYear:"seek-prev",prevYear:"seek-next"}},He=t.fullCalendar={version:"2.1.1"},xe=He.views={};t.fn.fullCalendar=function(e){var i=Array.prototype.slice.call(arguments,1),n=this;return this.each(function(r,o){var s,a=t(o),c=a.data("fullCalendar");"string"==typeof e?c&&t.isFunction(c[e])&&(s=c[e].apply(c,i),r||(n=s),"destroy"===e&&a.removeData("fullCalendar")):c||(c=new l(a,e),a.data("fullCalendar",c),c.render())}),n},He.langs=Te,He.datepickerLang=function(e,i,n){var r=Te[e];r||(r=Te[e]={}),o(r,{isRTL:n.isRTL,weekNumberTitle:n.weekHeader,titleFormat:{month:n.showMonthAfterYear?"YYYY["+n.yearSuffix+"] MMMM":"MMMM YYYY["+n.yearSuffix+"]"},defaultButtonText:{prev:P(n.prevText),next:P(n.nextText),today:P(n.currentText)}}),t.datepicker&&(t.datepicker.regional[i]=t.datepicker.regional[e]=n,t.datepicker.regional.en=t.datepicker.regional[""],t.datepicker.setDefaults(n))},He.lang=function(t,e){var i;e&&(i=Te[t],i||(i=Te[t]={}),o(i,e||{})),De.lang=t},He.sourceNormalizers=[],He.sourceFetchers=[];var ke={dataType:"json",cache:!1},Me=1,Re=["sun","mon","tue","wed","thu","fri","sat"];He.applyAll=k;var Pe=/^\s*\d{4}-\d\d$/,ze=/^\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+)?)?)?)?)?$/;He.moment=function(){return G(arguments)},He.moment.utc=function(){var t=G(arguments,!0);return t.hasTime()&&t.utc(),t},He.moment.parseZone=function(){return G(arguments,!0,!0)},N.prototype=H(e.fn),N.prototype.clone=function(){return G([this])},N.prototype.time=function(t){if(null==t)return e.duration({hours:this.hours(),minutes:this.minutes(),seconds:this.seconds(),milliseconds:this.milliseconds()});delete this._ambigTime,e.isDuration(t)||e.isMoment(t)||(t=e.duration(t));var i=0;return e.isDuration(t)&&(i=24*Math.floor(t.asDays())),this.hours(i+t.hours()).minutes(t.minutes()).seconds(t.seconds()).milliseconds(t.milliseconds())},N.prototype.stripTime=function(){var t=this.toArray();return e.fn.utc.call(this),this.year(t[0]).month(t[1]).date(t[2]).hours(0).minutes(0).seconds(0).milliseconds(0),this._ambigTime=!0,this._ambigZone=!0,this},N.prototype.hasTime=function(){return!this._ambigTime},N.prototype.stripZone=function(){var t=this.toArray(),i=this._ambigTime;return e.fn.utc.call(this),this.year(t[0]).month(t[1]).date(t[2]).hours(t[3]).minutes(t[4]).seconds(t[5]).milliseconds(t[6]),i&&(this._ambigTime=!0),this._ambigZone=!0,this},N.prototype.hasZone=function(){return!this._ambigZone},N.prototype.zone=function(t){return null!=t&&(delete this._ambigTime,delete this._ambigZone),e.fn.zone.apply(this,arguments)},N.prototype.local=function(){var t=this.toArray(),i=this._ambigZone;return delete this._ambigTime,delete this._ambigZone,e.fn.local.apply(this,arguments),i&&this.year(t[0]).month(t[1]).date(t[2]).hours(t[3]).minutes(t[4]).seconds(t[5]).milliseconds(t[6]),this},N.prototype.utc=function(){return delete this._ambigTime,delete this._ambigZone,e.fn.utc.apply(this,arguments)},N.prototype.format=function(){return arguments[0]?V(this,arguments[0]):this._ambigTime?A(this,"YYYY-MM-DD"):this._ambigZone?A(this,"YYYY-MM-DD[T]HH:mm:ss"):A(this)},N.prototype.toISOString=function(){return this._ambigTime?A(this,"YYYY-MM-DD"):this._ambigZone?A(this,"YYYY-MM-DD[T]HH:mm:ss"):e.fn.toISOString.apply(this,arguments)},N.prototype.isWithin=function(t,e){var i=Y([this,t,e]);return i[0]>=i[1]&&i[0]').addClass(i.className||"").css({top:0,left:0}).append(i.content).appendTo(i.parentEl),this.el.on("click",".fc-close",function(){e.hide()}),i.autoHide&&t(document).on("mousedown",this.documentMousedownProxy=t.proxy(this,"documentMousedown"))},documentMousedown:function(e){this.el&&!t(e.target).closest(this.el).length&&this.hide()},destroy:function(){this.hide(),this.el&&(this.el.remove(),this.el=null),t(document).off("mousedown",this.documentMousedownProxy)},position:function(){var e,i,n,r,o,s=this.options,l=this.el.offsetParent().offset(),a=this.el.outerWidth(),c=this.el.outerHeight(),d=t(window),h=y(this.el);r=s.top||0,o=void 0!==s.left?s.left:void 0!==s.right?s.right-a:0,h.is(window)||h.is(document)?(h=d,e=0,i=0):(n=h.offset(),e=n.top,i=n.left),e+=d.scrollTop(),i+=d.scrollLeft(),s.viewportConstrain!==!1&&(r=Math.min(r,e+h.outerHeight()-c-this.margin),r=Math.max(r,e+this.margin),o=Math.min(o,i+h.outerWidth()-a-this.margin),o=Math.max(o,i+this.margin)),this.el.css({top:r-l.top,left:o-l.left})},trigger:function(t){this.options[t]&&this.options[t].apply(this,Array.prototype.slice.call(arguments,1))}},X.prototype={grid:null,rows:null,cols:null,containerEl:null,minX:null,maxX:null,minY:null,maxY:null,build:function(){this.grid.buildCoords(this.rows=[],this.cols=[]),this.computeBounds()},getCell:function(t,e){var i,n=null,r=this.rows,o=this.cols,s=-1,l=-1;if(this.inBounds(t,e)){for(i=0;r.length>i;i++)if(e>=r[i][0]&&r[i][1]>e){s=i;break}for(i=0;o.length>i;i++)if(t>=o[i][0]&&o[i][1]>t){l=i;break}s>=0&&l>=0&&(n={row:s,col:l},n.grid=this.grid,n.date=this.grid.getCellDate(n))}return n},computeBounds:function(){var t;this.containerEl&&(t=this.containerEl.offset(),this.minX=t.left,this.maxX=t.left+this.containerEl.outerWidth(),this.minY=t.top,this.maxY=t.top+this.containerEl.outerHeight())},inBounds:function(t,e){return this.containerEl?t>=this.minX&&this.maxX>t&&e>=this.minY&&this.maxY>e:!0}},$.prototype={coordMaps:null,build:function(){var t,e=this.coordMaps;for(t=0;e.length>t;t++)e[t].build()},getCell:function(t,e){var i,n=this.coordMaps,r=null;for(i=0;n.length>i&&!r;i++)r=n[i].getCell(t,e);return r}},q.prototype={coordMap:null,options:null,isListening:!1,isDragging:!1,origCell:null,origDate:null,cell:null,date:null,mouseX0:null,mouseY0:null,mousemoveProxy:null,mouseupProxy:null,scrollEl:null,scrollBounds:null,scrollTopVel:null,scrollLeftVel:null,scrollIntervalId:null,scrollHandlerProxy:null,scrollSensitivity:30,scrollSpeed:200,scrollIntervalMs:50,mousedown:function(t){E(t)&&(t.preventDefault(),this.startListening(t),this.options.distance||this.startDrag(t))},startListening:function(e){var i,n;this.isListening||(e&&this.options.scroll&&(i=y(t(e.target)),i.is(window)||i.is(document)||(this.scrollEl=i,this.scrollHandlerProxy=L(t.proxy(this,"scrollHandler"),100),this.scrollEl.on("scroll",this.scrollHandlerProxy))),this.computeCoords(),e&&(n=this.getCell(e),this.origCell=n,this.origDate=n?n.date:null,this.mouseX0=e.pageX,this.mouseY0=e.pageY),t(document).on("mousemove",this.mousemoveProxy=t.proxy(this,"mousemove")).on("mouseup",this.mouseupProxy=t.proxy(this,"mouseup")).on("selectstart",this.preventDefault),this.isListening=!0,this.trigger("listenStart",e))},computeCoords:function(){this.coordMap.build(),this.computeScrollBounds()},mousemove:function(t){var e,i;this.isDragging||(e=this.options.distance||1,i=Math.pow(t.pageX-this.mouseX0,2)+Math.pow(t.pageY-this.mouseY0,2),i>=e*e&&this.startDrag(t)),this.isDragging&&this.drag(t) -},startDrag:function(t){var e;this.isListening||this.startListening(),this.isDragging||(this.isDragging=!0,this.trigger("dragStart",t),e=this.getCell(t),e&&this.cellOver(e,!0))},drag:function(t){var e;this.isDragging&&(e=this.getCell(t),U(e,this.cell)||(this.cell&&this.cellOut(),e&&this.cellOver(e)),this.dragScroll(t))},cellOver:function(t){this.cell=t,this.date=t.date,this.trigger("cellOver",t,t.date)},cellOut:function(){this.cell&&(this.trigger("cellOut",this.cell),this.cell=null,this.date=null)},mouseup:function(t){this.stopDrag(t),this.stopListening(t)},stopDrag:function(t){this.isDragging&&(this.stopScrolling(),this.trigger("dragStop",t),this.isDragging=!1)},stopListening:function(e){this.isListening&&(this.scrollEl&&(this.scrollEl.off("scroll",this.scrollHandlerProxy),this.scrollHandlerProxy=null),t(document).off("mousemove",this.mousemoveProxy).off("mouseup",this.mouseupProxy).off("selectstart",this.preventDefault),this.mousemoveProxy=null,this.mouseupProxy=null,this.isListening=!1,this.trigger("listenStop",e),this.origCell=this.cell=null,this.origDate=this.date=null)},getCell:function(t){return this.coordMap.getCell(t.pageX,t.pageY)},trigger:function(t){this.options[t]&&this.options[t].apply(this,Array.prototype.slice.call(arguments,1))},preventDefault:function(t){t.preventDefault()},computeScrollBounds:function(){var t,e=this.scrollEl;e&&(t=e.offset(),this.scrollBounds={top:t.top,left:t.left,bottom:t.top+e.outerHeight(),right:t.left+e.outerWidth()})},dragScroll:function(t){var e,i,n,r,o=this.scrollSensitivity,s=this.scrollBounds,l=0,a=0;s&&(e=(o-(t.pageY-s.top))/o,i=(o-(s.bottom-t.pageY))/o,n=(o-(t.pageX-s.left))/o,r=(o-(s.right-t.pageX))/o,e>=0&&1>=e?l=-1*e*this.scrollSpeed:i>=0&&1>=i&&(l=i*this.scrollSpeed),n>=0&&1>=n?a=-1*n*this.scrollSpeed:r>=0&&1>=r&&(a=r*this.scrollSpeed)),this.setScrollVel(l,a)},setScrollVel:function(e,i){this.scrollTopVel=e,this.scrollLeftVel=i,this.constrainScrollVel(),!this.scrollTopVel&&!this.scrollLeftVel||this.scrollIntervalId||(this.scrollIntervalId=setInterval(t.proxy(this,"scrollIntervalFunc"),this.scrollIntervalMs))},constrainScrollVel:function(){var t=this.scrollEl;0>this.scrollTopVel?0>=t.scrollTop()&&(this.scrollTopVel=0):this.scrollTopVel>0&&t.scrollTop()+t[0].clientHeight>=t[0].scrollHeight&&(this.scrollTopVel=0),0>this.scrollLeftVel?0>=t.scrollLeft()&&(this.scrollLeftVel=0):this.scrollLeftVel>0&&t.scrollLeft()+t[0].clientWidth>=t[0].scrollWidth&&(this.scrollLeftVel=0)},scrollIntervalFunc:function(){var t=this.scrollEl,e=this.scrollIntervalMs/1e3;this.scrollTopVel&&t.scrollTop(t.scrollTop()+this.scrollTopVel*e),this.scrollLeftVel&&t.scrollLeft(t.scrollLeft()+this.scrollLeftVel*e),this.constrainScrollVel(),this.scrollTopVel||this.scrollLeftVel||this.stopScrolling()},stopScrolling:function(){this.scrollIntervalId&&(clearInterval(this.scrollIntervalId),this.scrollIntervalId=null,this.computeCoords())},scrollHandler:function(){this.scrollIntervalId||this.computeCoords()}},K.prototype={options:null,sourceEl:null,el:null,parentEl:null,top0:null,left0:null,mouseY0:null,mouseX0:null,topDelta:null,leftDelta:null,mousemoveProxy:null,isFollowing:!1,isHidden:!1,isAnimating:!1,start:function(e){this.isFollowing||(this.isFollowing=!0,this.mouseY0=e.pageY,this.mouseX0=e.pageX,this.topDelta=0,this.leftDelta=0,this.isHidden||this.updatePosition(),t(document).on("mousemove",this.mousemoveProxy=t.proxy(this,"mousemove")))},stop:function(e,i){function n(){this.isAnimating=!1,r.destroyEl(),this.top0=this.left0=null,i&&i()}var r=this,o=this.options.revertDuration;this.isFollowing&&!this.isAnimating&&(this.isFollowing=!1,t(document).off("mousemove",this.mousemoveProxy),e&&o&&!this.isHidden?(this.isAnimating=!0,this.el.animate({top:this.top0,left:this.left0},{duration:o,complete:n})):n())},getEl:function(){var t=this.el;return t||(this.sourceEl.width(),t=this.el=this.sourceEl.clone().css({position:"absolute",visibility:"",display:this.isHidden?"none":"",margin:0,right:"auto",bottom:"auto",width:this.sourceEl.width(),height:this.sourceEl.height(),opacity:this.options.opacity||"",zIndex:this.options.zIndex}).appendTo(this.parentEl)),t},destroyEl:function(){this.el&&(this.el.remove(),this.el=null)},updatePosition:function(){var t,e;this.getEl(),null===this.top0&&(this.sourceEl.width(),t=this.sourceEl.offset(),e=this.el.offsetParent().offset(),this.top0=t.top-e.top,this.left0=t.left-e.left),this.el.css({top:this.top0+this.topDelta,left:this.left0+this.leftDelta})},mousemove:function(t){this.topDelta=t.pageY-this.mouseY0,this.leftDelta=t.pageX-this.mouseX0,this.isHidden||this.updatePosition()},hide:function(){this.isHidden||(this.isHidden=!0,this.el&&this.el.hide())},show:function(){this.isHidden&&(this.isHidden=!1,this.updatePosition(),this.getEl().show())}},Q.prototype={view:null,cellHtml:"",rowHtml:function(t,e){var i,n,r=this.view,o=this.getHtmlRenderer("cell",t),s="";for(e=e||0,i=0;r.colCnt>i;i++)n=r.cellToDate(e,i),s+=o(e,i,n);return s=this.bookendCells(s,t,e),""+s+""},bookendCells:function(t,e,i){var n=this.view,r=this.getHtmlRenderer("intro",e)(i||0),o=this.getHtmlRenderer("outro",e)(i||0),s=n.opt("isRTL"),l=s?o:r,a=s?r:o;return"string"==typeof t?l+t+a:t.prepend(l).append(a)},getHtmlRenderer:function(t,e){var i,n,r,o,s=this.view;return i=t+"Html",e&&(n=e+z(t)+"Html"),n&&(o=s[n])?r=s:n&&(o=this[n])?r=this:(o=s[i])?r=s:(o=this[i])&&(r=this),"function"==typeof o?function(){return o.apply(r,arguments)||""}:function(){return o||""}}},J.prototype=H(Q.prototype),t.extend(J.prototype,{el:null,coordMap:null,cellDuration:null,render:function(){this.bindHandlers()},destroy:function(){},buildCoords:function(){},getCellDate:function(){},getCellDayEl:function(){},rangeToSegs:function(){},bindHandlers:function(){var e=this;this.el.on("mousedown",function(i){t(i.target).is(".fc-event-container *, .fc-more")||t(i.target).closest(".fc-popover").length||e.dayMousedown(i)}),this.bindSegHandlers()},dayMousedown:function(t){var e,i,n,r=this,o=this.view,s=o.opt("selectable"),l=null,a=new q(this.coordMap,{scroll:o.opt("dragScroll"),dragStart:function(){o.unselect()},cellOver:function(t,o){a.origDate&&(n=r.getCellDayEl(t),l=[o,a.origDate].sort(C),e=l[0],i=l[1].clone().add(r.cellDuration),s&&r.renderSelection(e,i))},cellOut:function(){l=null,r.destroySelection()},listenStop:function(t){l&&(l[0].isSame(l[1])&&o.trigger("dayClick",n[0],e,t),s&&o.reportSelection(e,i,t))}});a.mousedown(t)},renderDrag:function(){},destroyDrag:function(){},renderResize:function(){},destroyResize:function(){},renderRangeHelper:function(t,e,i){var n,r=this.view;!e&&r.opt("forceEventDuration")&&(e=r.calendar.getDefaultEventEnd(!t.hasTime(),t)),n=i?H(i.event):{},n.start=t,n.end=e,n.allDay=!(t.hasTime()||e&&e.hasTime()),n.className=(n.className||[]).concat("fc-helper"),i||(n.editable=!1),this.renderHelper(n,i)},renderHelper:function(){},destroyHelper:function(){},renderSelection:function(t,e){this.renderHighlight(t,e)},destroySelection:function(){this.destroyHighlight()},renderHighlight:function(){},destroyHighlight:function(){},headHtml:function(){return'
'+""+""+this.rowHtml("head")+""+"
"+"
"},headCellHtml:function(t,e,i){var n=this.view,r=n.calendar,o=n.opt("columnFormat");return''+R(r.formatDate(i,o))+""},bgCellHtml:function(t,e,i){var n=this.view,r=this.getDayClasses(i);return r.unshift("fc-day",n.widgetContentClass),''},getDayClasses:function(t){var e=this.view,i=e.calendar.getNow().stripTime(),n=["fc-"+Re[t.day()]];return"month"===e.name&&t.month()!=e.intervalStart.month()&&n.push("fc-other-month"),t.isSame(i,"day")?n.push("fc-today",e.highlightStateClass):i>t?n.push("fc-past"):n.push("fc-future"),n}}),t.extend(J.prototype,{mousedOverSeg:null,isDraggingSeg:!1,isResizingSeg:!1,renderEvents:function(){},getSegs:function(){},destroyEvents:function(){this.triggerSegMouseout()},renderSegs:function(e,i){var n,r=this.view,o="",s=[];for(n=0;e.length>n;n++)o+=this.renderSegHtml(e[n],i);return t(o).each(function(i,n){var o=e[i],l=r.resolveEventEl(o.event,t(n));l&&(l.data("fc-seg",o),o.el=l,s.push(o))}),s},renderSegHtml:function(){},eventsToSegs:function(e,i,n){var r=this;return t.map(e,function(t){return r.eventToSegs(t,i,n)})},eventToSegs:function(t,e,i){var n,r,o,s=t.start.clone().stripZone(),l=this.view.calendar.getEventEnd(t).stripZone();for(e&&i?(o=b(s,l,e,i),n=o?[o]:[]):n=this.rangeToSegs(s,l),r=0;n.length>r;r++)o=n[r],o.event=t,o.eventStartMS=+s,o.eventDurationMS=l-s;return n},bindSegHandlers:function(){var e=this,i=this.view;t.each({mouseenter:function(t,i){e.triggerSegMouseover(t,i)},mouseleave:function(t,i){e.triggerSegMouseout(t,i)},click:function(t,e){return i.trigger("eventClick",this,t.event,e)},mousedown:function(n,r){t(r.target).is(".fc-resizer")&&i.isEventResizable(n.event)?e.segResizeMousedown(n,r):i.isEventDraggable(n.event)&&e.segDragMousedown(n,r)}},function(i,n){e.el.on(i,".fc-event-container > *",function(i){var r=t(this).data("fc-seg");return!r||e.isDraggingSeg||e.isResizingSeg?void 0:n.call(this,r,i)})})},triggerSegMouseover:function(t,e){this.mousedOverSeg||(this.mousedOverSeg=t,this.view.trigger("eventMouseover",t.el[0],t.event,e))},triggerSegMouseout:function(t,e){e=e||{},this.mousedOverSeg&&(t=t||this.mousedOverSeg,this.mousedOverSeg=null,this.view.trigger("eventMouseout",t.el[0],t.event,e))},segDragMousedown:function(t,e){var i,n,r=this,o=this.view,s=t.el,l=t.event,a=new K(t.el,{parentEl:o.el,opacity:o.opt("dragOpacity"),revertDuration:o.opt("dragRevertDuration"),zIndex:2}),c=new q(o.coordMap,{distance:5,scroll:o.opt("dragScroll"),listenStart:function(t){a.hide(),a.start(t)},dragStart:function(e){r.triggerSegMouseout(t,e),r.isDraggingSeg=!0,o.hideEvent(l),o.trigger("eventDragStart",s[0],l,e,{})},cellOver:function(e,s){var l=t.cellDate||c.origDate,d=r.computeDraggedEventDates(t,l,s);i=d.start,n=d.end,o.renderDrag(i,n,t)?a.hide():a.show()},cellOut:function(){i=null,o.destroyDrag(),a.show()},dragStop:function(t){var e=i&&!i.isSame(l.start);a.stop(!e,function(){r.isDraggingSeg=!1,o.destroyDrag(),o.showEvent(l),o.trigger("eventDragStop",s[0],l,t,{}),e&&o.eventDrop(s[0],l,i,t)})},listenStop:function(){a.stop()}});c.mousedown(e)},computeDraggedEventDates:function(t,e,i){var n,r,o,s=this.view,l=t.event,a=l.start,c=s.calendar.getEventEnd(l);return i.hasTime()===e.hasTime()?(n=D(i,e),r=a.clone().add(n),o=null===l.end?null:c.clone().add(n)):(r=i,o=null),{start:r,end:o}},segResizeMousedown:function(t,e){function i(){r.destroyResize(),o.showEvent(l)}var n,r=this,o=this.view,s=t.el,l=t.event,a=l.start,c=o.calendar.getEventEnd(l),d=null;n=new q(this.coordMap,{distance:5,scroll:o.opt("dragScroll"),dragStart:function(e){r.triggerSegMouseout(t,e),r.isResizingSeg=!0,o.trigger("eventResizeStart",s[0],l,e,{})},cellOver:function(e,n){n.isBefore(a)&&(n=a),d=n.clone().add(r.cellDuration),d.isSame(c)?(d=null,i()):(r.renderResize(a,d,t),o.hideEvent(l))},cellOut:function(){d=null,i()},dragStop:function(t){r.isResizingSeg=!1,i(),o.trigger("eventResizeStop",s[0],l,t,{}),d&&o.eventResize(s[0],l,d,t)}}),n.mousedown(e)},getSegClasses:function(t,e,i){var n=t.event,r=["fc-event",t.isStart?"fc-start":"fc-not-start",t.isEnd?"fc-end":"fc-not-end"].concat(n.className,n.source?n.source.className:[]);return e&&r.push("fc-draggable"),i&&r.push("fc-resizable"),r},getEventSkinCss:function(t){var e=this.view,i=t.source||{},n=t.color,r=i.color,o=e.opt("eventColor"),s=t.backgroundColor||n||i.backgroundColor||r||e.opt("eventBackgroundColor")||o,l=t.borderColor||n||i.borderColor||r||e.opt("eventBorderColor")||o,a=t.textColor||i.textColor||e.opt("eventTextColor"),c=[];return s&&c.push("background-color:"+s),l&&c.push("border-color:"+l),a&&c.push("color:"+a),c.join(";")}}),ee.prototype=H(J.prototype),t.extend(ee.prototype,{numbersVisible:!1,cellDuration:e.duration({days:1}),bottomCoordPadding:0,rowEls:null,dayEls:null,helperEls:null,highlightEls:null,render:function(e){var i,n=this.view,r="";for(i=0;n.rowCnt>i;i++)r+=this.dayRowHtml(i,e);this.el.html(r),this.rowEls=this.el.find(".fc-row"),this.dayEls=this.el.find(".fc-day"),this.dayEls.each(function(e,i){var r=n.cellToDate(Math.floor(e/n.colCnt),e%n.colCnt);n.trigger("dayRender",null,r,t(i))}),J.prototype.render.call(this)},destroy:function(){this.destroySegPopover()},dayRowHtml:function(t,e){var i=this.view,n=["fc-row","fc-week",i.widgetContentClass];return e&&n.push("fc-rigid"),'
'+'
'+""+this.rowHtml("day",t)+"
"+"
"+'
'+""+(this.numbersVisible?""+this.rowHtml("number",t)+"":"")+"
"+"
"+"
"},dayCellHtml:function(t,e,i){return this.bgCellHtml(t,e,i)},buildCoords:function(e,i){var n,r,o,s=this.view.colCnt;this.dayEls.slice(0,s).each(function(e,s){n=t(s),r=n.offset().left,e&&(o[1]=r),o=[r],i[e]=o}),o[1]=r+n.outerWidth(),this.rowEls.each(function(i,s){n=t(s),r=n.offset().top,i&&(o[1]=r),o=[r],e[i]=o}),o[1]=r+n.outerHeight()+this.bottomCoordPadding},getCellDate:function(t){return this.view.cellToDate(t)},getCellDayEl:function(t){return this.dayEls.eq(t.row*this.view.colCnt+t.col)},rangeToSegs:function(t,e){return this.view.rangeToSegments(t,e)},renderDrag:function(t,e,i){var n;return this.renderHighlight(t,e||this.view.calendar.getDefaultEventEnd(!0,t)),i&&!i.el.closest(this.el).length?(this.renderRangeHelper(t,e,i),n=this.view.opt("dragOpacity"),void 0!==n&&this.helperEls.css("opacity",n),!0):void 0},destroyDrag:function(){this.destroyHighlight(),this.destroyHelper()},renderResize:function(t,e,i){this.renderHighlight(t,e),this.renderRangeHelper(t,e,i)},destroyResize:function(){this.destroyHighlight(),this.destroyHelper()},renderHelper:function(e,i){var n=[],r=this.renderEventRows([e]);this.rowEls.each(function(e,o){var s,l=t(o),a=t('
');s=i&&i.row===e?i.el.position().top:l.find(".fc-content-skeleton tbody").position().top,a.css("top",s).find("table").append(r[e].tbodyEl),l.append(a),n.push(a[0])}),this.helperEls=t(n)},destroyHelper:function(){this.helperEls&&(this.helperEls.remove(),this.helperEls=null)},renderHighlight:function(e,i){var n,r,o,s=this.rangeToSegs(e,i),l=[];for(n=0;s.length>n;n++)r=s[n],o=t(this.highlightSkeletonHtml(r.leftCol,r.rightCol+1)),o.appendTo(this.rowEls[r.row]),l.push(o[0]);this.highlightEls=t(l)},destroyHighlight:function(){this.highlightEls&&(this.highlightEls.remove(),this.highlightEls=null)},highlightSkeletonHtml:function(t,e){var i=this.view.colCnt,n="";return t>0&&(n+='"),m=[],v=[],y=[];for(r=0;g>r;r++){if(o=f[r],s=0,l=t(""),m.push([]),v.push([]),y.push([]),o)for(a=0;o.length>a;a++){for(c=o[a],n(c.leftCol),d=t('",l+=""+(s?"":n)+'",c.add(this.slotDuration);return l},processOptions:function(){var t=this.view,i=t.opt("slotDuration"),n=t.opt("snapDuration");i=e.duration(i),n=n?e.duration(n):i,this.slotDuration=i,this.snapDuration=n,this.cellDuration=n,this.minTime=e.duration(t.opt("minTime")),this.maxTime=e.duration(t.opt("maxTime"))},rangeToSegs:function(t,e){var i,n,r,o,s,l=this.view,a=[];for(t=t.clone().stripZone(),e=e.clone().stripZone(),n=0;l.colCnt>n;n++)r=l.cellToDate(0,n),o=r.clone().time(this.minTime),s=r.clone().time(this.maxTime),i=b(t,e,o,s),i&&(i.col=n,a.push(i));return a},resize:function(){this.computeSlatTops(),this.updateSegVerticals()},buildCoords:function(i,n){var r,o,s=this.view.colCnt,l=this.el.offset().top,a=e.duration(+this.minTime),c=null;for(this.dayEls.slice(0,s).each(function(e,i){r=t(i),o=r.offset().left,c&&(c[1]=o),c=[o],n[e]=c}),c[1]=o+r.outerWidth(),c=null;this.maxTime>a;)o=l+this.computeTimeTop(a),c&&(c[1]=o),c=[o],i.push(c),a.add(this.snapDuration);c[1]=l+this.computeTimeTop(a)},getCellDate:function(t){var e=this.view,i=e.calendar;return i.rezoneDate(e.cellToDate(0,t.col).time(this.minTime+this.snapDuration*t.row))},getCellDayEl:function(t){return this.dayEls.eq(t.col)},computeDateTop:function(t,i){return this.computeTimeTop(e.duration(t.clone().stripZone()-i.clone().stripTime()))},computeTimeTop:function(t){var e,i,n,r,o=(t-this.minTime)/this.slotDuration;return o=Math.max(0,o),o=Math.min(this.slatEls.length,o),e=Math.floor(o),i=o-e,n=this.slatTops[e],i?(r=this.slatTops[e+1],n+(r-n)*i):n},computeSlatTops:function(){var e,i=[];this.slatEls.each(function(n,r){e=t(r).position().top,i.push(e)}),i.push(e+this.slatEls.last().outerHeight()),this.slatTops=i},renderDrag:function(t,e,i){var n;return i?(this.renderRangeHelper(t,e,i),n=this.view.opt("dragOpacity"),void 0!==n&&this.helperEl.css("opacity",n),!0):(this.renderHighlight(t,e||this.view.calendar.getDefaultEventEnd(!1,t)),void 0)},destroyDrag:function(){this.destroyHelper(),this.destroyHighlight()},renderResize:function(t,e,i){this.renderRangeHelper(t,e,i)},destroyResize:function(){this.destroyHelper()},renderHelper:function(e,i){var n,r,o,s=this.renderEventTable([e]),l=s.tableEl,a=s.segs;for(n=0;a.length>n;n++)r=a[n],i&&i.col===r.col&&(o=i.el,r.el.css({left:o.css("left"),right:o.css("right"),"margin-left":o.css("margin-left"),"margin-right":o.css("margin-right")}));this.helperEl=t('
').append(l).appendTo(this.el)},destroyHelper:function(){this.helperEl&&(this.helperEl.remove(),this.helperEl=null)},renderSelection:function(t,e){this.view.opt("selectHelper")?this.renderRangeHelper(t,e):this.renderHighlight(t,e)},destroySelection:function(){this.destroyHelper(),this.destroyHighlight()},renderHighlight:function(e,i){this.highlightEl=t(this.highlightSkeletonHtml(e,i)).appendTo(this.el)},destroyHighlight:function(){this.highlightEl&&(this.highlightEl.remove(),this.highlightEl=null)},highlightSkeletonHtml:function(t,e){var i,n,r,o,s,l=this.view,a=this.rangeToSegs(t,e),c="",d=0;for(i=0;a.length>i;i++)n=a[i],n.col>d&&(c+='
",d++;return l.colCnt>d&&(c+='":void 0},numberIntroHtml:function(t){return this.weekNumbersVisible?'":void 0},dayIntroHtml:function(){return this.weekNumbersVisible?'":void 0},introHtml:function(){return this.weekNumbersVisible?'":void 0},numberCellHtml:function(t,e,i){var n;return this.dayNumbersVisible?(n=this.dayGrid.getDayClasses(i),n.unshift("fc-day-number"),'"):""):'"},dayIntroHtml:function(){return'"},slotBgIntroHtml:function(){return'"},introHtml:function(){return'"},axisStyleAttr:function(){return null!==this.axisWidth?'style="width:'+this.axisWidth+'px"':""},updateSize:function(t){t&&this.timeGrid.resize(),fe.prototype.updateSize.call(this,t)},updateWidth:function(){this.axisWidth=p(this.el.find(".fc-axis"))},setHeight:function(t,e){var i,n;null===this.bottomRuleHeight&&(this.bottomRuleHeight=this.bottomRuleEl.outerHeight()),this.bottomRuleEl.hide(),this.scrollerEl.css("overflow",""),v(this.scrollerEl),u(this.noScrollRowEls),this.dayGrid&&(this.dayGrid.destroySegPopover(),i=this.opt("eventLimit"),i&&"number"!=typeof i&&(i=Ye),i&&this.dayGrid.limitRows(i)),e||(n=this.computeScrollerHeight(t),m(this.scrollerEl,n)?(h(this.noScrollRowEls,w(this.scrollerEl)),n=this.computeScrollerHeight(t),this.scrollerEl.height(n),this.restoreScroll()):(this.scrollerEl.height(n).css("overflow","hidden"),this.bottomRuleEl.show()))},resetScroll:function(){function t(){i.scrollerEl.scrollTop(r)}var i=this,n=e.duration(this.opt("scrollTime")),r=this.timeGrid.computeTimeTop(n);r=Math.ceil(r),r&&r++,t(),setTimeout(t,0)},renderEvents:function(t){var e,i,n=[],r=[],o=[];for(i=0;t.length>i;i++)t[i].allDay?n.push(t[i]):r.push(t[i]);e=this.timeGrid.renderEvents(r),this.dayGrid&&(o=this.dayGrid.renderEvents(n)),this.updateHeight(),fe.prototype.renderEvents.call(this,t)},getSegs:function(){return this.timeGrid.getSegs().concat(this.dayGrid?this.dayGrid.getSegs():[])},destroyEvents:function(){fe.prototype.destroyEvents.call(this),this.recordScroll(),this.timeGrid.destroyEvents(),this.dayGrid&&this.dayGrid.destroyEvents()},renderDrag:function(t,e,i){return t.hasTime()?this.timeGrid.renderDrag(t,e,i):this.dayGrid?this.dayGrid.renderDrag(t,e,i):void 0},destroyDrag:function(){this.timeGrid.destroyDrag(),this.dayGrid&&this.dayGrid.destroyDrag()},renderSelection:function(t,e){t.hasTime()||e.hasTime()?this.timeGrid.renderSelection(t,e):this.dayGrid&&this.dayGrid.renderSelection(t,e)},destroySelection:function(){this.timeGrid.destroySelection(),this.dayGrid&&this.dayGrid.destroySelection()}}),xe.agendaWeek=be,be.prototype=H(Ee.prototype),t.extend(be.prototype,{name:"agendaWeek",incrementDate:function(t,e){return t.clone().stripTime().add(e,"weeks").startOf("week")},render:function(t){this.intervalStart=t.clone().stripTime().startOf("week"),this.intervalEnd=this.intervalStart.clone().add(1,"weeks"),this.start=this.skipHiddenDays(this.intervalStart),this.end=this.skipHiddenDays(this.intervalEnd,-1,!0),this.title=this.calendar.formatRange(this.start,this.end.clone().subtract(1),this.opt("titleFormat")," — "),Ee.prototype.render.call(this,this.getCellsPerWeek())}}),xe.agendaDay=Se,Se.prototype=H(Ee.prototype),t.extend(Se.prototype,{name:"agendaDay",incrementDate:function(t,e){var i=t.clone().stripTime().add(e,"days");return i=this.skipHiddenDays(i,0>e?-1:1)},render:function(t){this.start=this.intervalStart=t.clone().stripTime(),this.end=this.intervalEnd=this.start.clone().add(1,"days"),this.title=this.calendar.formatDate(this.start,this.opt("titleFormat")),Ee.prototype.render.call(this,1)}})}); \ No newline at end of file +(function(t){"function"==typeof define&&define.amd?define(["jquery","moment"],t):t(jQuery,moment)})(function(t,e){function i(t,e){return e.longDateFormat("LT").replace(":mm","(:mm)").replace(/(\Wmm)$/,"($1)").replace(/\s*a$/i,"t")}function n(t,e){var i=e.longDateFormat("L");return i=i.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g,""),t.isRTL?i+=" ddd":i="ddd "+i,i}function r(t){o(ke,t)}function o(e){function i(i,n){t.isPlainObject(n)&&t.isPlainObject(e[i])&&!s(i)?e[i]=o({},e[i],n):void 0!==n&&(e[i]=n)}for(var n=1;arguments.length>n;n++)t.each(arguments[n],i);return e}function s(t){return/(Time|Duration)$/.test(t)}function l(i,n){function r(t){var i=e.localeData||e.langData;return i.call(e,t)||i.call(e,"en")}function s(t){ne?u()&&(m(),p(t)):l()}function l(){re=Q.theme?"ui":"fc",i.addClass("fc"),Q.isRTL?i.addClass("fc-rtl"):i.addClass("fc-ltr"),Q.theme?i.addClass("ui-widget"):i.addClass("fc-unthemed"),ne=t("
").prependTo(i),ee=new a(U,Q),ie=ee.render(),ie&&i.prepend(ie),f(Q.defaultView),Q.handleWindowResize&&(le=L(y,Q.windowResizeDelay),t(window).resize(le))}function d(){oe&&oe.destroy(),ee.destroy(),ne.remove(),i.removeClass("fc fc-ltr fc-rtl fc-unthemed ui-widget"),t(window).unbind("resize",le)}function u(){return i.is(":visible")}function f(t){p(0,t)}function p(e,i){ue++,oe&&i&&oe.name!==i&&(ee.deactivateButton(oe.name),I(),oe.start&&oe.destroy(),oe.el.remove(),oe=null),!oe&&i&&(oe=new ze[i](U),oe.el=t("
").appendTo(ne),ee.activateButton(i)),oe&&(e&&(ae=oe.incrementDate(ae,e)),oe.start&&!e&&ae.isWithin(oe.intervalStart,oe.intervalEnd)||u()&&(I(),oe.start&&oe.destroy(),oe.render(ae),B(),H(),k(),S())),B(),ue--}function g(t){return u()?(t&&v(),ue++,oe.updateSize(!0),ue--,!0):void 0}function m(){u()&&v()}function v(){se="number"==typeof Q.contentHeight?Q.contentHeight:"number"==typeof Q.height?Q.height-(ie?ie.outerHeight(!0):0):Math.round(ne.width()/Math.max(Q.aspectRatio,.5))}function y(t){!ue&&t.target===window&&oe.start&&g(!0)&&oe.trigger("windowResize",he)}function w(){D(),b()}function E(){u()&&(I(),oe.destroyEvents(),oe.renderEvents(fe),B())}function D(){I(),oe.destroyEvents(),B()}function S(){!Q.lazyFetching||ce(oe.start,oe.end)?b():E()}function b(){de(oe.start,oe.end)}function T(t){fe=t,E()}function C(){E()}function H(){ee.updateTitle(oe.title)}function k(){var t=U.getNow();t.isWithin(oe.intervalStart,oe.intervalEnd)?ee.disableButton("today"):ee.enableButton("today")}function M(t,e){t=U.moment(t),e=e?U.moment(e):t.hasTime()?t.clone().add(U.defaultTimedEventDuration):t.clone().add(U.defaultAllDayEventDuration),oe.select(t,e)}function R(){oe&&oe.unselect()}function P(){p(-1)}function z(){p(1)}function N(){ae.add(-1,"years"),p()}function _(){ae.add(1,"years"),p()}function A(){ae=U.getNow(),p()}function Y(t){ae=U.moment(t),p()}function O(t){ae.add(e.duration(t)),p()}function F(t,e){var i,n;e&&void 0!==ze[e]||(e=e||"day",i=ee.getViewsWithButtons().join(" "),n=i.match(RegExp("\\w+"+G(e))),n||(n=i.match(/\w+Day/)),e=n?n[0]:"agendaDay"),ae=t,f(e)}function Z(){return ae.clone()}function I(){ne.css({width:"100%",height:ne.height(),overflow:"hidden"})}function B(){ne.css({width:"",height:"",overflow:""})}function j(){return U}function X(){return oe}function $(t,e){return void 0===e?Q[t]:(("height"==t||"contentHeight"==t||"aspectRatio"==t)&&(Q[t]=e,g(!0)),void 0)}function q(t,e){return Q[t]?Q[t].apply(e||he,Array.prototype.slice.call(arguments,2)):void 0}var U=this;n=n||{};var K,Q=o({},ke,n);K=Q.lang in Me?Me[Q.lang]:Me[ke.lang],K&&(Q=o({},ke,K,n)),Q.isRTL&&(Q=o({},ke,Re,K||{},n)),U.options=Q,U.render=s,U.destroy=d,U.refetchEvents=w,U.reportEvents=T,U.reportEventChange=C,U.rerenderEvents=E,U.changeView=f,U.select=M,U.unselect=R,U.prev=P,U.next=z,U.prevYear=N,U.nextYear=_,U.today=A,U.gotoDate=Y,U.incrementDate=O,U.zoomTo=F,U.getDate=Z,U.getCalendar=j,U.getView=X,U.option=$,U.trigger=q;var J=x(r(Q.lang));if(Q.monthNames&&(J._months=Q.monthNames),Q.monthNamesShort&&(J._monthsShort=Q.monthNamesShort),Q.dayNames&&(J._weekdays=Q.dayNames),Q.dayNamesShort&&(J._weekdaysShort=Q.dayNamesShort),null!=Q.firstDay){var te=x(J._week);te.dow=Q.firstDay,J._week=te}U.defaultAllDayEventDuration=e.duration(Q.defaultAllDayEventDuration),U.defaultTimedEventDuration=e.duration(Q.defaultTimedEventDuration),U.moment=function(){var t;return"local"===Q.timezone?(t=Pe.moment.apply(null,arguments),t.hasTime()&&t.local()):t="UTC"===Q.timezone?Pe.moment.utc.apply(null,arguments):Pe.moment.parseZone.apply(null,arguments),"_locale"in t?t._locale=J:t._lang=J,t},U.getIsAmbigTimezone=function(){return"local"!==Q.timezone&&"UTC"!==Q.timezone},U.rezoneDate=function(t){return U.moment(t.toArray())},U.getNow=function(){var t=Q.now;return"function"==typeof t&&(t=t()),U.moment(t)},U.calculateWeekNumber=function(t){var e=Q.weekNumberCalculation;return"function"==typeof e?e(t):"local"===e?t.week():"ISO"===e.toUpperCase()?t.isoWeek():void 0},U.getEventEnd=function(t){return t.end?t.end.clone():U.getDefaultEventEnd(t.allDay,t.start)},U.getDefaultEventEnd=function(t,e){var i=e.clone();return t?i.stripTime().add(U.defaultAllDayEventDuration):i.add(U.defaultTimedEventDuration),U.getIsAmbigTimezone()&&i.stripZone(),i},U.formatRange=function(t,e,i){return"function"==typeof i&&(i=i.call(U,Q,J)),W(t,e,i,null,Q.isRTL)},U.formatDate=function(t,e){return"function"==typeof e&&(e=e.call(U,Q,J)),V(t,e)},c.call(U,Q),h.call(U,Q);var ee,ie,ne,re,oe,se,le,ae,ce=U.isFetchNeeded,de=U.fetchEvents,he=i[0],ue=0,fe=[];ae=null!=Q.defaultDate?U.moment(Q.defaultDate):U.getNow(),U.getSuggestedViewHeight=function(){return void 0===se&&m(),se},U.isHeightAuto=function(){return"auto"===Q.contentHeight||"auto"===Q.height}}function a(e,i){function n(){var e=i.header;return f=i.theme?"ui":"fc",e?p=t("
").append(o("left")).append(o("right")).append(o("center")).append('
'):void 0}function r(){p.remove()}function o(n){var r=t('
'),o=i.header[n];return o&&t.each(o.split(" "),function(){var n,o=t(),s=!0;t.each(this.split(","),function(n,r){var l,a,c,d,h,u,p,m;"title"==r?(o=o.add(t("

 

")),s=!1):(e[r]?l=function(){e[r]()}:ze[r]&&(l=function(){e.changeView(r)},g.push(r)),l&&(a=b(i.themeButtonIcons,r),c=b(i.buttonIcons,r),d=b(i.defaultButtonText,r),h=b(i.buttonText,r),u=h?P(h):a&&i.theme?"":c&&!i.theme?"":P(d||r),p=["fc-"+r+"-button",f+"-button",f+"-state-default"],m=t('").click(function(){m.hasClass(f+"-state-disabled")||(l(),(m.hasClass(f+"-state-active")||m.hasClass(f+"-state-disabled"))&&m.removeClass(f+"-state-hover"))}).mousedown(function(){m.not("."+f+"-state-active").not("."+f+"-state-disabled").addClass(f+"-state-down")}).mouseup(function(){m.removeClass(f+"-state-down")}).hover(function(){m.not("."+f+"-state-active").not("."+f+"-state-disabled").addClass(f+"-state-hover")},function(){m.removeClass(f+"-state-hover").removeClass(f+"-state-down")}),o=o.add(m)))}),s&&o.first().addClass(f+"-corner-left").end().last().addClass(f+"-corner-right").end(),o.length>1?(n=t("
"),s&&n.addClass("fc-button-group"),n.append(o),r.append(n)):r.append(o)}),r}function s(t){p.find("h2").text(t)}function l(t){p.find(".fc-"+t+"-button").addClass(f+"-state-active")}function a(t){p.find(".fc-"+t+"-button").removeClass(f+"-state-active")}function c(t){p.find(".fc-"+t+"-button").attr("disabled","disabled").addClass(f+"-state-disabled")}function d(t){p.find(".fc-"+t+"-button").removeAttr("disabled").removeClass(f+"-state-disabled")}function h(){return g}var u=this;u.render=n,u.destroy=r,u.updateTitle=s,u.activateButton=l,u.deactivateButton=a,u.disableButton=c,u.enableButton=d,u.getViewsWithButtons=h;var f,p=t(),g=[]}function c(e){function i(t,e){return!b||t.clone().stripZone()C.clone().stripZone()}function n(t,e){b=t,C=e,A=[];var i=++L,n=G.length;N=n;for(var o=0;n>o;o++)r(G[o],i)}function r(e,i){o(e,function(n){var r,o,s=t.isArray(e.events);if(i==L){if(n)for(r=0;n.length>r;r++)o=n[r],s||(o=w(o,e)),o&&A.push(o);N--,N||k(A)}})}function o(i,n){var r,s,l=Pe.sourceFetchers;for(r=0;l.length>r;r++){if(s=l[r].call(S,i,b.clone(),C.clone(),e.timezone,n),s===!0)return;if("object"==typeof s)return o(s,n),void 0}var a=i.events;if(a)t.isFunction(a)?(v(),a.call(S,b.clone(),C.clone(),e.timezone,function(t){n(t),y()})):t.isArray(a)?n(a):n();else{var c=i.url;if(c){var d,h=i.success,u=i.error,f=i.complete;d=t.isFunction(i.data)?i.data():i.data;var p=t.extend({},d||{}),g=R(i.startParam,e.startParam),m=R(i.endParam,e.endParam),w=R(i.timezoneParam,e.timezoneParam);g&&(p[g]=b.format()),m&&(p[m]=C.format()),e.timezone&&"local"!=e.timezone&&(p[w]=e.timezone),v(),t.ajax(t.extend({},Ge,i,{data:p,success:function(e){e=e||[];var i=M(h,this,arguments);t.isArray(i)&&(e=i),n(e)},error:function(){M(u,this,arguments),n()},complete:function(){M(f,this,arguments),y()}}))}else n()}}function s(t){var e=l(t);e&&(G.push(e),N++,r(e,L))}function l(e){var i,n,r=Pe.sourceNormalizers;if(t.isFunction(e)||t.isArray(e)?i={events:e}:"string"==typeof e?i={url:e}:"object"==typeof e&&(i=t.extend({},e)),i){for(i.className?"string"==typeof i.className&&(i.className=i.className.split(/\s+/)):i.className=[],t.isArray(i.events)&&(i.origArray=i.events,i.events=t.map(i.events,function(t){return w(t,i)})),n=0;r.length>n;n++)r[n].call(S,i);return i}}function a(e){G=t.grep(G,function(t){return!c(t,e)}),A=t.grep(A,function(t){return!c(t.source,e)}),k(A)}function c(t,e){return t&&e&&h(t)==h(e)}function h(t){return("object"==typeof t?t.origArray||t.url||t.events:null)||t}function u(t){t.start=S.moment(t.start),t.end&&(t.end=S.moment(t.end)),E(t),f(t),k(A)}function f(t){var e,i,n,r;for(e=0;A.length>e;e++)if(i=A[e],i._id==t._id&&i!==t)for(n=0;Y.length>n;n++)r=Y[n],void 0!==t[r]&&(i[r]=t[r])}function p(t,e){var i=w(t);i&&(i.source||(e&&(z.events.push(i),i.source=z),A.push(i)),k(A))}function g(e){var i,n;for(null==e?e=function(){return!0}:t.isFunction(e)||(i=e+"",e=function(t){return t._id==i}),A=t.grep(A,e,!0),n=0;G.length>n;n++)t.isArray(G[n].events)&&(G[n].events=t.grep(G[n].events,e,!0));k(A)}function m(e){return t.isFunction(e)?t.grep(A,e):null!=e?(e+="",t.grep(A,function(t){return t._id==e})):A}function v(){_++||H("loading",null,!0,x())}function y(){--_||H("loading",null,!1,x())}function w(i,n){var r,o,s,l,a={};return e.eventDataTransform&&(i=e.eventDataTransform(i)),n&&n.eventDataTransform&&(i=n.eventDataTransform(i)),r=S.moment(i.start||i.date),r.isValid()&&(o=null,!i.end||(o=S.moment(i.end),o.isValid()))?(s=i.allDay,void 0===s&&(l=R(n?n.allDayDefault:void 0,e.allDayDefault),s=void 0!==l?l:!(r.hasTime()||o&&o.hasTime())),s?(r.hasTime()&&r.stripTime(),o&&o.hasTime()&&o.stripTime()):(r.hasTime()||(r=S.rezoneDate(r)),o&&!o.hasTime()&&(o=S.rezoneDate(o))),t.extend(a,i),n&&(a.source=n),a._id=i._id||(void 0===i.id?"_fc"+Le++:i.id+""),a.className=i.className?"string"==typeof i.className?i.className.split(/\s+/):i.className:[],a.resources?"string"==typeof a.resources&&(a.resources=a.resources.split(/\s+/)):a.resources=[],a.allDay=s,a.start=r,a.end=o,e.forceEventDuration&&!a.end&&(a.end=P(a)),d(a),a):void 0}function E(t,e,i){var n,r,o,s,l=t._allDay,a=t._start,c=t._end,d=!1;return e||i||(e=t.start,i=t.end),n=t.allDay!=l?t.allDay:!(e||i).hasTime(),n&&(e&&(e=e.clone().stripTime()),i&&(i=i.clone().stripTime())),e&&(r=n?T(e,a.clone().stripTime()):T(e,a)),n!=l?d=!0:i&&(o=T(i||S.getDefaultEventEnd(n,e||a),e||a).subtract(T(c||S.getDefaultEventEnd(l,a),a))),s=D(m(t._id),d,n,r,o),{dateDelta:r,durationDelta:o,undo:s}}function D(i,n,r,o,s){var l=S.getIsAmbigTimezone(),a=[];return t.each(i,function(t,i){var c=i._allDay,h=i._start,u=i._end,f=null!=r?r:c,p=h.clone(),g=!n&&u?u.clone():null;f?(p.stripTime(),g&&g.stripTime()):(p.hasTime()||(p=S.rezoneDate(p)),g&&!g.hasTime()&&(g=S.rezoneDate(g))),g||!e.forceEventDuration&&!+s||(g=S.getDefaultEventEnd(f,p)),p.add(o),g&&g.add(o).add(s),l&&(+o||+s)&&(p.stripZone(),g&&g.stripZone()),i.allDay=f,i.start=p,i.end=g,d(i),a.push(function(){i.allDay=c,i.start=h,i.end=u,d(i)})}),function(){for(var t=0;a.length>t;t++)a[t]()}}var S=this;S.isFetchNeeded=i,S.fetchEvents=n,S.addEventSource=s,S.removeEventSource=a,S.updateEvent=u,S.renderEvent=p,S.removeEvents=g,S.clientEvents=m,S.mutateEvent=E;var b,C,H=S.trigger,x=S.getView,k=S.reportEvents,P=S.getEventEnd,z={events:[]},G=[z],L=0,N=0,_=0,A=[];t.each((e.events?[e.events]:[]).concat(e.eventSources||[]),function(t,e){var i=l(e);i&&G.push(i)});var Y=["title","url","allDay","className","editable","color","backgroundColor","borderColor","textColor"]}function d(t){t._allDay=t.allDay,t._start=t.start.clone(),t._end=t.end?t.end.clone():null}function h(e){function i(e){h=[];var i;if(t.isFunction(e))i={resources:e},h.push(i),c=void 0;else if("string"==typeof e)i={url:e},h.push(i),c=void 0;else if("object"==typeof e&&null!=e){for(var n=0;e.length>n;n++){var r=e[n];o(r),i={resources:r},h.push(i)}c=void 0}}function n(i,n){var o;if(i=i!==void 0?i:!0,!i||void 0===o){o=[];for(var s=h.length,l=0;s>l;l++){var a=r(h[l],n);o=o.concat(a)}}return t.isFunction(e.resourceFilter)?t.grep(o,e.resourceFilter):o}function r(i,n){var r=i.resources;if(r){if(t.isFunction(r))return r()}else{var o=i.url;if(o){var s={};if("object"==typeof n){var l=e.startParam,a=e.endParam;l&&(s[l]=Math.round(+n.intervalStart/1e3)),a&&(s[a]=Math.round(+n.intervalEnd/1e3))}t.ajax(t.extend({},Ge,i,{data:s,dataType:"json",cache:!1,success:function(t){t=t||[],r=t},error:function(){},async:!1}))}}return r}function o(t){t.className?"string"==typeof t.className&&(t.className=t.className.split(/\s+/)):t.className=[];for(var e=Pe.sourceNormalizers,i=0;e.length>i;i++)e[i](t)}function s(t,e,i,n){var r,o,s,c,d=t._allDay,h=t._start,u=t._end,f=!1;return i||n||(i=t.start,n=t.end),r=t.allDay!=d?t.allDay:!(i||n).hasTime(),r&&(i&&(i=i.clone().stripTime()),n&&(n=n.clone().stripTime())),i&&(o=r?T(i,h.clone().stripTime()):T(i,h)),r!=d?f=!0:n&&(s=T(n||a.getDefaultEventEnd(r,i||h),i||h).subtract(T(u||a.getDefaultEventEnd(d,h),h))),c=l(a.clientEvents(t._id),f,r,o,s,e),{dateDelta:o,durationDelta:s,undo:c}}function l(i,n,r,o,s,l){var c=a.getIsAmbigTimezone(),h=[];return t.each(i,function(t,i){var u=i.resources,f=i._allDay,p=i._start,g=i._end,m=null!=r?r:f,v=p.clone(),y=!n&&g?g.clone():null;m?(v.stripTime(),y&&y.stripTime()):(v.hasTime()||(v=a.rezoneDate(v)),y&&!y.hasTime()&&(y=a.rezoneDate(y))),y||!e.forceEventDuration&&!+s||(y=a.getDefaultEventEnd(m,v)),v.add(o),y&&y.add(o).add(s),c&&(+o||+s)&&(v.stripZone(),y&&y.stripZone()),i.allDay=m,i.start=v,i.end=y,i.resources=l,d(i),h.push(function(){i.allDay=f,i.start=p,i.end=g,i.resources=u,d(i)})}),function(){for(var t=0;h.length>t;t++)h[t]()}}var a=this;a.fetchResources=n,a.setResources=i,a.mutateResourceEvent=s;var c,h=[];i(e.resources)}function u(t,e){e.left&&t.css({"border-left-width":1,"margin-left":e.left-1}),e.right&&t.css({"border-right-width":1,"margin-right":e.right-1})}function f(t){t.css({"margin-left":"","margin-right":"","border-left-width":"","border-right-width":""})}function p(e,i,n){var r=Math.floor(i/e.length),o=Math.floor(i-r*(e.length-1)),s=[],l=[],a=[],c=0;g(e),e.each(function(i,n){var d=i===e.length-1?o:r,h=t(n).outerHeight(!0);d>h?(s.push(n),l.push(h),a.push(t(n).height())):c+=h}),n&&(i-=c,r=Math.floor(i/s.length),o=Math.floor(i-r*(s.length-1))),t(s).each(function(e,i){var n=e===s.length-1?o:r,c=l[e],d=a[e],h=n-(c-d);n>c&&t(i).height(h)})}function g(t){t.height("")}function m(e){var i=0;return e.find("> *").each(function(e,n){var r=t(n).outerWidth();r>i&&(i=r)}),i++,e.width(i),i}function v(t,e){return t.height(e).addClass("fc-scroller"),t[0].scrollHeight-1>t[0].clientHeight?!0:(y(t),!1)}function y(t){t.height("").removeClass("fc-scroller")}function w(e){var i=e.css("position"),n=e.parents().filter(function(){var e=t(this);return/(auto|scroll)/.test(e.css("overflow")+e.css("overflow-y")+e.css("overflow-x"))}).eq(0);return"fixed"!==i&&n.length?n:t(e[0].ownerDocument||document)}function E(t){var e=t.offset().left,i=e+t.width(),n=t.children(),r=n.offset().left,o=r+n.outerWidth();return{left:r-e,right:i-o}}function D(t){return 1==t.which&&!t.ctrlKey}function S(t,e,i,n){var r,o,s,l;return e>i&&n>t?(t>=i?(r=t.clone(),s=!0):(r=i.clone(),s=!1),n>=e?(o=e.clone(),l=!0):(o=n.clone(),l=!1),{start:r,end:o,isStart:s,isEnd:l}):void 0}function b(t,e){if(t=t||{},void 0!==t[e])return t[e];for(var i,n=e.split(/(?=[A-Z])/),r=n.length-1;r>=0;r--)if(i=t[n[r].toLowerCase()],void 0!==i)return i;return t["default"]}function T(t,i){return e.duration({days:t.clone().stripTime().diff(i.clone().stripTime(),"days"),ms:t.time()-i.time()})}function C(t){return"[object Date]"===Object.prototype.toString.call(t)||t instanceof Date}function H(t,e){return t-e}function x(t){var e=function(){};return e.prototype=t,new e}function k(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i])}function M(e,i,n){if(t.isFunction(e)&&(e=[e]),e){var r,o;for(r=0;e.length>r;r++)o=e[r].apply(i,n)||o;return o}}function R(){for(var t=0;arguments.length>t;t++)if(void 0!==arguments[t])return arguments[t]}function P(t){return(t+"").replace(/&/g,"&").replace(//g,">").replace(/'/g,"'").replace(/"/g,""").replace(/\n/g,"
")}function z(t){return t.replace(/&.*?;/g,"")}function G(t){return t.charAt(0).toUpperCase()+t.slice(1)}function L(t,e){var i,n,r,o,s=function(){var l=+new Date-o;e>l&&l>0?i=setTimeout(s,e-l):(i=null,t.apply(r,n),i||(r=n=null))};return function(){r=this,n=arguments,o=+new Date,i||(i=setTimeout(s,e))}}function N(i,n,r){var o,s,l,a,c=i[0],d=1==i.length&&"string"==typeof c;return e.isMoment(c)?(a=e.apply(null,i),c._ambigTime&&(a._ambigTime=!0),c._ambigZone&&(a._ambigZone=!0)):C(c)||void 0===c?a=e.apply(null,i):(o=!1,s=!1,d?_e.test(c)?(c+="-01",i=[c],o=!0,s=!0):(l=Ae.exec(c))&&(o=!l[5],s=!0):t.isArray(c)&&(s=!0),a=n?e.utc.apply(e,i):e.apply(null,i),o?(a._ambigTime=!0,a._ambigZone=!0):r&&(s?a._ambigZone=!0:d&&a.zone(c))),new _(a)}function _(t){k(this,t)}function A(t,e){var i,n=[],r=!1,o=!1;for(i=0;t.length>i;i++)n.push(Pe.moment.parseZone(t[i])),r=r||n[i]._ambigTime,o=o||n[i]._ambigZone;for(i=0;n.length>i;i++)r&&!e?n[i].stripTime():o&&n[i].stripZone();return n}function Y(t,i){return e.fn.format.call(t,i)}function V(t,e){return O(t,B(e))}function O(t,e){var i,n="";for(i=0;e.length>i;i++)n+=F(t,e[i]);return n}function F(t,e){var i,n;return"string"==typeof e?e:(i=e.token)?Ye[i]?Ye[i](t):Y(t,i):e.maybe&&(n=O(t,e.maybe),n.match(/[1-9]/))?n:""}function W(t,e,i,n,r){var o;return t=Pe.moment.parseZone(t),e=Pe.moment.parseZone(e),o=(t.localeData||t.lang).call(t),i=o.longDateFormat(i)||i,n=n||" - ",Z(t,e,B(i),n,r)}function Z(t,e,i,n,r){var o,s,l,a,c="",d="",h="",u="",f="";for(s=0;i.length>s&&(o=I(t,e,i[s]),o!==!1);s++)c+=o;for(l=i.length-1;l>s&&(o=I(t,e,i[l]),o!==!1);l--)d=o+d;for(a=s;l>=a;a++)h+=F(t,i[a]),u+=F(e,i[a]);return(h||u)&&(f=r?u+n+h:h+n+u),c+f+d}function I(t,e,i){var n,r;return"string"==typeof i?i:(n=i.token)&&(r=Ve[n.charAt(0)],r&&t.isSame(e,r))?Y(t,n):!1}function B(t){return t in Oe?Oe[t]:Oe[t]=j(t)}function j(t){for(var e,i=[],n=/\[([^\]]*)\]|\(([^\)]*)\)|(LT|(\w)\4*o?)|([^\w\[\(]+)/g;e=n.exec(t);)e[1]?i.push(e[1]):e[2]?i.push({maybe:j(e[2])}):e[3]?i.push({token:e[3]}):e[5]&&i.push(e[5]);return i}function X(t){this.options=t||{}}function $(t){this.grid=t}function q(t){this.coordMaps=t}function U(t,e){this.coordMap=t,this.options=e||{}}function K(t,e){return t||e?t&&e?t.grid===e.grid&&t.row===e.row&&t.col===e.col:!1:!0}function Q(e,i){this.options=i=i||{},this.sourceEl=e,this.parentEl=i.parentEl?t(i.parentEl):e.parent()}function J(t){this.view=t}function te(t){J.call(this,t),this.coordMap=new $(this)}function ee(t,e){return t.eventStartMS-e.eventStartMS||e.eventDurationMS-t.eventDurationMS||e.event.allDay-t.event.allDay||(t.event.title||"").localeCompare(e.event.title)}function ie(t){te.call(this,t)}function ne(t,e){var i,n;for(i=0;e.length>i;i++)if(n=e[i],n.leftCol<=t.rightCol&&n.rightCol>=t.leftCol)return!0;return!1}function re(t,e){return t.leftCol-e.leftCol}function oe(t){te.call(this,t)}function se(t){var e,i,n;if(t.sort(ee),e=le(t),ae(e),i=e[0]){for(n=0;i.length>n;n++)ce(i[n]);for(n=0;i.length>n;n++)de(i[n],0,0)}}function le(t){var e,i,n,r=[];for(e=0;t.length>e;e++){for(i=t[e],n=0;r.length>n&&he(i,r[n]).length;n++);i.level=n,(r[n]||(r[n]=[])).push(i)}return r}function ae(t){var e,i,n,r,o;for(e=0;t.length>e;e++)for(i=t[e],n=0;i.length>n;n++)for(r=i[n],r.forwardSegs=[],o=e+1;t.length>o;o++)he(r,t[o],r.forwardSegs)}function ce(t){var e,i,n=t.forwardSegs,r=0;if(void 0===t.forwardPressure){for(e=0;n.length>e;e++)i=n[e],ce(i),r=Math.max(r,1+i.forwardPressure);t.forwardPressure=r}}function de(t,e,i){var n,r=t.forwardSegs;if(void 0===t.forwardCoord)for(r.length?(r.sort(fe),de(r[0],e+1,i),t.forwardCoord=r[0].backwardCoord):t.forwardCoord=1,t.backwardCoord=t.forwardCoord-(t.forwardCoord-i)/(e+1),n=0;r.length>n;n++)de(r[n],0,t.forwardCoord)}function he(t,e,i){i=i||[];for(var n=0;e.length>n;n++)ue(t,e[n])&&i.push(e[n]);return i}function ue(t,e){return t.bottom>e.top&&t.topd;d++){var h=d*n,u=h+n-1,f=Math.max(a,h),p=Math.min(c,u);if(p>=f){var m=E(f),v=E(p),D=[m.col,v.col].sort(),b=g(f)==s,T=g(p)+1==l;r.push({row:d,leftCol:D[0],rightCol:D[1],isStart:b,isEnd:T})}}return r}function S(t,e){var i,n,r=t.clone().stripTime();return e&&(i=e.clone().stripTime(),n=+e.time(),n&&n>=k&&i.add(1,"days")),(!e||r>=i)&&(i=r.clone().add(1,"days")),{start:r,end:i}}function T(t){var e=S(t.start,t.end);return e.end.diff(e.start,"days")>1}var C=this;C.calendar=i,C.opt=n,C.trigger=r,C.isEventDraggable=o,C.isEventResizable=l,C.eventDrop=a,C.eventResize=c;var H=i.reportEventChange,x=i.options,k=e.duration(x.nextDayThreshold);C.init(),C.getEventTimeText=function(t,e){var r,o;return"object"==typeof t&&"object"==typeof e?(r=t,o=e,e=arguments[2]):(r=t.start,o=t.end),e=e||n("timeFormat"),o&&n("displayEventEnd")?i.formatRange(r,o,e):i.formatDate(r,e)},C.isHiddenDay=d,C.skipHiddenDays=u,C.getCellsPerWeek=h,C.dateToCell=v,C.dateToDayOffset=y,C.dayOffsetToCellOffset=w,C.cellOffsetToCell=E,C.cellToDate=f,C.cellToCellOffset=p,C.cellOffsetToDayOffset=g,C.dayOffsetToDate=m,C.rangeToSegments=D,C.isMultiDayEvent=T;var M,P=n("hiddenDays")||[],z=[],G=[],L=[],N=n("isRTL");(function(){n("weekends")===!1&&P.push(0,6);for(var e=0,i=0;7>e;e++)G[e]=i,z[e]=-1!=t.inArray(e,P),z[e]||(L[i]=e,i++);if(M=i,!M)throw"invalid hiddenDays"})()}function ge(t){pe.call(this,t),this.dayGrid=new ie(this),this.coordMap=this.dayGrid.coordMap}function me(t){ge.call(this,t)}function ve(t){ge.call(this,t)}function ye(t){ge.call(this,t)}function we(t,e){return e.longDateFormat("LT").replace(":mm","(:mm)").replace(/(\Wmm)$/,"($1)").replace(/\s*a$/i,"a")}function Ee(t,e){return e.longDateFormat("LT").replace(/\s*a$/i,"")}function De(t){pe.call(this,t),this.timeGrid=new oe(this),this.opt("allDaySlot")?(this.dayGrid=new ie(this),this.coordMap=new q([this.dayGrid.coordMap,this.timeGrid.coordMap])):this.coordMap=this.timeGrid.coordMap}function Se(t){De.call(this,t)}function be(t){De.call(this,t)}function Te(t){ie.call(this,t)}function Ce(t){oe.call(this,t)}function He(t){pe.call(this,t),this.cellToDate=He.prototype.cellToDate,this.timeGrid=new Ce(this),this.opt("allDaySlot")?(this.dayGrid=new Te(this),this.coordMap=new q([this.dayGrid.coordMap,this.timeGrid.coordMap])):this.coordMap=this.timeGrid.coordMap}function xe(t){He.call(this,t)}var ke={lang:"en",defaultTimedEventDuration:"02:00:00",defaultAllDayEventDuration:{days:1},forceEventDuration:!1,nextDayThreshold:"09:00:00",defaultView:"month",aspectRatio:1.35,header:{left:"title",center:"",right:"today prev,next"},weekends:!0,weekNumbers:!1,weekNumberTitle:"W",weekNumberCalculation:"local",lazyFetching:!0,startParam:"start",endParam:"end",timezoneParam:"timezone",timezone:!1,titleFormat:{month:"MMMM YYYY",week:"ll",day:"LL"},columnFormat:{month:"ddd",week:n,day:"dddd"},timeFormat:{"default":i},displayEventEnd:{month:!1,basicWeek:!1,"default":!0},isRTL:!1,defaultButtonText:{prev:"prev",next:"next",prevYear:"prev year",nextYear:"next year",today:"today",month:"month",week:"week",day:"day"},buttonIcons:{prev:"left-single-arrow",next:"right-single-arrow",prevYear:"left-double-arrow",nextYear:"right-double-arrow"},theme:!1,themeButtonIcons:{prev:"circle-triangle-w",next:"circle-triangle-e",prevYear:"seek-prev",nextYear:"seek-next"},dragOpacity:.75,dragRevertDuration:500,dragScroll:!0,unselectAuto:!0,dropAccept:"*",eventLimit:!1,eventLimitText:"more",eventLimitClick:"popover",dayPopoverFormat:"LL",handleWindowResize:!0,windowResizeDelay:200},Me={en:{columnFormat:{week:"ddd M/D"},dayPopoverFormat:"dddd, MMMM D"}},Re={header:{left:"next,prev today",center:"",right:"title"},buttonIcons:{prev:"right-single-arrow",next:"left-single-arrow",prevYear:"right-double-arrow",nextYear:"left-double-arrow"},themeButtonIcons:{prev:"circle-triangle-e",next:"circle-triangle-w",nextYear:"seek-prev",prevYear:"seek-next"}},Pe=t.fullCalendar={version:"2.1.1"},ze=Pe.views={};t.fn.fullCalendar=function(e){var i=Array.prototype.slice.call(arguments,1),n=this;return this.each(function(r,o){var s,a=t(o),c=a.data("fullCalendar");"string"==typeof e?c&&t.isFunction(c[e])&&(s=c[e].apply(c,i),r||(n=s),"destroy"===e&&a.removeData("fullCalendar")):c||(c=new l(a,e),a.data("fullCalendar",c),c.render())}),n},Pe.langs=Me,Pe.datepickerLang=function(e,i,n){var r=Me[e];r||(r=Me[e]={}),o(r,{isRTL:n.isRTL,weekNumberTitle:n.weekHeader,titleFormat:{month:n.showMonthAfterYear?"YYYY["+n.yearSuffix+"] MMMM":"MMMM YYYY["+n.yearSuffix+"]"},defaultButtonText:{prev:z(n.prevText),next:z(n.nextText),today:z(n.currentText)}}),t.datepicker&&(t.datepicker.regional[i]=t.datepicker.regional[e]=n,t.datepicker.regional.en=t.datepicker.regional[""],t.datepicker.setDefaults(n))},Pe.lang=function(t,e){var i;e&&(i=Me[t],i||(i=Me[t]={}),o(i,e||{})),ke.lang=t},Pe.sourceNormalizers=[],Pe.sourceFetchers=[];var Ge={dataType:"json",cache:!1},Le=1,Ne=["sun","mon","tue","wed","thu","fri","sat"];Pe.applyAll=M;var _e=/^\s*\d{4}-\d\d$/,Ae=/^\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+)?)?)?)?)?$/;Pe.moment=function(){return N(arguments)},Pe.moment.utc=function(){var t=N(arguments,!0);return t.hasTime()&&t.utc(),t},Pe.moment.parseZone=function(){return N(arguments,!0,!0)},_.prototype=x(e.fn),_.prototype.clone=function(){return N([this])},_.prototype.time=function(t){if(null==t)return e.duration({hours:this.hours(),minutes:this.minutes(),seconds:this.seconds(),milliseconds:this.milliseconds()});delete this._ambigTime,e.isDuration(t)||e.isMoment(t)||(t=e.duration(t));var i=0;return e.isDuration(t)&&(i=24*Math.floor(t.asDays())),this.hours(i+t.hours()).minutes(t.minutes()).seconds(t.seconds()).milliseconds(t.milliseconds())},_.prototype.stripTime=function(){var t=this.toArray();return e.fn.utc.call(this),this.year(t[0]).month(t[1]).date(t[2]).hours(0).minutes(0).seconds(0).milliseconds(0),this._ambigTime=!0,this._ambigZone=!0,this},_.prototype.hasTime=function(){return!this._ambigTime},_.prototype.stripZone=function(){var t=this.toArray(),i=this._ambigTime;return e.fn.utc.call(this),this.year(t[0]).month(t[1]).date(t[2]).hours(t[3]).minutes(t[4]).seconds(t[5]).milliseconds(t[6]),i&&(this._ambigTime=!0),this._ambigZone=!0,this},_.prototype.hasZone=function(){return!this._ambigZone},_.prototype.zone=function(t){return null!=t&&(delete this._ambigTime,delete this._ambigZone),e.fn.zone.apply(this,arguments)},_.prototype.local=function(){var t=this.toArray(),i=this._ambigZone;return delete this._ambigTime,delete this._ambigZone,e.fn.local.apply(this,arguments),i&&this.year(t[0]).month(t[1]).date(t[2]).hours(t[3]).minutes(t[4]).seconds(t[5]).milliseconds(t[6]),this},_.prototype.utc=function(){return delete this._ambigTime,delete this._ambigZone,e.fn.utc.apply(this,arguments)},_.prototype.format=function(){return arguments[0]?V(this,arguments[0]):this._ambigTime?Y(this,"YYYY-MM-DD"):this._ambigZone?Y(this,"YYYY-MM-DD[T]HH:mm:ss"):Y(this)},_.prototype.toISOString=function(){return this._ambigTime?Y(this,"YYYY-MM-DD"):this._ambigZone?Y(this,"YYYY-MM-DD[T]HH:mm:ss"):e.fn.toISOString.apply(this,arguments)},_.prototype.isWithin=function(t,e){var i=A([this,t,e]);return i[0]>=i[1]&&i[0]').addClass(i.className||"").css({top:0,left:0}).append(i.content).appendTo(i.parentEl),this.el.on("click",".fc-close",function(){e.hide()}),i.autoHide&&t(document).on("mousedown",this.documentMousedownProxy=t.proxy(this,"documentMousedown"))},documentMousedown:function(e){this.el&&!t(e.target).closest(this.el).length&&this.hide()},destroy:function(){this.hide(),this.el&&(this.el.remove(),this.el=null),t(document).off("mousedown",this.documentMousedownProxy)},position:function(){var e,i,n,r,o,s=this.options,l=this.el.offsetParent().offset(),a=this.el.outerWidth(),c=this.el.outerHeight(),d=t(window),h=w(this.el);r=s.top||0,o=void 0!==s.left?s.left:void 0!==s.right?s.right-a:0,h.is(window)||h.is(document)?(h=d,e=0,i=0):(n=h.offset(),e=n.top,i=n.left),e+=d.scrollTop(),i+=d.scrollLeft(),s.viewportConstrain!==!1&&(r=Math.min(r,e+h.outerHeight()-c-this.margin),r=Math.max(r,e+this.margin),o=Math.min(o,i+h.outerWidth()-a-this.margin),o=Math.max(o,i+this.margin)),this.el.css({top:r-l.top,left:o-l.left}) +},trigger:function(t){this.options[t]&&this.options[t].apply(this,Array.prototype.slice.call(arguments,1))}},$.prototype={grid:null,rows:null,cols:null,containerEl:null,minX:null,maxX:null,minY:null,maxY:null,build:function(){this.grid.buildCoords(this.rows=[],this.cols=[]),this.computeBounds()},getCell:function(t,e){var i,n=null,r=this.rows,o=this.cols,s=-1,l=-1;if(this.inBounds(t,e)){for(i=0;r.length>i;i++)if(e>=r[i][0]&&r[i][1]>e){s=i;break}for(i=0;o.length>i;i++)if(t>=o[i][0]&&o[i][1]>t){l=i;break}s>=0&&l>=0&&(n={row:s,col:l},n.grid=this.grid,n.date=this.grid.getCellDate(n))}return n},computeBounds:function(){var t;this.containerEl&&(t=this.containerEl.offset(),this.minX=t.left,this.maxX=t.left+this.containerEl.outerWidth(),this.minY=t.top,this.maxY=t.top+this.containerEl.outerHeight())},inBounds:function(t,e){return this.containerEl?t>=this.minX&&this.maxX>t&&e>=this.minY&&this.maxY>e:!0}},q.prototype={coordMaps:null,build:function(){var t,e=this.coordMaps;for(t=0;e.length>t;t++)e[t].build()},getCell:function(t,e){var i,n=this.coordMaps,r=null;for(i=0;n.length>i&&!r;i++)r=n[i].getCell(t,e);return r}},U.prototype={coordMap:null,options:null,isListening:!1,isDragging:!1,origCell:null,origDate:null,cell:null,date:null,mouseX0:null,mouseY0:null,mousemoveProxy:null,mouseupProxy:null,scrollEl:null,scrollBounds:null,scrollTopVel:null,scrollLeftVel:null,scrollIntervalId:null,scrollHandlerProxy:null,scrollSensitivity:30,scrollSpeed:200,scrollIntervalMs:50,mousedown:function(t){D(t)&&(t.preventDefault(),this.startListening(t),this.options.distance||this.startDrag(t))},startListening:function(e){var i,n;this.isListening||(e&&this.options.scroll&&(i=w(t(e.target)),i.is(window)||i.is(document)||(this.scrollEl=i,this.scrollHandlerProxy=L(t.proxy(this,"scrollHandler"),100),this.scrollEl.on("scroll",this.scrollHandlerProxy))),this.computeCoords(),e&&(n=this.getCell(e),this.origCell=n,this.origDate=n?n.date:null,this.mouseX0=e.pageX,this.mouseY0=e.pageY),t(document).on("mousemove",this.mousemoveProxy=t.proxy(this,"mousemove")).on("mouseup",this.mouseupProxy=t.proxy(this,"mouseup")).on("selectstart",this.preventDefault),this.isListening=!0,this.trigger("listenStart",e))},computeCoords:function(){this.coordMap.build(),this.computeScrollBounds()},mousemove:function(t){var e,i;this.isDragging||(e=this.options.distance||1,i=Math.pow(t.pageX-this.mouseX0,2)+Math.pow(t.pageY-this.mouseY0,2),i>=e*e&&this.startDrag(t)),this.isDragging&&this.drag(t)},startDrag:function(t){var e;this.isListening||this.startListening(),this.isDragging||(this.isDragging=!0,this.trigger("dragStart",t),e=this.getCell(t),e&&this.cellOver(e,!0))},drag:function(t){var e;this.isDragging&&(e=this.getCell(t),K(e,this.cell)||(this.cell&&this.cellOut(),e&&this.cellOver(e)),this.dragScroll(t))},cellOver:function(t){this.cell=t,this.date=t.date,this.trigger("cellOver",t,t.date)},cellOut:function(){this.cell&&(this.trigger("cellOut",this.cell),this.cell=null,this.date=null)},mouseup:function(t){this.stopDrag(t),this.stopListening(t)},stopDrag:function(t){this.isDragging&&(this.stopScrolling(),this.trigger("dragStop",t),this.isDragging=!1)},stopListening:function(e){this.isListening&&(this.scrollEl&&(this.scrollEl.off("scroll",this.scrollHandlerProxy),this.scrollHandlerProxy=null),t(document).off("mousemove",this.mousemoveProxy).off("mouseup",this.mouseupProxy).off("selectstart",this.preventDefault),this.mousemoveProxy=null,this.mouseupProxy=null,this.isListening=!1,this.trigger("listenStop",e),this.origCell=this.cell=null,this.origDate=this.date=null)},getCell:function(t){return this.coordMap.getCell(t.pageX,t.pageY)},trigger:function(t){this.options[t]&&this.options[t].apply(this,Array.prototype.slice.call(arguments,1))},preventDefault:function(t){t.preventDefault()},computeScrollBounds:function(){var t,e=this.scrollEl;e&&(t=e.offset(),this.scrollBounds={top:t.top,left:t.left,bottom:t.top+e.outerHeight(),right:t.left+e.outerWidth()})},dragScroll:function(t){var e,i,n,r,o=this.scrollSensitivity,s=this.scrollBounds,l=0,a=0;s&&(e=(o-(t.pageY-s.top))/o,i=(o-(s.bottom-t.pageY))/o,n=(o-(t.pageX-s.left))/o,r=(o-(s.right-t.pageX))/o,e>=0&&1>=e?l=-1*e*this.scrollSpeed:i>=0&&1>=i&&(l=i*this.scrollSpeed),n>=0&&1>=n?a=-1*n*this.scrollSpeed:r>=0&&1>=r&&(a=r*this.scrollSpeed)),this.setScrollVel(l,a)},setScrollVel:function(e,i){this.scrollTopVel=e,this.scrollLeftVel=i,this.constrainScrollVel(),!this.scrollTopVel&&!this.scrollLeftVel||this.scrollIntervalId||(this.scrollIntervalId=setInterval(t.proxy(this,"scrollIntervalFunc"),this.scrollIntervalMs))},constrainScrollVel:function(){var t=this.scrollEl;0>this.scrollTopVel?0>=t.scrollTop()&&(this.scrollTopVel=0):this.scrollTopVel>0&&t.scrollTop()+t[0].clientHeight>=t[0].scrollHeight&&(this.scrollTopVel=0),0>this.scrollLeftVel?0>=t.scrollLeft()&&(this.scrollLeftVel=0):this.scrollLeftVel>0&&t.scrollLeft()+t[0].clientWidth>=t[0].scrollWidth&&(this.scrollLeftVel=0)},scrollIntervalFunc:function(){var t=this.scrollEl,e=this.scrollIntervalMs/1e3;this.scrollTopVel&&t.scrollTop(t.scrollTop()+this.scrollTopVel*e),this.scrollLeftVel&&t.scrollLeft(t.scrollLeft()+this.scrollLeftVel*e),this.constrainScrollVel(),this.scrollTopVel||this.scrollLeftVel||this.stopScrolling()},stopScrolling:function(){this.scrollIntervalId&&(clearInterval(this.scrollIntervalId),this.scrollIntervalId=null,this.computeCoords())},scrollHandler:function(){this.scrollIntervalId||this.computeCoords()}},Q.prototype={options:null,sourceEl:null,el:null,parentEl:null,top0:null,left0:null,mouseY0:null,mouseX0:null,topDelta:null,leftDelta:null,mousemoveProxy:null,isFollowing:!1,isHidden:!1,isAnimating:!1,start:function(e){this.isFollowing||(this.isFollowing=!0,this.mouseY0=e.pageY,this.mouseX0=e.pageX,this.topDelta=0,this.leftDelta=0,this.isHidden||this.updatePosition(),t(document).on("mousemove",this.mousemoveProxy=t.proxy(this,"mousemove")))},stop:function(e,i){function n(){this.isAnimating=!1,r.destroyEl(),this.top0=this.left0=null,i&&i()}var r=this,o=this.options.revertDuration;this.isFollowing&&!this.isAnimating&&(this.isFollowing=!1,t(document).off("mousemove",this.mousemoveProxy),e&&o&&!this.isHidden?(this.isAnimating=!0,this.el.animate({top:this.top0,left:this.left0},{duration:o,complete:n})):n())},getEl:function(){var t=this.el;return t||(this.sourceEl.width(),t=this.el=this.sourceEl.clone().css({position:"absolute",visibility:"",display:this.isHidden?"none":"",margin:0,right:"auto",bottom:"auto",width:this.sourceEl.width(),height:this.sourceEl.height(),opacity:this.options.opacity||"",zIndex:this.options.zIndex}).appendTo(this.parentEl)),t},destroyEl:function(){this.el&&(this.el.remove(),this.el=null)},updatePosition:function(){var t,e;this.getEl(),null===this.top0&&(this.sourceEl.width(),t=this.sourceEl.offset(),e=this.el.offsetParent().offset(),this.top0=t.top-e.top,this.left0=t.left-e.left),this.el.css({top:this.top0+this.topDelta,left:this.left0+this.leftDelta})},mousemove:function(t){this.topDelta=t.pageY-this.mouseY0,this.leftDelta=t.pageX-this.mouseX0,this.isHidden||this.updatePosition()},hide:function(){this.isHidden||(this.isHidden=!0,this.el&&this.el.hide())},show:function(){this.isHidden&&(this.isHidden=!1,this.updatePosition(),this.getEl().show())}},J.prototype={view:null,cellHtml:"
"+s+""},bookendCells:function(t,e,i){var n=this.view,r=this.getHtmlRenderer("intro",e)(i||0),o=this.getHtmlRenderer("outro",e)(i||0),s=n.opt("isRTL"),l=s?o:r,a=s?r:o;return"string"==typeof t?l+t+a:t.prepend(l).append(a)},getHtmlRenderer:function(t,e){var i,n,r,o,s=this.view;return i=t+"Html",e&&(n=e+G(t)+"Html"),n&&(o=s[n])?r=s:n&&(o=this[n])?r=this:(o=s[i])?r=s:(o=this[i])&&(r=this),"function"==typeof o?function(){return o.apply(r,arguments)||""}:function(){return o||""}}},te.prototype=x(J.prototype),t.extend(te.prototype,{el:null,coordMap:null,cellDuration:null,render:function(){this.bindHandlers()},destroy:function(){},buildCoords:function(){},getCellDate:function(){},getCellDayEl:function(){},rangeToSegs:function(){},bindHandlers:function(){var e=this;this.el.on("mousedown",function(i){t(i.target).is(".fc-event-container *, .fc-more")||t(i.target).closest(".fc-popover").length||e.dayMousedown(i)}),this.bindSegHandlers()},dayMousedown:function(t){var e,i,n,r=this,o=this.view,s=o.opt("selectable"),l=null,a=new U(this.coordMap,{scroll:o.opt("dragScroll"),dragStart:function(){o.unselect()},cellOver:function(t,o){a.origDate&&(n=r.getCellDayEl(t),l=[o,a.origDate].sort(H),e=l[0],i=l[1].clone().add(r.cellDuration),s&&r.renderSelection(e,i))},cellOut:function(){l=null,r.destroySelection()},listenStop:function(t){l&&(l[0].isSame(l[1])&&o.trigger("dayClick",n[0],e,t),s&&o.reportSelection(e,i,t))}});a.mousedown(t)},renderDrag:function(){},destroyDrag:function(){},renderResize:function(){},destroyResize:function(){},renderRangeHelper:function(t,e,i){var n,r=this.view;!e&&r.opt("forceEventDuration")&&(e=r.calendar.getDefaultEventEnd(!t.hasTime(),t)),n=i?x(i.event):{},n.start=t,n.end=e,n.allDay=!(t.hasTime()||e&&e.hasTime()),n.className=(n.className||[]).concat("fc-helper"),i||(n.editable=!1),this.renderHelper(n,i)},renderHelper:function(){},destroyHelper:function(){},renderSelection:function(t,e){this.renderHighlight(t,e)},destroySelection:function(){this.destroyHighlight()},renderHighlight:function(){},destroyHighlight:function(){},headHtml:function(){return'
'+"
'),e>t&&(n+=''),i>e&&(n+=''),n=this.bookendCells(n,"highlight"),'
'+n+""+"
"+"
"}}),t.extend(ee.prototype,{segs:null,rowStructs:null,renderEvents:function(e){var i=this.rowStructs=this.renderEventRows(e),n=[];this.rowEls.each(function(e,r){t(r).find(".fc-content-skeleton > table").append(i[e].tbodyEl),n.push.apply(n,i[e].segs)}),this.segs=n},getSegs:function(){return(this.segs||[]).concat(this.popoverSegs||[])},destroyEvents:function(){var t,e;for(J.prototype.destroyEvents.call(this),t=this.rowStructs||[];e=t.pop();)e.tbodyEl.remove();this.segs=null,this.destroySegPopover()},renderEventRows:function(t){var e,i,n=this.eventsToSegs(t),r=[];for(n=this.renderSegs(n),e=this.groupSegRows(n),i=0;e.length>i;i++)r.push(this.renderEventRow(i,e[i]));return r},renderSegHtml:function(t,e){var i,n=this.view,r=n.opt("isRTL"),o=t.event,s=n.isEventDraggable(o),l=!e&&o.allDay&&t.isEnd&&n.isEventResizable(o),a=this.getSegClasses(t,s,l),c=this.getEventSkinCss(o),d="";return a.unshift("fc-day-grid-event"),!o.allDay&&t.isStart&&(d=''+R(n.getEventTimeText(o))+""),i=''+(R(o.title||"")||" ")+"",'"+'
'+(r?i+" "+d:d+" "+i)+"
"+(l?'
':"")+""},renderEventRow:function(e,i){function n(e){for(;e>s;)d=(y[r-1]||[])[s],d?d.attr("rowspan",parseInt(d.attr("rowspan")||1,10)+1):(d=t("
"),l.append(d)),v[r][s]=d,y[r][s]=d,s++}var r,o,s,l,a,c,d,h=this.view,u=h.colCnt,f=this.buildSegLevels(i),g=Math.max(1,f.length),p=t("
').append(c.el),c.leftCol!=c.rightCol?d.attr("colspan",c.rightCol-c.leftCol+1):y[r][s]=d;c.rightCol>=s;)v[r][s]=d,m[r][s]=c,s++;l.append(d)}n(u),this.bookendCells(l,"eventSkeleton"),p.append(l)}return{row:e,tbodyEl:p,cellMatrix:v,segMatrix:m,segLevels:f,segs:i}},buildSegLevels:function(t){var e,i,n,r=[];for(t.sort(te),e=0;t.length>e;e++){for(i=t[e],n=0;r.length>n&&ie(i,r[n]);n++);i.level=n,(r[n]||(r[n]=[])).push(i)}for(n=0;r.length>n;n++)r[n].sort(ne);return r},groupSegRows:function(t){var e,i=this.view,n=[];for(e=0;i.rowCnt>e;e++)n.push([]);for(e=0;t.length>e;e++)n[t[e].row].push(t[e]);return n}}),t.extend(ee.prototype,{segPopover:null,popoverSegs:null,destroySegPopover:function(){this.segPopover&&this.segPopover.hide()},limitRows:function(t){var e,i,n=this.rowStructs||[];for(e=0;n.length>e;e++)this.unlimitRow(e),i=t?"number"==typeof t?t:this.computeRowLevelLimit(e):!1,i!==!1&&this.limitRow(e,i)},computeRowLevelLimit:function(t){var e,i,n=this.rowEls.eq(t),r=n.height(),o=this.rowStructs[t].tbodyEl.children();for(e=0;o.length>e;e++)if(i=o.eq(e).removeClass("fc-limited"),i.position().top+i.outerHeight()>r)return e;return!1},limitRow:function(e,i){function n(n){for(;n>T;)r={row:e,col:T},d=E.getCellSegs(r,i),d.length&&(f=s[i-1][T],w=E.renderMoreLink(r,d),y=t("
").append(w),f.append(y),D.push(y[0])),T++}var r,o,s,l,a,c,d,h,u,f,g,p,m,v,y,w,E=this,b=this.view,S=this.rowStructs[e],D=[],T=0;if(i&&S.segLevels.length>i){for(o=S.segLevels[i-1],s=S.cellMatrix,l=S.tbodyEl.children().slice(i).addClass("fc-limited").get(),a=0;o.length>a;a++){for(c=o[a],n(c.leftCol),u=[],h=0;c.rightCol>=T;)r={row:e,col:T},d=this.getCellSegs(r,i),u.push(d),h+=d.length,T++;if(h){for(f=s[i-1][c.leftCol],g=f.attr("rowspan")||1,p=[],m=0;u.length>m;m++)v=t('
').attr("rowspan",g),d=u[m],r={row:e,col:c.leftCol+m},w=this.renderMoreLink(r,[c].concat(d)),y=t("
").append(w),v.append(y),p.push(v[0]),D.push(v[0]);f.addClass("fc-limited").after(t(p)),l.push(f[0])}}n(b.colCnt),S.moreEls=t(D),S.limitedEls=t(l)}},unlimitRow:function(t){var e=this.rowStructs[t];e.moreEls&&(e.moreEls.remove(),e.moreEls=null),e.limitedEls&&(e.limitedEls.removeClass("fc-limited"),e.limitedEls=null)},renderMoreLink:function(e,i){var n=this,r=this.view;return t('').text(this.getMoreLinkText(i.length)).on("click",function(o){var s=r.opt("eventLimitClick"),l=r.cellToDate(e),a=t(this),c=n.getCellDayEl(e),d=n.getCellSegs(e),h=n.resliceDaySegs(d,l),u=n.resliceDaySegs(i,l);"function"==typeof s&&(s=r.trigger("eventLimitClick",null,{date:l,dayEl:c,moreEl:a,segs:h,hiddenSegs:u},o)),"popover"===s?n.showSegPopover(l,e,a,h):"string"==typeof s&&r.calendar.zoomTo(l,s)})},showSegPopover:function(t,e,i,n){var r,o,s=this,l=this.view,a=i.parent();r=1==l.rowCnt?this.view.el:this.rowEls.eq(e.row),o={className:"fc-more-popover",content:this.renderSegPopoverContent(t,n),parentEl:this.el,top:r.offset().top,autoHide:!0,viewportConstrain:l.opt("popoverViewportConstrain"),hide:function(){s.segPopover.destroy(),s.segPopover=null,s.popoverSegs=null}},l.opt("isRTL")?o.right=a.offset().left+a.outerWidth()+1:o.left=a.offset().left-1,this.segPopover=new j(o),this.segPopover.show()},renderSegPopoverContent:function(e,i){var n,r=this.view,o=r.opt("theme"),s=e.format(r.opt("dayPopoverFormat")),l=t('
'+''+''+R(s)+""+'
'+"
"+'
'+'
'+"
"),a=l.find(".fc-event-container");for(i=this.renderSegs(i,!0),this.popoverSegs=i,n=0;i.length>n;n++)i[n].cellDate=e,a.append(i[n].el);return l},resliceDaySegs:function(e,i){var n=t.map(e,function(t){return t.event}),r=i.clone().stripTime(),o=r.clone().add(1,"days");return this.eventsToSegs(n,r,o)},getMoreLinkText:function(t){var e=this.view,i=e.opt("eventLimitText");return"function"==typeof i?i(t):"+"+t+" "+i},getCellSegs:function(t,e){for(var i,n=this.rowStructs[t.row].segMatrix,r=e||0,o=[];n.length>r;)i=n[r][t.col],i&&o.push(i),r++;return o}}),re.prototype=H(J.prototype),t.extend(re.prototype,{slotDuration:null,snapDuration:null,minTime:null,maxTime:null,dayEls:null,slatEls:null,slatTops:null,highlightEl:null,helperEl:null,render:function(){this.processOptions(),this.el.html(this.renderHtml()),this.dayEls=this.el.find(".fc-day"),this.slatEls=this.el.find(".fc-slats tr"),this.computeSlatTops(),J.prototype.render.call(this)},renderHtml:function(){return'
'+this.rowHtml("slotBg")+"
"+"
"+'
'+""+this.slatRowHtml()+"
"+"
"},slotBgCellHtml:function(t,e,i){return this.bgCellHtml(t,e,i)},slatRowHtml:function(){for(var t,i,n,r=this.view,o=r.calendar,s=r.opt("isRTL"),l="",a=0===this.slotDuration.asMinutes()%15,c=e.duration(+this.minTime);this.maxTime>c;)t=r.start.clone().time(c),i=t.minutes(),n='
"+(a&&i?"":""+R(o.formatDate(t,r.opt("axisFormat")))+"")+"
'+(s?n:"")+"
',d=n.col),r=l.cellToDate(0,d),o=this.computeDateTop(n.start,r),s=this.computeDateTop(n.end,r),c+='
'+"
"+"
'),c=this.bookendCells(c,"highlight"),'
'+c+""+"
"+"
"}}),t.extend(re.prototype,{segs:null,eventSkeletonEl:null,renderEvents:function(e){var i=this.renderEventTable(e);this.eventSkeletonEl=t('
').append(i.tableEl),this.el.append(this.eventSkeletonEl),this.segs=i.segs},getSegs:function(){return this.segs||[]},destroyEvents:function(){J.prototype.destroyEvents.call(this),this.eventSkeletonEl&&(this.eventSkeletonEl.remove(),this.eventSkeletonEl=null),this.segs=null},renderEventTable:function(e){var i,n,r,o,s,l,a=t("
"),c=a.find("tr"),d=this.eventsToSegs(e);for(d=this.renderSegs(d),i=this.groupSegCols(d),this.computeSegVerticals(d),o=0;i.length>o;o++){for(s=i[o],oe(s),l=t('
'),n=0;s.length>n;n++)r=s[n],r.el.css(this.generateSegPositionCss(r)),30>r.bottom-r.top&&r.el.addClass("fc-short"),l.append(r.el);c.append(t("
").append(l))}return this.bookendCells(c,"eventSkeleton"),{tableEl:a,segs:d}},updateSegVerticals:function(){var t,e=this.segs;if(e)for(this.computeSegVerticals(e),t=0;e.length>t;t++)e[t].el.css(this.generateSegVerticalCss(e[t]))},computeSegVerticals:function(t){var e,i;for(e=0;t.length>e;e++)i=t[e],i.top=this.computeDateTop(i.start,i.start),i.bottom=this.computeDateTop(i.end,i.start)},renderSegHtml:function(t,e){var i,n,r,o=this.view,s=t.event,l=o.isEventDraggable(s),a=!e&&t.isEnd&&o.isEventResizable(s),c=this.getSegClasses(t,l,a),d=this.getEventSkinCss(s);return c.unshift("fc-time-grid-event"),o.isMultiDayEvent(s)?(t.isStart||t.isEnd)&&(i=o.getEventTimeText(t.start,t.end),n=o.getEventTimeText(t.start,t.end,"LT"),r=o.getEventTimeText(t.start,null)):(i=o.getEventTimeText(s),n=o.getEventTimeText(s,"LT"),r=o.getEventTimeText(s.start,null)),'"+'
'+(i?'
"+""+R(i)+""+"
":"")+(s.title?'
'+R(s.title)+"
":"")+"
"+'
'+(a?'
':"")+""},generateSegPositionCss:function(t){var e,i,n=this.view,r=n.opt("isRTL"),o=n.opt("slotEventOverlap"),s=t.backwardCoord,l=t.forwardCoord,a=this.generateSegVerticalCss(t);return o&&(l=Math.min(1,s+2*(l-s))),r?(e=1-l,i=s):(e=s,i=1-l),a.zIndex=t.level+1,a.left=100*e+"%",a.right=100*i+"%",o&&t.forwardPressure&&(a[r?"marginLeft":"marginRight"]=20),a},generateSegVerticalCss:function(t){return{top:t.top,bottom:-t.bottom}},groupSegCols:function(t){var e,i=this.view,n=[];for(e=0;i.colCnt>e;e++)n.push([]);for(e=0;t.length>e;e++)n[t[e].col].push(t[e]);return n}}),fe.prototype={calendar:null,coordMap:null,el:null,start:null,end:null,intervalStart:null,intervalEnd:null,rowCnt:null,colCnt:null,isSelected:!1,scrollerEl:null,scrollTop:null,widgetHeaderClass:null,widgetContentClass:null,highlightStateClass:null,documentMousedownProxy:null,documentDragStartProxy:null,init:function(){var e=this.opt("theme")?"ui":"fc";this.widgetHeaderClass=e+"-widget-header",this.widgetContentClass=e+"-widget-content",this.highlightStateClass=e+"-state-highlight",this.documentMousedownProxy=t.proxy(this,"documentMousedown"),this.documentDragStartProxy=t.proxy(this,"documentDragStart")},render:function(){this.updateSize(),this.trigger("viewRender",this,this,this.el),t(document).on("mousedown",this.documentMousedownProxy).on("dragstart",this.documentDragStartProxy)},destroy:function(){this.unselect(),this.trigger("viewDestroy",this,this,this.el),this.destroyEvents(),this.el.empty(),t(document).off("mousedown",this.documentMousedownProxy).off("dragstart",this.documentDragStartProxy)},incrementDate:function(){},updateSize:function(t){t&&this.recordScroll(),this.updateHeight(),this.updateWidth()},updateWidth:function(){},updateHeight:function(){var t=this.calendar;this.setHeight(t.getSuggestedViewHeight(),t.isHeightAuto())},setHeight:function(){},computeScrollerHeight:function(t){var e,i=this.el.add(this.scrollerEl);return i.css({position:"relative",left:-1}),e=this.el.outerHeight()-this.scrollerEl.height(),i.css({position:"",left:""}),t-e},recordScroll:function(){this.scrollerEl&&(this.scrollTop=this.scrollerEl.scrollTop())},restoreScroll:function(){null!==this.scrollTop&&this.scrollerEl.scrollTop(this.scrollTop)},renderEvents:function(){this.segEach(function(t){this.trigger("eventAfterRender",t.event,t.event,t.el)}),this.trigger("eventAfterAllRender")},destroyEvents:function(){this.segEach(function(t){this.trigger("eventDestroy",t.event,t.event,t.el)})},resolveEventEl:function(e,i){var n=this.trigger("eventRender",e,e,i);return n===!1?i=null:n&&n!==!0&&(i=t(n)),i},showEvent:function(t){this.segEach(function(t){t.el.css("visibility","")},t)},hideEvent:function(t){this.segEach(function(t){t.el.css("visibility","hidden")},t)},segEach:function(t,e){var i,n=this.getSegs();for(i=0;n.length>i;i++)e&&n[i].event._id!==e._id||t.call(this,n[i])},getSegs:function(){},renderDrag:function(){},destroyDrag:function(){},documentDragStart:function(e){var i,n=this,r=null;this.opt("droppable")&&(i=new q(this.coordMap,{cellOver:function(t,e){r=e,n.renderDrag(e)},cellOut:function(){r=null,n.destroyDrag()}}),t(document).one("dragstop",function(t,e){n.destroyDrag(),r&&n.trigger("drop",t.target,r,t,e)}),i.startDrag(e))},select:function(t,e,i){this.unselect(i),this.renderSelection(t,e),this.reportSelection(t,e,i)},renderSelection:function(){},reportSelection:function(t,e,i){this.isSelected=!0,this.trigger("select",null,t,e,i)},unselect:function(t){this.isSelected&&(this.isSelected=!1,this.destroySelection(),this.trigger("unselect",null,t))},destroySelection:function(){},documentMousedown:function(e){var i;this.isSelected&&this.opt("unselectAuto")&&E(e)&&(i=this.opt("unselectCancel"),i&&t(e.target).closest(i).length||this.unselect(e))}},ge.prototype=H(fe.prototype),t.extend(ge.prototype,{dayGrid:null,dayNumbersVisible:!1,weekNumbersVisible:!1,weekNumberWidth:null,headRowEl:null,render:function(t,e,i){this.rowCnt=t,this.colCnt=e,this.dayNumbersVisible=i,this.weekNumbersVisible=this.opt("weekNumbers"),this.dayGrid.numbersVisible=this.dayNumbersVisible||this.weekNumbersVisible,this.el.addClass("fc-basic-view").html(this.renderHtml()),this.headRowEl=this.el.find("thead .fc-row"),this.scrollerEl=this.el.find(".fc-day-grid-container"),this.dayGrid.coordMap.containerEl=this.scrollerEl,this.dayGrid.el=this.el.find(".fc-day-grid"),this.dayGrid.render(this.hasRigidRows()),fe.prototype.render.call(this)},destroy:function(){this.dayGrid.destroy(),fe.prototype.destroy.call(this)},renderHtml:function(){return'"+""+""+""+""+'"+""+""+"
'+this.dayGrid.headHtml()+"
'+'
'+'
'+"
"+"
" -},headIntroHtml:function(){return this.weekNumbersVisible?'
"+""+R(this.opt("weekNumberTitle"))+""+""+""+this.calendar.calculateWeekNumber(this.cellToDate(t,0))+""+"'+i.date()+""},weekNumberStyleAttr:function(){return null!==this.weekNumberWidth?'style="width:'+this.weekNumberWidth+'px"':""},hasRigidRows:function(){var t=this.opt("eventLimit");return t&&"number"!=typeof t},updateWidth:function(){this.weekNumbersVisible&&(this.weekNumberWidth=p(this.el.find(".fc-week-number")))},setHeight:function(t,e){var i,n=this.opt("eventLimit");v(this.scrollerEl),u(this.headRowEl),this.dayGrid.destroySegPopover(),n&&"number"==typeof n&&this.dayGrid.limitRows(n),i=this.computeScrollerHeight(t),this.setGridHeight(i,e),n&&"number"!=typeof n&&this.dayGrid.limitRows(n),!e&&m(this.scrollerEl,i)&&(h(this.headRowEl,w(this.scrollerEl)),i=this.computeScrollerHeight(t),this.scrollerEl.height(i),this.restoreScroll())},setGridHeight:function(t,e){e?g(this.dayGrid.rowEls):f(this.dayGrid.rowEls,t,!0)},renderEvents:function(t){this.dayGrid.renderEvents(t),this.updateHeight(),fe.prototype.renderEvents.call(this,t)},getSegs:function(){return this.dayGrid.getSegs()},destroyEvents:function(){fe.prototype.destroyEvents.call(this),this.recordScroll(),this.dayGrid.destroyEvents()},renderDrag:function(t,e,i){return this.dayGrid.renderDrag(t,e,i)},destroyDrag:function(){this.dayGrid.destroyDrag()},renderSelection:function(t,e){this.dayGrid.renderSelection(t,e)},destroySelection:function(){this.dayGrid.destroySelection()}}),r({fixedWeekCount:!0}),xe.month=pe,pe.prototype=H(ge.prototype),t.extend(pe.prototype,{name:"month",incrementDate:function(t,e){return t.clone().stripTime().add(e,"months").startOf("month")},render:function(t){var e;this.intervalStart=t.clone().stripTime().startOf("month"),this.intervalEnd=this.intervalStart.clone().add(1,"months"),this.start=this.intervalStart.clone(),this.start=this.skipHiddenDays(this.start),this.start.startOf("week"),this.start=this.skipHiddenDays(this.start),this.end=this.intervalEnd.clone(),this.end=this.skipHiddenDays(this.end,-1,!0),this.end.add((7-this.end.weekday())%7,"days"),this.end=this.skipHiddenDays(this.end,-1,!0),e=Math.ceil(this.end.diff(this.start,"weeks",!0)),this.isFixedWeeks()&&(this.end.add(6-e,"weeks"),e=6),this.title=this.calendar.formatDate(this.intervalStart,this.opt("titleFormat")),ge.prototype.render.call(this,e,this.getCellsPerWeek(),!0)},setGridHeight:function(t,e){e=e||"variable"===this.opt("weekMode"),e&&(t*=this.rowCnt/6),f(this.dayGrid.rowEls,t,!e)},isFixedWeeks:function(){var t=this.opt("weekMode");return t?"fixed"===t:this.opt("fixedWeekCount")}}),xe.basicWeek=me,me.prototype=H(ge.prototype),t.extend(me.prototype,{name:"basicWeek",incrementDate:function(t,e){return t.clone().stripTime().add(e,"weeks").startOf("week")},render:function(t){this.intervalStart=t.clone().stripTime().startOf("week"),this.intervalEnd=this.intervalStart.clone().add(1,"weeks"),this.start=this.skipHiddenDays(this.intervalStart),this.end=this.skipHiddenDays(this.intervalEnd,-1,!0),this.title=this.calendar.formatRange(this.start,this.end.clone().subtract(1),this.opt("titleFormat")," — "),ge.prototype.render.call(this,1,this.getCellsPerWeek(),!1)}}),xe.basicDay=ve,ve.prototype=H(ge.prototype),t.extend(ve.prototype,{name:"basicDay",incrementDate:function(t,e){var i=t.clone().stripTime().add(e,"days");return i=this.skipHiddenDays(i,0>e?-1:1)},render:function(t){this.start=this.intervalStart=t.clone().stripTime(),this.end=this.intervalEnd=this.start.clone().add(1,"days"),this.title=this.calendar.formatDate(this.start,this.opt("titleFormat")),ge.prototype.render.call(this,1,1,!1)}}),r({allDaySlot:!0,allDayText:"all-day",scrollTime:"06:00:00",slotDuration:"00:30:00",axisFormat:ye,timeFormat:{agenda:we},minTime:"00:00:00",maxTime:"24:00:00",slotEventOverlap:!0});var Ye=5;Ee.prototype=H(fe.prototype),t.extend(Ee.prototype,{timeGrid:null,dayGrid:null,axisWidth:null,noScrollRowEls:null,bottomRuleEl:null,bottomRuleHeight:null,render:function(e){this.rowCnt=1,this.colCnt=e,this.el.addClass("fc-agenda-view").html(this.renderHtml()),this.scrollerEl=this.el.find(".fc-time-grid-container"),this.timeGrid.coordMap.containerEl=this.scrollerEl,this.timeGrid.el=this.el.find(".fc-time-grid"),this.timeGrid.render(),this.bottomRuleEl=t('
').appendTo(this.timeGrid.el),this.dayGrid&&(this.dayGrid.el=this.el.find(".fc-day-grid"),this.dayGrid.render(),this.dayGrid.bottomCoordPadding=this.dayGrid.el.next("hr").outerHeight()),this.noScrollRowEls=this.el.find(".fc-row:not(.fc-scroller *)"),fe.prototype.render.call(this),this.resetScroll()},destroy:function(){this.timeGrid.destroy(),this.dayGrid&&this.dayGrid.destroy(),fe.prototype.destroy.call(this)},renderHtml:function(){return'"+""+""+""+""+'"+""+""+"
'+this.timeGrid.headHtml()+"
'+(this.dayGrid?'

':"")+'
'+'
'+"
"+"
"},headIntroHtml:function(){var t,e,i,n;return this.opt("weekNumbers")?(t=this.cellToDate(0,0),e=this.calendar.calculateWeekNumber(t),i=this.opt("weekNumberTitle"),n=this.opt("isRTL")?e+i:i+e,'
"+""+R(n)+""+""+""+(this.opt("allDayHtml")||R(this.opt("allDayText")))+""+"",rowHtml:function(t,e){var i,n,r=this.view,o=this.getHtmlRenderer("cell",t),s="";for(e=e||0,i=0;r.colCnt>i;i++)n=r.cellToDate(e,i),s+=o(e,i,n);return s=this.bookendCells(s,t,e),"
"+""+this.rowHtml("head")+""+"
"+"
"},headCellHtml:function(t,e,i){var n=this.view,r=n.calendar,o=n.opt("columnFormat");return''+P(r.formatDate(i,o))+""},bgCellHtml:function(t,e,i){var n=this.view,r=this.getDayClasses(i);return r.unshift("fc-day",n.widgetContentClass),''},getDayClasses:function(t){var e=this.view,i=e.calendar.getNow().stripTime(),n=["fc-"+Ne[t.day()]];return"month"===e.name&&t.month()!=e.intervalStart.month()&&n.push("fc-other-month"),t.isSame(i,"day")?n.push("fc-today",e.highlightStateClass):i>t?n.push("fc-past"):n.push("fc-future"),n}}),t.extend(te.prototype,{mousedOverSeg:null,isDraggingSeg:!1,isResizingSeg:!1,renderEvents:function(){},getSegs:function(){},destroyEvents:function(){this.triggerSegMouseout()},renderSegs:function(e,i){var n,r=this.view,o="",s=[];for(n=0;e.length>n;n++)o+=this.renderSegHtml(e[n],i);return t(o).each(function(i,n){var o=e[i],l=r.resolveEventEl(o.event,t(n));l&&(l.data("fc-seg",o),o.el=l,s.push(o))}),s},renderSegHtml:function(){},eventsToSegs:function(e,i,n){var r=this;return t.map(e,function(t){return r.eventToSegs(t,i,n)})},eventToSegs:function(t,e,i){var n,r,o,s=t.start.clone().stripZone(),l=this.view.calendar.getEventEnd(t).stripZone();for(e&&i?(o=S(s,l,e,i),n=o?[o]:[]):n=this.rangeToSegs(s,l),r=0;n.length>r;r++)o=n[r],o.event=t,o.eventStartMS=+s,o.eventDurationMS=l-s;return n},bindSegHandlers:function(){var e=this,i=this.view;t.each({mouseenter:function(t,i){e.triggerSegMouseover(t,i)},mouseleave:function(t,i){e.triggerSegMouseout(t,i)},click:function(t,e){return i.trigger("eventClick",this,t.event,e)},mousedown:function(n,r){t(r.target).is(".fc-resizer")&&i.isEventResizable(n.event)?e.segResizeMousedown(n,r):i.isEventDraggable(n.event)&&e.segDragMousedown(n,r)}},function(i,n){e.el.on(i,".fc-event-container > *",function(i){var r=t(this).data("fc-seg");return!r||e.isDraggingSeg||e.isResizingSeg?void 0:n.call(this,r,i)})})},triggerSegMouseover:function(t,e){this.mousedOverSeg||(this.mousedOverSeg=t,this.view.trigger("eventMouseover",t.el[0],t.event,e))},triggerSegMouseout:function(t,e){e=e||{},this.mousedOverSeg&&(t=t||this.mousedOverSeg,this.mousedOverSeg=null,this.view.trigger("eventMouseout",t.el[0],t.event,e))},segDragMousedown:function(t,e){var i,n,r=this,o=this.view,s=t.el,l=t.event,a=new Q(t.el,{parentEl:o.el,opacity:o.opt("dragOpacity"),revertDuration:o.opt("dragRevertDuration"),zIndex:2}),c=new U(o.coordMap,{distance:5,scroll:o.opt("dragScroll"),listenStart:function(t){a.hide(),a.start(t)},dragStart:function(e){r.triggerSegMouseout(t,e),r.isDraggingSeg=!0,o.hideEvent(l),o.trigger("eventDragStart",s[0],l,e,{})},cellOver:function(e,s){var l=t.cellDate||c.origDate,d=r.computeDraggedEventDates(t,l,s);i=d.start,n=d.end,o.renderDrag(i,n,t)?a.hide():a.show()},cellOut:function(){i=null,o.destroyDrag(),a.show()},dragStop:function(t){var e=i&&!i.isSame(l.start);a.stop(!e,function(){r.isDraggingSeg=!1,o.destroyDrag(),o.showEvent(l),o.trigger("eventDragStop",s[0],l,t,{}),e&&o.eventDrop(s[0],l,i,t)})},listenStop:function(){a.stop()}});c.mousedown(e)},computeDraggedEventDates:function(t,e,i){var n,r,o,s=this.view,l=t.event,a=l.start,c=s.calendar.getEventEnd(l);return i.hasTime()===e.hasTime()?(n=T(i,e),r=a.clone().add(n),o=null===l.end?null:c.clone().add(n)):(r=i,o=null),{start:r,end:o}},segResizeMousedown:function(t,e){function i(){r.destroyResize(),o.showEvent(l)}var n,r=this,o=this.view,s=t.el,l=t.event,a=l.start,c=o.calendar.getEventEnd(l),d=null;n=new U(this.coordMap,{distance:5,scroll:o.opt("dragScroll"),dragStart:function(e){r.triggerSegMouseout(t,e),r.isResizingSeg=!0,o.trigger("eventResizeStart",s[0],l,e,{})},cellOver:function(e,n){n.isBefore(a)&&(n=a),d=n.clone().add(r.cellDuration),d.isSame(c)?(d=null,i()):(r.renderResize(a,d,t),o.hideEvent(l))},cellOut:function(){d=null,i()},dragStop:function(t){r.isResizingSeg=!1,i(),o.trigger("eventResizeStop",s[0],l,t,{}),d&&o.eventResize(s[0],l,d,t)}}),n.mousedown(e)},getSegClasses:function(t,e,i){var n=t.event,r=["fc-event",t.isStart?"fc-start":"fc-not-start",t.isEnd?"fc-end":"fc-not-end"].concat(n.className,n.source?n.source.className:[]);return e&&r.push("fc-draggable"),i&&r.push("fc-resizable"),r},getEventSkinCss:function(t){var e=this.view,i=t.source||{},n=t.color,r=i.color,o=e.opt("eventColor"),s=t.backgroundColor||n||i.backgroundColor||r||e.opt("eventBackgroundColor")||o,l=t.borderColor||n||i.borderColor||r||e.opt("eventBorderColor")||o,a=t.textColor||i.textColor||e.opt("eventTextColor"),c=[];return s&&c.push("background-color:"+s),l&&c.push("border-color:"+l),a&&c.push("color:"+a),c.join(";")}}),ie.prototype=x(te.prototype),t.extend(ie.prototype,{numbersVisible:!1,cellDuration:e.duration({days:1}),bottomCoordPadding:0,rowEls:null,dayEls:null,helperEls:null,highlightEls:null,render:function(e){var i,n=this.view,r="";for(i=0;n.rowCnt>i;i++)r+=this.dayRowHtml(i,e);this.el.html(r),this.rowEls=this.el.find(".fc-row"),this.dayEls=this.el.find(".fc-day"),this.dayEls.each(function(e,i){var r=n.cellToDate(Math.floor(e/n.colCnt),e%n.colCnt);n.trigger("dayRender",null,r,t(i))}),te.prototype.render.call(this)},destroy:function(){this.destroySegPopover()},dayRowHtml:function(t,e){var i=this.view,n=["fc-row","fc-week",i.widgetContentClass];return e&&n.push("fc-rigid"),'
'+'
'+""+this.rowHtml("day",t)+"
"+"
"+'
'+""+(this.numbersVisible?""+this.rowHtml("number",t)+"":"")+"
"+"
"+"
"},dayCellHtml:function(t,e,i){return this.bgCellHtml(t,e,i)},buildCoords:function(e,i){var n,r,o,s=this.view.colCnt;this.dayEls.slice(0,s).each(function(e,s){n=t(s),r=n.offset().left,e&&(o[1]=r),o=[r],i[e]=o}),o[1]=r+n.outerWidth(),this.rowEls.each(function(i,s){n=t(s),r=n.offset().top,i&&(o[1]=r),o=[r],e[i]=o}),o[1]=r+n.outerHeight()+this.bottomCoordPadding},getCellDate:function(t){return this.view.cellToDate(t)},getCellDayEl:function(t){return this.dayEls.eq(t.row*this.view.colCnt+t.col)},rangeToSegs:function(t,e){return this.view.rangeToSegments(t,e)},renderDrag:function(t,e,i){var n;return this.renderHighlight(t,e||this.view.calendar.getDefaultEventEnd(!0,t)),i&&!i.el.closest(this.el).length?(this.renderRangeHelper(t,e,i),n=this.view.opt("dragOpacity"),void 0!==n&&this.helperEls.css("opacity",n),!0):void 0},destroyDrag:function(){this.destroyHighlight(),this.destroyHelper()},renderResize:function(t,e,i){this.renderHighlight(t,e),this.renderRangeHelper(t,e,i)},destroyResize:function(){this.destroyHighlight(),this.destroyHelper()},renderHelper:function(e,i){var n=[],r=this.renderEventRows([e]);this.rowEls.each(function(e,o){var s,l=t(o),a=t('
');s=i&&i.row===e?i.el.position().top:l.find(".fc-content-skeleton tbody").position().top,a.css("top",s).find("table").append(r[e].tbodyEl),l.append(a),n.push(a[0])}),this.helperEls=t(n)},destroyHelper:function(){this.helperEls&&(this.helperEls.remove(),this.helperEls=null)},renderHighlight:function(e,i){var n,r,o,s=this.rangeToSegs(e,i),l=[];for(n=0;s.length>n;n++)r=s[n],o=t(this.highlightSkeletonHtml(r.leftCol,r.rightCol+1)),o.appendTo(this.rowEls[r.row]),l.push(o[0]);this.highlightEls=t(l)},destroyHighlight:function(){this.highlightEls&&(this.highlightEls.remove(),this.highlightEls=null)},highlightSkeletonHtml:function(t,e){var i=this.view.colCnt,n="";return t>0&&(n+='"),m=[],v=[],y=[];for(r=0;p>r;r++){if(o=f[r],s=0,l=t(""),m.push([]),v.push([]),y.push([]),o)for(a=0;o.length>a;a++){for(c=o[a],n(c.leftCol),d=t('",l+=""+(s?"":n)+'",c.add(this.slotDuration);return l},processOptions:function(){var t=this.view,i=t.opt("slotDuration"),n=t.opt("snapDuration");i=e.duration(i),n=n?e.duration(n):i,this.slotDuration=i,this.snapDuration=n,this.cellDuration=n,this.minTime=e.duration(t.opt("minTime")),this.maxTime=e.duration(t.opt("maxTime"))},rangeToSegs:function(t,e){var i,n,r,o,s,l=this.view,a=[];for(t=t.clone().stripZone(),e=e.clone().stripZone(),n=0;l.colCnt>n;n++)r=l.cellToDate(0,n),o=r.clone().time(this.minTime),s=r.clone().time(this.maxTime),i=S(t,e,o,s),i&&(i.col=n,a.push(i));return a},resize:function(){this.computeSlatTops(),this.updateSegVerticals()},buildCoords:function(i,n){var r,o,s=this.view.colCnt,l=this.el.offset().top,a=e.duration(+this.minTime),c=null;for(this.dayEls.slice(0,s).each(function(e,i){r=t(i),o=r.offset().left,c&&(c[1]=o),c=[o],n[e]=c}),c[1]=o+r.outerWidth(),c=null;this.maxTime>a;)o=l+this.computeTimeTop(a),c&&(c[1]=o),c=[o],i.push(c),a.add(this.snapDuration);c[1]=l+this.computeTimeTop(a)},getCellDate:function(t){var e=this.view,i=e.calendar;return i.rezoneDate(e.cellToDate(0,t.col).time(this.minTime+this.snapDuration*t.row))},getCellDayEl:function(t){return this.dayEls.eq(t.col)},computeDateTop:function(t,i){return this.computeTimeTop(e.duration(t.clone().stripZone()-i.clone().stripTime()))},computeTimeTop:function(t){var e,i,n,r,o=(t-this.minTime)/this.slotDuration;return o=Math.max(0,o),o=Math.min(this.slatEls.length,o),e=Math.floor(o),i=o-e,n=this.slatTops[e],i?(r=this.slatTops[e+1],n+(r-n)*i):n},computeSlatTops:function(){var e,i=[];this.slatEls.each(function(n,r){e=t(r).position().top,i.push(e)}),i.push(e+this.slatEls.last().outerHeight()),this.slatTops=i},renderDrag:function(t,e,i){var n;return i?(this.renderRangeHelper(t,e,i),n=this.view.opt("dragOpacity"),void 0!==n&&this.helperEl.css("opacity",n),!0):(this.renderHighlight(t,e||this.view.calendar.getDefaultEventEnd(!1,t)),void 0)},destroyDrag:function(){this.destroyHelper(),this.destroyHighlight()},renderResize:function(t,e,i){this.renderRangeHelper(t,e,i)},destroyResize:function(){this.destroyHelper()},renderHelper:function(e,i){var n,r,o,s=this.renderEventTable([e]),l=s.tableEl,a=s.segs;for(n=0;a.length>n;n++)r=a[n],i&&i.col===r.col&&(o=i.el,r.el.css({left:o.css("left"),right:o.css("right"),"margin-left":o.css("margin-left"),"margin-right":o.css("margin-right")}));this.helperEl=t('
').append(l).appendTo(this.el)},destroyHelper:function(){this.helperEl&&(this.helperEl.remove(),this.helperEl=null)},renderSelection:function(t,e){this.view.opt("selectHelper")?this.renderRangeHelper(t,e):this.renderHighlight(t,e)},destroySelection:function(){this.destroyHelper(),this.destroyHighlight()},renderHighlight:function(e,i){this.highlightEl=t(this.highlightSkeletonHtml(e,i)).appendTo(this.el)},destroyHighlight:function(){this.highlightEl&&(this.highlightEl.remove(),this.highlightEl=null)},highlightSkeletonHtml:function(t,e){var i,n,r,o,s,l=this.view,a=this.rangeToSegs(t,e),c="",d=0;for(i=0;a.length>i;i++)n=a[i],n.col>d&&(c+='
",d++;return l.colCnt>d&&(c+='":void 0},numberIntroHtml:function(t){return this.weekNumbersVisible?'":void 0},dayIntroHtml:function(){return this.weekNumbersVisible?'":void 0},introHtml:function(){return this.weekNumbersVisible?'":void 0},numberCellHtml:function(t,e,i){var n;return this.dayNumbersVisible?(n=this.dayGrid.getDayClasses(i),n.unshift("fc-day-number"),'"):""):'"},dayIntroHtml:function(){return'"},slotBgIntroHtml:function(){return'"},introHtml:function(){return'"},axisStyleAttr:function(){return null!==this.axisWidth?'style="width:'+this.axisWidth+'px"':""},updateSize:function(t){t&&this.timeGrid.resize(),pe.prototype.updateSize.call(this,t)},updateWidth:function(){this.axisWidth=m(this.el.find(".fc-axis"))},setHeight:function(t,e){var i,n;null===this.bottomRuleHeight&&(this.bottomRuleHeight=this.bottomRuleEl.outerHeight()),this.bottomRuleEl.hide(),this.scrollerEl.css("overflow",""),y(this.scrollerEl),f(this.noScrollRowEls),this.dayGrid&&(this.dayGrid.destroySegPopover(),i=this.opt("eventLimit"),i&&"number"!=typeof i&&(i=Fe),i&&this.dayGrid.limitRows(i)),e||(n=this.computeScrollerHeight(t),v(this.scrollerEl,n)?(u(this.noScrollRowEls,E(this.scrollerEl)),n=this.computeScrollerHeight(t),this.scrollerEl.height(n),this.restoreScroll()):(this.scrollerEl.height(n).css("overflow","hidden"),this.bottomRuleEl.show()))},resetScroll:function(){function t(){i.scrollerEl.scrollTop(r)}var i=this,n=e.duration(this.opt("scrollTime")),r=this.timeGrid.computeTimeTop(n);r=Math.ceil(r),r&&r++,t(),setTimeout(t,0)},renderEvents:function(t){var e,i,n=[],r=[],o=[];for(i=0;t.length>i;i++)t[i].allDay?n.push(t[i]):r.push(t[i]);e=this.timeGrid.renderEvents(r),this.dayGrid&&(o=this.dayGrid.renderEvents(n)),this.updateHeight()},getSegs:function(){return this.timeGrid.getSegs().concat(this.dayGrid?this.dayGrid.getSegs():[])},destroyEvents:function(){pe.prototype.destroyEvents.call(this),this.recordScroll(),this.timeGrid.destroyEvents(),this.dayGrid&&this.dayGrid.destroyEvents()},renderDrag:function(t,e,i){return t.hasTime()?this.timeGrid.renderDrag(t,e,i):this.dayGrid?this.dayGrid.renderDrag(t,e,i):void 0},destroyDrag:function(){this.timeGrid.destroyDrag(),this.dayGrid&&this.dayGrid.destroyDrag()},renderSelection:function(t,e){t.hasTime()||e.hasTime()?this.timeGrid.renderSelection(t,e):this.dayGrid&&this.dayGrid.renderSelection(t,e)},destroySelection:function(){this.timeGrid.destroySelection(),this.dayGrid&&this.dayGrid.destroySelection()}}),ze.agendaWeek=Se,Se.prototype=x(De.prototype),t.extend(Se.prototype,{name:"agendaWeek",incrementDate:function(t,e){return t.clone().stripTime().add(e,"weeks").startOf("week")},render:function(t){this.intervalStart=t.clone().stripTime().startOf("week"),this.intervalEnd=this.intervalStart.clone().add(1,"weeks"),this.start=this.skipHiddenDays(this.intervalStart),this.end=this.skipHiddenDays(this.intervalEnd,-1,!0),this.title=this.calendar.formatRange(this.start,this.end.clone().subtract(1),this.opt("titleFormat")," — "),De.prototype.render.call(this,this.getCellsPerWeek())}}),ze.agendaDay=be,be.prototype=x(De.prototype),t.extend(be.prototype,{name:"agendaDay",incrementDate:function(t,e){var i=t.clone().stripTime().add(e,"days");return i=this.skipHiddenDays(i,0>e?-1:1)},render:function(t){this.start=this.intervalStart=t.clone().stripTime(),this.end=this.intervalEnd=this.start.clone().add(1,"days"),this.title=this.calendar.formatDate(this.start,this.opt("titleFormat")),De.prototype.render.call(this,1)}}),Te.prototype=x(ie.prototype),t.extend(Te.prototype,{rangeToSegs:function(t,e,i){var n,r=[],o=this.view.calendar.getDate();if(o>=t&&e>o){var s=this.view,l=s.calendar.fetchResources();for(n=0;s.colCnt>n;n++){var a=l[n];i.indexOf(a.id)>-1&&r.push({row:0,leftCol:n,rightCol:n,isStart:!0,isEnd:!0})}}return r}}),t.extend(Te.prototype,{eventToSegs:function(t,e,i){var n,r,o,s=t.start.clone().stripZone(),l=this.view.calendar.getEventEnd(t).stripZone();for(e&&i?(o=S(s,l,e,i),n=o?[o]:[]):n=this.rangeToSegs(s,l,t.resources),r=0;n.length>r;r++)o=n[r],o.event=t,o.eventStartMS=+s,o.eventDurationMS=l-s;return n}}),Ce.prototype=x(oe.prototype),t.extend(Ce.prototype,{rangeToSegs:function(t,e,i){var n,r,o,s,l,a=this.view,c=[],d=a.calendar.fetchResources();for(t=t.clone().stripZone(),e=e.clone().stripZone(),r=0;a.colCnt>r;r++){var h=d[r];i.indexOf(h.id)>-1&&(o=a.cellToDate(0,r),s=o.clone().time(this.minTime),l=o.clone().time(this.maxTime),n=S(t,e,s,l),n&&(n.col=r,c.push(n)))}return c}}),t.extend(Ce.prototype,{eventToSegs:function(t,e,i){var n,r,o,s=t.start.clone().stripZone(),l=this.view.calendar.getEventEnd(t).stripZone();for(e&&i?(o=S(s,l,e,i),n=o?[o]:[]):n=this.rangeToSegs(s,l,t.resources),r=0;n.length>r;r++)o=n[r],o.event=t,o.eventStartMS=+s,o.eventDurationMS=l-s;return n}}),r({allDaySlot:!0,allDayText:"all-day",scrollTime:"06:00:00",slotDuration:"00:30:00",axisFormat:we,timeFormat:{agenda:Ee},minTime:"00:00:00",maxTime:"24:00:00",slotEventOverlap:!0}),He.prototype=x(De.prototype),t.extend(He.prototype,{cellToDate:function(){return this.start.clone()},headCellHtml:function(t,e){var i=this,n=i.calendar,r=n.fetchResources()[e];return'"}}),ze.resourceDay=xe,xe.prototype=x(He.prototype),t.extend(xe.prototype,{name:"resourceDay",incrementDate:function(t,e){var i=t.clone().stripTime().add(e,"days");return i=this.skipHiddenDays(i,0>e?-1:1)},render:function(t){this.start=this.intervalStart=t.clone().stripTime(),this.end=this.intervalEnd=this.start.clone().add(1,"days"),this.title=this.calendar.formatDate(this.start,this.opt("titleFormat")),He.prototype.render.call(this,this.calendar.fetchResources().length)},getDayClasses:function(t){var e=this.view,i=e.calendar.getNow().stripTime(),n=["fc-"+Ne[t.day()]];return"month"===e.name&&t.month()!=e.intervalStart.month()&&n.push("fc-other-month"),t.isSame(i,"day")?n.push("fc-todaysss",e.highlightStateClass):i>t?n.push("fc-past"):n.push("fc-future"),n}})}); \ No newline at end of file diff --git a/dist/fullcalendar.print.css b/dist/fullcalendar.print.css index dc073b7..c276fe3 100644 --- a/dist/fullcalendar.print.css +++ b/dist/fullcalendar.print.css @@ -1,201 +1,201 @@ -/*! - * FullCalendar v2.1.1 Print Stylesheet - * Docs & License: http://arshaw.com/fullcalendar/ - * (c) 2013 Adam Shaw - */ - -/* - * Include this stylesheet on your page to get a more printer-friendly calendar. - * When including this stylesheet, use the media='print' attribute of the tag. - * Make sure to include this stylesheet IN ADDITION to the regular fullcalendar.css. - */ - -.fc { - max-width: 100% !important; -} - - -/* Global Event Restyling ---------------------------------------------------------------------------------------------------*/ - -.fc-event { - background: #fff !important; - color: #000 !important; - page-break-inside: avoid; -} - -.fc-event .fc-resizer { - display: none; -} - - -/* Table & Day-Row Restyling ---------------------------------------------------------------------------------------------------*/ - -th, -td, -hr, -thead, -tbody, -.fc-row { - border-color: #ccc !important; - background: #fff !important; -} - -/* kill the overlaid, absolutely-positioned common components */ -.fc-bg, -.fc-highlight-skeleton, -.fc-helper-skeleton { - display: none; -} - -/* don't force a min-height on rows (for DayGrid) */ -.fc tbody .fc-row { - height: auto !important; /* undo height that JS set in distributeHeight */ - min-height: 0 !important; /* undo the min-height from each view's specific stylesheet */ -} - -.fc tbody .fc-row .fc-content-skeleton { - position: static; /* undo .fc-rigid */ - padding-bottom: 0 !important; /* use a more border-friendly method for this... */ -} - -.fc tbody .fc-row .fc-content-skeleton tbody tr:last-child td { /* only works in newer browsers */ - padding-bottom: 1em; /* ...gives space within the skeleton. also ensures min height in a way */ -} - -.fc tbody .fc-row .fc-content-skeleton table { - /* provides a min-height for the row, but only effective for IE, which exaggerates this value, - making it look more like 3em. for other browers, it will already be this tall */ - height: 1em; -} - - -/* Undo month-view event limiting. Display all events and hide the "more" links ---------------------------------------------------------------------------------------------------*/ - -.fc-more-cell, -.fc-more { - display: none !important; -} - -.fc tr.fc-limited { - display: table-row !important; -} - -.fc td.fc-limited { - display: table-cell !important; -} - -.fc-popover { - display: none; /* never display the "more.." popover in print mode */ -} - - -/* TimeGrid Restyling ---------------------------------------------------------------------------------------------------*/ - -/* undo the min-height 100% trick used to fill the container's height */ -.fc-time-grid { - min-height: 0 !important; -} - -/* don't display the side axis at all ("all-day" and time cells) */ -.fc-agenda-view .fc-axis { - display: none; -} - -/* don't display the horizontal lines */ -.fc-slats, -.fc-time-grid hr { /* this hr is used when height is underused and needs to be filled */ - display: none !important; /* important overrides inline declaration */ -} - -/* let the container that holds the events be naturally positioned and create real height */ -.fc-time-grid .fc-content-skeleton { - position: static; -} - -/* in case there are no events, we still want some height */ -.fc-time-grid .fc-content-skeleton table { - height: 4em; -} - -/* kill the horizontal spacing made by the event container. event margins will be done below */ -.fc-time-grid .fc-event-container { - margin: 0 !important; -} - - -/* TimeGrid *Event* Restyling ---------------------------------------------------------------------------------------------------*/ - -/* naturally position events, vertically stacking them */ -.fc-time-grid .fc-event { - position: static !important; - margin: 3px 2px !important; -} - -/* for events that continue to a future day, give the bottom border back */ -.fc-time-grid .fc-event.fc-not-end { - border-bottom-width: 1px !important; -} - -/* indicate the event continues via "..." text */ -.fc-time-grid .fc-event.fc-not-end:after { - content: "..."; -} - -/* for events that are continuations from previous days, give the top border back */ -.fc-time-grid .fc-event.fc-not-start { - border-top-width: 1px !important; -} - -/* indicate the event is a continuation via "..." text */ -.fc-time-grid .fc-event.fc-not-start:before { - content: "..."; -} - -/* time */ - -/* undo a previous declaration and let the time text span to a second line */ -.fc-time-grid .fc-event .fc-time { - white-space: normal !important; -} - -/* hide the the time that is normally displayed... */ -.fc-time-grid .fc-event .fc-time span { - display: none; -} - -/* ...replace it with a more verbose version (includes AM/PM) stored in an html attribute */ -.fc-time-grid .fc-event .fc-time:after { - content: attr(data-full); -} - - -/* Vertical Scroller & Containers ---------------------------------------------------------------------------------------------------*/ - -/* kill the scrollbars and allow natural height */ -.fc-scroller, -.fc-day-grid-container, /* these divs might be assigned height, which we need to cleared */ -.fc-time-grid-container { /* */ - overflow: visible !important; - height: auto !important; -} - -/* kill the horizontal border/padding used to compensate for scrollbars */ -.fc-row { - border: 0 !important; - margin: 0 !important; -} - - -/* Button Controls ---------------------------------------------------------------------------------------------------*/ - -.fc-button-group, -.fc button { - display: none; /* don't display any button-related controls */ -} +/*! + * FullCalendar v2.1.1 Print Stylesheet + * Docs & License: http://arshaw.com/fullcalendar/ + * (c) 2013 Adam Shaw + */ + +/* + * Include this stylesheet on your page to get a more printer-friendly calendar. + * When including this stylesheet, use the media='print' attribute of the tag. + * Make sure to include this stylesheet IN ADDITION to the regular fullcalendar.css. + */ + +.fc { + max-width: 100% !important; +} + + +/* Global Event Restyling +--------------------------------------------------------------------------------------------------*/ + +.fc-event { + background: #fff !important; + color: #000 !important; + page-break-inside: avoid; +} + +.fc-event .fc-resizer { + display: none; +} + + +/* Table & Day-Row Restyling +--------------------------------------------------------------------------------------------------*/ + +th, +td, +hr, +thead, +tbody, +.fc-row { + border-color: #ccc !important; + background: #fff !important; +} + +/* kill the overlaid, absolutely-positioned common components */ +.fc-bg, +.fc-highlight-skeleton, +.fc-helper-skeleton { + display: none; +} + +/* don't force a min-height on rows (for DayGrid) */ +.fc tbody .fc-row { + height: auto !important; /* undo height that JS set in distributeHeight */ + min-height: 0 !important; /* undo the min-height from each view's specific stylesheet */ +} + +.fc tbody .fc-row .fc-content-skeleton { + position: static; /* undo .fc-rigid */ + padding-bottom: 0 !important; /* use a more border-friendly method for this... */ +} + +.fc tbody .fc-row .fc-content-skeleton tbody tr:last-child td { /* only works in newer browsers */ + padding-bottom: 1em; /* ...gives space within the skeleton. also ensures min height in a way */ +} + +.fc tbody .fc-row .fc-content-skeleton table { + /* provides a min-height for the row, but only effective for IE, which exaggerates this value, + making it look more like 3em. for other browers, it will already be this tall */ + height: 1em; +} + + +/* Undo month-view event limiting. Display all events and hide the "more" links +--------------------------------------------------------------------------------------------------*/ + +.fc-more-cell, +.fc-more { + display: none !important; +} + +.fc tr.fc-limited { + display: table-row !important; +} + +.fc td.fc-limited { + display: table-cell !important; +} + +.fc-popover { + display: none; /* never display the "more.." popover in print mode */ +} + + +/* TimeGrid Restyling +--------------------------------------------------------------------------------------------------*/ + +/* undo the min-height 100% trick used to fill the container's height */ +.fc-time-grid { + min-height: 0 !important; +} + +/* don't display the side axis at all ("all-day" and time cells) */ +.fc-agenda-view .fc-axis { + display: none; +} + +/* don't display the horizontal lines */ +.fc-slats, +.fc-time-grid hr { /* this hr is used when height is underused and needs to be filled */ + display: none !important; /* important overrides inline declaration */ +} + +/* let the container that holds the events be naturally positioned and create real height */ +.fc-time-grid .fc-content-skeleton { + position: static; +} + +/* in case there are no events, we still want some height */ +.fc-time-grid .fc-content-skeleton table { + height: 4em; +} + +/* kill the horizontal spacing made by the event container. event margins will be done below */ +.fc-time-grid .fc-event-container { + margin: 0 !important; +} + + +/* TimeGrid *Event* Restyling +--------------------------------------------------------------------------------------------------*/ + +/* naturally position events, vertically stacking them */ +.fc-time-grid .fc-event { + position: static !important; + margin: 3px 2px !important; +} + +/* for events that continue to a future day, give the bottom border back */ +.fc-time-grid .fc-event.fc-not-end { + border-bottom-width: 1px !important; +} + +/* indicate the event continues via "..." text */ +.fc-time-grid .fc-event.fc-not-end:after { + content: "..."; +} + +/* for events that are continuations from previous days, give the top border back */ +.fc-time-grid .fc-event.fc-not-start { + border-top-width: 1px !important; +} + +/* indicate the event is a continuation via "..." text */ +.fc-time-grid .fc-event.fc-not-start:before { + content: "..."; +} + +/* time */ + +/* undo a previous declaration and let the time text span to a second line */ +.fc-time-grid .fc-event .fc-time { + white-space: normal !important; +} + +/* hide the the time that is normally displayed... */ +.fc-time-grid .fc-event .fc-time span { + display: none; +} + +/* ...replace it with a more verbose version (includes AM/PM) stored in an html attribute */ +.fc-time-grid .fc-event .fc-time:after { + content: attr(data-full); +} + + +/* Vertical Scroller & Containers +--------------------------------------------------------------------------------------------------*/ + +/* kill the scrollbars and allow natural height */ +.fc-scroller, +.fc-day-grid-container, /* these divs might be assigned height, which we need to cleared */ +.fc-time-grid-container { /* */ + overflow: visible !important; + height: auto !important; +} + +/* kill the horizontal border/padding used to compensate for scrollbars */ +.fc-row { + border: 0 !important; + margin: 0 !important; +} + + +/* Button Controls +--------------------------------------------------------------------------------------------------*/ + +.fc-button-group, +.fc button { + display: none; /* don't display any button-related controls */ +} diff --git a/dist/gcal.js b/dist/gcal.js index 754fb72..c7f3b3b 100644 --- a/dist/gcal.js +++ b/dist/gcal.js @@ -1,100 +1,100 @@ -/*! - * FullCalendar v2.1.1 Google Calendar Plugin - * Docs & License: http://arshaw.com/fullcalendar/ - * (c) 2013 Adam Shaw - */ - -(function(factory) { - if (typeof define === 'function' && define.amd) { - define([ 'jquery' ], factory); - } - else { - factory(jQuery); - } -})(function($) { - - -var fc = $.fullCalendar; -var applyAll = fc.applyAll; - - -fc.sourceNormalizers.push(function(sourceOptions) { - if (sourceOptions.dataType == 'gcal' || - sourceOptions.dataType === undefined && - (sourceOptions.url || '').match(/^(http|https):\/\/www.google.com\/calendar\/feeds\//)) { - sourceOptions.dataType = 'gcal'; - if (sourceOptions.editable === undefined) { - sourceOptions.editable = false; - } - } -}); - - -fc.sourceFetchers.push(function(sourceOptions, start, end, timezone) { - if (sourceOptions.dataType == 'gcal') { - return transformOptions(sourceOptions, start, end, timezone); - } -}); - - -function transformOptions(sourceOptions, start, end, timezone) { - - var success = sourceOptions.success; - var data = $.extend({}, sourceOptions.data || {}, { - singleevents: true, - 'max-results': 9999 - }); - - return $.extend({}, sourceOptions, { - url: sourceOptions.url.replace(/\/basic$/, '/full') + '?alt=json-in-script&callback=?', - dataType: 'jsonp', - data: data, - timezoneParam: 'ctz', - startParam: 'start-min', - endParam: 'start-max', - success: function(data) { - var events = []; - if (data.feed.entry) { - $.each(data.feed.entry, function(i, entry) { - - var url; - $.each(entry.link, function(i, link) { - if (link.type == 'text/html') { - url = link.href; - if (timezone && timezone != 'local') { - url += (url.indexOf('?') == -1 ? '?' : '&') + 'ctz=' + encodeURIComponent(timezone); - } - } - }); - - events.push({ - id: entry.gCal$uid.value, - title: entry.title.$t, - start: entry.gd$when[0].startTime, - end: entry.gd$when[0].endTime, - url: url, - location: entry.gd$where[0].valueString, - description: entry.content.$t - }); - - }); - } - var args = [events].concat(Array.prototype.slice.call(arguments, 1)); - var res = applyAll(success, this, args); - if ($.isArray(res)) { - return res; - } - return events; - } - }); - -} - - -// legacy -fc.gcalFeed = function(url, sourceOptions) { - return $.extend({}, sourceOptions, { url: url, dataType: 'gcal' }); -}; - - -}); +/*! + * FullCalendar v2.1.1 Google Calendar Plugin + * Docs & License: http://arshaw.com/fullcalendar/ + * (c) 2013 Adam Shaw + */ + +(function(factory) { + if (typeof define === 'function' && define.amd) { + define([ 'jquery' ], factory); + } + else { + factory(jQuery); + } +})(function($) { + + +var fc = $.fullCalendar; +var applyAll = fc.applyAll; + + +fc.sourceNormalizers.push(function(sourceOptions) { + if (sourceOptions.dataType == 'gcal' || + sourceOptions.dataType === undefined && + (sourceOptions.url || '').match(/^(http|https):\/\/www.google.com\/calendar\/feeds\//)) { + sourceOptions.dataType = 'gcal'; + if (sourceOptions.editable === undefined) { + sourceOptions.editable = false; + } + } +}); + + +fc.sourceFetchers.push(function(sourceOptions, start, end, timezone) { + if (sourceOptions.dataType == 'gcal') { + return transformOptions(sourceOptions, start, end, timezone); + } +}); + + +function transformOptions(sourceOptions, start, end, timezone) { + + var success = sourceOptions.success; + var data = $.extend({}, sourceOptions.data || {}, { + singleevents: true, + 'max-results': 9999 + }); + + return $.extend({}, sourceOptions, { + url: sourceOptions.url.replace(/\/basic$/, '/full') + '?alt=json-in-script&callback=?', + dataType: 'jsonp', + data: data, + timezoneParam: 'ctz', + startParam: 'start-min', + endParam: 'start-max', + success: function(data) { + var events = []; + if (data.feed.entry) { + $.each(data.feed.entry, function(i, entry) { + + var url; + $.each(entry.link, function(i, link) { + if (link.type == 'text/html') { + url = link.href; + if (timezone && timezone != 'local') { + url += (url.indexOf('?') == -1 ? '?' : '&') + 'ctz=' + encodeURIComponent(timezone); + } + } + }); + + events.push({ + id: entry.gCal$uid.value, + title: entry.title.$t, + start: entry.gd$when[0].startTime, + end: entry.gd$when[0].endTime, + url: url, + location: entry.gd$where[0].valueString, + description: entry.content.$t + }); + + }); + } + var args = [events].concat(Array.prototype.slice.call(arguments, 1)); + var res = applyAll(success, this, args); + if ($.isArray(res)) { + return res; + } + return events; + } + }); + +} + + +// legacy +fc.gcalFeed = function(url, sourceOptions) { + return $.extend({}, sourceOptions, { url: url, dataType: 'gcal' }); +}; + + +}); diff --git a/dist/lang/is.js b/dist/lang/is.js index 7bbdebf..4601695 100644 --- a/dist/lang/is.js +++ b/dist/lang/is.js @@ -1 +1 @@ -(function(e){"function"==typeof define&&define.amd?define(["jquery","moment"],e):e(jQuery,moment)})(function(e,t){function n(e){return 11===e%100?!0:1===e%10?!1:!0}function i(e,t,i,r){var a=e+" ";switch(i){case"s":return t||r?"nokkrar sekúndur":"nokkrum sekúndum";case"m":return t?"mínúta":"mínútu";case"mm":return n(e)?a+(t||r?"mínútur":"mínútum"):t?a+"mínúta":a+"mínútu";case"hh":return n(e)?a+(t||r?"klukkustundir":"klukkustundum"):a+"klukkustund";case"d":return t?"dagur":r?"dag":"degi";case"dd":return n(e)?t?a+"dagar":a+(r?"daga":"dögum"):t?a+"dagur":a+(r?"dag":"degi");case"M":return t?"mánuður":r?"mánuð":"mánuði";case"MM":return n(e)?t?a+"mánuðir":a+(r?"mánuði":"mánuðum"):t?a+"mánuður":a+(r?"mánuð":"mánuði");case"y":return t||r?"ár":"ári";case"yy":return n(e)?a+(t||r?"ár":"árum"):a+(t||r?"ár":"ári")}}(t.defineLocale||t.lang).call(t,"is",{months:"janúar_febrúar_mars_apríl_maí_júní_júlí_ágúst_september_október_nóvember_desember".split("_"),monthsShort:"jan_feb_mar_apr_maí_jún_júl_ágú_sep_okt_nóv_des".split("_"),weekdays:"sunnudagur_mánudagur_þriðjudagur_miðvikudagur_fimmtudagur_föstudagur_laugardagur".split("_"),weekdaysShort:"sun_mán_þri_mið_fim_fös_lau".split("_"),weekdaysMin:"Su_Má_Þr_Mi_Fi_Fö_La".split("_"),longDateFormat:{LT:"H:mm",L:"DD/MM/YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] LT",LLLL:"dddd, D. MMMM YYYY [kl.] LT"},calendar:{sameDay:"[í dag kl.] LT",nextDay:"[á morgun kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[í gær kl.] LT",lastWeek:"[síðasta] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"eftir %s",past:"fyrir %s síðan",s:i,m:i,mm:i,h:"klukkustund",hh:i,d:i,dd:i,M:i,MM:i,y:i,yy:i},ordinal:"%d.",week:{dow:1,doy:4}}),e.fullCalendar.datepickerLang("is","is",{closeText:"Loka",prevText:"< Fyrri",nextText:"Næsti >",currentText:"Í dag",monthNames:["Janúar","Febrúar","Mars","Apríl","Maí","Júní","Júlí","Ágúst","September","Október","Nóvember","Desember"],monthNamesShort:["Jan","Feb","Mar","Apr","Maí","Jún","Júl","Ágú","Sep","Okt","Nóv","Des"],dayNames:["Sunnudagur","Mánudagur","Þriðjudagur","Miðvikudagur","Fimmtudagur","Föstudagur","Laugardagur"],dayNamesShort:["Sun","Mán","Þri","Mið","Fim","Fös","Lau"],dayNamesMin:["Su","Má","Þr","Mi","Fi","Fö","La"],weekHeader:"Vika",dateFormat:"dd.mm.yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.lang("is",{defaultButtonText:{month:"Mánuður",week:"Vika",day:"Dagur",list:"Dagskrá"},allDayHtml:"Allan
daginn",eventLimitText:"meira"})}); \ No newline at end of file +(function(e){"function"==typeof define&&define.amd?define(["jquery","moment"],e):e(jQuery,moment)})(function(e,t){function n(e){return 11===e%100?!0:1===e%10?!1:!0}function r(e,t,r,i){var a=e+" ";switch(r){case"s":return t||i?"nokkrar sekúndur":"nokkrum sekúndum";case"m":return t?"mínúta":"mínútu";case"mm":return n(e)?a+(t||i?"mínútur":"mínútum"):t?a+"mínúta":a+"mínútu";case"hh":return n(e)?a+(t||i?"klukkustundir":"klukkustundum"):a+"klukkustund";case"d":return t?"dagur":i?"dag":"degi";case"dd":return n(e)?t?a+"dagar":a+(i?"daga":"dögum"):t?a+"dagur":a+(i?"dag":"degi");case"M":return t?"mánuður":i?"mánuð":"mánuði";case"MM":return n(e)?t?a+"mánuðir":a+(i?"mánuði":"mánuðum"):t?a+"mánuður":a+(i?"mánuð":"mánuði");case"y":return t||i?"ár":"ári";case"yy":return n(e)?a+(t||i?"ár":"árum"):a+(t||i?"ár":"ári")}}(t.defineLocale||t.lang).call(t,"is",{months:"janúar_febrúar_mars_apríl_maí_júní_júlí_ágúst_september_október_nóvember_desember".split("_"),monthsShort:"jan_feb_mar_apr_maí_jún_júl_ágú_sep_okt_nóv_des".split("_"),weekdays:"sunnudagur_mánudagur_þriðjudagur_miðvikudagur_fimmtudagur_föstudagur_laugardagur".split("_"),weekdaysShort:"sun_mán_þri_mið_fim_fös_lau".split("_"),weekdaysMin:"Su_Má_Þr_Mi_Fi_Fö_La".split("_"),longDateFormat:{LT:"H:mm",L:"DD/MM/YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] LT",LLLL:"dddd, D. MMMM YYYY [kl.] LT"},calendar:{sameDay:"[í dag kl.] LT",nextDay:"[á morgun kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[í gær kl.] LT",lastWeek:"[síðasta] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"eftir %s",past:"fyrir %s síðan",s:r,m:r,mm:r,h:"klukkustund",hh:r,d:r,dd:r,M:r,MM:r,y:r,yy:r},ordinal:"%d.",week:{dow:1,doy:4}}),e.fullCalendar.datepickerLang("is","is",{closeText:"Loka",prevText:"< Fyrri",nextText:"Næsti >",currentText:"Í dag",monthNames:["Janúar","Febrúar","Mars","Apríl","Maí","Júní","Júlí","Ágúst","September","Október","Nóvember","Desember"],monthNamesShort:["Jan","Feb","Mar","Apr","Maí","Jún","Júl","Ágú","Sep","Okt","Nóv","Des"],dayNames:["Sunnudagur","Mánudagur","Þriðjudagur","Miðvikudagur","Fimmtudagur","Föstudagur","Laugardagur"],dayNamesShort:["Sun","Mán","Þri","Mið","Fim","Fös","Lau"],dayNamesMin:["Su","Má","Þr","Mi","Fi","Fö","La"],weekHeader:"Vika",dateFormat:"dd.mm.yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.lang("is",{defaultButtonText:{month:"Mánuður",week:"Vika",day:"Dagur",list:"Dagskrá"},allDayHtml:"Allan
daginn",eventLimitText:"meira"})}); \ No newline at end of file
'),e>t&&(n+=''),i>e&&(n+=''),n=this.bookendCells(n,"highlight"),'
'+n+""+"
"+"
"}}),t.extend(ie.prototype,{segs:null,rowStructs:null,renderEvents:function(e){var i=this.rowStructs=this.renderEventRows(e),n=[];this.rowEls.each(function(e,r){t(r).find(".fc-content-skeleton > table").append(i[e].tbodyEl),n.push.apply(n,i[e].segs)}),this.segs=n},getSegs:function(){return(this.segs||[]).concat(this.popoverSegs||[])},destroyEvents:function(){var t,e;for(te.prototype.destroyEvents.call(this),t=this.rowStructs||[];e=t.pop();)e.tbodyEl.remove();this.segs=null,this.destroySegPopover()},renderEventRows:function(t){var e,i,n=this.eventsToSegs(t),r=[];for(n=this.renderSegs(n),e=this.groupSegRows(n),i=0;e.length>i;i++)r.push(this.renderEventRow(i,e[i]));return r},renderSegHtml:function(t,e){var i,n=this.view,r=n.opt("isRTL"),o=t.event,s=n.isEventDraggable(o),l=!e&&o.allDay&&t.isEnd&&n.isEventResizable(o),a=this.getSegClasses(t,s,l),c=this.getEventSkinCss(o),d="";return a.unshift("fc-day-grid-event"),!o.allDay&&t.isStart&&(d=''+P(n.getEventTimeText(o))+""),i=''+(P(o.title||"")||" ")+"",'"+'
'+(r?i+" "+d:d+" "+i)+"
"+(l?'
':"")+""},renderEventRow:function(e,i){function n(e){for(;e>s;)d=(y[r-1]||[])[s],d?d.attr("rowspan",parseInt(d.attr("rowspan")||1,10)+1):(d=t("
"),l.append(d)),v[r][s]=d,y[r][s]=d,s++}var r,o,s,l,a,c,d,h=this.view,u=h.colCnt,f=this.buildSegLevels(i),p=Math.max(1,f.length),g=t("
').append(c.el),c.leftCol!=c.rightCol?d.attr("colspan",c.rightCol-c.leftCol+1):y[r][s]=d;c.rightCol>=s;)v[r][s]=d,m[r][s]=c,s++;l.append(d)}n(u),this.bookendCells(l,"eventSkeleton"),g.append(l)}return{row:e,tbodyEl:g,cellMatrix:v,segMatrix:m,segLevels:f,segs:i}},buildSegLevels:function(t){var e,i,n,r=[];for(t.sort(ee),e=0;t.length>e;e++){for(i=t[e],n=0;r.length>n&&ne(i,r[n]);n++);i.level=n,(r[n]||(r[n]=[])).push(i)}for(n=0;r.length>n;n++)r[n].sort(re);return r},groupSegRows:function(t){var e,i=this.view,n=[];for(e=0;i.rowCnt>e;e++)n.push([]);for(e=0;t.length>e;e++)n[t[e].row].push(t[e]);return n}}),t.extend(ie.prototype,{segPopover:null,popoverSegs:null,destroySegPopover:function(){this.segPopover&&this.segPopover.hide()},limitRows:function(t){var e,i,n=this.rowStructs||[];for(e=0;n.length>e;e++)this.unlimitRow(e),i=t?"number"==typeof t?t:this.computeRowLevelLimit(e):!1,i!==!1&&this.limitRow(e,i)},computeRowLevelLimit:function(t){var e,i,n=this.rowEls.eq(t),r=n.height(),o=this.rowStructs[t].tbodyEl.children();for(e=0;o.length>e;e++)if(i=o.eq(e).removeClass("fc-limited"),i.position().top+i.outerHeight()>r)return e;return!1},limitRow:function(e,i){function n(n){for(;n>T;)r={row:e,col:T},d=E.getCellSegs(r,i),d.length&&(f=s[i-1][T],w=E.renderMoreLink(r,d),y=t("
").append(w),f.append(y),b.push(y[0])),T++}var r,o,s,l,a,c,d,h,u,f,p,g,m,v,y,w,E=this,D=this.view,S=this.rowStructs[e],b=[],T=0;if(i&&S.segLevels.length>i){for(o=S.segLevels[i-1],s=S.cellMatrix,l=S.tbodyEl.children().slice(i).addClass("fc-limited").get(),a=0;o.length>a;a++){for(c=o[a],n(c.leftCol),u=[],h=0;c.rightCol>=T;)r={row:e,col:T},d=this.getCellSegs(r,i),u.push(d),h+=d.length,T++;if(h){for(f=s[i-1][c.leftCol],p=f.attr("rowspan")||1,g=[],m=0;u.length>m;m++)v=t('
').attr("rowspan",p),d=u[m],r={row:e,col:c.leftCol+m},w=this.renderMoreLink(r,[c].concat(d)),y=t("
").append(w),v.append(y),g.push(v[0]),b.push(v[0]);f.addClass("fc-limited").after(t(g)),l.push(f[0])}}n(D.colCnt),S.moreEls=t(b),S.limitedEls=t(l)}},unlimitRow:function(t){var e=this.rowStructs[t];e.moreEls&&(e.moreEls.remove(),e.moreEls=null),e.limitedEls&&(e.limitedEls.removeClass("fc-limited"),e.limitedEls=null)},renderMoreLink:function(e,i){var n=this,r=this.view;return t('').text(this.getMoreLinkText(i.length)).on("click",function(o){var s=r.opt("eventLimitClick"),l=r.cellToDate(e),a=t(this),c=n.getCellDayEl(e),d=n.getCellSegs(e),h=n.resliceDaySegs(d,l),u=n.resliceDaySegs(i,l);"function"==typeof s&&(s=r.trigger("eventLimitClick",null,{date:l,dayEl:c,moreEl:a,segs:h,hiddenSegs:u},o)),"popover"===s?n.showSegPopover(l,e,a,h):"string"==typeof s&&r.calendar.zoomTo(l,s)})},showSegPopover:function(t,e,i,n){var r,o,s=this,l=this.view,a=i.parent();r=1==l.rowCnt?this.view.el:this.rowEls.eq(e.row),o={className:"fc-more-popover",content:this.renderSegPopoverContent(t,n),parentEl:this.el,top:r.offset().top,autoHide:!0,viewportConstrain:l.opt("popoverViewportConstrain"),hide:function(){s.segPopover.destroy(),s.segPopover=null,s.popoverSegs=null}},l.opt("isRTL")?o.right=a.offset().left+a.outerWidth()+1:o.left=a.offset().left-1,this.segPopover=new X(o),this.segPopover.show()},renderSegPopoverContent:function(e,i){var n,r=this.view,o=r.opt("theme"),s=e.format(r.opt("dayPopoverFormat")),l=t('
'+''+''+P(s)+""+'
'+"
"+'
'+'
'+"
"),a=l.find(".fc-event-container");for(i=this.renderSegs(i,!0),this.popoverSegs=i,n=0;i.length>n;n++)i[n].cellDate=e,a.append(i[n].el);return l},resliceDaySegs:function(e,i){var n=t.map(e,function(t){return t.event}),r=i.clone().stripTime(),o=r.clone().add(1,"days");return this.eventsToSegs(n,r,o)},getMoreLinkText:function(t){var e=this.view,i=e.opt("eventLimitText");return"function"==typeof i?i(t):"+"+t+" "+i},getCellSegs:function(t,e){for(var i,n=this.rowStructs[t.row].segMatrix,r=e||0,o=[];n.length>r;)i=n[r][t.col],i&&o.push(i),r++;return o}}),oe.prototype=x(te.prototype),t.extend(oe.prototype,{slotDuration:null,snapDuration:null,minTime:null,maxTime:null,dayEls:null,slatEls:null,slatTops:null,highlightEl:null,helperEl:null,render:function(){this.processOptions(),this.el.html(this.renderHtml()),this.dayEls=this.el.find(".fc-day"),this.slatEls=this.el.find(".fc-slats tr"),this.computeSlatTops(),te.prototype.render.call(this)},renderHtml:function(){return'
'+this.rowHtml("slotBg")+"
"+"
"+'
'+""+this.slatRowHtml()+"
"+"
"},slotBgCellHtml:function(t,e,i){return this.bgCellHtml(t,e,i)},slatRowHtml:function(){for(var t,i,n,r=this.view,o=r.calendar,s=r.opt("isRTL"),l="",a=0===this.slotDuration.asMinutes()%15,c=e.duration(+this.minTime);this.maxTime>c;)t=r.start.clone().time(c),i=t.minutes(),n='
"+(a&&i?"":""+P(o.formatDate(t,r.opt("axisFormat")))+"")+"
'+(s?n:"")+"
',d=n.col),r=l.cellToDate(0,d),o=this.computeDateTop(n.start,r),s=this.computeDateTop(n.end,r),c+='
'+"
"+"
'),c=this.bookendCells(c,"highlight"),'
'+c+""+"
"+"
"}}),t.extend(oe.prototype,{segs:null,eventSkeletonEl:null,renderEvents:function(e){var i=this.renderEventTable(e);this.eventSkeletonEl=t('
').append(i.tableEl),this.el.append(this.eventSkeletonEl),this.segs=i.segs},getSegs:function(){return this.segs||[]},destroyEvents:function(){te.prototype.destroyEvents.call(this),this.eventSkeletonEl&&(this.eventSkeletonEl.remove(),this.eventSkeletonEl=null),this.segs=null},renderEventTable:function(e){var i,n,r,o,s,l,a=t("
"),c=a.find("tr"),d=this.eventsToSegs(e);for(d=this.renderSegs(d),i=this.groupSegCols(d),this.computeSegVerticals(d),o=0;i.length>o;o++){for(s=i[o],se(s),l=t('
'),n=0;s.length>n;n++)r=s[n],r.el.css(this.generateSegPositionCss(r)),30>r.bottom-r.top&&r.el.addClass("fc-short"),l.append(r.el);c.append(t("
").append(l))}return this.bookendCells(c,"eventSkeleton"),{tableEl:a,segs:d}},updateSegVerticals:function(){var t,e=this.segs;if(e)for(this.computeSegVerticals(e),t=0;e.length>t;t++)e[t].el.css(this.generateSegVerticalCss(e[t]))},computeSegVerticals:function(t){var e,i;for(e=0;t.length>e;e++)i=t[e],i.top=this.computeDateTop(i.start,i.start),i.bottom=this.computeDateTop(i.end,i.start)},renderSegHtml:function(t,e){var i,n,r,o=this.view,s=t.event,l=o.isEventDraggable(s),a=!e&&t.isEnd&&o.isEventResizable(s),c=this.getSegClasses(t,l,a),d=this.getEventSkinCss(s);return c.unshift("fc-time-grid-event"),o.isMultiDayEvent(s)?(t.isStart||t.isEnd)&&(i=o.getEventTimeText(t.start,t.end),n=o.getEventTimeText(t.start,t.end,"LT"),r=o.getEventTimeText(t.start,null)):(i=o.getEventTimeText(s),n=o.getEventTimeText(s,"LT"),r=o.getEventTimeText(s.start,null)),'"+'
'+(i?'
"+""+P(i)+""+"
":"")+(s.title?'
'+P(s.title)+"
":"")+"
"+'
'+(a?'
':"")+""},generateSegPositionCss:function(t){var e,i,n=this.view,r=n.opt("isRTL"),o=n.opt("slotEventOverlap"),s=t.backwardCoord,l=t.forwardCoord,a=this.generateSegVerticalCss(t);return o&&(l=Math.min(1,s+2*(l-s))),r?(e=1-l,i=s):(e=s,i=1-l),a.zIndex=t.level+1,a.left=100*e+"%",a.right=100*i+"%",o&&t.forwardPressure&&(a[r?"marginLeft":"marginRight"]=20),a},generateSegVerticalCss:function(t){return{top:t.top,bottom:-t.bottom}},groupSegCols:function(t){var e,i=this.view,n=[];for(e=0;i.colCnt>e;e++)n.push([]);for(e=0;t.length>e;e++)n[t[e].col].push(t[e]);return n}}),pe.prototype={calendar:null,coordMap:null,el:null,start:null,end:null,intervalStart:null,intervalEnd:null,rowCnt:null,colCnt:null,isSelected:!1,scrollerEl:null,scrollTop:null,widgetHeaderClass:null,widgetContentClass:null,highlightStateClass:null,documentMousedownProxy:null,documentDragStartProxy:null,init:function(){var e=this.opt("theme")?"ui":"fc";this.widgetHeaderClass=e+"-widget-header",this.widgetContentClass=e+"-widget-content",this.highlightStateClass=e+"-state-highlight",this.documentMousedownProxy=t.proxy(this,"documentMousedown"),this.documentDragStartProxy=t.proxy(this,"documentDragStart")},render:function(){this.updateSize(),this.trigger("viewRender",this,this,this.el),t(document).on("mousedown",this.documentMousedownProxy).on("dragstart",this.documentDragStartProxy)},destroy:function(){this.unselect(),this.trigger("viewDestroy",this,this,this.el),this.destroyEvents(),this.el.empty(),t(document).off("mousedown",this.documentMousedownProxy).off("dragstart",this.documentDragStartProxy)},incrementDate:function(){},updateSize:function(t){t&&this.recordScroll(),this.updateHeight(),this.updateWidth()},updateWidth:function(){},updateHeight:function(){var t=this.calendar;this.setHeight(t.getSuggestedViewHeight(),t.isHeightAuto())},setHeight:function(){},computeScrollerHeight:function(t){var e,i=this.el.add(this.scrollerEl);return i.css({position:"relative",left:-1}),e=this.el.outerHeight()-this.scrollerEl.height(),i.css({position:"",left:""}),t-e},recordScroll:function(){this.scrollerEl&&(this.scrollTop=this.scrollerEl.scrollTop()) +},restoreScroll:function(){null!==this.scrollTop&&this.scrollerEl.scrollTop(this.scrollTop)},renderEvents:function(){this.segEach(function(t){this.trigger("eventAfterRender",t.event,t.event,t.el)}),this.trigger("eventAfterAllRender")},destroyEvents:function(){this.segEach(function(t){this.trigger("eventDestroy",t.event,t.event,t.el)})},resolveEventEl:function(e,i){var n=this.trigger("eventRender",e,e,i);return n===!1?i=null:n&&n!==!0&&(i=t(n)),i},showEvent:function(t){this.segEach(function(t){t.el.css("visibility","")},t)},hideEvent:function(t){this.segEach(function(t){t.el.css("visibility","hidden")},t)},segEach:function(t,e){var i,n=this.getSegs();for(i=0;n.length>i;i++)e&&n[i].event._id!==e._id||t.call(this,n[i])},getSegs:function(){},renderDrag:function(){},destroyDrag:function(){},documentDragStart:function(e){var i,n=this,r=null;this.opt("droppable")&&(i=new U(this.coordMap,{cellOver:function(t,e){r=e,n.renderDrag(e)},cellOut:function(){r=null,n.destroyDrag()}}),t(document).one("dragstop",function(t,e){n.destroyDrag(),r&&n.trigger("drop",t.target,r,t,e)}),i.startDrag(e))},select:function(t,e,i){this.unselect(i),this.renderSelection(t,e),this.reportSelection(t,e,i)},renderSelection:function(){},reportSelection:function(t,e,i){this.isSelected=!0,this.trigger("select",null,t,e,i)},unselect:function(t){this.isSelected&&(this.isSelected=!1,this.destroySelection(),this.trigger("unselect",null,t))},destroySelection:function(){},documentMousedown:function(e){var i;this.isSelected&&this.opt("unselectAuto")&&D(e)&&(i=this.opt("unselectCancel"),i&&t(e.target).closest(i).length||this.unselect(e))}},ge.prototype=x(pe.prototype),t.extend(ge.prototype,{dayGrid:null,dayNumbersVisible:!1,weekNumbersVisible:!1,weekNumberWidth:null,headRowEl:null,render:function(t,e,i){this.rowCnt=t,this.colCnt=e,this.dayNumbersVisible=i,this.weekNumbersVisible=this.opt("weekNumbers"),this.dayGrid.numbersVisible=this.dayNumbersVisible||this.weekNumbersVisible,this.el.addClass("fc-basic-view").html(this.renderHtml()),this.headRowEl=this.el.find("thead .fc-row"),this.scrollerEl=this.el.find(".fc-day-grid-container"),this.dayGrid.coordMap.containerEl=this.scrollerEl,this.dayGrid.el=this.el.find(".fc-day-grid"),this.dayGrid.render(this.hasRigidRows()),pe.prototype.render.call(this)},destroy:function(){this.dayGrid.destroy(),pe.prototype.destroy.call(this)},renderHtml:function(){return'"+""+""+""+""+'"+""+""+"
'+this.dayGrid.headHtml()+"
'+'
'+'
'+"
"+"
"},headIntroHtml:function(){return this.weekNumbersVisible?'
"+""+P(this.opt("weekNumberTitle"))+""+""+""+this.calendar.calculateWeekNumber(this.cellToDate(t,0))+""+"'+i.date()+""},weekNumberStyleAttr:function(){return null!==this.weekNumberWidth?'style="width:'+this.weekNumberWidth+'px"':""},hasRigidRows:function(){var t=this.opt("eventLimit");return t&&"number"!=typeof t},updateWidth:function(){this.weekNumbersVisible&&(this.weekNumberWidth=m(this.el.find(".fc-week-number")))},setHeight:function(t,e){var i,n=this.opt("eventLimit");y(this.scrollerEl),f(this.headRowEl),this.dayGrid.destroySegPopover(),n&&"number"==typeof n&&this.dayGrid.limitRows(n),i=this.computeScrollerHeight(t),this.setGridHeight(i,e),n&&"number"!=typeof n&&this.dayGrid.limitRows(n),!e&&v(this.scrollerEl,i)&&(u(this.headRowEl,E(this.scrollerEl)),i=this.computeScrollerHeight(t),this.scrollerEl.height(i),this.restoreScroll())},setGridHeight:function(t,e){e?g(this.dayGrid.rowEls):p(this.dayGrid.rowEls,t,!0)},renderEvents:function(t){this.dayGrid.renderEvents(t),this.updateHeight(),pe.prototype.renderEvents.call(this,t)},getSegs:function(){return this.dayGrid.getSegs()},destroyEvents:function(){pe.prototype.destroyEvents.call(this),this.recordScroll(),this.dayGrid.destroyEvents()},renderDrag:function(t,e,i){return this.dayGrid.renderDrag(t,e,i)},destroyDrag:function(){this.dayGrid.destroyDrag()},renderSelection:function(t,e){this.dayGrid.renderSelection(t,e)},destroySelection:function(){this.dayGrid.destroySelection()}}),r({fixedWeekCount:!0}),ze.month=me,me.prototype=x(ge.prototype),t.extend(me.prototype,{name:"month",incrementDate:function(t,e){return t.clone().stripTime().add(e,"months").startOf("month")},render:function(t){var e;this.intervalStart=t.clone().stripTime().startOf("month"),this.intervalEnd=this.intervalStart.clone().add(1,"months"),this.start=this.intervalStart.clone(),this.start=this.skipHiddenDays(this.start),this.start.startOf("week"),this.start=this.skipHiddenDays(this.start),this.end=this.intervalEnd.clone(),this.end=this.skipHiddenDays(this.end,-1,!0),this.end.add((7-this.end.weekday())%7,"days"),this.end=this.skipHiddenDays(this.end,-1,!0),e=Math.ceil(this.end.diff(this.start,"weeks",!0)),this.isFixedWeeks()&&(this.end.add(6-e,"weeks"),e=6),this.title=this.calendar.formatDate(this.intervalStart,this.opt("titleFormat")),ge.prototype.render.call(this,e,this.getCellsPerWeek(),!0)},setGridHeight:function(t,e){e=e||"variable"===this.opt("weekMode"),e&&(t*=this.rowCnt/6),p(this.dayGrid.rowEls,t,!e)},isFixedWeeks:function(){var t=this.opt("weekMode");return t?"fixed"===t:this.opt("fixedWeekCount")}}),ze.basicWeek=ve,ve.prototype=x(ge.prototype),t.extend(ve.prototype,{name:"basicWeek",incrementDate:function(t,e){return t.clone().stripTime().add(e,"weeks").startOf("week")},render:function(t){this.intervalStart=t.clone().stripTime().startOf("week"),this.intervalEnd=this.intervalStart.clone().add(1,"weeks"),this.start=this.skipHiddenDays(this.intervalStart),this.end=this.skipHiddenDays(this.intervalEnd,-1,!0),this.title=this.calendar.formatRange(this.start,this.end.clone().subtract(1),this.opt("titleFormat")," — "),ge.prototype.render.call(this,1,this.getCellsPerWeek(),!1)}}),ze.basicDay=ye,ye.prototype=x(ge.prototype),t.extend(ye.prototype,{name:"basicDay",incrementDate:function(t,e){var i=t.clone().stripTime().add(e,"days");return i=this.skipHiddenDays(i,0>e?-1:1)},render:function(t){this.start=this.intervalStart=t.clone().stripTime(),this.end=this.intervalEnd=this.start.clone().add(1,"days"),this.title=this.calendar.formatDate(this.start,this.opt("titleFormat")),ge.prototype.render.call(this,1,1,!1)}}),r({allDaySlot:!0,allDayText:"all-day",scrollTime:"06:00:00",slotDuration:"00:30:00",axisFormat:we,timeFormat:{agenda:Ee},minTime:"00:00:00",maxTime:"24:00:00",slotEventOverlap:!0});var Fe=5;De.prototype=x(pe.prototype),t.extend(De.prototype,{timeGrid:null,dayGrid:null,axisWidth:null,noScrollRowEls:null,bottomRuleEl:null,bottomRuleHeight:null,render:function(e){this.rowCnt=1,this.colCnt=e,this.el.addClass("fc-agenda-view").html(this.renderHtml()),this.scrollerEl=this.el.find(".fc-time-grid-container"),this.timeGrid.coordMap.containerEl=this.scrollerEl,this.timeGrid.el=this.el.find(".fc-time-grid"),this.timeGrid.render(),this.bottomRuleEl=t('
').appendTo(this.timeGrid.el),this.dayGrid&&(this.dayGrid.el=this.el.find(".fc-day-grid"),this.dayGrid.render(),this.dayGrid.bottomCoordPadding=this.dayGrid.el.next("hr").outerHeight()),this.noScrollRowEls=this.el.find(".fc-row:not(.fc-scroller *)"),pe.prototype.render.call(this),this.resetScroll()},destroy:function(){this.timeGrid.destroy(),this.dayGrid&&this.dayGrid.destroy(),pe.prototype.destroy.call(this)},renderHtml:function(){return'"+""+""+""+""+'"+""+""+"
'+this.timeGrid.headHtml()+"
'+(this.dayGrid?'

':"")+'
'+'
'+"
"+"
"},headIntroHtml:function(){var t,e,i,n;return this.opt("weekNumbers")?(t=this.cellToDate(0,0),e=this.calendar.calculateWeekNumber(t),i=this.opt("weekNumberTitle"),n=this.opt("isRTL")?e+i:i+e,'
"+""+P(n)+""+""+""+(this.opt("allDayHtml")||P(this.opt("allDayText")))+""+"'+P(r.name)+"