Вход Регистрация
Файл: assets/plugins/fullcalendar-2.6.1/fullcalendar.js
Строк: 16109
<?php
/*!
 * FullCalendar v2.6.1
 * Docs & License: http://fullcalendar.io/
 * (c) 2015 Adam Shaw
 */

(function(factory) {
    if (
typeof define === 'function' && define.amd) {
        
define([ 'jquery''moment' ], factory);
    }
    else if (
typeof exports === 'object') { // Node/CommonJS
        
module.exports factory(require('jquery'), require('moment'));
    }
    else {
        
factory(jQuerymoment);
    }
})(function($, 
moment) {

;;

var 
FC = $.fullCalendar = {
    
version"2.6.1",
    
internalApiVersion3
};
var 
fcViews FC.views = {};


$.
fn.fullCalendar = function(options) {
    var 
args = Array.prototype.slice.call(arguments1); // 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(calendarargs);
                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(elementoptions);
            
element.data('fullCalendar'calendar);
            
calendar.render();
        }
    });
    
    return 
res;
};


var 
complexOptions = [ // names of options that are objects whose properties should be combined
    
'header',
    
'buttonText',
    
'buttonIcons',
    
'themeButtonIcons'
];


// Merges an array of option objects into a single object
function mergeOptions(optionObjs) {
    return 
mergeProps(optionObjscomplexOptions);
}


// Given options specified for the calendar's constructor, massages any legacy options into a non-legacy form.
// Converts View-Option-Hashes into the View-Specific-Options format.
function massageOverrides(input) {
    var 
overrides = { viewsinput.views || {} }; // the output. ensure a `views` hash
    
var subObj;

    
// iterate through all option override properties (except `views`)
    
$.each(input, function(nameval) {
        if (
name != 'views') {

            
// could the value be a legacy View-Option-Hash?
            
if (
                $.
isPlainObject(val) &&
                !/(
time|duration|interval)$/i.test(name) && // exclude duration options. might be given as objects
                
$.inArray(namecomplexOptions) == -// complex options aren't allowed to be View-Option-Hashes
            
) {
                
subObj null;

                
// iterate through the properties of this possible View-Option-Hash value
                
$.each(val, function(subNamesubVal) {

                    
// is the property targeting a view?
                    
if (/^(month|week|day|default|basic(Week|Day)?|agenda(Week|Day)?)$/.test(subName)) {
                        if (!
overrides.views[subName]) { // ensure the view-target entry exists
                            
overrides.views[subName] = {};
                        }
                        
overrides.views[subName][name] = subVal// record the value in the `views` object
                    
}
                    else { 
// a non-View-Option-Hash property
                        
if (!subObj) {
                            
subObj = {};
                        }
                        
subObj[subName] = subVal// accumulate these unrelated values for later
                    
}
                });

                if (
subObj) { // non-View-Option-Hash properties? transfer them as-is
                    
overrides[name] = subObj;
                }
            }
            else {
                
overrides[name] = val// transfer normal options as-is
            
}
        }
    });

    return 
overrides;
}

;;

// exports
FC.intersectRanges intersectRanges;
FC.applyAll applyAll;
FC.debounce debounce;
FC.isInt isInt;
FC.htmlEscape htmlEscape;
FC.cssToStr cssToStr;
FC.proxy proxy;
FC.capitaliseFirstLetter capitaliseFirstLetter;


/* FullCalendar-specific DOM Utilities
----------------------------------------------------------------------------------------------------------------------*/


// Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left
// and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that.
function compensateScroll(rowElsscrollbarWidths) {
    if (
scrollbarWidths.left) {
        
rowEls.css({
            
'border-left-width'1,
            
'margin-left'scrollbarWidths.left 1
        
});
    }
    if (
scrollbarWidths.right) {
        
rowEls.css({
            
'border-right-width'1,
            
'margin-right'scrollbarWidths.right 1
        
});
    }
}


// Undoes compensateScroll and restores all borders/margins
function uncompensateScroll(rowEls) {
    
rowEls.css({
        
'margin-left''',
        
'margin-right''',
        
'border-left-width''',
        
'border-right-width'''
    
});
}


// Make the mouse cursor express that an event is not allowed in the current area
function disableCursor() {
    $(
'body').addClass('fc-not-allowed');
}


// Returns the mouse cursor to its original look
function enableCursor() {
    $(
'body').removeClass('fc-not-allowed');
}


// Given a total available height to fill, have `els` (essentially child rows) expand to accomodate.
// By default, all elements that are shorter than the recommended height are expanded uniformly, not considering
// any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and 
// reduces the available height.
function distributeHeight(elsavailableHeightshouldRedistribute) {

    
// *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions,
    // and it is better to be shorter than taller, to avoid creating unnecessary scrollbars.

    
var minOffset1 Math.floor(availableHeight els.length); // for non-last element
    
var minOffset2 Math.floor(availableHeight minOffset1 * (els.length 1)); // for last element *FLOORING NOTE*
    
var flexEls = []; // elements that are allowed to expand. array of DOM nodes
    
var flexOffsets = []; // amount of vertical space it takes up
    
var flexHeights = []; // actual css height
    
var usedHeight 0;

    
undistributeHeight(els); // give all elements their natural height

    // find elements that are below the recommended height (expandable).
    // important to query for heights in a single first pass (to avoid reflow oscillation).
    
els.each(function(iel) {
        var 
minOffset === els.length minOffset2 minOffset1;
        var 
naturalOffset = $(el).outerHeight(true);

        if (
naturalOffset minOffset) {
            
flexEls.push(el);
            
flexOffsets.push(naturalOffset);
            
flexHeights.push($(el).height());
        }
        else {
            
// this element stretches past recommended height (non-expandable). mark the space as occupied.
            
usedHeight += naturalOffset;
        }
    });

    
// readjust the recommended height to only consider the height available to non-maxed-out rows.
    
if (shouldRedistribute) {
        
availableHeight -= usedHeight;
        
minOffset1 Math.floor(availableHeight flexEls.length);
        
minOffset2 Math.floor(availableHeight minOffset1 * (flexEls.length 1)); // *FLOORING NOTE*
    
}

    
// assign heights to all expandable elements
    
$(flexEls).each(function(iel) {
        var 
minOffset === flexEls.length minOffset2 minOffset1;
        var 
naturalOffset flexOffsets[i];
        var 
naturalHeight flexHeights[i];
        var 
newHeight minOffset - (naturalOffset naturalHeight); // subtract the margin/padding

        
if (naturalOffset minOffset) { // we check this again because redistribution might have changed things
            
$(el).height(newHeight);
        }
    });
}


// Undoes distrubuteHeight, restoring all els to their natural height
function undistributeHeight(els) {
    
els.height('');
}


// Given `els`, a jQuery set of <td> 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('> span').each(function(iinnerEl) {
        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(containerElheight) {
    
containerEl.height(height).addClass('fc-scroller');

    
// are scrollbars needed?
    
if (containerEl[0].scrollHeight 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
----------------------------------------------------------------------------------------------------------------------*/

FC.getOuterRect getOuterRect;
FC.getClientRect getClientRect;
FC.getContentRect getContentRect;
FC.getScrollbarWidths getScrollbarWidths;


// 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;
}


// Queries the outer bounding area of a jQuery element.
// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
function getOuterRect(el) {
    var 
offset el.offset();

    return {
        
leftoffset.left,
        
rightoffset.left el.outerWidth(),
        
topoffset.top,
        
bottomoffset.top el.outerHeight()
    };
}


// Queries the area within the margin/border/scrollbars of a jQuery element. Does not go within the padding.
// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
// NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
function getClientRect(el) {
    var 
offset el.offset();
    var 
scrollbarWidths getScrollbarWidths(el);
    var 
left offset.left getCssFloat(el'border-left-width') + scrollbarWidths.left;
    var 
top offset.top getCssFloat(el'border-top-width') + scrollbarWidths.top;

    return {
        
leftleft,
        
rightleft el[0].clientWidth// clientWidth includes padding but NOT scrollbars
        
toptop,
        
bottomtop el[0].clientHeight // clientHeight includes padding but NOT scrollbars
    
};
}


// Queries the area within the margin/border/padding of a jQuery element. Assumed not to have scrollbars.
// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
function getContentRect(el) {
    var 
offset el.offset(); // just outside of border, margin not included
    
var left offset.left getCssFloat(el'border-left-width') + getCssFloat(el'padding-left');
    var 
top offset.top getCssFloat(el'border-top-width') + getCssFloat(el'padding-top');

    return {
        
leftleft,
        
rightleft el.width(),
        
toptop,
        
bottomtop el.height()
    };
}


// Returns the computed left/right/top/bottom scrollbar widths for the given jQuery element.
// NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
function getScrollbarWidths(el) {
    var 
leftRightWidth el.innerWidth() - el[0].clientWidth// the paddings cancel out, leaving the scrollbars
    
var widths = {
        
left0,
        
right0,
        
top0,
        
bottomel.innerHeight() - el[0].clientHeight // the paddings cancel out, leaving the bottom scrollbar
    
};

    if (
getIsLeftRtlScrollbars() && el.css('direction') == 'rtl') { // is the scrollbar on the left side?
        
widths.left leftRightWidth;
    }
    else {
        
widths.right leftRightWidth;
    }

    return 
widths;
}


// Logic for determining if, when the element is right-to-left, the scrollbar appears on the left side

var _isLeftRtlScrollbars null;

function 
getIsLeftRtlScrollbars() { // responsible for caching the computation
    
if (_isLeftRtlScrollbars === null) {
        
_isLeftRtlScrollbars computeIsLeftRtlScrollbars();
    }
    return 
_isLeftRtlScrollbars;
}

function 
computeIsLeftRtlScrollbars() { // creates an offscreen test element, then removes it
    
var el = $('<div><div/></div>')
        .
css({
            
position'absolute',
            
top: -1000,
            
left0,
            
border0,
            
padding0,
            
overflow'scroll',
            
direction'rtl'
        
})
        .
appendTo('body');
    var 
innerEl el.children();
    var 
res innerEl.offset().left el.offset().left// is the inner div shifted to accommodate a left scrollbar?
    
el.remove();
    return 
res;
}


// Retrieves a jQuery element's computed CSS value as a floating-point number.
// If the queried value is non-numeric (ex: IE can return "medium" for border width), will just return zero.
function getCssFloat(elprop) {
    return 
parseFloat(el.css(prop)) || 0;
}


// 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 == && !ev.ctrlKey;
}


/* Geometry
----------------------------------------------------------------------------------------------------------------------*/

FC.intersectRects intersectRects;

// Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false
function intersectRects(rect1rect2) {
    var 
res = {
        
leftMath.max(rect1.leftrect2.left),
        
rightMath.min(rect1.rightrect2.right),
        
topMath.max(rect1.toprect2.top),
        
bottomMath.min(rect1.bottomrect2.bottom)
    };

    if (
res.left res.right && res.top res.bottom) {
        return 
res;
    }
    return 
false;
}


// Returns a new point that will have been moved to reside within the given rectangle
function constrainPoint(pointrect) {
    return {
        
leftMath.min(Math.max(point.leftrect.left), rect.right),
        
topMath.min(Math.max(point.toprect.top), rect.bottom)
    };
}


// Returns a point that is the center of the given rectangle
function getRectCenter(rect) {
    return {
        
left: (rect.left rect.right) / 2,
        
top: (rect.top rect.bottom) / 2
    
};
}


// Subtracts point2's coordinates from point1's coordinates, returning a delta
function diffPoints(point1point2) {
    return {
        
leftpoint1.left point2.left,
        
toppoint1.top point2.top
    
};
}


/* Object Ordering by Field
----------------------------------------------------------------------------------------------------------------------*/

FC.parseFieldSpecs parseFieldSpecs;
FC.compareByFieldSpecs compareByFieldSpecs;
FC.compareByFieldSpec compareByFieldSpec;
FC.flexibleCompare flexibleCompare;


function 
parseFieldSpecs(input) {
    var 
specs = [];
    var 
tokens = [];
    var 
itoken;

    if (
typeof input === 'string') {
        
tokens input.split(/s*,s*/);
    }
    else if (
typeof input === 'function') {
        
tokens = [ input ];
    }
    else if ($.
isArray(input)) {
        
tokens input;
    }

    for (
0tokens.lengthi++) {
        
token tokens[i];

        if (
typeof token === 'string') {
            
specs.push(
                
token.charAt(0) == '-' ?
                    { 
fieldtoken.substring(1), order: -} :
                    { 
fieldtokenorder}
            );
        }
        else if (
typeof token === 'function') {
            
specs.push({ functoken });
        }
    }

    return 
specs;
}


function 
compareByFieldSpecs(obj1obj2fieldSpecs) {
    var 
i;
    var 
cmp;

    for (
0fieldSpecs.lengthi++) {
        
cmp compareByFieldSpec(obj1obj2fieldSpecs[i]);
        if (
cmp) {
            return 
cmp;
        }
    }

    return 
0;
}


function 
compareByFieldSpec(obj1obj2fieldSpec) {
    if (
fieldSpec.func) {
        return 
fieldSpec.func(obj1obj2);
    }
    return 
flexibleCompare(obj1[fieldSpec.field], obj2[fieldSpec.field]) *
        (
fieldSpec.order || 1);
}


function 
flexibleCompare(ab) {
    if (!
&& !b) {
        return 
0;
    }
    if (
== null) {
        return -
1;
    }
    if (
== null) {
        return 
1;
    }
    if ($.
type(a) === 'string' || $.type(b) === 'string') {
        return 
String(a).localeCompare(String(b));
    }
    return 
b;
}


/* FullCalendar-specific Misc Utilities
----------------------------------------------------------------------------------------------------------------------*/


// Computes the intersection of the two ranges. Returns undefined if no intersection.
// Expects all dates to be normalized to the same timezone beforehand.
// TODO: move to date section?
function intersectRanges(subjectRangeconstraintRange) {
    var 
subjectStart subjectRange.start;
    var 
subjectEnd subjectRange.end;
    var 
constraintStart constraintRange.start;
    var 
constraintEnd constraintRange.end;
    var 
segStartsegEnd;
    var 
isStartisEnd;

    if (
subjectEnd constraintStart && subjectStart constraintEnd) { // in bounds at all?

        
if (subjectStart >= constraintStart) {
            
segStart subjectStart.clone();
            
isStart true;
        }
        else {
            
segStart constraintStart.clone();
            
isStart =  false;
        }

        if (
subjectEnd <= constraintEnd) {
            
segEnd subjectEnd.clone();
            
isEnd true;
        }
        else {
            
segEnd constraintEnd.clone();
            
isEnd false;
        }

        return {
            
startsegStart,
            
endsegEnd,
            
isStartisStart,
            
isEndisEnd
        
};
    }
}


/* Date Utilities
----------------------------------------------------------------------------------------------------------------------*/

FC.computeIntervalUnit computeIntervalUnit;
FC.divideRangeByDuration divideRangeByDuration;
FC.divideDurationByDuration divideDurationByDuration;
FC.multiplyDuration multiplyDuration;
FC.durationHasTime durationHasTime;

var 
dayIDs = [ 'sun''mon''tue''wed''thu''fri''sat' ];
var 
intervalUnits = [ 'year''month''week''day''hour''minute''second''millisecond' ];


// Diffs the two moments into a Duration where full-days are recorded first, then the remaining time.
// Moments will have their timezones normalized.
function diffDayTime(ab) {
    return 
moment.duration({
        
daysa.clone().stripTime().diff(b.clone().stripTime(), 'days'),
        
msa.time() - b.time() // time-of-day from day start. disregards timezone
    
});
}


// Diffs the two moments via their start-of-day (regardless of timezone). Produces whole-day durations.
function diffDay(ab) {
    return 
moment.duration({
        
daysa.clone().stripTime().diff(b.clone().stripTime(), 'days')
    });
}


// Diffs two moments, producing a duration, made of a whole-unit-increment of the given unit. Uses rounding.
function diffByUnit(abunit) {
    return 
moment.duration(
        
Math.round(a.diff(bunittrue)), // returnFloat=true
        
unit
    
);
}


// Computes the unit name of the largest whole-unit period of time.
// For example, 48 hours will be "days" whereas 49 hours will be "hours".
// Accepts start/end, a range object, or an original duration object.
function computeIntervalUnit(startend) {
    var 
iunit;
    var 
val;

    for (
0intervalUnits.lengthi++) {
        
unit intervalUnits[i];
        
val computeRangeAs(unitstartend);

        if (
val >= && isInt(val)) {
            break;
        }
    }

    return 
unit// will be "milliseconds" if nothing else matches
}


// Computes the number of units (like "hours") in the given range.
// Range can be a {start,end} object, separate start/end args, or a Duration.
// Results are based on Moment's .as() and .diff() methods, so results can depend on internal handling
// of month-diffing logic (which tends to vary from version to version).
function computeRangeAs(unitstartend) {

    if (
end != null) { // given start, end
        
return end.diff(startunittrue);
    }
    else if (
moment.isDuration(start)) { // given duration
        
return start.as(unit);
    }
    else { 
// given { start, end } range object
        
return start.end.diff(start.startunittrue);
    }
}


// Intelligently divides a range (specified by a start/end params) by a duration
function divideRangeByDuration(startenddur) {
    var 
months;

    if (
durationHasTime(dur)) {
        return (
end start) / dur;
    }
    
months dur.asMonths();
    if (
Math.abs(months) >= && isInt(months)) {
        return 
end.diff(start'months'true) / months;
    }
    return 
end.diff(start'days'true) / dur.asDays();
}


// Intelligently divides one duration by another
function divideDurationByDuration(dur1dur2) {
    var 
months1months2;

    if (
durationHasTime(dur1) || durationHasTime(dur2)) {
        return 
dur1 dur2;
    }
    
months1 dur1.asMonths();
    
months2 dur2.asMonths();
    if (
        
Math.abs(months1) >= && isInt(months1) &&
        
Math.abs(months2) >= && isInt(months2)
    ) {
        return 
months1 months2;
    }
    return 
dur1.asDays() / dur2.asDays();
}


// Intelligently multiplies a duration by a number
function multiplyDuration(durn) {
    var 
months;

    if (
durationHasTime(dur)) {
        return 
moment.duration(dur n);
    }
    
months dur.asMonths();
    if (
Math.abs(months) >= && isInt(months)) {
        return 
moment.duration({ monthsmonths });
    }
    return 
moment.duration({ daysdur.asDays() * });
}


// Returns a boolean about whether the given duration has any time parts (hours/minutes/seconds/ms)
function durationHasTime(dur) {
    return 
Boolean(dur.hours() || dur.minutes() || dur.seconds() || dur.milliseconds());
}


function 
isNativeDate(input) {
    return  
Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date;
}


// Returns a boolean about whether the given input is a time string, like "06:40:00" or "06:00"
function isTimeString(str) {
    return /^
d+:d+(?::d+.?(?:d{3})?)?$/.test(str);
}


/* Logging and Debug
----------------------------------------------------------------------------------------------------------------------*/

FC.log = function() {
    var 
console window.console;

    if (
console && console.log) {
        return 
console.log.apply(consolearguments);
    }
};

FC.warn = function() {
    var 
console window.console;

    if (
console && console.warn) {
        return 
console.warn.apply(consolearguments);
    }
    else {
        return 
FC.log.apply(FCarguments);
    }
};


/* General Utilities
----------------------------------------------------------------------------------------------------------------------*/

var hasOwnPropMethod = {}.hasOwnProperty;


// Merges an array of objects into a single object.
// The second argument allows for an array of property names who's object values will be merged together.
function mergeProps(propObjscomplexProps) {
    var 
dest = {};
    var 
iname;
    var 
complexObjs;
    var 
jval;
    var 
props;

    if (
complexProps) {
        for (
0complexProps.lengthi++) {
            
name complexProps[i];
            
complexObjs = [];

            
// collect the trailing object values, stopping when a non-object is discovered
            
for (propObjs.length 1>= 0j--) {
                
val propObjs[j][name];

                if (
typeof val === 'object') {
                    
complexObjs.unshift(val);
                }
                else if (
val !== undefined) {
                    
dest[name] = val// if there were no objects, this value will be used
                    
break;
                }
            }

            
// if the trailing values were objects, use the merged value
            
if (complexObjs.length) {
                
dest[name] = mergeProps(complexObjs);
            }
        }
    }

    
// copy values into the destination, going from last to first
    
for (propObjs.length 1>= 0i--) {
        
props propObjs[i];

        for (
name in props) {
            if (!(
name in dest)) { // if already assigned by previous props or complex props, don't reassign
                
dest[name] = props[name];
            }
        }
    }

    return 
dest;
}


// Create an object that has the given prototype. Just like Object.create
function createObject(proto) {
    var 
= function() {};
    
f.prototype proto;
    return new 
f();
}


function 
copyOwnProps(srcdest) {
    for (var 
name in src) {
        if (
hasOwnProp(srcname)) {
            
dest[name] = src[name];
        }
    }
}


// Copies over certain methods with the same names as Object.prototype methods. Overcomes an IE<=8 bug:
// https://developer.mozilla.org/en-US/docs/ECMAScript_DontEnum_attribute#JScript_DontEnum_Bug
function copyNativeMethods(srcdest) {
    var 
names = [ 'constructor''toString''valueOf' ];
    var 
iname;

    for (
0names.lengthi++) {
        
name names[i];

        if (
src[name] !== Object.prototype[name]) {
            
dest[name] = src[name];
        }
    }
}


function 
hasOwnProp(objname) {
    return 
hasOwnPropMethod.call(objname);
}


// Is the given value a non-object non-function value?
function isAtomic(val) {
    return /
undefined|null|boolean|number|string/.test($.type(val));
}


function 
applyAll(functionsthisObjargs) {
    if ($.
isFunction(functions)) {
        
functions = [ functions ];
    }
    if (
functions) {
        var 
i;
        var 
ret;
        for (
i=0i<functions.lengthi++) {
            
ret functions[i].apply(thisObjargs) || ret;
        }
        return 
ret;
    }
}


function 
firstDefined() {
    for (var 
i=0i<arguments.lengthi++) {
        if (
arguments[i] !== undefined) {
            return 
arguments[i];
        }
    }
}


function 
htmlEscape(s) {
    return (
'').replace(/&/g'&amp;')
        .
replace(/</g'&lt;')
        .
replace(/>/g'&gt;')
        .
replace(/'/g, '&#039;')
        
.replace(/"/g, '&quot;')
        .replace(/n/g, '<br />');
}


function stripHtmlEntities(text) {
    return text.replace(/&.*?;/g, '');
}


// Given a hash of CSS properties, returns a string of CSS.
// Uses property names as-is (no camel-case conversion). Will not make statements for null/undefined values.
function cssToStr(cssProps) {
    var statements = [];

    $.each(cssProps, function(name, val) {
        if (val != null) {
            statements.push(name + ':' + val);
        }
    });

    return statements.join(';');
}


function capitaliseFirstLetter(str) {
    return str.charAt(0).toUpperCase() + str.slice(1);
}


function compareNumbers(a, b) { // for .sort()
    return a - b;
}


function isInt(n) {
    return n % 1 === 0;
}


// Returns a method bound to the given object context.
// Just like one of the jQuery.proxy signatures, but without the undesired behavior of treating the same method with
// different contexts as identical when binding/unbinding events.
function proxy(obj, methodName) {
    var method = obj[methodName];

    return function() {
        return method.apply(obj, arguments);
    };
}


// 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}-dd$/;
var ambigTimeOrZoneRegex =
    /^s*d{4}-(?:(dd-dd)|(Wdd$)|(Wdd-d)|(ddd))((T| )(dd(:dd(:dd(.d+)?)?)?)?)?$/;
var newMomentProto = moment.fn; // where we will attach our new methods
var oldMomentProto = $.extend({}, newMomentProto); // copy of original moment methods
var allowValueOptimization;
var setUTCValues; // function defined below
var setLocalValues; // function defined below


// Creating
// -------------------------------------------------------------------------------------------------

// Creates a new moment, similar to the vanilla moment(...) constructor, but with
// extra features (ambiguous time, enhanced formatting). When given 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 given a pre-existing moment for example)
    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 enhanced moment 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 mom;

    if (moment.isMoment(input)) {
        mom = moment.apply(null, args); // clone it
        transferAmbigs(input, mom); // the ambig flags weren't transfered with the clone
    }
    else if (isNativeDate(input) || input === undefined) {
        mom = 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 || isAmbigTime) {
            mom = moment.utc.apply(moment, args);
        }
        else {
            mom = moment.apply(null, args);
        }

        if (isAmbigTime) {
            mom._ambigTime = true;
            mom._ambigZone = true; // ambiguous time always means ambiguous zone
        }
        else if (parseZone) { // let's record the inputted zone somehow
            if (isAmbigZone) {
                mom._ambigZone = true;
            }
            else if (isSingleString) {
                if (mom.utcOffset) {
                    mom.utcOffset(input); // if not a valid zone, will assign UTC
                }
                else {
                    mom.zone(input); // for moment-pre-2.9
                }
            }
        }
    }

    mom._fullCalendar = true; // flag for extended functionality

    return mom;
}


// A clone method that works with the flags related to our enhanced functionality.
// In the future, use moment.momentProperties
newMomentProto.clone = function() {
    var mom = oldMomentProto.clone.apply(this, arguments);

    // these flags weren't transfered with the clone
    transferAmbigs(this, mom);
    if (this._fullCalendar) {
        mom._fullCalendar = true;
    }

    return mom;
};


// Week Number
// -------------------------------------------------------------------------------------------------


// Returns the week number, considering the locale's custom week number calcuation
// `weeks` is an alias for `week`
newMomentProto.week = newMomentProto.weeks = function(input) {
    var weekCalc = (this._locale || this._lang) // works pre-moment-2.8
        ._fullCalendar_weekCalc;

    if (input == null && typeof weekCalc === 'function') { // custom function only works for getter
        return weekCalc(this);
    }
    else if (weekCalc === 'ISO') {
        return oldMomentProto.isoWeek.apply(this, arguments); // ISO getter/setter
    }

    return oldMomentProto.week.apply(this, arguments); // local getter/setter
};


// 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.
newMomentProto.time = function(time) {

    // Fallback to the original method (if there is one) if this moment wasn't created via FullCalendar.
    // `time` is a generic enough method name where this precaution is necessary to avoid collisions w/ other plugins.
    if (!this._fullCalendar) {
        return oldMomentProto.time.apply(this, arguments);
    }

    if (time == null) { // getter
        return moment.duration({
            hours: this.hours(),
            minutes: this.minutes(),
            seconds: this.seconds(),
            milliseconds: this.milliseconds()
        });
    }
    else { // setter

        this._ambigTime = false; // 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.
newMomentProto.stripTime = function() {
    var a;

    if (!this._ambigTime) {

        // get the values before any conversion happens
        a = this.toArray(); // array of y/m/d/h/m/s/ms

        // TODO: use keepLocalTime in the future
        this.utc(); // set the internal UTC flag (will clear the ambig flags)
        setUTCValues(this, a.slice(0, 3)); // set the year/month/date. time will be zero

        // Mark the time as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(),
        // which clears all ambig flags. Same with setUTCValues with 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)
newMomentProto.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.
// TODO: look into Moment's keepLocalTime functionality
newMomentProto.stripZone = function() {
    var a, wasAmbigTime;

    if (!this._ambigZone) {

        // get the values before any conversion happens
        a = this.toArray(); // array of y/m/d/h/m/s/ms
        wasAmbigTime = this._ambigTime;

        this.utc(); // set the internal UTC flag (might clear the ambig flags, depending on Moment internals)
        setUTCValues(this, a); // will set the year/month/date/hours/minutes/seconds/ms

        // the above call to .utc()/.utcOffset() unfortunately might clear the ambig flags, so restore
        this._ambigTime = wasAmbigTime || false;

        // Mark the zone as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(),
        // which clears the ambig flags. Same with setUTCValues with moment-timezone.
        this._ambigZone = true;
    }

    return this; // for chaining
};

// Returns of the moment has a non-ambiguous timezone offset (boolean)
newMomentProto.hasZone = function() {
    return !this._ambigZone;
};


// this method implicitly marks a zone
newMomentProto.local = function() {
    var a = this.toArray(); // year,month,date,hours,minutes,seconds,ms as an array
    var wasAmbigZone = this._ambigZone;

    oldMomentProto.local.apply(this, arguments);

    // ensure non-ambiguous
    // this probably already happened via local() -> utcOffset(), but don't rely on Moment's internals
    this._ambigTime = false;
    this._ambigZone = false;

    if (wasAmbigZone) {
        // If the moment was ambiguously zoned, the date fields were stored as UTC.
        // We want to preserve these, but in local time.
        // TODO: look into Moment's keepLocalTime functionality
        setLocalValues(this, a);
    }

    return this; // for chaining
};


// implicitly marks a zone
newMomentProto.utc = function() {
    oldMomentProto.utc.apply(this, arguments);

    // ensure non-ambiguous
    // this probably already happened via utc() -> utcOffset(), but don't rely on Moment's internals
    this._ambigTime = false;
    this._ambigZone = false;

    return this;
};


// methods for arbitrarily manipulating timezone offset.
// should clear time/zone ambiguity when called.
$.each([
    'zone', // only in moment-pre-2.9. deprecated afterwards
    'utcOffset'
], function(i, name) {
    if (oldMomentProto[name]) { // original method exists?

        // this method implicitly marks a zone (will probably get called upon .utc() and .local())
        newMomentProto[name] = function(tzo) {

            if (tzo != null) { // setter
                // these assignments needs to happen before the original zone method is called.
                // I forget why, something to do with a browser crash.
                this._ambigTime = false;
                this._ambigZone = false;
            }

            return oldMomentProto[name].apply(this, arguments);
        };
    }
});


// Formatting
// -------------------------------------------------------------------------------------------------

newMomentProto.format = function() {
    if (this._fullCalendar && arguments[0]) { // an enhanced moment? and a format string provided?
        return formatDate(this, arguments[0]); // our extended formatting
    }
    if (this._ambigTime) {
        return oldMomentFormat(this, 'YYYY-MM-DD');
    }
    if (this._ambigZone) {
        return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
    }
    return oldMomentProto.format.apply(this, arguments);
};

newMomentProto.toISOString = function() {
    if (this._ambigTime) {
        return oldMomentFormat(this, 'YYYY-MM-DD');
    }
    if (this._ambigZone) {
        return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
    }
    return oldMomentProto.toISOString.apply(this, arguments);
};


// Querying
// -------------------------------------------------------------------------------------------------

// Is the moment within the specified range? `end` is exclusive.
// FYI, this method is not a standard Moment method, so always do our enhanced logic.
newMomentProto.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 specified, the two moments must be identically the same, with matching ambig flags.
newMomentProto.isSame = function(input, units) {
    var a;

    // only do custom logic if this is an enhanced moment
    if (!this._fullCalendar) {
        return oldMomentProto.isSame.apply(this, arguments);
    }

    if (units) {
        a = commonlyAmbiguate([ this, input ], true); // normalize timezones but don't erase times
        return oldMomentProto.isSame.call(a[0], a[1], units);
    }
    else {
        input = FC.moment.parseZone(input); // normalize input
        return oldMomentProto.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) {
    newMomentProto[methodName] = function(input, units) {
        var a;

        // only do custom logic if this is an enhanced moment
        if (!this._fullCalendar) {
            return oldMomentProto[methodName].apply(this, arguments);
        }

        a = commonlyAmbiguate([ this, input ]);
        return oldMomentProto[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.
// returns the original moments if no modifications are necessary.
function commonlyAmbiguate(inputs, preserveTime) {
    var anyAmbigTime = false;
    var anyAmbigZone = false;
    var len = inputs.length;
    var moms = [];
    var i, mom;

    // parse inputs into real moments and query their ambig flags
    for (i = 0; i < len; i++) {
        mom = inputs[i];
        if (!moment.isMoment(mom)) {
            mom = FC.moment.parseZone(mom);
        }
        anyAmbigTime = anyAmbigTime || mom._ambigTime;
        anyAmbigZone = anyAmbigZone || mom._ambigZone;
        moms.push(mom);
    }

    // strip each moment down to lowest common ambiguity
    // use clones to avoid modifying the original moments
    for (i = 0; i < len; i++) {
        mom = moms[i];
        if (!preserveTime && anyAmbigTime && !mom._ambigTime) {
            moms[i] = mom.clone().stripTime();
        }
        else if (anyAmbigZone && !mom._ambigZone) {
            moms[i] = mom.clone().stripZone();
        }
    }

    return moms;
}

// Transfers all the flags related to ambiguous time/zone from the `src` moment to the `dest` moment
// TODO: look into moment.momentProperties for this.
function transferAmbigs(src, dest) {
    if (src._ambigTime) {
        dest._ambigTime = true;
    }
    else if (dest._ambigTime) {
        dest._ambigTime = false;
    }

    if (src._ambigZone) {
        dest._ambigZone = true;
    }
    else if (dest._ambigZone) {
        dest._ambigZone = false;
    }
}


// Sets the year/month/date/etc values of the moment from the given array.
// Inefficient because it calls each individual setter.
function setMomentValues(mom, a) {
    mom.year(a[0] || 0)
        .month(a[1] || 0)
        .date(a[2] || 0)
        .hours(a[3] || 0)
        .minutes(a[4] || 0)
        .seconds(a[5] || 0)
        .milliseconds(a[6] || 0);
}

// Can we set the moment's internal date directly?
allowValueOptimization = '_d' in moment() && 'updateOffset' in moment;

// Utility function. Accepts a moment and an array of the UTC year/month/date/etc values to set.
// Assumes the given moment is already in UTC mode.
setUTCValues = allowValueOptimization ? function(mom, a) {
    // simlate what moment's accessors do
    mom._d.setTime(Date.UTC.apply(Date, a));
    moment.updateOffset(mom, false); // keepTime=false
} : setMomentValues;

// Utility function. Accepts a moment and an array of the local year/month/date/etc values to set.
// Assumes the given moment is already in local mode.
setLocalValues = allowValueOptimization ? function(mom, a) {
    // simlate what moment's accessors do
    mom._d.setTime(+new Date( // FYI, there is now way to apply an array of args to a constructor
        a[0] || 0,
        a[1] || 0,
        a[2] || 0,
        a[3] || 0,
        a[4] || 0,
        a[5] || 0,
        a[6] || 0
    ));
    moment.updateOffset(mom, false); // keepTime=false
} : setMomentValues;

;;

// Single Date Formatting
// -------------------------------------------------------------------------------------------------


// call this if you want Moment's original format method to be used
function oldMomentFormat(mom, formatStr) {
    return oldMomentProto.format.call(mom, formatStr); // oldMomentProto defined in moment-ext.js
}


// Formats `date` with a Moment formatting string, but allow our non-zero areas and
// additional token.
function formatDate(date, formatStr) {
    return formatDateWithChunks(date, getFormatStringChunks(formatStr));
}


function formatDateWithChunks(date, chunks) {
    var s = '';
    var i;

    for (i=0; i<chunks.length; i++) {
        s += formatDateWithChunk(date, chunks[i]);
    }

    return s;
}


// addition formatting tokens we want recognized
var tokenOverrides = {
    t: function(date) { // "
a" or "p"
        return oldMomentFormat(date, 'a').charAt(0);
    },
    T: function(date) { // "
A" or "P"
        return oldMomentFormat(date, 'A').charAt(0);
    }
};


function formatDateWithChunk(date, chunk) {
    var token;
    var maybeStr;

    if (typeof chunk === 'string') { // a literal string
        return chunk;
    }
    else if ((token = chunk.token)) { // a token, like "
YYYY"
        if (tokenOverrides[token]) {
            return tokenOverrides[token](date); // use our custom token
        }
        return oldMomentFormat(date, token);
    }
    else if (chunk.maybe) { // a grouping of other chunks that must be non-zero
        maybeStr = formatDateWithChunks(date, chunk.maybe);
        if (maybeStr.match(/[1-9]/)) {
            return maybeStr;
        }
    }

    return '';
}


// Date Range Formatting
// -------------------------------------------------------------------------------------------------
// TODO: make it work with timezone offset

// Using a formatting string meant for a single date, generate a range string, like
// "
Sep 2 9 2013", that intelligently inserts a separator where the dates differ.
// If the dates are the same as far as the format string is concerned, just return a single
// rendering of one date, without any separator.
function formatRange(date1, date2, formatStr, separator, isRTL) {
    var localeData;

    date1 = FC.moment.parseZone(date1);
    date2 = FC.moment.parseZone(date2);

    localeData = (date1.localeData || date1.lang).call(date1); // works with moment-pre-2.8

    // Expand localized format strings, like "
LL" -> "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 unzonedDate1 = date1.clone().stripZone(); // for formatSimilarChunk
    var unzonedDate2 = date2.clone().stripZone(); // "
    
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=0leftI<chunks.lengthleftI++) {
        
chunkStr formatSimilarChunk(date1date2unzonedDate1unzonedDate2chunks[leftI]);
        if (
chunkStr === false) {
            break;
        }
        
leftStr += chunkStr;
    }

    
// Similarly, start at the rightmost side of the formatting string and move left
    
for (rightI=chunks.length-1rightI>leftIrightI--) {
        
chunkStr formatSimilarChunk(date1date2unzonedDate1unzonedDate2,  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=leftImiddleI<=rightImiddleI++) {
        
middleStr1 += formatDateWithChunk(date1chunks[middleI]);
        
middleStr2 += formatDateWithChunk(date2chunks[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(date1date2unzonedDate1unzonedDate2chunk) {
    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?
        // use the unzoned dates for this calculation because unreliable when near DST (bug #2396)
        
if (unit && unzonedDate1.isSame(unzonedDate2unit)) {
            return 
oldMomentFormat(date1token); // 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 = /[([^]]*)]|(([^)]*))|(LTS|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({ maybechunkFormatString(match[2]) });
        }
        else if (
match[3]) { // a formatting token
            
chunks.push({ tokenmatch[3] });
        }
        else if (
match[5]) { // an unenclosed literal string
            
chunks.push(match[5]);
        }
    }

    return 
chunks;
}

;;

FC.Class = Class; // export

// Class that all other classes will inherit from
function Class() { }


// Called on a class to create a subclass.
// Last argument contains instance methods. Any argument before the last are considered mixins.
Class.extend = function() {
    var 
len arguments.length;
    var 
i;
    var 
members;

    for (
0leni++) {
        
members arguments[i];
        if (
len 1) { // not the last argument?
            
mixIntoClass(thismembers);
        }
    }

    return 
extendClass(thismembers || {}); // members will be undefined if no arguments
};


// Adds new member variables/methods to the class's prototype.
// Can be called with another class, or a plain object hash containing new members.
Class.mixin = function(members) {
    
mixIntoClass(thismembers);
};


function 
extendClass(superClassmembers) {
    var 
subClass;

    
// ensure a constructor for the subclass, forwarding all arguments to the super-constructor if it doesn't exist
    
if (hasOwnProp(members'constructor')) {
        
subClass members.constructor;
    }
    if (
typeof subClass !== 'function') {
        
subClass members.constructor = function() {
            
superClass.apply(thisarguments);
        };
    }

    
// build the base prototype for the subclass, which is an new object chained to the superclass's prototype
    
subClass.prototype createObject(superClass.prototype);

    
// copy each member variable/method onto the the subclass's prototype
    
copyOwnProps(memberssubClass.prototype);
    
copyNativeMethods(memberssubClass.prototype); // hack for IE8

    // copy over all class variables/methods to the subclass, such as `extend` and `mixin`
    
copyOwnProps(superClasssubClass);

    return 
subClass;
}


function 
mixIntoClass(theClassmembers) {
    
copyOwnProps(members.prototype || memberstheClass.prototype); // TODO: copyNativeMethods?
}
;;

var 
Emitter FC.Emitter = Class.extend({

    
callbackHashnull,


    
on: function(namecallback) {
        
this.getCallbacks(name).add(callback);
        return 
this// for chaining
    
},


    
off: function(namecallback) {
        
this.getCallbacks(name).remove(callback);
        return 
this// for chaining
    
},


    
trigger: function(name) { // args...
        
var args = Array.prototype.slice.call(arguments1);

        
this.triggerWith(namethisargs);

        return 
this// for chaining
    
},


    
triggerWith: function(namecontextargs) {
        var 
callbacks this.getCallbacks(name);

        
callbacks.fireWith(contextargs);

        return 
this// for chaining
    
},


    
getCallbacks: function(name) {
        var 
callbacks;

        if (!
this.callbackHash) {
            
this.callbackHash = {};
        }

        
callbacks this.callbackHash[name];
        if (!
callbacks) {
            
callbacks this.callbackHash[name] = $.Callbacks();
        }

        return 
callbacks;
    }

});
;;

/* 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)
*/

var Popover = Class.extend({

    
isHiddentrue,
    
optionsnull,
    
elnull// the container element for the popover. generated by this object
    
documentMousedownProxynull// document mousedown handler bound to `this`
    
margin10// the space required between the popover and the edges of the scroll container


    
constructor: function(options) {
        
this.options options || {};
    },


    
// 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 = $('<div class="fc-popover"/>')
            .
addClass(options.className || '')
            .
css({
                
// position initially to the top left to avoid creating scrollbars
                
top0,
                
left0
            
})
            .
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
    
removeElement: 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(topviewportTop viewportEl.outerHeight() - height this.margin);
            
top Math.max(topviewportTop this.margin);
            
left Math.min(leftviewportLeft viewportEl.outerWidth() - width this.margin);
            
left Math.max(leftviewportLeft this.margin);
        }

        
this.el.css({
            
toptop origin.top,
            
leftleft 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(arguments1));
        }
    }

});

;;

/*
A cache for the left/right/top/bottom/width/height values for one or more elements.
Works with both offset (from topleft document) and position (from offsetParent).

options:
- els
- isHorizontal
- isVertical
*/
var CoordCache FC.CoordCache = Class.extend({

    
elsnull// jQuery set (assumed to be siblings)
    
forcedOffsetParentElnull// options can override the natural offsetParent
    
originnull// {left,top} position of offsetParent of els
    
boundingRectnull// constrain cordinates to this rectangle. {left,right,top,bottom} or null
    
isHorizontalfalse// whether to query for left/right/width
    
isVerticalfalse// whether to query for top/bottom/height

    // arrays of coordinates (offsets from topleft of document)
    
leftsnull,
    
rightsnull,
    
topsnull,
    
bottomsnull,


    
constructor: function(options) {
        
this.els = $(options.els);
        
this.isHorizontal options.isHorizontal;
        
this.isVertical options.isVertical;
        
this.forcedOffsetParentEl options.offsetParent ? $(options.offsetParent) : null;
    },


    
// Queries the els for coordinates and stores them.
    // Call this method before using and of the get* methods below.
    
build: function() {
        var 
offsetParentEl this.forcedOffsetParentEl || this.els.eq(0).offsetParent();

        
this.origin offsetParentEl.offset();
        
this.boundingRect this.queryBoundingRect();

        if (
this.isHorizontal) {
            
this.buildElHorizontals();
        }
        if (
this.isVertical) {
            
this.buildElVerticals();
        }
    },


    
// Destroys all internal data about coordinates, freeing memory
    
clear: function() {
        
this.origin null;
        
this.boundingRect null;
        
this.lefts null;
        
this.rights null;
        
this.tops null;
        
this.bottoms null;
    },


    
// When called, if coord caches aren't built, builds them
    
ensureBuilt: function() {
        if (!
this.origin) {
            
this.build();
        }
    },


    
// Compute and return what the elements' bounding rectangle is, from the user's perspective.
    // Right now, only returns a rectangle if constrained by an overflow:scroll element.
    
queryBoundingRect: function() {
        var 
scrollParentEl getScrollParent(this.els.eq(0));

        if (!
scrollParentEl.is(document)) {
            return 
getClientRect(scrollParentEl);
        }
    },


    
// Populates the left/right internal coordinate arrays
    
buildElHorizontals: function() {
        var 
lefts = [];
        var 
rights = [];

        
this.els.each(function(inode) {
            var 
el = $(node);
            var 
left el.offset().left;
            var 
width el.outerWidth();

            
lefts.push(left);
            
rights.push(left width);
        });

        
this.lefts lefts;
        
this.rights rights;
    },


    
// Populates the top/bottom internal coordinate arrays
    
buildElVerticals: function() {
        var 
tops = [];
        var 
bottoms = [];

        
this.els.each(function(inode) {
            var 
el = $(node);
            var 
top el.offset().top;
            var 
height el.outerHeight();

            
tops.push(top);
            
bottoms.push(top height);
        });

        
this.tops tops;
        
this.bottoms bottoms;
    },


    
// Given a left offset (from document left), returns the index of the el that it horizontally intersects.
    // If no intersection is made, or outside of the boundingRect, returns undefined.
    
getHorizontalIndex: function(leftOffset) {
        
this.ensureBuilt();

        var 
boundingRect this.boundingRect;
        var 
lefts this.lefts;
        var 
rights this.rights;
        var 
len lefts.length;
        var 
i;

        if (!
boundingRect || (leftOffset >= boundingRect.left && leftOffset boundingRect.right)) {
            for (
0leni++) {
                if (
leftOffset >= lefts[i] && leftOffset rights[i]) {
                    return 
i;
                }
            }
        }
    },


    
// Given a top offset (from document top), returns the index of the el that it vertically intersects.
    // If no intersection is made, or outside of the boundingRect, returns undefined.
    
getVerticalIndex: function(topOffset) {
        
this.ensureBuilt();

        var 
boundingRect this.boundingRect;
        var 
tops this.tops;
        var 
bottoms this.bottoms;
        var 
len tops.length;
        var 
i;

        if (!
boundingRect || (topOffset >= boundingRect.top && topOffset boundingRect.bottom)) {
            for (
0leni++) {
                if (
topOffset >= tops[i] && topOffset bottoms[i]) {
                    return 
i;
                }
            }
        }
    },


    
// Gets the left offset (from document left) of the element at the given index
    
getLeftOffset: function(leftIndex) {
        
this.ensureBuilt();
        return 
this.lefts[leftIndex];
    },


    
// Gets the left position (from offsetParent left) of the element at the given index
    
getLeftPosition: function(leftIndex) {
        
this.ensureBuilt();
        return 
this.lefts[leftIndex] - this.origin.left;
    },


    
// Gets the right offset (from document left) of the element at the given index.
    // This value is NOT relative to the document's right edge, like the CSS concept of "right" would be.
    
getRightOffset: function(leftIndex) {
        
this.ensureBuilt();
        return 
this.rights[leftIndex];
    },


    
// Gets the right position (from offsetParent left) of the element at the given index.
    // This value is NOT relative to the offsetParent's right edge, like the CSS concept of "right" would be.
    
getRightPosition: function(leftIndex) {
        
this.ensureBuilt();
        return 
this.rights[leftIndex] - this.origin.left;
    },


    
// Gets the width of the element at the given index
    
getWidth: function(leftIndex) {
        
this.ensureBuilt();
        return 
this.rights[leftIndex] - this.lefts[leftIndex];
    },


    
// Gets the top offset (from document top) of the element at the given index
    
getTopOffset: function(topIndex) {
        
this.ensureBuilt();
        return 
this.tops[topIndex];
    },


    
// Gets the top position (from offsetParent top) of the element at the given position
    
getTopPosition: function(topIndex) {
        
this.ensureBuilt();
        return 
this.tops[topIndex] - this.origin.top;
    },

    
// Gets the bottom offset (from the document top) of the element at the given index.
    // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be.
    
getBottomOffset: function(topIndex) {
        
this.ensureBuilt();
        return 
this.bottoms[topIndex];
    },


    
// Gets the bottom position (from the offsetParent top) of the element at the given index.
    // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be.
    
getBottomPosition: function(topIndex) {
        
this.ensureBuilt();
        return 
this.bottoms[topIndex] - this.origin.top;
    },


    
// Gets the height of the element at the given index
    
getHeight: function(topIndex) {
        
this.ensureBuilt();
        return 
this.bottoms[topIndex] - this.tops[topIndex];
    }

});

;;

/* Tracks a drag's mouse movement, firing various handlers
----------------------------------------------------------------------------------------------------------------------*/
// TODO: use Emitter

var DragListener FC.DragListener = Class.extend({

    
optionsnull,

    
isListeningfalse,
    
isDraggingfalse,

    
// coordinates of the initial mousedown
    
originXnull,
    
originYnull,

    
// handler attached to the document, bound to the DragListener's `this`
    
mousemoveProxynull,
    
mouseupProxynull,

    
// for IE8 bug-fighting behavior, for now
    
subjectElnull// the element being draged. optional
    
subjectHrefnull,

    
scrollElnull,
    
scrollBoundsnull// { top, bottom, left, right }
    
scrollTopVelnull// pixels per second
    
scrollLeftVelnull// pixels per second
    
scrollIntervalIdnull// ID of setTimeout for scrolling animation loop
    
scrollHandlerProxynull// this-scoped function for handling when scrollEl is scrolled

    
scrollSensitivity30// pixels from edge for scrolling to start
    
scrollSpeed200// pixels per second, at maximum speed
    
scrollIntervalMs50// millisecond wait between scroll increment


    
constructor: function(options) {
        
options options || {};
        
this.options options;
        
this.subjectEl options.subjectEl;
    },


    
// 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;

        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);
                }
            }

            $(
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

            
if (ev) {
                
this.originX ev.pageX;
                
this.originY ev.pageY;
            }
            else {
                
// if no starting information was given, origin will be the topleft corner of the screen.
                // if so, dx/dy in the future will be the absolute coordinates.
                
this.originX 0;
                
this.originY 0;
            }

            
this.isListening true;
            
this.listenStart(ev);
        }
    },


    
// Called when drag listening has started (but a real drag has not necessarily began)
    
listenStart: function(ev) {
        
this.trigger('listenStart'ev);
    },


    
// Called when the user moves the mouse
    
mousemove: function(ev) {
        var 
dx ev.pageX this.originX;
        var 
dy ev.pageY this.originY;
        var 
minDistance;
        var 
distanceSq// current distance from the origin, 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 dx dx dy dy;
            if (
distanceSq >= minDistance minDistance) { // use pythagorean theorem
                
this.startDrag(ev);
            }
        }

        if (
this.isDragging) {
            
this.drag(dxdyev); // 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) {

        if (!
this.isListening) { // startDrag must have manually initiated
            
this.startListening();
        }

        if (!
this.isDragging) {
            
this.isDragging true;
            
this.dragStart(ev);
        }
    },


    
// Called when the actual drag has started (went beyond minDistance)
    
dragStart: function(ev) {
        var 
subjectEl this.subjectEl;

        
this.trigger('dragStart'ev);

        
// remove a mousedown'd <a>'s href so it is not visited (IE8 bug)
        
if ((this.subjectHref subjectEl subjectEl.attr('href') : null)) {
            
subjectEl.removeAttr('href');
        }
    },


    
// Called while the mouse is being moved and when we know a legitimate drag is taking place
    
drag: function(dxdyev) {
        
this.trigger('drag'dxdyev);
        
this.updateScroll(ev); // will possibly cause scrolling
    
},


    
// Called when the user does a mouseup
    
mouseup: function(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.dragStop(ev);
            
this.isDragging false;
        }
    },


    
// Called when dragging has been stopped
    
dragStop: function(ev) {
        var 
_this this;

        
this.trigger('dragStop'ev);

        
// restore a mousedown'd <a>'s href (for IE8 bug)
        
setTimeout(function() { // must be outside of the click's execution
            
if (_this.subjectHref) {
                
_this.subjectEl.attr('href'_this.subjectHref);
            }
        }, 
0);
    },


    
// Call this to stop listening to the user's mouse events
    
stopListening: function(ev) {
        
this.stopDrag(ev); // if there's a current drag, kill it

        
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.listenStop(ev);
        }
    },


    
// Called when drag listening has stopped
    
listenStop: function(ev) {
        
this.trigger('listenStop'ev);
    },


    
// 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(arguments1));
        }
    },


    
// 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;

        
this.scrollBounds el getOuterRect(el) : null;
            
// TODO: use getClientRect in future. but prevents auto scrolling when on top of scrollbars
    
},


    
// Called when the dragging is in progress and scrolling should be updated
    
updateScroll: function(ev) {
        var 
sensitivity this.scrollSensitivity;
        var 
bounds this.scrollBounds;
        var 
topClosenessbottomCloseness;
        var 
leftClosenessrightCloseness;
        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 >= && topCloseness <= 1) {
                
topVel topCloseness this.scrollSpeed * -1// negative. for scrolling up
            
}
            else if (
bottomCloseness >= && bottomCloseness <= 1) {
                
topVel bottomCloseness this.scrollSpeed;
            }

            
// translate horizontal closeness into velocity
            
if (leftCloseness >= && leftCloseness <= 1) {
                
leftVel leftCloseness this.scrollSpeed * -1// negative. for scrolling left
            
}
            else if (
rightCloseness >= && rightCloseness <= 1) {
                
leftVel rightCloseness this.scrollSpeed;
            }
        }

        
this.setScrollVel(topVelleftVel);
    },


    
// Sets the speed-of-scrolling for the scrollEl
    
setScrollVel: function(topVelleftVel) {

        
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.scrollStop();
        }
    },


    
// 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.scrollStop();
        }
    },


    
// Called when scrolling has stopped, whether through auto scroll, or the user scrolling
    
scrollStop: function() {
    }

});

;;

/* Tracks mouse movements over a component and raises events about which hit the mouse is over.
------------------------------------------------------------------------------------------------------------------------
options:
- subjectEl
- subjectCenter
*/

var HitDragListener DragListener.extend({

    
componentnull// converts coordinates to hits
        // methods: prepareHits, releaseHits, queryHit

    
origHitnull// the hit the mouse was over when listening started
    
hitnull// the hit the mouse is over
    
coordAdjustnull// delta that will be added to the mouse coordinates when computing collisions


    
constructor: function(componentoptions) {
        
DragListener.call(thisoptions); // call the super-constructor

        
this.component component;
    },


    
// Called when drag listening starts (but a real drag has not necessarily began).
    // ev might be undefined if dragging was started manually.
    
listenStart: function(ev) {
        var 
subjectEl this.subjectEl;
        var 
subjectRect;
        var 
origPoint;
        var 
point;

        
DragListener.prototype.listenStart.apply(thisarguments); // call the super-method

        
this.computeCoords();

        if (
ev) {
            
origPoint = { leftev.pageXtopev.pageY };
            
point origPoint;

            
// constrain the point to bounds of the element being dragged
            
if (subjectEl) {
                
subjectRect getOuterRect(subjectEl); // used for centering as well
                
point constrainPoint(pointsubjectRect);
            }

            
this.origHit this.queryHit(point.leftpoint.top);

            
// treat the center of the subject as the collision point?
            
if (subjectEl && this.options.subjectCenter) {

                
// only consider the area the subject overlaps the hit. best for large subjects.
                // TODO: skip this if hit didn't supply left/right/top/bottom
                
if (this.origHit) {
                    
subjectRect intersectRects(this.origHitsubjectRect) ||
                        
subjectRect// in case there is no intersection
                
}

                
point getRectCenter(subjectRect);
            }

            
this.coordAdjust diffPoints(pointorigPoint); // point - origPoint
        
}
        else {
            
this.origHit null;
            
this.coordAdjust null;
        }
    },


    
// Recomputes the drag-critical positions of elements
    
computeCoords: function() {
        
this.component.prepareHits();
        
this.computeScrollBounds(); // why is this here???
    
},


    
// Called when the actual drag has started
    
dragStart: function(ev) {
        var 
hit;

        
DragListener.prototype.dragStart.apply(thisarguments); // call the super-method

        // might be different from this.origHit if the min-distance is large
        
hit this.queryHit(ev.pageXev.pageY);

        
// report the initial hit the mouse is over
        // especially important if no min-distance and drag starts immediately
        
if (hit) {
            
this.hitOver(hit);
        }
    },


    
// Called when the drag moves
    
drag: function(dxdyev) {
        var 
hit;

        
DragListener.prototype.drag.apply(thisarguments); // call the super-method

        
hit this.queryHit(ev.pageXev.pageY);

        if (!
isHitsEqual(hitthis.hit)) { // a different hit than before?
            
if (this.hit) {
                
this.hitOut();
            }
            if (
hit) {
                
this.hitOver(hit);
            }
        }
    },


    
// Called when dragging has been stopped
    
dragStop: function() {
        
this.hitDone();
        
DragListener.prototype.dragStop.apply(thisarguments); // call the super-method
    
},


    
// Called when a the mouse has just moved over a new hit
    
hitOver: function(hit) {
        var 
isOrig isHitsEqual(hitthis.origHit);

        
this.hit hit;

        
this.trigger('hitOver'this.hitisOrigthis.origHit);
    },


    
// Called when the mouse has just moved out of a hit
    
hitOut: function() {
        if (
this.hit) {
            
this.trigger('hitOut'this.hit);
            
this.hitDone();
            
this.hit null;
        }
    },


    
// Called after a hitOut. Also called before a dragStop
    
hitDone: function() {
        if (
this.hit) {
            
this.trigger('hitDone'this.hit);
        }
    },


    
// Called when drag listening has stopped
    
listenStop: function() {
        
DragListener.prototype.listenStop.apply(thisarguments); // call the super-method

        
this.origHit null;
        
this.hit null;

        
this.component.releaseHits();
    },


    
// Called when scrolling has stopped, whether through auto scroll, or the user scrolling
    
scrollStop: function() {
        
DragListener.prototype.scrollStop.apply(thisarguments); // call the super-method

        
this.computeCoords(); // hits' absolute positions will be in new places. recompute
    
},


    
// Gets the hit underneath the coordinates for the given mouse event
    
queryHit: function(lefttop) {

        if (
this.coordAdjust) {
            
left += this.coordAdjust.left;
            
top += this.coordAdjust.top;
        }

        return 
this.component.queryHit(lefttop);
    }

});


// Returns `true` if the hits are identically equal. `false` otherwise. Must be from the same component.
// Two null values will be considered equal, as two "out of the component" states are the same.
function isHitsEqual(hit0hit1) {

    if (!
hit0 && !hit1) {
        return 
true;
    }

    if (
hit0 && hit1) {
        return 
hit0.component === hit1.component &&
            
isHitPropsWithin(hit0hit1) &&
            
isHitPropsWithin(hit1hit0); // ensures all props are identical
    
}

    return 
false;
}


// Returns true if all of subHit's non-standard properties are within superHit
function isHitPropsWithin(subHitsuperHit) {
    for (var 
propName in subHit) {
        if (!/^(
component|left|right|top|bottom)$/.test(propName)) {
            if (
subHit[propName] !== superHit[propName]) {
                return 
false;
            }
        }
    }
    return 
true;
}

;;

/* Creates a clone of an element and lets it track the mouse as it moves
----------------------------------------------------------------------------------------------------------------------*/

var MouseFollower = Class.extend({

    
optionsnull,

    
sourceElnull// the element that will be cloned and made to look like it is dragging
    
elnull// the clone of `sourceEl` that will track the mouse
    
parentElnull// 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
    
top0null,
    
left0null,

    
// the initial position of the mouse
    
mouseY0null,
    
mouseX0null,

    
// the number of pixels the mouse has moved from its initial position
    
topDeltanull,
    
leftDeltanull,

    
mousemoveProxynull// document mousemove handler, bound to the MouseFollower's `this`

    
isFollowingfalse,
    
isHiddenfalse,
    
isAnimatingfalse// doing the revert animation?

    
constructor: function(sourceEloptions) {
        
this.options options options || {};
        
this.sourceEl sourceEl;
        
this.parentEl options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent
    
},


    
// 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(shouldRevertcallback) {
        var 
_this this;
        var 
revertDuration this.options.revertDuration;

        function 
complete() {
            
this.isAnimating false;
            
_this.removeElement();

            
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({
                    
topthis.top0,
                    
leftthis.left0
                
}, {
                    
durationrevertDuration,
                    
completecomplete
                
});
            }
            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())
                    
displaythis.isHidden 'none' ''// for when initially hidden
                    
margin0,
                    
right'auto'// erase and set width instead
                    
bottom'auto'// erase and set height instead
                    
widththis.sourceEl.width(), // explicit height in case there was a 'right' value
                    
heightthis.sourceEl.height(), // explicit width in case there was a 'bottom' value
                    
opacitythis.options.opacity || '',
                    
zIndexthis.options.zIndex
                
})
                .
appendTo(this.parentEl);
        }

        return 
el;
    },


    
// Removes the tracking element if it has already been created
    
removeElement: 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({
            
topthis.top0 this.topDelta,
            
leftthis.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();
        }
    }

});

;;

/* An abstract class comprised of a "grid" of areas that each represent a specific datetime
----------------------------------------------------------------------------------------------------------------------*/

var Grid FC.Grid = Class.extend({

    
viewnull// a View object
    
isRTLnull// shortcut to the view's isRTL option

    
startnull,
    
endnull,

    
elnull// the containing element
    
elsByFillnull// a hash of jQuery element sets used for rendering each fill. Keyed by fill name.

    
externalDragStartProxynull// binds the Grid's scope to externalDragStart (in DayGrid.events)

    // derived from options
    
eventTimeFormatnull,
    
displayEventTimenull,
    
displayEventEndnull,

    
minResizeDurationnull// TODO: hack. set by subclasses. minumum event resize duration

    // if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity
    // of the date areas. if not defined, assumes to be day and time granularity.
    // TODO: port isTimeScale into same system?
    
largeUnitnull,


    
constructor: function(view) {
        
this.view view;
        
this.isRTL view.opt('isRTL');

        
this.elsByFill = {};
        
this.externalDragStartProxy proxy(this'externalDragStart');
    },


    
/* Options
    ------------------------------------------------------------------------------------------------------------------*/


    // Generates the format string used for event time text, if not explicitly defined by 'timeFormat'
    
computeEventTimeFormat: function() {
        return 
this.view.opt('smallTimeFormat');
    },


    
// Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventTime'.
    // Only applies to non-all-day events.
    
computeDisplayEventTime: function() {
        return 
true;
    },


    
// Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd'
    
computeDisplayEventEnd: function() {
        return 
true;
    },


    
/* Dates
    ------------------------------------------------------------------------------------------------------------------*/


    // Tells the grid about what period of time to display.
    // Any date-related internal data should be generated.
    
setRange: function(range) {
        
this.start range.start.clone();
        
this.end range.end.clone();

        
this.rangeUpdated();
        
this.processRangeOptions();
    },


    
// Called when internal variables that rely on the range should be updated
    
rangeUpdated: function() {
    },


    
// Updates values that rely on options and also relate to range
    
processRangeOptions: function() {
        var 
view this.view;
        var 
displayEventTime;
        var 
displayEventEnd;

        
this.eventTimeFormat =
            
view.opt('eventTimeFormat') ||
            
view.opt('timeFormat') || // deprecated
            
this.computeEventTimeFormat();

        
displayEventTime view.opt('displayEventTime');
        if (
displayEventTime == null) {
            
displayEventTime this.computeDisplayEventTime(); // might be based off of range
        
}

        
displayEventEnd view.opt('displayEventEnd');
        if (
displayEventEnd == null) {
            
displayEventEnd this.computeDisplayEventEnd(); // might be based off of range
        
}

        
this.displayEventTime displayEventTime;
        
this.displayEventEnd displayEventEnd;
    },


    
// Converts a span (has unzoned start/end and any other grid-specific location information)
    // into an array of segments (pieces of events whose format is decided by the grid).
    
spanToSegs: function(span) {
        
// subclasses must implement
    
},


    
// Diffs the two dates, returning a duration, based on granularity of the grid
    // TODO: port isTimeScale into this system?
    
diffDates: function(ab) {
        if (
this.largeUnit) {
            return 
diffByUnit(abthis.largeUnit);
        }
        else {
            return 
diffDayTime(ab);
        }
    },


    
/* Hit Area
    ------------------------------------------------------------------------------------------------------------------*/


    // Called before one or more queryHit calls might happen. Should prepare any cached coordinates for queryHit
    
prepareHits: function() {
    },


    
// Called when queryHit calls have subsided. Good place to clear any coordinate caches.
    
releaseHits: function() {
    },


    
// Given coordinates from the topleft of the document, return data about the date-related area underneath.
    // Can return an object with arbitrary properties (although top/right/left/bottom are encouraged).
    // Must have a `grid` property, a reference to this current grid. TODO: avoid this
    // The returned object will be processed by getHitSpan and getHitEl.
    
queryHit: function(leftOffsettopOffset) {
    },


    
// Given position-level information about a date-related area within the grid,
    // should return an object with at least a start/end date. Can provide other information as well.
    
getHitSpan: function(hit) {
    },


    
// Given position-level information about a date-related area within the grid,
    // should return a jQuery element that best represents it. passed to dayClick callback.
    
getHitEl: function(hit) {
    },


    
/* Rendering
    ------------------------------------------------------------------------------------------------------------------*/


    // Sets the container element that the grid should render inside of.
    // Does other DOM-related initializations.
    
setElement: function(el) {
        var 
_this this;

        
this.el el;

        
// attach a handler to the grid's root element.
        // jQuery will take care of unregistering them when removeElement gets called.
        
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);
            }
        });

        
// attach event-element-related handlers. in Grid.events
        // same garbage collection note as above.
        
this.bindSegHandlers();

        
this.bindGlobalHandlers();
    },


    
// Removes the grid's container element from the DOM. Undoes any other DOM-related attachments.
    // DOES NOT remove any content beforehand (doesn't clear events or call unrenderDates), unlike View
    
removeElement: function() {
        
this.unbindGlobalHandlers();

        
this.el.remove();

        
// NOTE: we don't null-out this.el for the same reasons we don't do it within View::removeElement
    
},


    
// Renders the basic structure of grid view before any content is rendered
    
renderSkeleton: function() {
        
// subclasses should implement
    
},


    
// Renders the grid's date-related content (like areas that represent days/times).
    // Assumes setRange has already been called and the skeleton has already been rendered.
    
renderDates: function() {
        
// subclasses should implement
    
},


    
// Unrenders the grid's date-related content
    
unrenderDates: function() {
        
// subclasses should implement
    
},


    
/* Handlers
    ------------------------------------------------------------------------------------------------------------------*/


    // Binds DOM handlers to elements that reside outside the grid, such as the document
    
bindGlobalHandlers: function() {
        $(
document).on('dragstart sortstart'this.externalDragStartProxy); // jqui
    
},


    
// Unbinds DOM handlers from elements that reside outside the grid
    
unbindGlobalHandlers: function() {
        $(
document).off('dragstart sortstart'this.externalDragStartProxy); // jqui
    
},


    
// 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 
dayClickHit// null if invalid dayClick
        
var selectionSpan// null if invalid selection

        // 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 HitDragListener(this, {
            
//distance: 5, // needs more work if we want dayClick to fire correctly
            
scrollview.opt('dragScroll'),
            
dragStart: function() {
                
view.unselect(); // since we could be rendering a new selection, we want to clear any old one
            
},
            
hitOver: function(hitisOrigorigHit) {
                if (
origHit) { // click needs to have started on a hit
                    
dayClickHit isOrig hit null// single-hit selection is a day click
                    
if (isSelectable) {
                        
selectionSpan _this.computeSelection(
                            
_this.getHitSpan(origHit),
                            
_this.getHitSpan(hit)
                        );
                        if (
selectionSpan) {
                            
_this.renderSelection(selectionSpan);
                        }
                        else if (
selectionSpan === false) {
                            
disableCursor();
                        }
                    }
                }
            },
            
hitOut: function() {
                
dayClickHit null;
                
selectionSpan null;
                
_this.unrenderSelection();
                
enableCursor();
            },
            
listenStop: function(ev) {
                if (
dayClickHit) {
                    
view.triggerDayClick(
                        
_this.getHitSpan(dayClickHit),
                        
_this.getHitEl(dayClickHit),
                        
ev
                    
);
                }
                if (
selectionSpan) {
                    
// the selection will already have been rendered. just report it
                    
view.reportSelection(selectionSpanev);
                }
                
enableCursor();
            }
        });

        
dragListener.mousedown(ev); // start listening, which will eventually initiate a dragStart
    
},


    
/* Event Helper
    ------------------------------------------------------------------------------------------------------------------*/
    // TODO: should probably move this to Grid.events, like we did event dragging / resizing


    // Renders a mock event at the given event location, which contains zoned start/end properties.
    
renderEventLocationHelper: function(eventLocationsourceSeg) {
        var 
fakeEvent this.fabricateHelperEvent(eventLocationsourceSeg);

        
this.renderHelper(fakeEventsourceSeg); // do the actual rendering
    
},


    
// Builds a fake event given zoned event date properties and a segment is should be inspired from.
    // The range'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.
    
fabricateHelperEvent: function(eventLocationsourceSeg) {
        var 
fakeEvent sourceSeg createObject(sourceSeg.event) : {}; // mask the original event object if possible

        
fakeEvent.start eventLocation.start.clone();
        
fakeEvent.end eventLocation.end eventLocation.end.clone() : null;
        
fakeEvent.allDay null// force it to be freshly computed by normalizeEventDates
        
this.view.calendar.normalizeEventDates(fakeEvent);

        
// 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;
        }

        return 
fakeEvent;
    },


    
// Renders a mock event. Given zoned event date properties.
    
renderHelper: function(eventLocationsourceSeg) {
        
// subclasses must implement
    
},


    
// Unrenders a mock event
    
unrenderHelper: function() {
        
// subclasses must implement
    
},


    
/* Selection
    ------------------------------------------------------------------------------------------------------------------*/


    // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses.
    // Given a span (unzoned start/end and other misc data)
    
renderSelection: function(span) {
        
this.renderHighlight(span);
    },


    
// Unrenders any visual indications of a selection. Will unrender a highlight by default.
    
unrenderSelection: function() {
        
this.unrenderHighlight();
    },


    
// Given the first and last date-spans of a selection, returns another date-span object.
    // Subclasses can override and provide additional data in the span object. Will be passed to renderSelection().
    // Will return false if the selection is invalid and this should be indicated to the user.
    // Will return null/undefined if a selection invalid but no error should be reported.
    
computeSelection: function(span0span1) {
        var 
span this.computeSelectionSpan(span0span1);

        if (
span && !this.view.calendar.isSelectionSpanAllowed(span)) {
            return 
false;
        }

        return 
span;
    },


    
// Given two spans, must return the combination of the two.
    // TODO: do this separation of concerns (combining VS validation) for event dnd/resize too.
    
computeSelectionSpan: function(span0span1) {
        var 
dates = [ span0.startspan0.endspan1.startspan1.end ];

        
dates.sort(compareNumbers); // sorts chronologically. works with Moments

        
return { startdates[0].clone(), enddates[3].clone() };
    },


    
/* Highlight
    ------------------------------------------------------------------------------------------------------------------*/


    // Renders an emphasis on the given date range. Given a span (unzoned start/end and other misc data)
    
renderHighlight: function(span) {
        
this.renderFill('highlight'this.spanToSegs(span));
    },


    
// Unrenders the emphasis on a date range
    
unrenderHighlight: function() {
        
this.unrenderFill('highlight');
    },


    
// Generates an array of classNames for rendering the highlight. Used by the fill system.
    
highlightSegClasses: function() {
        return [ 
'fc-highlight' ];
    },


    
/* Business Hours
    ------------------------------------------------------------------------------------------------------------------*/


    
renderBusinessHours: function() {
    },


    
unrenderBusinessHours: function() {
    },


    
/* Now Indicator
    ------------------------------------------------------------------------------------------------------------------*/


    
getNowIndicatorUnit: function() {
    },


    
renderNowIndicator: function(date) {
    },


    
unrenderNowIndicator: function() {
    },


    
/* Fill System (highlight, background events, business hours)
    --------------------------------------------------------------------------------------------------------------------
    TODO: remove this system. like we did in TimeGrid
    */


    // Renders a set of rectangles over the given segments of time.
    // MUST RETURN a subset of segs, the segs that were actually rendered.
    // Responsible for populating this.elsByFill. TODO: better API for expressing this requirement
    
renderFill: function(typesegs) {
        
// subclasses must implement
    
},


    
// Unrenders a specific type of fill that is currently rendered on the grid
    
unrenderFill: function(type) {
        var 
el this.elsByFill[type];

        if (
el) {
            
el.remove();
            
delete this.elsByFill[type];
        }
    },


    
// Renders and assigns an `el` property for each fill segment. Generic enough to work with different types.
    // Only returns segments that successfully rendered.
    // To be harnessed by renderFill (implemented by subclasses).
    // Analagous to renderFgSegEls.
    
renderFillSegEls: function(typesegs) {
        var 
_this this;
        var 
segElMethod this[type 'SegEl'];
        var 
html '';
        var 
renderedSegs = [];
        var 
i;

        if (
segs.length) {

            
// build a large concatenation of segment HTML
            
for (0segs.lengthi++) {
                
html += this.fillSegHtml(typesegs[i]);
            }

            
// Grab individual elements from the combined HTML string. Use each as the default rendering.
            // Then, compute the 'el' for each segment.
            
$(html).each(function(inode) {
                var 
seg segs[i];
                var 
el = $(node);

                
// allow custom filter methods per-type
                
if (segElMethod) {
                    
el segElMethod.call(_thissegel);
                }

                if (
el) { // custom filters did not cancel the render
                    
el = $(el); // allow custom filter to return raw DOM node

                    // correct element type? (would be bad if a non-TD were inserted into a table for example)
                    
if (el.is(_this.fillSegTag)) {
                        
seg.el el;
                        
renderedSegs.push(seg);
                    }
                }
            });
        }

        return 
renderedSegs;
    },


    
fillSegTag'div'// subclasses can override


    // Builds the HTML needed for one fill segment. Generic enought o work with different types.
    
fillSegHtml: function(typeseg) {

        
// custom hooks per-type
        
var classesMethod this[type 'SegClasses'];
        var 
cssMethod this[type 'SegCss'];

        var 
classes classesMethod classesMethod.call(thisseg) : [];
        var 
css cssToStr(cssMethod cssMethod.call(thisseg) : {});

        return 
'<' this.fillSegTag +
            (
classes.length ' class="' classes.join(' ') + '"' '') +
            (
css ' style="' css '"' '') +
            
' />';
    },



    
/* Generic rendering utilities for subclasses
    ------------------------------------------------------------------------------------------------------------------*/


    // Computes HTML classNames for a single-day element
    
getDayClasses: function(date) {
        var 
view this.view;
        var 
today view.calendar.getNow();
        var 
classes = [ 'fc-' dayIDs[date.day()] ];

        if (
            
view.intervalDuration.as('months') == &&
            
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
----------------------------------------------------------------------------------------------------------------------*/

Grid.mixin({

    
mousedOverSegnull// the segment object the user's mouse is over. null if over nothing
    
isDraggingSegfalse// is a segment being dragged? boolean
    
isResizingSegfalse// is a segment being resized? boolean
    
isDraggingExternalfalse// jqui-dragging an external element? boolean
    
segsnull// the *event* segments currently rendered in the grid. TODO: rename to `eventSegs`


    // Renders the given events onto the grid
    
renderEvents: function(events) {
        var 
bgEvents = [];
        var 
fgEvents = [];
        var 
i;

        for (
0events.lengthi++) {
            (
isBgEvent(events[i]) ? bgEvents fgEvents).push(events[i]);
        }

        
this.segs = [].concat// record all segs
            
this.renderBgEvents(bgEvents),
            
this.renderFgEvents(fgEvents)
        );
    },


    
renderBgEvents: function(events) {
        var 
segs this.eventsToSegs(events);

        
// renderBgSegs might return a subset of segs, segs that were actually rendered
        
return this.renderBgSegs(segs) || segs;
    },


    
renderFgEvents: function(events) {
        var 
segs this.eventsToSegs(events);

        
// renderFgSegs might return a subset of segs, segs that were actually rendered
        
return this.renderFgSegs(segs) || segs;
    },


    
// Unrenders all events currently rendered on the grid
    
unrenderEvents: function() {
        
this.triggerSegMouseout(); // trigger an eventMouseout if user's mouse is over an event

        
this.unrenderFgSegs();
        
this.unrenderBgSegs();

        
this.segs null;
    },


    
// Retrieves all rendered segment objects currently rendered on the grid
    
getEventSegs: function() {
        return 
this.segs || [];
    },


    
/* Foreground Segment Rendering
    ------------------------------------------------------------------------------------------------------------------*/


    // Renders foreground event segments onto the grid. May return a subset of segs that were rendered.
    
renderFgSegs: function(segs) {
        
// subclasses must implement
    
},


    
// Unrenders all currently rendered foreground segments
    
unrenderFgSegs: function() {
        
// subclasses must implement
    
},


    
// Renders and assigns an `el` property for each foreground event segment.
    // Only returns segments that successfully rendered.
    // A utility that subclasses may use.
    
renderFgSegEls: function(segsdisableResizing) {
        var 
view this.view;
        var 
html '';
        var 
renderedSegs = [];
        var 
i;

        if (
segs.length) { // don't build an empty html string

            // build a large concatenation of event segment HTML
            
for (0segs.lengthi++) {
                
html += this.fgSegHtml(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(inode) {
                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 foreground event segment. Used by renderFgSegEls()
    
fgSegHtml: function(segdisableResizing) {
        
// subclasses should implement
    
},


    
/* Background Segment Rendering
    ------------------------------------------------------------------------------------------------------------------*/


    // Renders the given background event segments onto the grid.
    // Returns a subset of the segs that were actually rendered.
    
renderBgSegs: function(segs) {
        return 
this.renderFill('bgEvent'segs);
    },


    
// Unrenders all the currently rendered background event segments
    
unrenderBgSegs: function() {
        
this.unrenderFill('bgEvent');
    },


    
// Renders a background event element, given the default rendering. Called by the fill system.
    
bgEventSegEl: function(segel) {
        return 
this.view.resolveEventEl(seg.eventel); // will filter through eventRender
    
},


    
// Generates an array of classNames to be used for the default rendering of a background event.
    // Called by the fill system.
    
bgEventSegClasses: function(seg) {
        var 
event seg.event;
        var 
source event.source || {};

        return [ 
'fc-bgevent' ].concat(
            
event.className,
            
source.className || []
        );
    },


    
// Generates a semicolon-separated CSS string to be used for the default rendering of a background event.
    // Called by the fill system.
    
bgEventSegCss: function(seg) {
        return {
            
'background-color'this.getSegSkinCss(seg)['background-color']
        };
    },


    
// Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system.
    
businessHoursSegClasses: function(seg) {
        return [ 
'fc-nonbusiness''fc-bgevent' ];
    },


    
/* 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(segev) {
                    
_this.triggerSegMouseover(segev);
                },
                
mouseleave: function(segev) {
                    
_this.triggerSegMouseout(segev);
                },
                
click: function(segev) {
                    return 
view.trigger('eventClick'thisseg.eventev); // can return `false` to cancel
                
},
                
mousedown: function(segev) {
                    if ($(
ev.target).is('.fc-resizer') && view.isEventResizable(seg.event)) {
                        
_this.segResizeMousedown(segev, $(ev.target).is('.fc-start-resizer'));
                    }
                    else if (
view.isEventDraggable(seg.event)) {
                        
_this.segDragMousedown(segev);
                    }
                }
            },
            function(
namefunc) {
                
// 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(thissegev); // `this` will be the event element
                    
}
                });
            }
        );
    },


    
// Updates internal state and triggers handlers for when an event element is moused over
    
triggerSegMouseover: function(segev) {
        if (!
this.mousedOverSeg) {
            
this.mousedOverSeg seg;
            
this.view.trigger('eventMouseover'seg.el[0], seg.eventev);
        }
    },


    
// 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(segev) {
        
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.eventev);
        }
    },


    
/* Event 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(segev) {
        var 
_this this;
        var 
view this.view;
        var 
calendar view.calendar;
        var 
el seg.el;
        var 
event seg.event;
        var 
dropLocation// zoned event date properties

        // A clone of the original element that will move with the mouse
        
var mouseFollower = new MouseFollower(seg.el, {
            
parentElview.el,
            
opacityview.opt('dragOpacity'),
            
revertDurationview.opt('dragRevertDuration'),
            
zIndex// 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 HitDragListener(view, {
            
distance5,
            
scrollview.opt('dragScroll'),
            
subjectElel,
            
subjectCentertrue,
            
listenStart: function(ev) {
                
mouseFollower.hide(); // don't show until we know this is a real drag
                
mouseFollower.start(ev);
            },
            
dragStart: function(ev) {
                
_this.triggerSegMouseout(segev); // ensure a mouseout on the manipulated event has been reported
                
_this.segDragStart(segev);
                
view.hideEvent(event); // hide all event segments. our mouseFollower will take over
            
},
            
hitOver: function(hitisOrigorigHit) {

                
// starting hit could be forced (DayGrid.limit)
                
if (seg.hit) {
                    
origHit seg.hit;
                }

                
// since we are querying the parent view, might not belong to this grid
                
dropLocation _this.computeEventDrop(
                    
origHit.component.getHitSpan(origHit),
                    
hit.component.getHitSpan(hit),
                    
event
                
);

                if (
dropLocation && !calendar.isEventSpanAllowed(_this.eventToSpan(dropLocation), event)) {
                    
disableCursor();
                    
dropLocation null;
                }

                
// if a valid drop location, have the subclass render a visual indication
                
if (dropLocation && view.renderDrag(dropLocationseg)) {
                    
mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own
                
}
                else {
                    
mouseFollower.show(); // otherwise, have the helper follow the mouse (no snapping)
                
}

                if (
isOrig) {
                    
dropLocation null// needs to have moved hits to be a valid drop
                
}
            },
            
hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
                
view.unrenderDrag(); // unrender whatever was done in renderDrag
                
mouseFollower.show(); // show in case we are moving out of all hits
                
dropLocation null;
            },
            
hitDone: function() { // Called after a hitOut OR before a dragStop
                
enableCursor();
            },
            
dragStop: function(ev) {
                
// do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
                
mouseFollower.stop(!dropLocation, function() {
                    
view.unrenderDrag();
                    
view.showEvent(event);
                    
_this.segDragStop(segev);

                    if (
dropLocation) {
                        
view.reportEventDrop(eventdropLocationthis.largeUnitelev);
                    }
                });
            },
            
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
    
},


    
// Called before event segment dragging starts
    
segDragStart: function(segev) {
        
this.isDraggingSeg true;
        
this.view.trigger('eventDragStart'seg.el[0], seg.eventev, {}); // last argument is jqui dummy
    
},


    
// Called after event segment dragging stops
    
segDragStop: function(segev) {
        
this.isDraggingSeg false;
        
this.view.trigger('eventDragStop'seg.el[0], seg.eventev, {}); // last argument is jqui dummy
    
},


    
// Given the spans an event drag began, and the span event was dropped, calculates the new zoned start/end/allDay
    // values for the event. Subclasses may override and set additional properties to be used by renderDrag.
    // A falsy returned value indicates an invalid drop.
    // DOES NOT consider overlap/constraint.
    
computeEventDrop: function(startSpanendSpanevent) {
        var 
calendar this.view.calendar;
        var 
dragStart startSpan.start;
        var 
dragEnd endSpan.start;
        var 
delta;
        var 
dropLocation// zoned event date properties

        
if (dragStart.hasTime() === dragEnd.hasTime()) {
            
delta this.diffDates(dragEnddragStart);

            
// if an all-day event was in a timed area and it was dragged to a different time,
            // guarantee an end and adjust start/end to have times
            
if (event.allDay && durationHasTime(delta)) {
                
dropLocation = {
                    
startevent.start.clone(),
                    
endcalendar.getEventEnd(event), // will be an ambig day
                    
allDayfalse // for normalizeEventTimes
                
};
                
calendar.normalizeEventTimes(dropLocation);
            }
            
// othewise, work off existing values
            
else {
                
dropLocation = {
                    
startevent.start.clone(),
                    
endevent.end event.end.clone() : null,
                    
allDayevent.allDay // keep it the same
                
};
            }

            
dropLocation.start.add(delta);
            if (
dropLocation.end) {
                
dropLocation.end.add(delta);
            }
        }
        else {
            
// if switching from day <-> timed, start should be reset to the dropped date, and the end cleared
            
dropLocation = {
                
startdragEnd.clone(),
                
endnull// end should be cleared
                
allDay: !dragEnd.hasTime()
            };
        }

        return 
dropLocation;
    },


    
// Utility for apply dragOpacity to a jQuery set
    
applyDragOpacity: function(els) {
        var 
opacity this.view.opt('dragOpacity');

        if (
opacity != null) {
            
els.each(function(inode) {
                
// Don't use jQuery (will set an IE filter), do it the old fashioned way.
                // In IE8, a helper element will disappears if there's a filter.
                
node.style.opacity opacity;
            });
        }
    },


    
/* External Element Dragging
    ------------------------------------------------------------------------------------------------------------------*/


    // Called when a jQuery UI drag is initiated anywhere in the DOM
    
externalDragStart: function(evui) {
        var 
view this.view;
        var 
el;
        var 
accept;

        if (
view.opt('droppable')) { // only listen if this setting is on
            
el = $((ui ui.item null) || ev.target);

            
// Test that the dragged element passes the dropAccept selector or filter function.
            // FYI, the default is "*" (matches all)
            
accept view.opt('dropAccept');
            if ($.
isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) {
                if (!
this.isDraggingExternal) { // prevent double-listening if fired twice
                    
this.listenToExternalDrag(elevui);
                }
            }
        }
    },


    
// Called when a jQuery UI drag starts and it needs to be monitored for dropping
    
listenToExternalDrag: function(elevui) {
        var 
_this this;
        var 
calendar this.view.calendar;
        var 
meta getDraggedElMeta(el); // extra data about event drop, including possible event to create
        
var dropLocation// a null value signals an unsuccessful drag

        // listener that tracks mouse movement over date-associated pixel regions
        
var dragListener = new HitDragListener(this, {
            
listenStart: function() {
                
_this.isDraggingExternal true;
            },
            
hitOver: function(hit) {
                
dropLocation _this.computeExternalDrop(
                    
hit.component.getHitSpan(hit), // since we are querying the parent view, might not belong to this grid
                    
meta
                
);

                if ( 
// invalid hit?
                    
dropLocation &&
                    !
calendar.isExternalSpanAllowed(_this.eventToSpan(dropLocation), dropLocationmeta.eventProps)
                ) {
                    
disableCursor();
                    
dropLocation null;
                }

                if (
dropLocation) {
                    
_this.renderDrag(dropLocation); // called without a seg parameter
                
}
            },
            
hitOut: function() {
                
dropLocation null// signal unsuccessful
            
},
            
hitDone: function() { // Called after a hitOut OR before a dragStop
                
enableCursor();
                
_this.unrenderDrag();
            },
            
dragStop: function() {
                if (
dropLocation) { // element was dropped on a valid hit
                    
_this.view.reportExternalDrop(metadropLocationelevui);
                }
            },
            
listenStop: function() {
                
_this.isDraggingExternal false;
            }
        });

        
dragListener.startDrag(ev); // start listening immediately
    
},


    
// Given a hit to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object),
    // returns the zoned start/end dates for the event that would result from the hypothetical drop. end might be null.
    // Returning a null value signals an invalid drop hit.
    // DOES NOT consider overlap/constraint.
    
computeExternalDrop: function(spanmeta) {
        var 
calendar this.view.calendar;
        var 
dropLocation = {
            
startcalendar.applyTimezone(span.start), // simulate a zoned event start date
            
endnull
        
};

        
// if dropped on an all-day span, and element's metadata specified a time, set it
        
if (meta.startTime && !dropLocation.start.hasTime()) {
            
dropLocation.start.time(meta.startTime);
        }

        if (
meta.duration) {
            
dropLocation.end dropLocation.start.clone().add(meta.duration);
        }

        return 
dropLocation;
    },



    
/* Drag Rendering (for both events and an external elements)
    ------------------------------------------------------------------------------------------------------------------*/


    // Renders a visual indication of an event or external element being dragged.
    // `dropLocation` contains hypothetical start/end/allDay values the event would have if dropped. end can be null.
    // `seg` is the internal segment object that is being dragged. If dragging an external element, `seg` is null.
    // A truthy returned value indicates this method has rendered a helper element.
    
renderDrag: function(dropLocationseg) {
        
// subclasses must implement
    
},


    
// Unrenders a visual indication of an event or external element being dragged
    
unrenderDrag: function() {
        
// subclasses must implement
    
},


    
/* 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(segevisStart) {
        var 
_this this;
        var 
view this.view;
        var 
calendar view.calendar;
        var 
el seg.el;
        var 
event seg.event;
        var 
eventEnd calendar.getEventEnd(event);
        var 
resizeLocation// zoned event date properties. falsy if invalid resize

        // Tracks mouse movement over the *grid's* coordinate map
        
var dragListener = new HitDragListener(this, {
            
distance5,
            
scrollview.opt('dragScroll'),
            
subjectElel,
            
dragStart: function(ev) {
                
_this.triggerSegMouseout(segev); // ensure a mouseout on the manipulated event has been reported
                
_this.segResizeStart(segev);
            },
            
hitOver: function(hitisOrigorigHit) {
                var 
origHitSpan _this.getHitSpan(origHit);
                var 
hitSpan _this.getHitSpan(hit);

                
resizeLocation isStart ?
                    
_this.computeEventStartResize(origHitSpanhitSpanevent) :
                    
_this.computeEventEndResize(origHitSpanhitSpanevent);

                if (
resizeLocation) {
                    if (!
calendar.isEventSpanAllowed(_this.eventToSpan(resizeLocation), event)) {
                        
disableCursor();
                        
resizeLocation null;
                    }
                    
// no change? (TODO: how does this work with timezones?)
                    
else if (resizeLocation.start.isSame(event.start) && resizeLocation.end.isSame(eventEnd)) {
                        
resizeLocation null;
                    }
                }

                if (
resizeLocation) {
                    
view.hideEvent(event);
                    
_this.renderEventResize(resizeLocationseg);
                }
            },
            
hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
                
resizeLocation null;
            },
            
hitDone: function() { // resets the rendering to show the original event
                
_this.unrenderEventResize();
                
view.showEvent(event);
                
enableCursor();
            },
            
dragStop: function(ev) {
                
_this.segResizeStop(segev);

                if (
resizeLocation) { // valid date to resize to?
                    
view.reportEventResize(eventresizeLocationthis.largeUnitelev);
                }
            }
        });

        
dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart
    
},


    
// Called before event segment resizing starts
    
segResizeStart: function(segev) {
        
this.isResizingSeg true;
        
this.view.trigger('eventResizeStart'seg.el[0], seg.eventev, {}); // last argument is jqui dummy
    
},


    
// Called after event segment resizing stops
    
segResizeStop: function(segev) {
        
this.isResizingSeg false;
        
this.view.trigger('eventResizeStop'seg.el[0], seg.eventev, {}); // last argument is jqui dummy
    
},


    
// Returns new date-information for an event segment being resized from its start
    
computeEventStartResize: function(startSpanendSpanevent) {
        return 
this.computeEventResize('start'startSpanendSpanevent);
    },


    
// Returns new date-information for an event segment being resized from its end
    
computeEventEndResize: function(startSpanendSpanevent) {
        return 
this.computeEventResize('end'startSpanendSpanevent);
    },


    
// Returns new zoned date information for an event segment being resized from its start OR end
    // `type` is either 'start' or 'end'.
    // DOES NOT consider overlap/constraint.
    
computeEventResize: function(typestartSpanendSpanevent) {
        var 
calendar this.view.calendar;
        var 
delta this.diffDates(endSpan[type], startSpan[type]);
        var 
resizeLocation// zoned event date properties
        
var defaultDuration;

        
// build original values to work from, guaranteeing a start and end
        
resizeLocation = {
            
startevent.start.clone(),
            
endcalendar.getEventEnd(event),
            
allDayevent.allDay
        
};

        
// if an all-day event was in a timed area and was resized to a time, adjust start/end to have times
        
if (resizeLocation.allDay && durationHasTime(delta)) {
            
resizeLocation.allDay false;
            
calendar.normalizeEventTimes(resizeLocation);
        }

        
resizeLocation[type].add(delta); // apply delta to start or end

        // if the event was compressed too small, find a new reasonable duration for it
        
if (!resizeLocation.start.isBefore(resizeLocation.end)) {

            
defaultDuration =
                
this.minResizeDuration || // TODO: hack
                
(event.allDay ?
                    
calendar.defaultAllDayEventDuration :
                    
calendar.defaultTimedEventDuration);

            if (
type == 'start') { // resizing the start?
                
resizeLocation.start resizeLocation.end.clone().subtract(defaultDuration);
            }
            else { 
// resizing the end?
                
resizeLocation.end resizeLocation.start.clone().add(defaultDuration);
            }
        }

        return 
resizeLocation;
    },


    
// Renders a visual indication of an event being resized.
    // `range` has the updated dates of the event. `seg` is the original segment object involved in the drag.
    
renderEventResize: function(rangeseg) {
        
// subclasses must implement
    
},


    
// Unrenders a visual indication of an event being resized.
    
unrenderEventResize: function() {
        
// subclasses must implement
    
},


    
/* Rendering Utils
    ------------------------------------------------------------------------------------------------------------------*/


    // Compute the text that should be displayed on an event's element.
    // `range` can be the Event object itself, or something range-like, with at least a `start`.
    // If event times are disabled, or the event has no time, will return a blank string.
    // If not specified, formatStr will default to the eventTimeFormat setting,
    // and displayEnd will default to the displayEventEnd setting.
    
getEventTimeText: function(rangeformatStrdisplayEnd) {

        if (
formatStr == null) {
            
formatStr this.eventTimeFormat;
        }

        if (
displayEnd == null) {
            
displayEnd this.displayEventEnd;
        }

        if (
this.displayEventTime && range.start.hasTime()) {
            if (
displayEnd && range.end) {
                return 
this.view.formatRange(rangeformatStr);
            }
            else {
                return 
range.start.format(formatStr);
            }
        }

        return 
'';
    },


    
// Generic utility for generating the HTML classNames for an event segment's element
    
getSegClasses: function(segisDraggableisResizable) {
        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 event skin-related CSS properties
    
getSegSkinCss: function(seg) {
        var 
event seg.event;
        var 
view this.view;
        var 
source event.source || {};
        var 
eventColor event.color;
        var 
sourceColor source.color;
        var 
optionColor view.opt('eventColor');

        return {
            
'background-color':
                
event.backgroundColor ||
                
eventColor ||
                
source.backgroundColor ||
                
sourceColor ||
                
view.opt('eventBackgroundColor') ||
                
optionColor,
            
'border-color':
                
event.borderColor ||
                
eventColor ||
                
source.borderColor ||
                
sourceColor ||
                
view.opt('eventBorderColor') ||
                
optionColor,
            
color:
                
event.textColor ||
                
source.textColor ||
                
view.opt('eventTextColor')
        };
    },


    
/* Converting events -> eventRange -> eventSpan -> eventSegs
    ------------------------------------------------------------------------------------------------------------------*/


    // Generates an array of segments for the given single event
    // Can accept an event "location" as well (which only has start/end and no allDay)
    
eventToSegs: function(event) {
        return 
this.eventsToSegs([ event ]);
    },


    
eventToSpan: function(event) {
        return 
this.eventToSpans(event)[0];
    },


    
// Generates spans (always unzoned) for the given event.
    // Does not do any inverting for inverse-background events.
    // Can accept an event "location" as well (which only has start/end and no allDay)
    
eventToSpans: function(event) {
        var 
range this.eventToRange(event);
        return 
this.eventRangeToSpans(rangeevent);
    },



    
// Converts an array of event objects into an array of event segment objects.
    // A custom `segSliceFunc` may be given for arbitrarily slicing up events.
    // Doesn't guarantee an order for the resulting array.
    
eventsToSegs: function(allEventssegSliceFunc) {
        var 
_this this;
        var 
eventsById groupEventsById(allEvents);
        var 
segs = [];

        $.
each(eventsById, function(idevents) {
            var 
ranges = [];
            var 
i;

            for (
0events.lengthi++) {
                
ranges.push(_this.eventToRange(events[i]));
            }

            
// inverse-background events (utilize only the first event in calculations)
            
if (isInverseBgEvent(events[0])) {
                
ranges _this.invertRanges(ranges);

                for (
0ranges.lengthi++) {
                    
segs.push.apply(segs// append to
                        
_this.eventRangeToSegs(ranges[i], events[0], segSliceFunc));
                }
            }
            
// normal event ranges
            
else {
                for (
0ranges.lengthi++) {
                    
segs.push.apply(segs// append to
                        
_this.eventRangeToSegs(ranges[i], events[i], segSliceFunc));
                }
            }
        });

        return 
segs;
    },


    
// Generates the unzoned start/end dates an event appears to occupy
    // Can accept an event "location" as well (which only has start/end and no allDay)
    
eventToRange: function(event) {
        return {
            
startevent.start.clone().stripZone(),
            
end: (
                
event.end ?
                    
event.end.clone() :
                    
// derive the end from the start and allDay. compute allDay if necessary
                    
this.view.calendar.getDefaultEventEnd(
                        
event.allDay != null ?
                            
event.allDay :
                            !
event.start.hasTime(),
                        
event.start
                    
)
            ).
stripZone()
        };
    },


    
// Given an event's range (unzoned start/end), and the event itself,
    // slice into segments (using the segSliceFunc function if specified)
    
eventRangeToSegs: function(rangeeventsegSliceFunc) {
        var 
spans this.eventRangeToSpans(rangeevent);
        var 
segs = [];
        var 
i;

        for (
0spans.lengthi++) {
            
segs.push.apply(segs// append to
                
this.eventSpanToSegs(spans[i], eventsegSliceFunc));
        }

        return 
segs;
    },


    
// Given an event's unzoned date range, return an array of "span" objects.
    // Subclasses can override.
    
eventRangeToSpans: function(rangeevent) {
        return [ $.
extend({}, range) ]; // copy into a single-item array
    
},


    
// Given an event's span (unzoned start/end and other misc data), and the event itself,
    // slices into segments and attaches event-derived properties to them.
    
eventSpanToSegs: function(spaneventsegSliceFunc) {
        var 
segs segSliceFunc segSliceFunc(span) : this.spanToSegs(span);
        var 
iseg;

        for (
0segs.lengthi++) {
            
seg segs[i];
            
seg.event event;
            
seg.eventStartMS = +span.start// TODO: not the best name after making spans unzoned
            
seg.eventDurationMS span.end span.start;
        }

        return 
segs;
    },


    
// Produces a new array of range objects that will cover all the time NOT covered by the given ranges.
    // SIDE EFFECT: will mutate the given array and will use its date references.
    
invertRanges: function(ranges) {
        var 
view this.view;
        var 
viewStart view.start.clone(); // need a copy
        
var viewEnd view.end.clone(); // need a copy
        
var inverseRanges = [];
        var 
start viewStart// the end of the previous range. the start of the new range
        
var irange;

        
// ranges need to be in order. required for our date-walking algorithm
        
ranges.sort(compareRanges);

        for (
0ranges.lengthi++) {
            
range ranges[i];

            
// add the span of time before the event (if there is any)
            
if (range.start start) { // compare millisecond time (skip any ambig logic)
                
inverseRanges.push({
                    
startstart,
                    
endrange.start
                
});
            }

            
start range.end;
        }

        
// add the span of time after the last event (if there is any)
        
if (start viewEnd) { // compare millisecond time (skip any ambig logic)
            
inverseRanges.push({
                
startstart,
                
endviewEnd
            
});
        }

        return 
inverseRanges;
    },


    
sortEventSegs: function(segs) {
        
segs.sort(proxy(this'compareEventSegs'));
    },


    
// A cmp function for determining which segments should take visual priority
    
compareEventSegs: function(seg1seg2) {
        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)
            
compareByFieldSpecs(seg1.eventseg2.eventthis.view.eventOrderSpecs);
    }

});


/* Utilities
----------------------------------------------------------------------------------------------------------------------*/


function isBgEvent(event) { // returns true if background OR inverse-background
    
var rendering getEventRendering(event);
    return 
rendering === 'background' || rendering === 'inverse-background';
}
FC.isBgEvent isBgEvent// export


function isInverseBgEvent(event) {
    return 
getEventRendering(event) === 'inverse-background';
}


function 
getEventRendering(event) {
    return 
firstDefined((event.source || {}).renderingevent.rendering);
}


function 
groupEventsById(events) {
    var 
eventsById = {};
    var 
ievent;

    for (
0events.lengthi++) {
        
event events[i];
        (
eventsById[event._id] || (eventsById[event._id] = [])).push(event);
    }

    return 
eventsById;
}


// A cmp function for determining which non-inverted "ranges" (see above) happen earlier
function compareRanges(range1range2) {
    return 
range1.start range2.start// earlier ranges go first
}


/* External-Dragging-Element Data
----------------------------------------------------------------------------------------------------------------------*/

// Require all HTML5 data-* attributes used by FullCalendar to have this prefix.
// A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event.
FC.dataAttrPrefix '';

// Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure
// to be used for Event Object creation.
// A defined `.eventProps`, even when empty, indicates that an event should be created.
function getDraggedElMeta(el) {
    var 
prefix FC.dataAttrPrefix;
    var 
eventProps// properties for creating the event, not related to date/time
    
var startTime// a Duration
    
var duration;
    var 
stick;

    if (
prefix) { prefix += '-'; }
    
eventProps el.data(prefix 'event') || null;

    if (
eventProps) {
        if (
typeof eventProps === 'object') {
            
eventProps = $.extend({}, eventProps); // make a copy
        
}
        else { 
// something like 1 or true. still signal event creation
            
eventProps = {};
        }

        
// pluck special-cased date/time properties
        
startTime eventProps.start;
        if (
startTime == null) { startTime eventProps.time; } // accept 'time' as well
        
duration eventProps.duration;
        
stick eventProps.stick;
        
delete eventProps.start;
        
delete eventProps.time;
        
delete eventProps.duration;
        
delete eventProps.stick;
    }

    
// fallback to standalone attribute values for each of the date/time properties
    
if (startTime == null) { startTime el.data(prefix 'start'); }
    if (
startTime == null) { startTime el.data(prefix 'time'); } // accept 'time' as well
    
if (duration == null) { duration el.data(prefix 'duration'); }
    if (
stick == null) { stick el.data(prefix 'stick'); }

    
// massage into correct data types
    
startTime startTime != null moment.duration(startTime) : null;
    
duration duration != null moment.duration(duration) : null;
    
stick Boolean(stick);

    return { 
eventPropseventPropsstartTimestartTimedurationdurationstickstick };
}


;;

/*
A set of rendering and date-related methods for a visual component comprised of one or more rows of day columns.
Prerequisite: the object being mixed into needs to be a *Grid*
*/
var DayTableMixin FC.DayTableMixin = {

    
breakOnWeeksfalse// should create a new row for each week?
    
dayDatesnull// whole-day dates for each column. left to right
    
dayIndicesnull// for each day from start, the offset
    
daysPerRownull,
    
rowCntnull,
    
colCntnull,
    
colHeadFormatnull,


    
// Populates internal variables used for date calculation and rendering
    
updateDayTable: function() {
        var 
view this.view;
        var 
date this.start.clone();
        var 
dayIndex = -1;
        var 
dayIndices = [];
        var 
dayDates = [];
        var 
daysPerRow;
        var 
firstDay;
        var 
rowCnt;

        while (
date.isBefore(this.end)) { // loop each day from start to end
            
if (view.isHiddenDay(date)) {
                
dayIndices.push(dayIndex 0.5); // mark that it's between indices
            
}
            else {
                
dayIndex++;
                
dayIndices.push(dayIndex);
                
dayDates.push(date.clone());
            }
            
date.add(1'days');
        }

        if (
this.breakOnWeeks) {
            
// count columns until the day-of-week repeats
            
firstDay dayDates[0].day();
            for (
daysPerRow 1daysPerRow dayDates.lengthdaysPerRow++) {
                if (
dayDates[daysPerRow].day() == firstDay) {
                    break;
                }
            }
            
rowCnt Math.ceil(dayDates.length daysPerRow);
        }
        else {
            
rowCnt 1;
            
daysPerRow dayDates.length;
        }

        
this.dayDates dayDates;
        
this.dayIndices dayIndices;
        
this.daysPerRow daysPerRow;
        
this.rowCnt rowCnt;
        
        
this.updateDayTableCols();
    },


    
// Computes and assigned the colCnt property and updates any options that may be computed from it
    
updateDayTableCols: function() {
        
this.colCnt this.computeColCnt();
        
this.colHeadFormat this.view.opt('columnFormat') || this.computeColHeadFormat();
    },


    
// Determines how many columns there should be in the table
    
computeColCnt: function() {
        return 
this.daysPerRow;
    },


    
// Computes the ambiguously-timed moment for the given cell
    
getCellDate: function(rowcol) {
        return 
this.dayDates[
                
this.getCellDayIndex(rowcol)
            ].clone();
    },


    
// Computes the ambiguously-timed date range for the given cell
    
getCellRange: function(rowcol) {
        var 
start this.getCellDate(rowcol);
        var 
end start.clone().add(1'days');

        return { 
startstartendend };
    },


    
// Returns the number of day cells, chronologically, from the first of the grid (0-based)
    
getCellDayIndex: function(rowcol) {
        return 
row this.daysPerRow this.getColDayIndex(col);
    },


    
// Returns the numner of day cells, chronologically, from the first cell in *any given row*
    
getColDayIndex: function(col) {
        if (
this.isRTL) {
            return 
this.colCnt col;
        }
        else {
            return 
col;
        }
    },


    
// Given a date, returns its chronolocial cell-index from the first cell of the grid.
    // If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets.
    // If before the first offset, returns a negative number.
    // If after the last offset, returns an offset past the last cell offset.
    // Only works for *start* dates of cells. Will not work for exclusive end dates for cells.
    
getDateDayIndex: function(date) {
        var 
dayIndices this.dayIndices;
        var 
dayOffset date.diff(this.start'days');

        if (
dayOffset 0) {
            return 
dayIndices[0] - 1;
        }
        else if (
dayOffset >= dayIndices.length) {
            return 
dayIndices[dayIndices.length 1] + 1;
        }
        else {
            return 
dayIndices[dayOffset];
        }
    },


    
/* Options
    ------------------------------------------------------------------------------------------------------------------*/


    // Computes a default column header formatting string if `colFormat` is not explicitly defined
    
computeColHeadFormat: function() {
        
// if more than one week row, or if there are a lot of columns with not much space,
        // put just the day numbers will be in each cell
        
if (this.rowCnt || this.colCnt 10) {
            return 
'ddd'// "Sat"
        
}
        
// multiple days, so full single date string WON'T be in title text
        
else if (this.colCnt 1) {
            return 
this.view.opt('dayOfMonthFormat'); // "Sat 12/10"
        
}
        
// single day, so full single date string will probably be in title text
        
else {
            return 
'dddd'// "Saturday"
        
}
    },


    
/* Slicing
    ------------------------------------------------------------------------------------------------------------------*/


    // Slices up a date range into a segment for every week-row it intersects with
    
sliceRangeByRow: function(range) {
        var 
daysPerRow this.daysPerRow;
        var 
normalRange this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold
        
var rangeFirst this.getDateDayIndex(normalRange.start); // inclusive first index
        
var rangeLast this.getDateDayIndex(normalRange.end.clone().subtract(1'days')); // inclusive last index
        
var segs = [];
        var 
row;
        var 
rowFirstrowLast// inclusive day-index range for current row
        
var segFirstsegLast// inclusive day-index range for segment

        
for (row 0row this.rowCntrow++) {
            
rowFirst row daysPerRow;
            
rowLast rowFirst daysPerRow 1;

            
// intersect segment's offset range with the row's
            
segFirst Math.max(rangeFirstrowFirst);
            
segLast Math.min(rangeLastrowLast);

            
// deal with in-between indices
            
segFirst Math.ceil(segFirst); // in-between starts round to next cell
            
segLast Math.floor(segLast); // in-between ends round to prev cell

            
if (segFirst <= segLast) { // was there any intersection with the current row?
                
segs.push({
                    
rowrow,

                    
// normalize to start of row
                    
firstRowDayIndexsegFirst rowFirst,
                    
lastRowDayIndexsegLast rowFirst,

                    
// must be matching integers to be the segment's start/end
                    
isStartsegFirst === rangeFirst,
                    
isEndsegLast === rangeLast
                
});
            }
        }

        return 
segs;
    },


    
// Slices up a date range into a segment for every day-cell it intersects with.
    // TODO: make more DRY with sliceRangeByRow somehow.
    
sliceRangeByDay: function(range) {
        var 
daysPerRow this.daysPerRow;
        var 
normalRange this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold
        
var rangeFirst this.getDateDayIndex(normalRange.start); // inclusive first index
        
var rangeLast this.getDateDayIndex(normalRange.end.clone().subtract(1'days')); // inclusive last index
        
var segs = [];
        var 
row;
        var 
rowFirstrowLast// inclusive day-index range for current row
        
var i;
        var 
segFirstsegLast// inclusive day-index range for segment

        
for (row 0row this.rowCntrow++) {
            
rowFirst row daysPerRow;
            
rowLast rowFirst daysPerRow 1;

            for (
rowFirst<= rowLasti++) {

                
// intersect segment's offset range with the row's
                
segFirst Math.max(rangeFirsti);
                
segLast Math.min(rangeLasti);

                
// deal with in-between indices
                
segFirst Math.ceil(segFirst); // in-between starts round to next cell
                
segLast Math.floor(segLast); // in-between ends round to prev cell

                
if (segFirst <= segLast) { // was there any intersection with the current row?
                    
segs.push({
                        
rowrow,

                        
// normalize to start of row
                        
firstRowDayIndexsegFirst rowFirst,
                        
lastRowDayIndexsegLast rowFirst,

                        
// must be matching integers to be the segment's start/end
                        
isStartsegFirst === rangeFirst,
                        
isEndsegLast === rangeLast
                    
});
                }
            }
        }

        return 
segs;
    },


    
/* Header Rendering
    ------------------------------------------------------------------------------------------------------------------*/


    
renderHeadHtml: function() {
        var 
view this.view;

        return 
'' +
            
'<div class="fc-row ' view.widgetHeaderClass '">' +
                
'<table>' +
                    
'<thead>' +
                        
this.renderHeadTrHtml() +
                    
'</thead>' +
                
'</table>' +
            
'</div>';
    },


    
renderHeadIntroHtml: function() {
        return 
this.renderIntroHtml(); // fall back to generic
    
},


    
renderHeadTrHtml: function() {
        return 
'' +
            
'<tr>' +
                (
this.isRTL '' this.renderHeadIntroHtml()) +
                
this.renderHeadDateCellsHtml() +
                (
this.isRTL this.renderHeadIntroHtml() : '') +
            
'</tr>';
    },


    
renderHeadDateCellsHtml: function() {
        var 
htmls = [];
        var 
coldate;

        for (
col 0col this.colCntcol++) {
            
date this.getCellDate(0col);
            
htmls.push(this.renderHeadDateCellHtml(date));
        }

        return 
htmls.join('');
    },


    
// TODO: when internalApiVersion, accept an object for HTML attributes
    // (colspan should be no different)
    
renderHeadDateCellHtml: function(datecolspanotherAttrs) {
        var 
view this.view;

        return 
'' +
            
'<th class="fc-day-header ' view.widgetHeaderClass ' fc-' dayIDs[date.day()] + '"' +
                (
this.rowCnt == ?
                    
' data-date="' date.format('YYYY-MM-DD') + '"' :
                    
'') +
                (
colspan ?
                    
' colspan="' colspan '"' :
                    
'') +
                (
otherAttrs ?
                    
' ' otherAttrs :
                    
'') +
            
'>' +
                
htmlEscape(date.format(this.colHeadFormat)) +
            
'</th>';
    },


    
/* Background Rendering
    ------------------------------------------------------------------------------------------------------------------*/


    
renderBgTrHtml: function(row) {
        return 
'' +
            
'<tr>' +
                (
this.isRTL '' this.renderBgIntroHtml(row)) +
                
this.renderBgCellsHtml(row) +
                (
this.isRTL this.renderBgIntroHtml(row) : '') +
            
'</tr>';
    },


    
renderBgIntroHtml: function(row) {
        return 
this.renderIntroHtml(); // fall back to generic
    
},


    
renderBgCellsHtml: function(row) {
        var 
htmls = [];
        var 
coldate;

        for (
col 0col this.colCntcol++) {
            
date this.getCellDate(rowcol);
            
htmls.push(this.renderBgCellHtml(date));
        }

        return 
htmls.join('');
    },


    
renderBgCellHtml: function(dateotherAttrs) {
        var 
view this.view;
        var 
classes this.getDayClasses(date);

        
classes.unshift('fc-day'view.widgetContentClass);

        return 
'<td class="' classes.join(' ') + '"' +
            
' data-date="' date.format('YYYY-MM-DD') + '"' // if date has a time, won't format it
            
(otherAttrs ?
                
' ' otherAttrs :
                
'') +
            
'></td>';
    },


    
/* Generic
    ------------------------------------------------------------------------------------------------------------------*/


    // Generates the default HTML intro for any row. User classes should override
    
renderIntroHtml: function() {
    },


    
// TODO: a generic method for dealing with <tr>, RTL, intro
    // when increment internalApiVersion
    // wrapTr (scheduler)


    /* Utils
    ------------------------------------------------------------------------------------------------------------------*/


    // Applies the generic "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.
    
bookendCells: function(trEl) {
        var 
introHtml this.renderIntroHtml();

        if (
introHtml) {
            if (
this.isRTL) {
                
trEl.append(introHtml);
            }
            else {
                
trEl.prepend(introHtml);
            }
        }
    }

};

;;

/* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week.
----------------------------------------------------------------------------------------------------------------------*/

var DayGrid FC.DayGrid Grid.extend(DayTableMixin, {

    
numbersVisiblefalse// should render a row for day/week numbers? set by outside view. TODO: make internal
    
bottomCoordPadding0// hack for extending the hit area for the last row of the coordinate grid

    
rowElsnull// set of fake row elements
    
cellElsnull// set of whole-day elements comprising the row's background
    
helperElsnull// set of cell skeleton elements for rendering the mock event "helper"

    
rowCoordCachenull,
    
colCoordCachenull,


    
// 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.
    
renderDates: function(isRigid) {
        var 
view this.view;
        var 
rowCnt this.rowCnt;
        var 
colCnt this.colCnt;
        var 
html '';
        var 
row;
        var 
col;

        for (
row 0row rowCntrow++) {
            
html += this.renderDayRowHtml(rowisRigid);
        }
        
this.el.html(html);

        
this.rowEls this.el.find('.fc-row');
        
this.cellEls this.el.find('.fc-day');

        
this.rowCoordCache = new CoordCache({
            
elsthis.rowEls,
            
isVerticaltrue
        
});
        
this.colCoordCache = new CoordCache({
            
elsthis.cellEls.slice(0this.colCnt), // only the first row
            
isHorizontaltrue
        
});

        
// trigger dayRender with each cell's element
        
for (row 0row rowCntrow++) {
            for (
col 0col colCntcol++) {
                
view.trigger(
                    
'dayRender',
                    
null,
                    
this.getCellDate(rowcol),
                    
this.getCellEl(rowcol)
                );
            }
        }
    },


    
unrenderDates: function() {
        
this.removeSegPopover();
    },


    
renderBusinessHours: function() {
        var 
events this.view.calendar.getBusinessHoursEvents(true); // wholeDay=true
        
var segs this.eventsToSegs(events);

        
this.renderFill('businessHours'segs'bgevent');
    },


    
// Generates the HTML for a single row, which is a div that wraps a table.
    // `row` is the row number.
    
renderDayRowHtml: function(rowisRigid) {
        var 
view this.view;
        var 
classes = [ 'fc-row''fc-week'view.widgetContentClass ];

        if (
isRigid) {
            
classes.push('fc-rigid');
        }

        return 
'' +
            
'<div class="' classes.join(' ') + '">' +
                
'<div class="fc-bg">' +
                    
'<table>' +
                        
this.renderBgTrHtml(row) +
                    
'</table>' +
                
'</div>' +
                
'<div class="fc-content-skeleton">' +
                    
'<table>' +
                        (
this.numbersVisible ?
                            
'<thead>' +
                                
this.renderNumberTrHtml(row) +
                            
'</thead>' :
                            
''
                            
) +
                    
'</table>' +
                
'</div>' +
            
'</div>';
    },


    
/* Grid Number Rendering
    ------------------------------------------------------------------------------------------------------------------*/


    
renderNumberTrHtml: function(row) {
        return 
'' +
            
'<tr>' +
                (
this.isRTL '' this.renderNumberIntroHtml(row)) +
                
this.renderNumberCellsHtml(row) +
                (
this.isRTL this.renderNumberIntroHtml(row) : '') +
            
'</tr>';
    },


    
renderNumberIntroHtml: function(row) {
        return 
this.renderIntroHtml();
    },


    
renderNumberCellsHtml: function(row) {
        var 
htmls = [];
        var 
coldate;

        for (
col 0col this.colCntcol++) {
            
date this.getCellDate(rowcol);
            
htmls.push(this.renderNumberCellHtml(date));
        }

        return 
htmls.join('');
    },


    
// Generates the HTML for the <td>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.
    
renderNumberCellHtml: function(date) {
        var 
classes;

        if (!
this.view.dayNumbersVisible) { // if there are week numbers but not day numbers
            
return '<td/>'//  will create an empty space above events :(
        
}

        
classes this.getDayClasses(date);
        
classes.unshift('fc-day-number');

        return 
'' +
            
'<td class="' classes.join(' ') + '" data-date="' date.format() + '">' +
                
date.date() +
            
'</td>';
    },


    
/* Options
    ------------------------------------------------------------------------------------------------------------------*/


    // Computes a default event time formatting string if `timeFormat` is not explicitly defined
    
computeEventTimeFormat: function() {
        return 
this.view.opt('extraSmallTimeFormat'); // like "6p" or "6:30p"
    
},


    
// Computes a default `displayEventEnd` value if one is not expliclty defined
    
computeDisplayEventEnd: function() {
        return 
this.colCnt == 1// we'll likely have space if there's only one day
    
},


    
/* Dates
    ------------------------------------------------------------------------------------------------------------------*/


    
rangeUpdated: function() {
        
this.updateDayTable();
    },


    
// Slices up the given span (unzoned start/end with other misc data) into an array of segments
    
spanToSegs: function(span) {
        var 
segs this.sliceRangeByRow(span);
        var 
iseg;

        for (
0segs.lengthi++) {
            
seg segs[i];
            if (
this.isRTL) {
                
seg.leftCol this.daysPerRow seg.lastRowDayIndex;
                
seg.rightCol this.daysPerRow seg.firstRowDayIndex;
            }
            else {
                
seg.leftCol seg.firstRowDayIndex;
                
seg.rightCol seg.lastRowDayIndex;
            }
        }

        return 
segs;
    },


    
/* Hit System
    ------------------------------------------------------------------------------------------------------------------*/


    
prepareHits: function() {
        
this.colCoordCache.build();
        
this.rowCoordCache.build();
        
this.rowCoordCache.bottoms[this.rowCnt 1] += this.bottomCoordPadding// hack
    
},


    
releaseHits: function() {
        
this.colCoordCache.clear();
        
this.rowCoordCache.clear();
    },


    
queryHit: function(leftOffsettopOffset) {
        var 
col this.colCoordCache.getHorizontalIndex(leftOffset);
        var 
row this.rowCoordCache.getVerticalIndex(topOffset);

        if (
row != null && col != null) {
            return 
this.getCellHit(rowcol);
        }
    },


    
getHitSpan: function(hit) {
        return 
this.getCellRange(hit.rowhit.col);
    },


    
getHitEl: function(hit) {
        return 
this.getCellEl(hit.rowhit.col);
    },


    
/* Cell System
    ------------------------------------------------------------------------------------------------------------------*/
    // FYI: the first column is the leftmost column, regardless of date


    
getCellHit: function(rowcol) {
        return {
            
rowrow,
            
colcol,
            
componentthis// needed unfortunately :(
            
leftthis.colCoordCache.getLeftOffset(col),
            
rightthis.colCoordCache.getRightOffset(col),
            
topthis.rowCoordCache.getTopOffset(row),
            
bottomthis.rowCoordCache.getBottomOffset(row)
        };
    },


    
getCellEl: function(rowcol) {
        return 
this.cellEls.eq(row this.colCnt col);
    },


    
/* Event Drag Visualization
    ------------------------------------------------------------------------------------------------------------------*/
    // TODO: move to DayGrid.event, similar to what we did with Grid's drag methods


    // Renders a visual indication of an event or external element being dragged.
    // `eventLocation` has zoned start and end (optional)
    
renderDrag: function(eventLocationseg) {

        
// always render a highlight underneath
        
this.renderHighlight(this.eventToSpan(eventLocation));

        
// 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.renderEventLocationHelper(eventLocationseg);
            
this.applyDragOpacity(this.helperEls);

            return 
true// a helper has been rendered
        
}
    },


    
// Unrenders any visual indication of a hovering event
    
unrenderDrag: function() {
        
this.unrenderHighlight();
        
this.unrenderHelper();
    },


    
/* Event Resize Visualization
    ------------------------------------------------------------------------------------------------------------------*/


    // Renders a visual indication of an event being resized
    
renderEventResize: function(eventLocationseg) {
        
this.renderHighlight(this.eventToSpan(eventLocation));
        
this.renderEventLocationHelper(eventLocationseg);
    },


    
// Unrenders a visual indication of an event being resized
    
unrenderEventResize: function() {
        
this.unrenderHighlight();
        
this.unrenderHelper();
    },


    
/* Event Helper
    ------------------------------------------------------------------------------------------------------------------*/


    // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null.
    
renderHelper: function(eventsourceSeg) {
        var 
helperNodes = [];
        var 
segs this.eventToSegs(event);
        var 
rowStructs;

        
segs this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered
        
rowStructs this.renderSegRows(segs);

        
// inject each new event skeleton into each associated row
        
this.rowEls.each(function(rowrowNode) {
            var 
rowEl = $(rowNode); // the .fc-row
            
var skeletonEl = $('<div class="fc-helper-skeleton"><table/></div>'); // 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
    
unrenderHelper: function() {
        if (
this.helperEls) {
            
this.helperEls.remove();
            
this.helperEls null;
        }
    },


    
/* Fill System (highlight, background events, business hours)
    ------------------------------------------------------------------------------------------------------------------*/


    
fillSegTag'td'// override the default tag name


    // Renders a set of rectangles over the given segments of days.
    // Only returns segments that successfully rendered.
    
renderFill: function(typesegsclassName) {
        var 
nodes = [];
        var 
iseg;
        var 
skeletonEl;

        
segs this.renderFillSegEls(typesegs); // assignes `.el` to each seg. returns successfully rendered segs

        
for (0segs.lengthi++) {
            
seg segs[i];
            
skeletonEl this.renderFillRow(typesegclassName);
            
this.rowEls.eq(seg.row).append(skeletonEl);
            
nodes.push(skeletonEl[0]);
        }

        
this.elsByFill[type] = $(nodes);

        return 
segs;
    },


    
// Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered.
    
renderFillRow: function(typesegclassName) {
        var 
colCnt this.colCnt;
        var 
startCol seg.leftCol;
        var 
endCol seg.rightCol 1;
        var 
skeletonEl;
        var 
trEl;

        
className className || type.toLowerCase();

        
skeletonEl = $(
            
'<div class="fc-' className '-skeleton">' +
                
'<table><tr/></table>' +
            
'</div>'
        
);
        
trEl skeletonEl.find('tr');

        if (
startCol 0) {
            
trEl.append('<td colspan="' startCol '"/>');
        }

        
trEl.append(
            
seg.el.attr('colspan'endCol startCol)
        );

        if (
endCol colCnt) {
            
trEl.append('<td colspan="' + (colCnt endCol) + '"/>');
        }

        
this.bookendCells(trEl);

        return 
skeletonEl;
    }

});

;;

/* Event-rendering methods for the DayGrid class
----------------------------------------------------------------------------------------------------------------------*/

DayGrid.mixin({

    
rowStructsnull// an array of objects, each holding information about a row's foreground event-rendering


    // Unrenders all events currently rendered on the grid
    
unrenderEvents: function() {
        
this.removeSegPopover(); // removes the "more.." events popover
        
Grid.prototype.unrenderEvents.apply(thisarguments); // calls the super-method
    
},


    
// Retrieves all rendered segment objects currently rendered on the grid
    
getEventSegs: function() {
        return 
Grid.prototype.getEventSegs.call(this// get the segments from the super-method
            
.concat(this.popoverSegs || []); // append the segments from the "more..." popover
    
},


    
// Renders the given background event segments onto the grid
    
renderBgSegs: function(segs) {

        
// don't render timed background events
        
var allDaySegs = $.grep(segs, function(seg) {
            return 
seg.event.allDay;
        });

        return 
Grid.prototype.renderBgSegs.call(thisallDaySegs); // call the super-method
    
},


    
// Renders the given foreground event segments onto the grid
    
renderFgSegs: function(segs) {
        var 
rowStructs;

        
// render an `.el` on each seg
        // returns a subset of the segs. segs that were actually rendered
        
segs this.renderFgSegEls(segs);

        
rowStructs this.rowStructs this.renderSegRows(segs);

        
// append to each row's content skeleton
        
this.rowEls.each(function(irowNode) {
            $(
rowNode).find('.fc-content-skeleton > table').append(
                
rowStructs[i].tbodyEl
            
);
        });

        return 
segs// return only the segs that were actually rendered
    
},


    
// Unrenders all currently rendered foreground event segments
    
unrenderFgSegs: function() {
        var 
rowStructs this.rowStructs || [];
        var 
rowStruct;

        while ((
rowStruct rowStructs.pop())) {
            
rowStruct.tbodyEl.remove();
        }

        
this.rowStructs null;
    },


    
// Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton.
    // Returns an array of rowStruct objects (see the bottom of `renderSegRow`).
    // PRECONDITION: each segment shoud already have a rendered and assigned `.el`
    
renderSegRows: function(segs) {
        var 
rowStructs = [];
        var 
segRows;
        var 
row;

        
segRows this.groupSegRows(segs); // group into nested arrays

        // iterate each row of segment groupings
        
for (row 0row segRows.lengthrow++) {
            
rowStructs.push(
                
this.renderSegRow(rowsegRows[row])
            );
        }

        return 
rowStructs;
    },


    
// Builds the HTML to be used for the default element for an individual segment
    
fgSegHtml: function(segdisableResizing) {
        var 
view this.view;
        var 
event seg.event;
        var 
isDraggable view.isEventDraggable(event);
        var 
isResizableFromStart = !disableResizing && event.allDay &&
            
seg.isStart && view.isEventResizableFromStart(event);
        var 
isResizableFromEnd = !disableResizing && event.allDay &&
            
seg.isEnd && view.isEventResizableFromEnd(event);
        var 
classes this.getSegClasses(segisDraggableisResizableFromStart || isResizableFromEnd);
        var 
skinCss cssToStr(this.getSegSkinCss(seg));
        var 
timeHtml '';
        var 
timeText;
        var 
titleHtml;

        
classes.unshift('fc-day-grid-event''fc-h-event');

        
// Only display a timed events time if it is the starting segment
        
if (seg.isStart) {
            
timeText this.getEventTimeText(event);
            if (
timeText) {
                
timeHtml '<span class="fc-time">' htmlEscape(timeText) + '</span>';
            }
        }

        
titleHtml =
            
'<span class="fc-title">' +
                (
htmlEscape(event.title || '') || '&nbsp;') + // we always want one line of height
            
'</span>';
        
        return 
'<a class="' classes.join(' ') + '"' +
                (
event.url ?
                    
' href="' htmlEscape(event.url) + '"' :
                    
''
                    
) +
                (
skinCss ?
                    
' style="' skinCss '"' :
                    
''
                    
) +
            
'>' +
                
'<div class="fc-content">' +
                    (
this.isRTL ?
                        
titleHtml ' ' timeHtml // put a natural space in between
                        
timeHtml ' ' titleHtml   //
                        
) +
                
'</div>' +
                (
isResizableFromStart ?
                    
'<div class="fc-resizer fc-start-resizer" />' :
                    
''
                    
) +
                (
isResizableFromEnd ?
                    
'<div class="fc-resizer fc-end-resizer" />' :
                    
''
                    
) +
            
'</a>';
    },


    
// Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains
    // the segments. Returns object with a bunch of internal data about how the render was calculated.
    // NOTE: modifies rowSegs
    
renderSegRow: function(rowrowSegs) {
        var 
colCnt this.colCnt;
        var 
segLevels this.buildSegLevels(rowSegs); // group into sub-arrays of levels
        
var levelCnt Math.max(1segLevels.length); // ensure at least one level
        
var tbody = $('<tbody/>');
        var 
segMatrix = []; // lookup for which segments are rendered into which level+col cells
        
var cellMatrix = []; // lookup for all <td> elements of the level+col matrix
        
var loneCellMatrix = []; // lookup for <td> elements that only take up a single column
        
var ilevelSegs;
        var 
col;
        var 
tr;
        var 
jseg;
        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[1] || [])[col];
                if (
td) {
                    
td.attr(
                        
'rowspan',
                        
parseInt(td.attr('rowspan') || 110) + 1
                    
);
                }
                else {
                    
td = $('<td/>');
                    
tr.append(td);
                }
                
cellMatrix[i][col] = td;
                
loneCellMatrix[i][col] = td;
                
col++;
            }
        }

        for (
0levelCnti++) { // iterate through all levels
            
levelSegs segLevels[i];
            
col 0;
            
tr = $('<tr/>');

            
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 (
0levelSegs.lengthj++) { // iterate through segments in level
                    
seg levelSegs[j];

                    
emptyCellsUntil(seg.leftCol);

                    
// create a container that occupies or more columns. append the event element.
                    
td = $('<td class="fc-event-container"/>').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);
            
tbody.append(tr);
        }

        return { 
// a "rowStruct"
            
rowrow// the row number
            
tbodyEltbody,
            
cellMatrixcellMatrix,
            
segMatrixsegMatrix,
            
segLevelssegLevels,
            
segsrowSegs
        
};
    },


    
// Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels.
    // NOTE: modifies segs
    
buildSegLevels: function(segs) {
        var 
levels = [];
        var 
iseg;
        var 
j;

        
// Give preference to elements with certain criteria, so they have
        // a chance to be closer to the top.
        
this.sortEventSegs(segs);
        
        for (
0segs.lengthi++) {
            
seg segs[i];

            
// loop through levels, starting with the topmost, until the segment doesn't collide with other segments
            
for (0levels.lengthj++) {
                if (!
isDaySegCollision(seglevels[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 (0levels.lengthj++) {
            
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 
segRows = [];
        var 
i;

        for (
0this.rowCnti++) {
            
segRows.push([]);
        }

        for (
0segs.lengthi++) {
            
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(segotherSegs) {
    var 
iotherSeg;

    for (
0otherSegs.lengthi++) {
        
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(ab) {
    return 
a.leftCol b.leftCol;
}

;;

/* Methods relate to limiting the number events for a given day on a DayGrid
----------------------------------------------------------------------------------------------------------------------*/
// NOTE: all the segs being passed around in here are foreground segs

DayGrid.mixin({

    
segPopovernull// the Popover that holds events that can't fit in a cell. null when not visible
    
popoverSegsnull// an array of segment objects that the segPopover holds. null when not visible


    
removeSegPopover: function() {
        if (
this.segPopover) {
            
this.segPopover.hide(); // in handler, will call segPopover's removeElement
        
}
    },


    
// 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 0row rowStructs.lengthrow++) {
            
this.unlimitRow(row);

            if (!
levelLimit) {
                
rowLevelLimit false;
            }
            else if (
typeof levelLimit === 'number') {
                
rowLevelLimit levelLimit;
            }
            else {
                
rowLevelLimit this.computeRowLevelLimit(row);
            }

            if (
rowLevelLimit !== false) {
                
this.limitRow(rowrowLevelLimit);
            }
        }
    },


    
// 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 
itrEl;
        var 
trHeight;

        function 
iterInnerHeights(ichildNode) {
            
trHeight Math.max(trHeight, $(childNode).outerHeight());
        }

        
// Reveal one level <tr> at a time and stop when we find one out of bounds
        
for (0trEls.lengthi++) {
            
trEl trEls.eq(i).removeClass('fc-limited'); // reset to original state (reveal)

            // with rowspans>1 and IE8, trEl.outerHeight() would return the height of the largest cell,
            // so instead, find the tallest inner content element.
            
trHeight 0;
            
trEl.find('> td > :first-child').each(iterInnerHeights);

            if (
trEl.position().top trHeight 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(rowlevelLimit) {
        var 
_this this;
        var 
rowStruct this.rowStructs[row];
        var 
moreNodes = []; // array of "more" <a> links and <td> DOM nodes
        
var col 0// col #, left-to-right (not chronologically)
        
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 <td> jQuery elements in the row
        
var limitedNodes// array of temporarily hidden level <tr> and segment <td> DOM nodes
        
var iseg;
        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 tdrowspan;
        var 
segMoreNodes// array of "more" <td> cells that will stand-in for the current seg's cell
        
var j;
        var 
moreTdmoreWrapmoreLink;

        
// 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) {
                
segsBelow _this.getCellSegs(rowcollevelLimit);
                if (
segsBelow.length) {
                    
td cellMatrix[levelLimit 1][col];
                    
moreLink _this.renderMoreLink(rowcolsegsBelow);
                    
moreWrap = $('<div/>').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 <tr> 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 (0levelSegs.lengthi++) {
                
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) {
                    
segsBelow this.getCellSegs(rowcollevelLimit);
                    
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 <td> for each column the segment occupies. will be one for each colspan
                    
for (0colSegsBelow.lengthj++) {
                        
moreTd = $('<td class="fc-more-cell"/>').attr('rowspan'rowspan);
                        
segsBelow colSegsBelow[j];
                        
moreLink this.renderMoreLink(
                            
row,
                            
seg.leftCol j,
                            [ 
seg ].concat(segsBelow// count seg as hidden too
                        
);
                        
moreWrap = $('<div/>').append(moreLink);
                        
moreTd.append(moreWrap);
                        
segMoreNodes.push(moreTd[0]);
                        
moreNodes.push(moreTd[0]);
                    }

                    
td.addClass('fc-limited').after($(segMoreNodes)); // hide original <td> and inject replacements
                    
limitedNodes.push(td[0]);
                }
            }

            
emptyCellsUntil(this.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 <a> element that represents hidden event element for a cell.
    // Responsible for attaching click handler as well.
    
renderMoreLink: function(rowcolhiddenSegs) {
        var 
_this this;
        var 
view this.view;

        return $(
'<a class="fc-more"/>')
            .
text(
                
this.getMoreLinkText(hiddenSegs.length)
            )
            .
on('click', function(ev) {
                var 
clickOption view.opt('eventLimitClick');
                var 
date _this.getCellDate(rowcol);
                var 
moreEl = $(this);
                var 
dayEl _this.getCellEl(rowcol);
                var 
allSegs _this.getCellSegs(rowcol);

                
// rescope the segments to be within the cell's date
                
var reslicedAllSegs _this.resliceDaySegs(allSegsdate);
                var 
reslicedHiddenSegs _this.resliceDaySegs(hiddenSegsdate);

                if (
typeof clickOption === 'function') {
                    
// the returned value can be an atomic option
                    
clickOption view.trigger('eventLimitClick'null, {
                        
datedate,
                        
dayEldayEl,
                        
moreElmoreEl,
                        
segsreslicedAllSegs,
                        
hiddenSegsreslicedHiddenSegs
                    
}, ev);
                }

                if (
clickOption === 'popover') {
                    
_this.showSegPopover(rowcolmoreElreslicedAllSegs);
                }
                else if (
typeof clickOption === 'string') { // a view name
                    
view.calendar.zoomTo(dateclickOption);
                }
            });
    },


    
// Reveals the popover that displays all events within a cell
    
showSegPopover: function(rowcolmoreLinksegs) {
        var 
_this this;
        var 
view this.view;
        var 
moreWrap moreLink.parent(); // the <div> wrapper around the <a>
        
var topEl// the element we want to match the top coordinate of
        
var options;

        if (
this.rowCnt == 1) {
            
topEl view.el// will cause the popover to cover any sort of header
        
}
        else {
            
topEl this.rowEls.eq(row); // will align with top of row
        
}

        
options = {
            
className'fc-more-popover',
            
contentthis.renderSegPopoverContent(rowcolsegs),
            
parentElthis.el,
            
toptopEl.offset().top,
            
autoHidetrue// when the user clicks elsewhere, hide the popover
            
viewportConstrainview.opt('popoverViewportConstrain'),
            
hide: function() {
                
// kill everything when the popover is hidden
                
_this.segPopover.removeElement();
                
_this.segPopover null;
                
_this.popoverSegs null;
            }
        };

        
// Determine horizontal coordinate.
        // We use the moreWrap instead of the <td> to avoid border confusion.
        
if (this.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(rowcolsegs) {
        var 
view this.view;
        var 
isTheme view.opt('theme');
        var 
title this.getCellDate(rowcol).format(view.opt('dayPopoverFormat'));
        var 
content = $(
            
'<div class="fc-header ' view.widgetHeaderClass '">' +
                
'<span class="fc-close ' +
                    (
isTheme 'ui-icon ui-icon-closethick' 'fc-icon fc-icon-x') +
                
'"></span>' +
                
'<span class="fc-title">' +
                    
htmlEscape(title) +
                
'</span>' +
                
'<div class="fc-clear"/>' +
            
'</div>' +
            
'<div class="fc-body ' view.widgetContentClass '">' +
                
'<div class="fc-event-container"></div>' +
            
'</div>'
        
);
        var 
segContainer content.find('.fc-event-container');
        var 
i;

        
// render each seg's `el` and only return the visible segs
        
segs this.renderFgSegEls(segstrue); // disableResizing=true
        
this.popoverSegs segs;

        for (
0segs.lengthi++) {

            
// 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
            
this.prepareHits();
            
segs[i].hit this.getCellHit(rowcol);
            
this.releaseHits();

            
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(segsdayDate) {

        
// build an array of the original events
        
var events = $.map(segs, function(seg) {
            return 
seg.event;
        });

        var 
dayStart dayDate.clone();
        var 
dayEnd dayStart.clone().add(1'days');
        var 
dayRange = { startdayStartenddayEnd };

        
// slice the events with a custom slicing function
        
segs this.eventsToSegs(
            
events,
            function(
range) {
                var 
seg intersectRanges(rangedayRange); // undefind if no intersection
                
return seg ? [ seg ] : []; // must return an array of segments
            
}
        );

        
// force an order because eventsToSegs doesn't guarantee one
        
this.sortEventSegs(segs);

        return 
segs;
    },


    
// Generates the text that should be inside a "more" link, given the number of events it represents
    
getMoreLinkText: function(num) {
        var 
opt this.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(rowcolstartLevel) {
        var 
segMatrix this.rowStructs[row].segMatrix;
        var 
level startLevel || 0;
        var 
segs = [];
        var 
seg;

        while (
level segMatrix.length) {
            
seg segMatrix[level][col];
            if (
seg) {
                
segs.push(seg);
            }
            
level++;
        }

        return 
segs;
    }

});

;;

/* A component that renders one or more columns of vertical time slots
----------------------------------------------------------------------------------------------------------------------*/
// We mixin DayTable, even though there is only a single row of days

var TimeGrid FC.TimeGrid Grid.extend(DayTableMixin, {

    
slotDurationnull// duration of a "slot", a distinct time segment on given day, visualized by lines
    
snapDurationnull// granularity of time for dragging and selecting
    
snapsPerSlotnull,
    
minTimenull// Duration object that denotes the first visible time of any given day
    
maxTimenull// Duration object that denotes the exclusive visible end time of any given day
    
labelFormatnull// formatting string for times running along vertical axis
    
labelIntervalnull// duration of how often a label should be displayed for a slot

    
colElsnull// cells elements in the day-row background
    
slatElsnull// elements running horizontally across all columns
    
nowIndicatorElsnull,

    
colCoordCachenull,
    
slatCoordCachenull,


    
constructor: function() {
        
Grid.apply(thisarguments); // call the super-constructor

        
this.processOptions();
    },


    
// 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.
    
renderDates: function() {
        
this.el.html(this.renderHtml());
        
this.colEls this.el.find('.fc-day');
        
this.slatEls this.el.find('.fc-slats tr');

        
this.colCoordCache = new CoordCache({
            
elsthis.colEls,
            
isHorizontaltrue
        
});
        
this.slatCoordCache = new CoordCache({
            
elsthis.slatEls,
            
isVerticaltrue
        
});

        
this.renderContentSkeleton();
    },


    
// Renders the basic HTML skeleton for the grid
    
renderHtml: function() {
        return 
'' +
            
'<div class="fc-bg">' +
                
'<table>' +
                    
this.renderBgTrHtml(0) + // row=0
                
'</table>' +
            
'</div>' +
            
'<div class="fc-slats">' +
                
'<table>' +
                    
this.renderSlatRowHtml() +
                
'</table>' +
            
'</div>';
    },


    
// Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL.
    
renderSlatRowHtml: function() {
        var 
view this.view;
        var 
isRTL this.isRTL;
        var 
html '';
        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 isLabeled;
        var 
axisHtml;

        
// Calculate the time for each slot
        
while (slotTime this.maxTime) {
            
slotDate this.start.clone().time(slotTime);
            
isLabeled isInt(divideDurationByDuration(slotTimethis.labelInterval));

            
axisHtml =
                
'<td class="fc-axis fc-time ' view.widgetContentClass '" ' view.axisStyleAttr() + '>' +
                    (
isLabeled ?
                        
'<span>' // for matchCellWidths
                            
htmlEscape(slotDate.format(this.labelFormat)) +
                        
'</span>' :
                        
''
                        
) +
                
'</td>';

            
html +=
                
'<tr data-time="' slotDate.format('HH:mm:ss') + '"' +
                    (
isLabeled '' ' class="fc-minor"') +
                    
'>' +
                    (!
isRTL axisHtml '') +
                    
'<td class="' view.widgetContentClass '"/>' +
                    (
isRTL axisHtml '') +
                
"</tr>";

            
slotTime.add(this.slotDuration);
        }

        return 
html;
    },


    
/* Options
    ------------------------------------------------------------------------------------------------------------------*/


    // Parses various options into properties of this object
    
processOptions: function() {
        var 
view this.view;
        var 
slotDuration view.opt('slotDuration');
        var 
snapDuration view.opt('snapDuration');
        var 
input;

        
slotDuration moment.duration(slotDuration);
        
snapDuration snapDuration moment.duration(snapDuration) : slotDuration;

        
this.slotDuration slotDuration;
        
this.snapDuration snapDuration;
        
this.snapsPerSlot slotDuration snapDuration// TODO: ensure an integer multiple?

        
this.minResizeDuration snapDuration// hack

        
this.minTime moment.duration(view.opt('minTime'));
        
this.maxTime moment.duration(view.opt('maxTime'));

        
// might be an array value (for TimelineView).
        // if so, getting the most granular entry (the last one probably).
        
input view.opt('slotLabelFormat');
        if ($.
isArray(input)) {
            
input input[input.length 1];
        }

        
this.labelFormat =
            
input ||
            
view.opt('axisFormat') || // deprecated
            
view.opt('smallTimeFormat'); // the computed default

        
input view.opt('slotLabelInterval');
        
this.labelInterval input ?
            
moment.duration(input) :
            
this.computeLabelInterval(slotDuration);
    },


    
// Computes an automatic value for slotLabelInterval
    
computeLabelInterval: function(slotDuration) {
        var 
i;
        var 
labelInterval;
        var 
slotsPerLabel;

        
// find the smallest stock label interval that results in more than one slots-per-label
        
for (AGENDA_STOCK_SUB_DURATIONS.length 1>= 0i--) {
            
labelInterval moment.duration(AGENDA_STOCK_SUB_DURATIONS[i]);
            
slotsPerLabel divideDurationByDuration(labelIntervalslotDuration);
            if (
isInt(slotsPerLabel) && slotsPerLabel 1) {
                return 
labelInterval;
            }
        }

        return 
moment.duration(slotDuration); // fall back. clone
    
},


    
// Computes a default event time formatting string if `timeFormat` is not explicitly defined
    
computeEventTimeFormat: function() {
        return 
this.view.opt('noMeridiemTimeFormat'); // like "6:30" (no AM/PM)
    
},


    
// Computes a default `displayEventEnd` value if one is not expliclty defined
    
computeDisplayEventEnd: function() {
        return 
true;
    },


    
/* Hit System
    ------------------------------------------------------------------------------------------------------------------*/


    
prepareHits: function() {
        
this.colCoordCache.build();
        
this.slatCoordCache.build();
    },


    
releaseHits: function() {
        
this.colCoordCache.clear();
        
// NOTE: don't clear slatCoordCache because we rely on it for computeTimeTop
    
},


    
queryHit: function(leftOffsettopOffset) {
        var 
snapsPerSlot this.snapsPerSlot;
        var 
colCoordCache this.colCoordCache;
        var 
slatCoordCache this.slatCoordCache;
        var 
colIndex colCoordCache.getHorizontalIndex(leftOffset);
        var 
slatIndex slatCoordCache.getVerticalIndex(topOffset);

        if (
colIndex != null && slatIndex != null) {
            var 
slatTop slatCoordCache.getTopOffset(slatIndex);
            var 
slatHeight slatCoordCache.getHeight(slatIndex);
            var 
partial = (topOffset slatTop) / slatHeight// floating point number between 0 and 1
            
var localSnapIndex Math.floor(partial snapsPerSlot); // the snap # relative to start of slat
            
var snapIndex slatIndex snapsPerSlot localSnapIndex;
            var 
snapTop slatTop + (localSnapIndex snapsPerSlot) * slatHeight;
            var 
snapBottom slatTop + ((localSnapIndex 1) / snapsPerSlot) * slatHeight;

            return {
                
colcolIndex,
                
snapsnapIndex,
                
componentthis// needed unfortunately :(
                
leftcolCoordCache.getLeftOffset(colIndex),
                
rightcolCoordCache.getRightOffset(colIndex),
                
topsnapTop,
                
bottomsnapBottom
            
};
        }
    },


    
getHitSpan: function(hit) {
        var 
start this.getCellDate(0hit.col); // row=0
        
var time this.computeSnapTime(hit.snap); // pass in the snap-index
        
var end;

        
start.time(time);
        
end start.clone().add(this.snapDuration);

        return { 
startstartendend };
    },


    
getHitEl: function(hit) {
        return 
this.colEls.eq(hit.col);
    },


    
/* Dates
    ------------------------------------------------------------------------------------------------------------------*/


    
rangeUpdated: function() {
        
this.updateDayTable();
    },


    
// Given a row number of the grid, representing a "snap", returns a time (Duration) from its start-of-day
    
computeSnapTime: function(snapIndex) {
        return 
moment.duration(this.minTime this.snapDuration snapIndex);
    },


    
// Slices up the given span (unzoned start/end with other misc data) into an array of segments
    
spanToSegs: function(span) {
        var 
segs this.sliceRangeByTimes(span);
        var 
i;

        for (
0segs.lengthi++) {
            if (
this.isRTL) {
                
segs[i].col this.daysPerRow segs[i].dayIndex;
            }
            else {
                
segs[i].col segs[i].dayIndex;
            }
        }

        return 
segs;
    },


    
sliceRangeByTimes: function(range) {
        var 
segs = [];
        var 
seg;
        var 
dayIndex;
        var 
dayDate;
        var 
dayRange;

        for (
dayIndex 0dayIndex this.daysPerRowdayIndex++) {
            
dayDate this.dayDates[dayIndex].clone(); // TODO: better API for this?
            
dayRange = {
                
startdayDate.clone().time(this.minTime),
                
enddayDate.clone().time(this.maxTime)
            };
            
seg intersectRanges(rangedayRange); // both will be ambig timezone
            
if (seg) {
                
seg.dayIndex dayIndex;
                
segs.push(seg);
            }
        }

        return 
segs;
    },


    
/* Coordinates
    ------------------------------------------------------------------------------------------------------------------*/


    
updateSize: function(isResize) { // NOT a standard Grid method
        
this.slatCoordCache.build();

        if (
isResize) {
            
this.updateSegVerticals(
                [].
concat(this.fgSegs || [], this.bgSegs || [], this.businessSegs || [])
            );
        }
    },


    
// 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(datestartOfDayDate) {
        return 
this.computeTimeTop(
            
moment.duration(
                
date startOfDayDate.clone().stripTime()
            )
        );
    },


    
// Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
    
computeTimeTop: function(time) {
        var 
len this.slatEls.length;
        var 
slatCoverage = (time this.minTime) / this.slotDuration// floating-point value of # of slots covered
        
var slatIndex;
        var 
slatRemainder;

        
// compute a floating-point number for how many slats should be progressed through.
        // from 0 to number of slats (inclusive)
        // constrained because minTime/maxTime might be customized.
        
slatCoverage Math.max(0slatCoverage);
        
slatCoverage Math.min(lenslatCoverage);

        
// an integer index of the furthest whole slat
        // from 0 to number slats (*exclusive*, so len-1)
        
slatIndex Math.floor(slatCoverage);
        
slatIndex Math.min(slatIndexlen 1);

        
// how much further through the slatIndex slat (from 0.0-1.0) must be covered in addition.
        // could be 1.0 if slatCoverage is covering *all* the slots
        
slatRemainder slatCoverage slatIndex;

        return 
this.slatCoordCache.getTopPosition(slatIndex) +
            
this.slatCoordCache.getHeight(slatIndex) * slatRemainder;
    },



    
/* Event Drag Visualization
    ------------------------------------------------------------------------------------------------------------------*/


    // Renders a visual indication of an event being dragged over the specified date(s).
    // A returned value of `true` signals that a mock "helper" event has been rendered.
    
renderDrag: function(eventLocationseg) {

        if (
seg) { // if there is event information for this drag, render a helper event
            
this.renderEventLocationHelper(eventLocationseg);

            for (var 
0this.helperSegs.lengthi++) {
                
this.applyDragOpacity(this.helperSegs[i].el);
            }

            return 
true// signal that a helper has been rendered
        
}
        else {
            
// otherwise, just render a highlight
            
this.renderHighlight(this.eventToSpan(eventLocation));
        }
    },


    
// Unrenders any visual indication of an event being dragged
    
unrenderDrag: function() {
        
this.unrenderHelper();
        
this.unrenderHighlight();
    },


    
/* Event Resize Visualization
    ------------------------------------------------------------------------------------------------------------------*/


    // Renders a visual indication of an event being resized
    
renderEventResize: function(eventLocationseg) {
        
this.renderEventLocationHelper(eventLocationseg);
    },


    
// Unrenders any visual indication of an event being resized
    
unrenderEventResize: function() {
        
this.unrenderHelper();
    },


    
/* Event Helper
    ------------------------------------------------------------------------------------------------------------------*/


    // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag)
    
renderHelper: function(eventsourceSeg) {
        
this.renderHelperSegs(this.eventToSegs(event), sourceSeg);
    },


    
// Unrenders any mock helper event
    
unrenderHelper: function() {
        
this.unrenderHelperSegs();
    },


    
/* Business Hours
    ------------------------------------------------------------------------------------------------------------------*/


    
renderBusinessHours: function() {
        var 
events this.view.calendar.getBusinessHoursEvents();
        var 
segs this.eventsToSegs(events);

        
this.renderBusinessSegs(segs);
    },


    
unrenderBusinessHours: function() {
        
this.unrenderBusinessSegs();
    },


    
/* Now Indicator
    ------------------------------------------------------------------------------------------------------------------*/


    
getNowIndicatorUnit: function() {
        return 
'minute'// will refresh on the minute
    
},


    
renderNowIndicator: function(date) {
        
// seg system might be overkill, but it handles scenario where line needs to be rendered
        //  more than once because of columns with the same date (resources columns for example)
        
var segs this.spanToSegs({ startdateenddate });
        var 
top this.computeDateTop(datedate);
        var 
nodes = [];
        var 
i;

        
// render lines within the columns
        
for (0segs.lengthi++) {
            
nodes.push($('<div class="fc-now-indicator fc-now-indicator-line"></div>')
                .
css('top'top)
                .
appendTo(this.colContainerEls.eq(segs[i].col))[0]);
        }

        
// render an arrow over the axis
        
if (segs.length 0) { // is the current time in view?
            
nodes.push($('<div class="fc-now-indicator fc-now-indicator-arrow"></div>')
                .
css('top'top)
                .
appendTo(this.el.find('.fc-content-skeleton'))[0]);
        }

        
this.nowIndicatorEls = $(nodes);
    },


    
unrenderNowIndicator: function() {
        if (
this.nowIndicatorEls) {
            
this.nowIndicatorEls.remove();
            
this.nowIndicatorEls null;
        }
    },


    
/* Selection
    ------------------------------------------------------------------------------------------------------------------*/


    // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight.
    
renderSelection: function(span) {
        if (
this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered

            // normally acceps an eventLocation, span has a start/end, which is good enough
            
this.renderEventLocationHelper(span);
        }
        else {
            
this.renderHighlight(span);
        }
    },


    
// Unrenders any visual indication of a selection
    
unrenderSelection: function() {
        
this.unrenderHelper();
        
this.unrenderHighlight();
    },


    
/* Highlight
    ------------------------------------------------------------------------------------------------------------------*/


    
renderHighlight: function(span) {
        
this.renderHighlightSegs(this.spanToSegs(span));
    },


    
unrenderHighlight: function() {
        
this.unrenderHighlightSegs();
    }

});

;;

/* Methods for rendering SEGMENTS, pieces of content that live on the view
 ( this file is no longer just for events )
----------------------------------------------------------------------------------------------------------------------*/

TimeGrid.mixin({

    
colContainerElsnull// containers for each column

    // inner-containers for each column where different types of segs live
    
fgContainerElsnull,
    
bgContainerElsnull,
    
helperContainerElsnull,
    
highlightContainerElsnull,
    
businessContainerElsnull,

    
// arrays of different types of displayed segments
    
fgSegsnull,
    
bgSegsnull,
    
helperSegsnull,
    
highlightSegsnull,
    
businessSegsnull,


    
// Renders the DOM that the view's content will live in
    
renderContentSkeleton: function() {
        var 
cellHtml '';
        var 
i;
        var 
skeletonEl;

        for (
0this.colCnti++) {
            
cellHtml +=
                
'<td>' +
                    
'<div class="fc-content-col">' +
                        
'<div class="fc-event-container fc-helper-container"></div>' +
                        
'<div class="fc-event-container"></div>' +
                        
'<div class="fc-highlight-container"></div>' +
                        
'<div class="fc-bgevent-container"></div>' +
                        
'<div class="fc-business-container"></div>' +
                    
'</div>' +
                
'</td>';
        }

        
skeletonEl = $(
            
'<div class="fc-content-skeleton">' +
                
'<table>' +
                    
'<tr>' cellHtml '</tr>' +
                
'</table>' +
            
'</div>'
        
);

        
this.colContainerEls skeletonEl.find('.fc-content-col');
        
this.helperContainerEls skeletonEl.find('.fc-helper-container');
        
this.fgContainerEls skeletonEl.find('.fc-event-container:not(.fc-helper-container)');
        
this.bgContainerEls skeletonEl.find('.fc-bgevent-container');
        
this.highlightContainerEls skeletonEl.find('.fc-highlight-container');
        
this.businessContainerEls skeletonEl.find('.fc-business-container');

        
this.bookendCells(skeletonEl.find('tr')); // TODO: do this on string level
        
this.el.append(skeletonEl);
    },


    
/* Foreground Events
    ------------------------------------------------------------------------------------------------------------------*/


    
renderFgSegs: function(segs) {
        
segs this.renderFgSegsIntoContainers(segsthis.fgContainerEls);
        
this.fgSegs segs;
        return 
segs// needed for Grid::renderEvents
    
},


    
unrenderFgSegs: function() {
        
this.unrenderNamedSegs('fgSegs');
    },


    
/* Foreground Helper Events
    ------------------------------------------------------------------------------------------------------------------*/


    
renderHelperSegs: function(segssourceSeg) {
        var 
iseg;
        var 
sourceEl;

        
segs this.renderFgSegsIntoContainers(segsthis.helperContainerEls);

        
// Try to make the segment that is in the same row as sourceSeg look the same
        
for (0segs.lengthi++) {
            
seg segs[i];
            if (
sourceSeg && sourceSeg.col === seg.col) {
                
sourceEl sourceSeg.el;
                
seg.el.css({
                    
leftsourceEl.css('left'),
                    
rightsourceEl.css('right'),
                    
'margin-left'sourceEl.css('margin-left'),
                    
'margin-right'sourceEl.css('margin-right')
                });
            }
        }

        
this.helperSegs segs;
    },


    
unrenderHelperSegs: function() {
        
this.unrenderNamedSegs('helperSegs');
    },


    
/* Background Events
    ------------------------------------------------------------------------------------------------------------------*/


    
renderBgSegs: function(segs) {
        
segs this.renderFillSegEls('bgEvent'segs); // TODO: old fill system
        
this.updateSegVerticals(segs);
        
this.attachSegsByCol(this.groupSegsByCol(segs), this.bgContainerEls);
        
this.bgSegs segs;
        return 
segs// needed for Grid::renderEvents
    
},


    
unrenderBgSegs: function() {
        
this.unrenderNamedSegs('bgSegs');
    },


    
/* Highlight
    ------------------------------------------------------------------------------------------------------------------*/


    
renderHighlightSegs: function(segs) {
        
segs this.renderFillSegEls('highlight'segs); // TODO: old fill system
        
this.updateSegVerticals(segs);
        
this.attachSegsByCol(this.groupSegsByCol(segs), this.highlightContainerEls);
        
this.highlightSegs segs;
    },


    
unrenderHighlightSegs: function() {
        
this.unrenderNamedSegs('highlightSegs');
    },


    
/* Business Hours
    ------------------------------------------------------------------------------------------------------------------*/


    
renderBusinessSegs: function(segs) {
        
segs this.renderFillSegEls('businessHours'segs); // TODO: old fill system
        
this.updateSegVerticals(segs);
        
this.attachSegsByCol(this.groupSegsByCol(segs), this.businessContainerEls);
        
this.businessSegs segs;
    },


    
unrenderBusinessSegs: function() {
        
this.unrenderNamedSegs('businessSegs');
    },


    
/* Seg Rendering Utils
    ------------------------------------------------------------------------------------------------------------------*/


    // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col
    
groupSegsByCol: function(segs) {
        var 
segsByCol = [];
        var 
i;

        for (
0this.colCnti++) {
            
segsByCol.push([]);
        }

        for (
0segs.lengthi++) {
            
segsByCol[segs[i].col].push(segs[i]);
        }

        return 
segsByCol;
    },


    
// Given segments grouped by column, insert the segments' elements into a parallel array of container
    // elements, each living within a column.
    
attachSegsByCol: function(segsByColcontainerEls) {
        var 
col;
        var 
segs;
        var 
i;

        for (
col 0col this.colCntcol++) { // iterate each column grouping
            
segs segsByCol[col];

            for (
0segs.lengthi++) {
                
containerEls.eq(col).append(segs[i].el);
            }
        }
    },


    
// Given the name of a property of `this` object, assumed to be an array of segments,
    // loops through each segment and removes from DOM. Will null-out the property afterwards.
    
unrenderNamedSegs: function(propName) {
        var 
segs this[propName];
        var 
i;

        if (
segs) {
            for (
0segs.lengthi++) {
                
segs[i].el.remove();
            }
            
this[propName] = null;
        }
    },



    
/* Foreground Event Rendering Utils
    ------------------------------------------------------------------------------------------------------------------*/


    // Given an array of foreground segments, render a DOM element for each, computes position,
    // and attaches to the column inner-container elements.
    
renderFgSegsIntoContainers: function(segscontainerEls) {
        var 
segsByCol;
        var 
col;

        
segs this.renderFgSegEls(segs); // will call fgSegHtml
        
segsByCol this.groupSegsByCol(segs);

        for (
col 0col this.colCntcol++) {
            
this.updateFgSegCoords(segsByCol[col]);
        }

        
this.attachSegsByCol(segsByColcontainerEls);

        return 
segs;
    },


    
// Renders the HTML for a single event segment's default rendering
    
fgSegHtml: function(segdisableResizing) {
        var 
view this.view;
        var 
event seg.event;
        var 
isDraggable view.isEventDraggable(event);
        var 
isResizableFromStart = !disableResizing && seg.isStart && view.isEventResizableFromStart(event);
        var 
isResizableFromEnd = !disableResizing && seg.isEnd && view.isEventResizableFromEnd(event);
        var 
classes this.getSegClasses(segisDraggableisResizableFromStart || isResizableFromEnd);
        var 
skinCss cssToStr(this.getSegSkinCss(seg));
        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''fc-v-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 this.getEventTimeText(seg);
                
fullTimeText this.getEventTimeText(seg'LT');
                
startTimeText this.getEventTimeText(segnullfalse); // displayEnd=false
            
}
        } else {
            
// Display the normal time text for the *event's* times
            
timeText this.getEventTimeText(event);
            
fullTimeText this.getEventTimeText(event'LT');
            
startTimeText this.getEventTimeText(eventnullfalse); // displayEnd=false
        
}

        return 
'<a class="' classes.join(' ') + '"' +
            (
event.url ?
                
' href="' htmlEscape(event.url) + '"' :
                
''
                
) +
            (
skinCss ?
                
' style="' skinCss '"' :
                
''
                
) +
            
'>' +
                
'<div class="fc-content">' +
                    (
timeText ?
                        
'<div class="fc-time"' +
                        
' data-start="' htmlEscape(startTimeText) + '"' +
                        
' data-full="' htmlEscape(fullTimeText) + '"' +
                        
'>' +
                            
'<span>' htmlEscape(timeText) + '</span>' +
                        
'</div>' :
                        
''
                        
) +
                    (
event.title ?
                        
'<div class="fc-title">' +
                            
htmlEscape(event.title) +
                        
'</div>' :
                        
''
                        
) +
                
'</div>' +
                
'<div class="fc-bg"/>' +
                
/* TODO: write CSS for this
                (isResizableFromStart ?
                    '<div class="fc-resizer fc-start-resizer" />' :
                    ''
                    ) +
                */
                
(isResizableFromEnd ?
                    
'<div class="fc-resizer fc-end-resizer" />' :
                    
''
                    
) +
            
'</a>';
    },


    
/* Seg Position Utils
    ------------------------------------------------------------------------------------------------------------------*/


    // Refreshes the CSS top/bottom coordinates for each segment element.
    // Works when called after initial render, after a window resize/zoom for example.
    
updateSegVerticals: function(segs) {
        
this.computeSegVerticals(segs);
        
this.assignSegVerticals(segs);
    },


    
// For each segment in an array, computes and assigns its top and bottom properties
    
computeSegVerticals: function(segs) {
        var 
iseg;

        for (
0segs.lengthi++) {
            
seg segs[i];
            
seg.top this.computeDateTop(seg.startseg.start);
            
seg.bottom this.computeDateTop(seg.endseg.start);
        }
    },


    
// Given segments that already have their top/bottom properties computed, applies those values to
    // the segments' elements.
    
assignSegVerticals: function(segs) {
        var 
iseg;

        for (
0segs.lengthi++) {
            
seg segs[i];
            
seg.el.css(this.generateSegVerticalCss(seg));
        }
    },


    
// Generates an object with CSS properties for the top/bottom coordinates of a segment element
    
generateSegVerticalCss: function(seg) {
        return {
            
topseg.top,
            
bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container
        
};
    },


    
/* Foreground Event Positioning Utils
    ------------------------------------------------------------------------------------------------------------------*/


    // Given segments that are assumed to all live in the *same column*,
    // compute their verical/horizontal coordinates and assign to their elements.
    
updateFgSegCoords: function(segs) {
        
this.computeSegVerticals(segs); // horizontals relies on this
        
this.computeFgSegHorizontals(segs); // compute horizontal coordinates, z-index's, and reorder the array
        
this.assignSegVerticals(segs);
        
this.assignFgSegHorizontals(segs);
    },


    
// Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each.
    // NOTE: Also reorders the given array by date!
    
computeFgSegHorizontals: function(segs) {
        var 
levels;
        var 
level0;
        var 
i;

        
this.sortEventSegs(segs); // order by certain criteria
        
levels buildSlotSegLevels(segs);
        
computeForwardSlotSegs(levels);

        if ((
level0 levels[0])) {

            for (
0level0.lengthi++) {
                
computeSlotSegPressures(level0[i]);
            }

            for (
0level0.lengthi++) {
                
this.computeFgSegForwardBack(level0[i], 00);
            }
        }
    },


    
// Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
    // from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
    // seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
    //
    // The segment might be part of a "series", which means consecutive segments with the same pressure
    // who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
    // segments behind this one in the current series, and `seriesBackwardCoord` is the starting
    // coordinate of the first segment in the series.
    
computeFgSegForwardBack: function(segseriesBackwardPressureseriesBackwardCoord) {
        var 
forwardSegs seg.forwardSegs;
        var 
i;

        if (
seg.forwardCoord === undefined) { // not already computed

            
if (!forwardSegs.length) {

                
// if there are no forward segments, this segment should butt up against the edge
                
seg.forwardCoord 1;
            }
            else {

                
// sort highest pressure first
                
this.sortForwardSegs(forwardSegs);

                
// this segment's forwardCoord will be calculated from the backwardCoord of the
                // highest-pressure forward segment.
                
this.computeFgSegForwardBack(forwardSegs[0], seriesBackwardPressure 1seriesBackwardCoord);
                
seg.forwardCoord forwardSegs[0].backwardCoord;
            }

            
// calculate the backwardCoord from the forwardCoord. consider the series
            
seg.backwardCoord seg.forwardCoord -
                (
seg.forwardCoord seriesBackwardCoord) / // available width for series
                
(seriesBackwardPressure 1); // # of segments in the series

            // use this segment's coordinates to computed the coordinates of the less-pressurized
            // forward segments
            
for (i=0i<forwardSegs.lengthi++) {
                
this.computeFgSegForwardBack(forwardSegs[i], 0seg.forwardCoord);
            }
        }
    },


    
sortForwardSegs: function(forwardSegs) {
        
forwardSegs.sort(proxy(this'compareForwardSegs'));
    },


    
// A cmp function for determining which forward segment to rely on more when computing coordinates.
    
compareForwardSegs: function(seg1seg2) {
        
// 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...
            
this.compareEventSegs(seg1seg2);
    },


    
// Given foreground event segments that have already had their position coordinates computed,
    // assigns position-related CSS values to their elements.
    
assignFgSegHorizontals: function(segs) {
        var 
iseg;

        for (
0segs.lengthi++) {
            
seg segs[i];
            
seg.el.css(this.generateFgSegHorizontalCss(seg));

            
// if the height is short, add a className for alternate styling
            
if (seg.bottom seg.top 30) {
                
seg.el.addClass('fc-short');
            }
        }
    },


    
// 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.
    
generateFgSegHorizontalCss: function(seg) {
        var 
shouldOverlap this.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(1backwardCoord + (forwardCoord backwardCoord) * 2);
        }

        if (
this.isRTL) {
            
left forwardCoord;
            
right backwardCoord;
        }
        else {
            
left backwardCoord;
            
right 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[this.isRTL 'marginLeft' 'marginRight'] = 10 2// 10 is a guesstimate of the icon's width
        
}

        return 
props;
    }

});


// 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 
iseg;
    var 
j;

    for (
i=0i<segs.lengthi++) {
        
seg segs[i];

        
// go through all the levels and stop on the first level where there are no collisions
        
for (j=0j<levels.lengthj++) {
            if (!
computeSlotSegCollisions(seglevels[j]).length) {
                break;
            }
        }

        
seg.level j;

        (
levels[j] || (levels[j] = [])).push(seg);
    }

    return 
levels;
}


// For every segment, figure out the other segments that are in subsequent
// levels that also occupy the same vertical space. Accumulate in seg.forwardSegs
function computeForwardSlotSegs(levels) {
    var 
ilevel;
    var 
jseg;
    var 
k;

    for (
i=0i<levels.lengthi++) {
        
level levels[i];

        for (
j=0j<level.lengthj++) {
            
seg level[j];

            
seg.forwardSegs = [];
            for (
k=i+1k<levels.lengthk++) {
                
computeSlotSegCollisions(seglevels[k], seg.forwardSegs);
            }
        }
    }
}


// Figure out which path forward (via seg.forwardSegs) results in the longest path until
// the furthest edge is reached. The number of segments in this path will be seg.forwardPressure
function computeSlotSegPressures(seg) {
    var 
forwardSegs seg.forwardSegs;
    var 
forwardPressure 0;
    var 
iforwardSeg;

    if (
seg.forwardPressure === undefined) { // not already computed

        
for (i=0i<forwardSegs.lengthi++) {
            
forwardSeg forwardSegs[i];

            
// figure out the child's maximum forward path
            
computeSlotSegPressures(forwardSeg);

            
// either use the existing maximum, or use the child's forward pressure
            // plus one (for the forwardSeg itself)
            
forwardPressure Math.max(
                
forwardPressure,
                
forwardSeg.forwardPressure
            
);
        }

        
seg.forwardPressure forwardPressure;
    }
}


// Find all the segments in `otherSegs` that vertically collide with `seg`.
// Append into an optionally-supplied `results` array and return.
function computeSlotSegCollisions(segotherSegsresults) {
    
results results || [];

    for (var 
i=0i<otherSegs.lengthi++) {
        if (
isSlotSegCollision(segotherSegs[i])) {
            
results.push(otherSegs[i]);
        }
    }

    return 
results;
}


// Do these segments occupy the same vertical space?
function isSlotSegCollision(seg1seg2) {
    return 
seg1.bottom seg2.top && seg1.top seg2.bottom;
}

;;

/* An abstract class from which other views inherit from
----------------------------------------------------------------------------------------------------------------------*/

var View FC.View = Class.extend({

    
typenull// subclass' view name (string)
    
namenull// deprecated. use `type` instead
    
titlenull// the text that will be displayed in the header's title

    
calendarnull// owner Calendar object
    
optionsnull// hash containing all options. already merged with view-specific-options
    
elnull// the view's containing element. set by Calendar

    
displayingnull// a promise representing the state of rendering. null if no render requested
    
isSkeletonRenderedfalse,
    
isEventsRenderedfalse,

    
// range the view is actually displaying (moments)
    
startnull,
    
endnull// exclusive

    // range the view is formally responsible for (moments)
    // may be different from start/end. for example, a month view might have 1st-31st, excluding padded dates
    
intervalStartnull,
    
intervalEndnull// exclusive
    
intervalDurationnull,
    
intervalUnitnull// name of largest unit being displayed, like "month" or "week"

    
isRTLfalse,
    
isSelectedfalse// boolean whether a range of time is user-selected or not

    
eventOrderSpecsnull// criteria for ordering events when they have same date/time

    // subclasses can optionally use a scroll container
    
scrollerElnull// the element that will most likely scroll when content is too tall
    
scrollTopnull// cached vertical scroll value

    // classNames styled by jqui themes
    
widgetHeaderClassnull,
    
widgetContentClassnull,
    
highlightStateClassnull,

    
// for date utils, computed from options
    
nextDayThresholdnull,
    
isHiddenDayHashnull,

    
// document handlers, bound to `this` object
    
documentMousedownProxynull// TODO: doesn't work with touch

    // now indicator
    
isNowIndicatorRenderednull,
    
initialNowDatenull// result first getNow call
    
initialNowQueriedMsnull// ms time the getNow was called
    
nowIndicatorTimeoutIDnull// for refresh timing of now indicator
    
nowIndicatorIntervalIDnull// "


    
constructor: function(calendartypeoptionsintervalDuration) {

        
this.calendar calendar;
        
this.type this.name type// .name is deprecated
        
this.options options;
        
this.intervalDuration intervalDuration || moment.duration(1'day');

        
this.nextDayThreshold moment.duration(this.opt('nextDayThreshold'));
        
this.initThemingProps();
        
this.initHiddenDays();
        
this.isRTL this.opt('isRTL');

        
this.eventOrderSpecs parseFieldSpecs(this.opt('eventOrder'));

        
this.documentMousedownProxy proxy(this'documentMousedown');

        
this.initialize();
    },


    
// A good place for subclasses to initialize member variables
    
initialize: function() {
        
// subclasses can implement
    
},


    
// Retrieves an option with the given name
    
opt: function(name) {
        return 
this.options[name];
    },


    
// Triggers handlers that are view-related. Modifies args before passing to calendar.
    
trigger: function(namethisObj) { // arguments beyond thisObj are passed along
        
var calendar this.calendar;

        return 
calendar.trigger.apply(
            
calendar,
            [
namethisObj || this].concat(
                Array.
prototype.slice.call(arguments2), // arguments beyond thisObj
                
this // always make the last argument a reference to the view. TODO: deprecate
            
)
        );
    },


    
/* Dates
    ------------------------------------------------------------------------------------------------------------------*/


    // Updates all internal dates to center around the given current unzoned date.
    
setDate: function(date) {
        
this.setRange(this.computeRange(date));
    },


    
// Updates all internal dates for displaying the given unzoned range.
    
setRange: function(range) {
        $.
extend(thisrange); // assigns every property to this object's member variables
        
this.updateTitle();
    },


    
// Given a single current unzoned date, produce information about what range to display.
    // Subclasses can override. Must return all properties.
    
computeRange: function(date) {
        var 
intervalUnit computeIntervalUnit(this.intervalDuration);
        var 
intervalStart date.clone().startOf(intervalUnit);
        var 
intervalEnd intervalStart.clone().add(this.intervalDuration);
        var 
startend;

        
// normalize the range's time-ambiguity
        
if (/year|month|week|day/.test(intervalUnit)) { // whole-days?
            
intervalStart.stripTime();
            
intervalEnd.stripTime();
        }
        else { 
// needs to have a time?
            
if (!intervalStart.hasTime()) {
                
intervalStart this.calendar.time(0); // give 00:00 time
            
}
            if (!
intervalEnd.hasTime()) {
                
intervalEnd this.calendar.time(0); // give 00:00 time
            
}
        }

        
start intervalStart.clone();
        
start this.skipHiddenDays(start);
        
end intervalEnd.clone();
        
end this.skipHiddenDays(end, -1true); // exclusively move backwards

        
return {
            
intervalUnitintervalUnit,
            
intervalStartintervalStart,
            
intervalEndintervalEnd,
            
startstart,
            
endend
        
};
    },


    
// Computes the new date when the user hits the prev button, given the current date
    
computePrevDate: function(date) {
        return 
this.massageCurrentDate(
            
date.clone().startOf(this.intervalUnit).subtract(this.intervalDuration), -1
        
);
    },


    
// Computes the new date when the user hits the next button, given the current date
    
computeNextDate: function(date) {
        return 
this.massageCurrentDate(
            
date.clone().startOf(this.intervalUnit).add(this.intervalDuration)
        );
    },


    
// Given an arbitrarily calculated current date of the calendar, returns a date that is ensured to be completely
    // visible. `direction` is optional and indicates which direction the current date was being
    // incremented or decremented (1 or -1).
    
massageCurrentDate: function(datedirection) {
        if (
this.intervalDuration.as('days') <= 1) { // if the view displays a single day or smaller
            
if (this.isHiddenDay(date)) {
                
date this.skipHiddenDays(datedirection);
                
date.startOf('day');
            }
        }

        return 
date;
    },


    
/* Title and Date Formatting
    ------------------------------------------------------------------------------------------------------------------*/


    // Sets the view's title property to the most updated computed value
    
updateTitle: function() {
        
this.title this.computeTitle();
    },


    
// Computes what the title at the top of the calendar should be for this view
    
computeTitle: function() {
        return 
this.formatRange(
            {
                
// in case intervalStart/End has a time, make sure timezone is correct
                
startthis.calendar.applyTimezone(this.intervalStart),
                
endthis.calendar.applyTimezone(this.intervalEnd)
            },
            
this.opt('titleFormat') || this.computeTitleFormat(),
            
this.opt('titleRangeSeparator')
        );
    },


    
// Generates the format string that should be used to generate the title for the current date range.
    // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
    
computeTitleFormat: function() {
        if (
this.intervalUnit == 'year') {
            return 
'YYYY';
        }
        else if (
this.intervalUnit == 'month') {
            return 
this.opt('monthYearFormat'); // like "September 2014"
        
}
        else if (
this.intervalDuration.as('days') > 1) {
            return 
'll'// multi-day range. shorter, like "Sep 9 - 10 2014"
        
}
        else {
            return 
'LL'// one day. longer, like "September 9 2014"
        
}
    },


    
// Utility for formatting a range. Accepts a range object, formatting string, and optional separator.
    // Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account.
    // The timezones of the dates within `range` will be respected.
    
formatRange: function(rangeformatStrseparator) {
        var 
end range.end;

        if (!
end.hasTime()) { // all-day?
            
end end.clone().subtract(1); // convert to inclusive. last ms of previous day
        
}

        return 
formatRange(range.startendformatStrseparatorthis.opt('isRTL'));
    },


    
/* Rendering
    ------------------------------------------------------------------------------------------------------------------*/


    // Sets the container element that the view should render inside of.
    // Does other DOM-related initializations.
    
setElement: function(el) {
        
this.el el;
        
this.bindGlobalHandlers();
    },


    
// Removes the view's container element from the DOM, clearing any content beforehand.
    // Undoes any other DOM-related attachments.
    
removeElement: function() {
        
this.clear(); // clears all content

        // clean up the skeleton
        
if (this.isSkeletonRendered) {
            
this.unrenderSkeleton();
            
this.isSkeletonRendered false;
        }

        
this.unbindGlobalHandlers();

        
this.el.remove();

        
// NOTE: don't null-out this.el in case the View was destroyed within an API callback.
        // We don't null-out the View's other jQuery element references upon destroy,
        //  so we shouldn't kill this.el either.
    
},


    
// Does everything necessary to display the view centered around the given unzoned date.
    // Does every type of rendering EXCEPT rendering events.
    // Is asychronous and returns a promise.
    
display: function(date) {
        var 
_this this;
        var 
scrollState null;

        if (
this.displaying) {
            
scrollState this.queryScroll();
        }

        
this.calendar.freezeContentHeight();

        return 
this.clear().then(function() { // clear the content first (async)
            
return (
                
_this.displaying =
                    $.
when(_this.displayView(date)) // displayView might return a promise
                        
.then(function() {
                            
_this.forceScroll(_this.computeInitialScroll(scrollState));
                            
_this.calendar.unfreezeContentHeight();
                            
_this.triggerRender();
                        })
            );
        });
    },


    
// Does everything necessary to clear the content of the view.
    // Clears dates and events. Does not clear the skeleton.
    // Is asychronous and returns a promise.
    
clear: function() {
        var 
_this this;
        var 
displaying this.displaying;

        if (
displaying) { // previously displayed, or in the process of being displayed?
            
return displaying.then(function() { // wait for the display to finish
                
_this.displaying null;
                
_this.clearEvents();
                return 
_this.clearView(); // might return a promise. chain it
            
});
        }
        else {
            return $.
when(); // an immediately-resolved promise
        
}
    },


    
// Displays the view's non-event content, such as date-related content or anything required by events.
    // Renders the view's non-content skeleton if necessary.
    // Can be asynchronous and return a promise.
    
displayView: function(date) {
        if (!
this.isSkeletonRendered) {
            
this.renderSkeleton();
            
this.isSkeletonRendered true;
        }
        if (
date) {
            
this.setDate(date);
        }
        if (
this.render) {
            
this.render(); // TODO: deprecate
        
}
        
this.renderDates();
        
this.updateSize();
        
this.renderBusinessHours(); // might need coordinates, so should go after updateSize()
        
this.startNowIndicator();
    },


    
// Unrenders the view content that was rendered in displayView.
    // Can be asynchronous and return a promise.
    
clearView: function() {
        
this.unselect();
        
this.stopNowIndicator();
        
this.triggerUnrender();
        
this.unrenderBusinessHours();
        
this.unrenderDates();
        if (
this.destroy) {
            
this.destroy(); // TODO: deprecate
        
}
    },


    
// Renders the basic structure of the view before any content is rendered
    
renderSkeleton: function() {
        
// subclasses should implement
    
},


    
// Unrenders the basic structure of the view
    
unrenderSkeleton: function() {
        
// subclasses should implement
    
},


    
// Renders the view's date-related content.
    // Assumes setRange has already been called and the skeleton has already been rendered.
    
renderDates: function() {
        
// subclasses should implement
    
},


    
// Unrenders the view's date-related content
    
unrenderDates: function() {
        
// subclasses should override
    
},


    
// Signals that the view's content has been rendered
    
triggerRender: function() {
        
this.trigger('viewRender'thisthisthis.el);
    },


    
// Signals that the view's content is about to be unrendered
    
triggerUnrender: function() {
        
this.trigger('viewDestroy'thisthisthis.el);
    },


    
// Binds DOM handlers to elements that reside outside the view container, such as the document
    
bindGlobalHandlers: function() {
        $(
document).on('mousedown'this.documentMousedownProxy);
    },


    
// Unbinds DOM handlers from elements that reside outside the view container
    
unbindGlobalHandlers: function() {
        $(
document).off('mousedown'this.documentMousedownProxy);
    },


    
// Initializes internal variables related to theming
    
initThemingProps: function() {
        var 
tm this.opt('theme') ? 'ui' 'fc';

        
this.widgetHeaderClass tm '-widget-header';
        
this.widgetContentClass tm '-widget-content';
        
this.highlightStateClass tm '-state-highlight';
    },


    
/* Business Hours
    ------------------------------------------------------------------------------------------------------------------*/


    // Renders business-hours onto the view. Assumes updateSize has already been called.
    
renderBusinessHours: function() {
        
// subclasses should implement
    
},


    
// Unrenders previously-rendered business-hours
    
unrenderBusinessHours: function() {
        
// subclasses should implement
    
},


    
/* Now Indicator
    ------------------------------------------------------------------------------------------------------------------*/


    // Immediately render the current time indicator and begins re-rendering it at an interval,
    // which is defined by this.getNowIndicatorUnit().
    // TODO: somehow do this for the current whole day's background too
    
startNowIndicator: function() {
        var 
_this this;
        var 
unit;
        var 
update;
        var 
delay// ms wait value

        
if (this.opt('nowIndicator')) {
            
unit this.getNowIndicatorUnit();
            if (
unit) {
                
update proxy(this'updateNowIndicator'); // bind to `this`

                
this.initialNowDate this.calendar.getNow();
                
this.initialNowQueriedMs = +new Date();
                
this.renderNowIndicator(this.initialNowDate);
                
this.isNowIndicatorRendered true;

                
// wait until the beginning of the next interval
                
delay this.initialNowDate.clone().startOf(unit).add(1unit) - this.initialNowDate;
                
this.nowIndicatorTimeoutID setTimeout(function() {
                    
_this.nowIndicatorTimeoutID null;
                    
update();
                    
delay = +moment.duration(1unit);
                    
delay Math.max(100delay); // prevent too frequent
                    
_this.nowIndicatorIntervalID setInterval(updatedelay); // update every interval
                
}, delay);
            }
        }
    },


    
// rerenders the now indicator, computing the new current time from the amount of time that has passed
    // since the initial getNow call.
    
updateNowIndicator: function() {
        if (
this.isNowIndicatorRendered) {
            
this.unrenderNowIndicator();
            
this.renderNowIndicator(
                
this.initialNowDate.clone().add(new Date() - this.initialNowQueriedMs// add ms
            
);
        }
    },


    
// Immediately unrenders the view's current time indicator and stops any re-rendering timers.
    // Won't cause side effects if indicator isn't rendered.
    
stopNowIndicator: function() {
        if (
this.isNowIndicatorRendered) {

            if (
this.nowIndicatorTimeoutID) {
                
clearTimeout(this.nowIndicatorTimeoutID);
                
this.nowIndicatorTimeoutID null;
            }
            if (
this.nowIndicatorIntervalID) {
                
clearTimeout(this.nowIndicatorIntervalID);
                
this.nowIndicatorIntervalID null;
            }

            
this.unrenderNowIndicator();
            
this.isNowIndicatorRendered false;
        }
    },


    
// Returns a string unit, like 'second' or 'minute' that defined how often the current time indicator
    // should be refreshed. If something falsy is returned, no time indicator is rendered at all.
    
getNowIndicatorUnit: function() {
        
// subclasses should implement
    
},


    
// Renders a current time indicator at the given datetime
    
renderNowIndicator: function(date) {
        
// subclasses should implement
    
},


    
// Undoes the rendering actions from renderNowIndicator
    
unrenderNowIndicator: function() {
        
// subclasses should implement
    
},


    
/* Dimensions
    ------------------------------------------------------------------------------------------------------------------*/


    // Refreshes anything dependant upon sizing of the container element of the grid
    
updateSize: function(isResize) {
        var 
scrollState;

        if (
isResize) {
            
scrollState this.queryScroll();
        }

        
this.updateHeight(isResize);
        
this.updateWidth(isResize);
        
this.updateNowIndicator();

        if (
isResize) {
            
this.setScroll(scrollState);
        }
    },


    
// Refreshes the horizontal dimensions of the calendar
    
updateWidth: function(isResize) {
        
// subclasses should implement
    
},


    
// Refreshes the vertical dimensions of the calendar
    
updateHeight: function(isResize) {
        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(heightisAuto) {
        
// subclasses should implement
    
},


    
/* Scroller
    ------------------------------------------------------------------------------------------------------------------*/


    // 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 
scrollerEl this.scrollerEl;
        var 
both;
        var 
otherHeight// cumulative height of everything that is not the scrollerEl in the view (header+borders)

        
both this.el.add(scrollerEl);

        
// 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: -// ensure reflow in case the el was already relative. negative is less likely to cause new scroll
        
});
        
otherHeight this.el.outerHeight() - scrollerEl.height(); // grab the dimensions
        
both.css({ position''left'' }); // undo hack

        
return totalHeight otherHeight;
    },


    
// Computes the initial pre-configured scroll state prior to allowing the user to change it.
    // Given the scroll state from the previous rendering. If first time rendering, given null.
    
computeInitialScroll: function(previousScrollState) {
        return 
0;
    },


    
// Retrieves the view's current natural scroll state. Can return an arbitrary format.
    
queryScroll: function() {
        if (
this.scrollerEl) {
            return 
this.scrollerEl.scrollTop(); // operates on scrollerEl by default
        
}
    },


    
// Sets the view's scroll state. Will accept the same format computeInitialScroll and queryScroll produce.
    
setScroll: function(scrollState) {
        if (
this.scrollerEl) {
            return 
this.scrollerEl.scrollTop(scrollState); // operates on scrollerEl by default
        
}
    },


    
// Sets the scroll state, making sure to overcome any predefined scroll value the browser has in mind
    
forceScroll: function(scrollState) {
        var 
_this this;

        
this.setScroll(scrollState);
        
setTimeout(function() {
            
_this.setScroll(scrollState);
        }, 
0);
    },


    
/* Event Elements / Segments
    ------------------------------------------------------------------------------------------------------------------*/


    // Does everything necessary to display the given events onto the current view
    
displayEvents: function(events) {
        var 
scrollState this.queryScroll();

        
this.clearEvents();
        
this.renderEvents(events);
        
this.isEventsRendered true;
        
this.setScroll(scrollState);
        
this.triggerEventRender();
    },


    
// Does everything necessary to clear the view's currently-rendered events
    
clearEvents: function() {
        var 
scrollState;

        if (
this.isEventsRendered) {

            
// TODO: optimize: if we know this is part of a displayEvents call, don't queryScroll/setScroll
            
scrollState this.queryScroll();

            
this.triggerEventUnrender();
            if (
this.destroyEvents) {
                
this.destroyEvents(); // TODO: deprecate
            
}
            
this.unrenderEvents();
            
this.setScroll(scrollState);
            
this.isEventsRendered false;
        }
    },


    
// Renders the events onto the view.
    
renderEvents: function(events) {
        
// subclasses should implement
    
},


    
// Removes event elements from the view.
    
unrenderEvents: function() {
        
// subclasses should implement
    
},


    
// Signals that all events have been rendered
    
triggerEventRender: function() {
        
this.renderedEventSegEach(function(seg) {
            
this.trigger('eventAfterRender'seg.eventseg.eventseg.el);
        });
        
this.trigger('eventAfterAllRender');
    },


    
// Signals that all event elements are about to be removed
    
triggerEventUnrender: function() {
        
this.renderedEventSegEach(function(seg) {
            
this.trigger('eventDestroy'seg.eventseg.eventseg.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(eventel) {
        var 
custom this.trigger('eventRender'eventeventel);

        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.renderedEventSegEach(function(seg) {
            
seg.el.css('visibility''');
        }, 
event);
    },


    
// Shows all rendered event segments linked to the given event
    
hideEvent: function(event) {
        
this.renderedEventSegEach(function(seg) {
            
seg.el.css('visibility''hidden');
        }, 
event);
    },


    
// Iterates through event segments that have been rendered (have an el). 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.
    
renderedEventSegEach: function(funcevent) {
        var 
segs this.getEventSegs();
        var 
i;

        for (
0segs.lengthi++) {
            if (!
event || segs[i].event._id === event._id) {
                if (
segs[i].el) {
                    
func.call(thissegs[i]);
                }
            }
        }
    },


    
// Retrieves all the rendered segment objects for the view
    
getEventSegs: function() {
        
// subclasses must implement
        
return [];
    },


    
/* Event Drag-n-Drop
    ------------------------------------------------------------------------------------------------------------------*/


    // Computes if the given event is allowed to be dragged by the user
    
isEventDraggable: function(event) {
        var 
source event.source || {};

        return 
firstDefined(
            
event.startEditable,
            
source.startEditable,
            
this.opt('eventStartEditable'),
            
event.editable,
            
source.editable,
            
this.opt('editable')
        );
    },


    
// Must be called when an event in the view is dropped onto new location.
    // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event.
    
reportEventDrop: function(eventdropLocationlargeUnitelev) {
        var 
calendar this.calendar;
        var 
mutateResult calendar.mutateEvent(eventdropLocationlargeUnit);
        var 
undoFunc = function() {
            
mutateResult.undo();
            
calendar.reportEventChange();
        };

        
this.triggerEventDrop(eventmutateResult.dateDeltaundoFuncelev);
        
calendar.reportEventChange(); // will rerender events
    
},


    
// Triggers event-drop handlers that have subscribed via the API
    
triggerEventDrop: function(eventdateDeltaundoFuncelev) {
        
this.trigger('eventDrop'el[0], eventdateDeltaundoFuncev, {}); // {} = jqui dummy
    
},


    
/* External Element Drag-n-Drop
    ------------------------------------------------------------------------------------------------------------------*/


    // Must be called when an external element, via jQuery UI, has been dropped onto the calendar.
    // `meta` is the parsed data that has been embedded into the dragging event.
    // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event.
    
reportExternalDrop: function(metadropLocationelevui) {
        var 
eventProps meta.eventProps;
        var 
eventInput;
        var 
event;

        
// Try to build an event object and render it. TODO: decouple the two
        
if (eventProps) {
            
eventInput = $.extend({}, eventPropsdropLocation);
            
event this.calendar.renderEvent(eventInputmeta.stick)[0]; // renderEvent returns an array
        
}

        
this.triggerExternalDrop(eventdropLocationelevui);
    },


    
// Triggers external-drop handlers that have subscribed via the API
    
triggerExternalDrop: function(eventdropLocationelevui) {

        
// trigger 'drop' regardless of whether element represents an event
        
this.trigger('drop'el[0], dropLocation.startevui);

        if (
event) {
            
this.trigger('eventReceive'nullevent); // signal an external event landed
        
}
    },


    
/* Drag-n-Drop Rendering (for both events and external elements)
    ------------------------------------------------------------------------------------------------------------------*/


    // Renders a visual indication of a event or external-element drag over the given drop zone.
    // If an external-element, seg will be `null`
    
renderDrag: function(dropLocationseg) {
        
// subclasses must implement
    
},


    
// Unrenders a visual indication of an event or external-element being dragged.
    
unrenderDrag: function() {
        
// subclasses must implement
    
},


    
/* Event Resizing
    ------------------------------------------------------------------------------------------------------------------*/


    // Computes if the given event is allowed to be resized from its starting edge
    
isEventResizableFromStart: function(event) {
        return 
this.opt('eventResizableFromStart') && this.isEventResizable(event);
    },


    
// Computes if the given event is allowed to be resized from its ending edge
    
isEventResizableFromEnd: function(event) {
        return 
this.isEventResizable(event);
    },


    
// Computes if the given event is allowed to be resized by the user at all
    
isEventResizable: function(event) {
        var 
source event.source || {};

        return 
firstDefined(
            
event.durationEditable,
            
source.durationEditable,
            
this.opt('eventDurationEditable'),
            
event.editable,
            
source.editable,
            
this.opt('editable')
        );
    },


    
// Must be called when an event in the view has been resized to a new length
    
reportEventResize: function(eventresizeLocationlargeUnitelev) {
        var 
calendar this.calendar;
        var 
mutateResult calendar.mutateEvent(eventresizeLocationlargeUnit);
        var 
undoFunc = function() {
            
mutateResult.undo();
            
calendar.reportEventChange();
        };

        
this.triggerEventResize(eventmutateResult.durationDeltaundoFuncelev);
        
calendar.reportEventChange(); // will rerender events
    
},


    
// Triggers event-resize handlers that have subscribed via the API
    
triggerEventResize: function(eventdurationDeltaundoFuncelev) {
        
this.trigger('eventResize'el[0], eventdurationDeltaundoFuncev, {}); // {} = jqui dummy
    
},


    
/* Selection
    ------------------------------------------------------------------------------------------------------------------*/


    // Selects a date span on the view. `start` and `end` are both Moments.
    // `ev` is the native mouse event that begin the interaction.
    
select: function(spanev) {
        
this.unselect(ev);
        
this.renderSelection(span);
        
this.reportSelection(spanev);
    },


    
// Renders a visual indication of the selection
    
renderSelection: function(span) {
        
// subclasses should implement
    
},


    
// Called when a new selection is made. Updates internal state and triggers handlers.
    
reportSelection: function(spanev) {
        
this.isSelected true;
        
this.triggerSelect(spanev);
    },


    
// Triggers handlers to 'select'
    
triggerSelect: function(spanev) {
        
this.trigger(
            
'select',
            
null,
            
this.calendar.applyTimezone(span.start), // convert to calendar's tz for external API
            
this.calendar.applyTimezone(span.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;
            if (
this.destroySelection) {
                
this.destroySelection(); // TODO: deprecate
            
}
            
this.unrenderSelection();
            
this.trigger('unselect'nullev);
        }
    },


    
// Unrenders a visual indication of selection
    
unrenderSelection: 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);
            }
        }
    },


    
/* Day Click
    ------------------------------------------------------------------------------------------------------------------*/


    // Triggers handlers to 'dayClick'
    // Span has start/end of the clicked area. Only the start is useful.
    
triggerDayClick: function(spandayElev) {
        
this.trigger(
            
'dayClick',
            
dayEl,
            
this.calendar.applyTimezone(span.start), // convert to calendar's timezone for external API
            
ev
        
);
    },


    
/* Date Utils
    ------------------------------------------------------------------------------------------------------------------*/


    // Initializes internal variables related to calculating hidden days-of-week
    
initHiddenDays: function() {
        var 
hiddenDays this.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 dayCnt 0;
        var 
i;

        if (
this.opt('weekends') === false) {
            
hiddenDays.push(06); // 0=sunday, 6=saturday
        
}

        for (
07i++) {
            if (
                !(
isHiddenDayHash[i] = $.inArray(ihiddenDays) !== -1)
            ) {
                
dayCnt++;
            }
        }

        if (!
dayCnt) {
            throw 
'invalid hiddenDays'// all days were hidden? bad.
        
}

        
this.isHiddenDayHash isHiddenDayHash;
    },


    
// Is the current day hidden?
    // `day` is a day-of-week index (0-6), or a Moment
    
isHiddenDay: function(day) {
        if (
moment.isMoment(day)) {
            
day day.day();
        }
        return 
this.isHiddenDayHash[day];
    },


    
// 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)
    
skipHiddenDays: function(dateincisExclusive) {
        var 
out date.clone();
        
inc inc || 1;
        while (
            
this.isHiddenDayHash[(out.day() + (isExclusive inc 0) + 7) % 7]
        ) {
            
out.add(inc'days');
        }
        return 
out;
    },


    
// Returns the date range of the full days the given range visually appears to occupy.
    // Returns a new range object.
    
computeDayRange: function(range) {
        var 
startDay range.start.clone().stripTime(); // the beginning of the day the range starts
        
var end range.end;
        var 
endDay null;
        var 
endTimeMS;

        if (
end) {
            
endDay end.clone().stripTime(); // the beginning of the day the range exclusively ends
            
endTimeMS = +end.time(); // # of milliseconds into `endDay`

            // If the end time is actually inclusively part of the next day and is equal to or
            // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
            // Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
            
if (endTimeMS && endTimeMS >= this.nextDayThreshold) {
                
endDay.add(1'days');
            }
        }

        
// If no end was specified, or if it is within `startDay` but not past nextDayThreshold,
        // assign the default duration of one day.
        
if (!end || endDay <= startDay) {
            
endDay startDay.clone().add(1'days');
        }

        return { 
startstartDayendendDay };
    },


    
// Does the given event visually appear to occupy more than one day?
    
isMultiDayEvent: function(event) {
        var 
range this.computeDayRange(event); // event is range-ish

        
return range.end.diff(range.start'days') > 1;
    }

});

;;

var 
Calendar FC.Calendar = Class.extend({

    
dirDefaultsnull// option defaults related to LTR or RTL
    
langDefaultsnull// option defaults related to current locale
    
overridesnull// option overrides given to the fullCalendar constructor
    
optionsnull// all defaults combined with overrides
    
viewSpecCachenull// cache of view definitions
    
viewnull// current View object
    
headernull,
    
loadingLevel0// number of simultaneous loading tasks


    // a lot of this class' OOP logic is scoped within this constructor function,
    // but in the future, write individual methods on the prototype.
    
constructorCalendar_constructor,


    
// Subclasses can override this for initialization logic after the constructor has been called
    
initialize: function() {
    },


    
// Initializes `this.options` and other important options-related objects
    
initOptions: function(overrides) {
        var 
langlangDefaults;
        var 
isRTLdirDefaults;

        
// converts legacy options into non-legacy ones.
        // in the future, when this is removed, don't use `overrides` reference. make a copy.
        
overrides massageOverrides(overrides);

        
lang overrides.lang;
        
langDefaults langOptionHash[lang];
        if (!
langDefaults) {
            
lang Calendar.defaults.lang;
            
langDefaults langOptionHash[lang] || {};
        }

        
isRTL firstDefined(
            
overrides.isRTL,
            
langDefaults.isRTL,
            
Calendar.defaults.isRTL
        
);
        
dirDefaults isRTL Calendar.rtlDefaults : {};

        
this.dirDefaults dirDefaults;
        
this.langDefaults langDefaults;
        
this.overrides overrides;
        
this.options mergeOptions([ // merge defaults and overrides. lowest to highest precedence
            
Calendar.defaults// global defaults
            
dirDefaults,
            
langDefaults,
            
overrides
        
]);
        
populateInstanceComputableOptions(this.options);

        
this.viewSpecCache = {}; // somewhat unrelated
    
},


    
// Gets information about how to create a view. Will use a cache.
    
getViewSpec: function(viewType) {
        var 
cache this.viewSpecCache;

        return 
cache[viewType] || (cache[viewType] = this.buildViewSpec(viewType));
    },


    
// Given a duration singular unit, like "week" or "day", finds a matching view spec.
    // Preference is given to views that have corresponding buttons.
    
getUnitViewSpec: function(unit) {
        var 
viewTypes;
        var 
i;
        var 
spec;

        if ($.
inArray(unitintervalUnits) != -1) {

            
// put views that have buttons first. there will be duplicates, but oh well
            
viewTypes this.header.getViewsWithButtons();
            $.
each(FC.views, function(viewType) { // all views
                
viewTypes.push(viewType);
            });

            for (
0viewTypes.lengthi++) {
                
spec this.getViewSpec(viewTypes[i]);
                if (
spec) {
                    if (
spec.singleUnit == unit) {
                        return 
spec;
                    }
                }
            }
        }
    },


    
// Builds an object with information on how to create a given view
    
buildViewSpec: function(requestedViewType) {
        var 
viewOverrides this.overrides.views || {};
        var 
specChain = []; // for the view. lowest to highest priority
        
var defaultsChain = []; // for the view. lowest to highest priority
        
var overridesChain = []; // for the view. lowest to highest priority
        
var viewType requestedViewType;
        var 
spec// for the view
        
var overrides// for the view
        
var duration;
        var 
unit;

        
// iterate from the specific view definition to a more general one until we hit an actual View class
        
while (viewType) {
            
spec fcViews[viewType];
            
overrides viewOverrides[viewType];
            
viewType null// clear. might repopulate for another iteration

            
if (typeof spec === 'function') { // TODO: deprecate
                
spec = { 'class'spec };
            }

            if (
spec) {
                
specChain.unshift(spec);
                
defaultsChain.unshift(spec.defaults || {});
                
duration duration || spec.duration;
                
viewType viewType || spec.type;
            }

            if (
overrides) {
                
overridesChain.unshift(overrides); // view-specific option hashes have options at zero-level
                
duration duration || overrides.duration;
                
viewType viewType || overrides.type;
            }
        }

        
spec mergeProps(specChain);
        
spec.type requestedViewType;
        if (!
spec['class']) {
            return 
false;
        }

        if (
duration) {
            
duration moment.duration(duration);
            if (
duration.valueOf()) { // valid?
                
spec.duration duration;
                
unit computeIntervalUnit(duration);

                
// view is a single-unit duration, like "week" or "day"
                // incorporate options for this. lowest priority
                
if (duration.as(unit) === 1) {
                    
spec.singleUnit unit;
                    
overridesChain.unshift(viewOverrides[unit] || {});
                }
            }
        }

        
spec.defaults mergeOptions(defaultsChain);
        
spec.overrides mergeOptions(overridesChain);

        
this.buildViewSpecOptions(spec);
        
this.buildViewSpecButtonText(specrequestedViewType);

        return 
spec;
    },


    
// Builds and assigns a view spec's options object from its already-assigned defaults and overrides
    
buildViewSpecOptions: function(spec) {
        
spec.options mergeOptions([ // lowest to highest priority
            
Calendar.defaults// global defaults
            
spec.defaults// view's defaults (from ViewSubclass.defaults)
            
this.dirDefaults,
            
this.langDefaults// locale and dir take precedence over view's defaults!
            
this.overrides// calendar's overrides (options given to constructor)
            
spec.overrides // view's overrides (view-specific options)
        
]);
        
populateInstanceComputableOptions(spec.options);
    },


    
// Computes and assigns a view spec's buttonText-related options
    
buildViewSpecButtonText: function(specrequestedViewType) {

        
// given an options object with a possible `buttonText` hash, lookup the buttonText for the
        // requested view, falling back to a generic unit entry like "week" or "day"
        
function queryButtonText(options) {
            var 
buttonText options.buttonText || {};
            return 
buttonText[requestedViewType] ||
                (
spec.singleUnit buttonText[spec.singleUnit] : null);
        }

        
// highest to lowest priority
        
spec.buttonTextOverride =
            
queryButtonText(this.overrides) || // constructor-specified buttonText lookup hash takes precedence
            
spec.overrides.buttonText// `buttonText` for view-specific options is a string

        // highest to lowest priority. mirrors buildViewSpecOptions
        
spec.buttonTextDefault =
            
queryButtonText(this.langDefaults) ||
            
queryButtonText(this.dirDefaults) ||
            
spec.defaults.buttonText || // a single string. from ViewSubclass.defaults
            
queryButtonText(Calendar.defaults) ||
            (
spec.duration this.humanizeDuration(spec.duration) : null) || // like "3 days"
            
requestedViewType// fall back to given view name
    
},


    
// Given a view name for a custom view or a standard view, creates a ready-to-go View object
    
instantiateView: function(viewType) {
        var 
spec this.getViewSpec(viewType);

        return new 
spec['class'](thisviewTypespec.optionsspec.duration);
    },


    
// Returns a boolean about whether the view is okay to instantiate at some point
    
isValidViewType: function(viewType) {
        return 
Boolean(this.getViewSpec(viewType));
    },


    
// Should be called when any type of async data fetching begins
    
pushLoading: function() {
        if (!(
this.loadingLevel++)) {
            
this.trigger('loading'nulltruethis.view);
        }
    },


    
// Should be called when any type of async data fetching completes
    
popLoading: function() {
        if (!(--
this.loadingLevel)) {
            
this.trigger('loading'nullfalsethis.view);
        }
    },


    
// Given arguments to the select method in the API, returns a span (unzoned start/end and other info)
    
buildSelectSpan: function(zonedStartInputzonedEndInput) {
        var 
start this.moment(zonedStartInput).stripZone();
        var 
end;

        if (
zonedEndInput) {
            
end this.moment(zonedEndInput).stripZone();
        }
        else if (
start.hasTime()) {
            
end start.clone().add(this.defaultTimedEventDuration);
        }
        else {
            
end start.clone().add(this.defaultAllDayEventDuration);
        }

        return { 
startstartendend };
    }

});


Calendar.mixin(Emitter);


function 
Calendar_constructor(elementoverrides) {
    var 
this;


    
t.initOptions(overrides || {});
    var 
options this.options;

    
    
// Exports
    // -----------------------------------------------------------------------------------

    
t.render render;
    
t.destroy destroy;
    
t.refetchEvents refetchEvents;
    
t.reportEvents reportEvents;
    
t.reportEventChange reportEventChange;
    
t.rerenderEvents renderEvents// `renderEvents` serves as a rerender. an API method
    
t.changeView renderView// `renderView` will switch to another view
    
t.select select;
    
t.unselect unselect;
    
t.prev prev;
    
t.next next;
    
t.prevYear prevYear;
    
t.nextYear nextYear;
    
t.today today;
    
t.gotoDate gotoDate;
    
t.incrementDate incrementDate;
    
t.zoomTo zoomTo;
    
t.getDate getDate;
    
t.getCalendar getCalendar;
    
t.getView getView;
    
t.option option;
    
t.trigger trigger;



    
// Language-data Internals
    // -----------------------------------------------------------------------------------
    // Apply overrides to the current language's data


    
var localeData createObject// make a cheap copy
        
getMomentLocaleData(options.lang// will fall back to en
    
);

    if (
options.monthNames) {
        
localeData._months options.monthNames;
    }
    if (
options.monthNamesShort) {
        
localeData._monthsShort options.monthNamesShort;
    }
    if (
options.dayNames) {
        
localeData._weekdays options.dayNames;
    }
    if (
options.dayNamesShort) {
        
localeData._weekdaysShort options.dayNamesShort;
    }
    if (
options.firstDay != null) {
        var 
_week createObject(localeData._week); // _week: { dow: # }
        
_week.dow options.firstDay;
        
localeData._week _week;
    }

    
// assign a normalized value, to be used by our .week() moment extension
    
localeData._fullCalendar_weekCalc = (function(weekCalc) {
        if (
typeof weekCalc === 'function') {
            return 
weekCalc;
        }
        else if (
weekCalc === 'local') {
            return 
weekCalc;
        }
        else if (
weekCalc === 'iso' || weekCalc === 'ISO') {
            return 
'ISO';
        }
    })(
options.weekNumberCalculation);



    
// Calendar-specific Date Utilities
    // -----------------------------------------------------------------------------------


    
t.defaultAllDayEventDuration moment.duration(options.defaultAllDayEventDuration);
    
t.defaultTimedEventDuration moment.duration(options.defaultTimedEventDuration);


    
// Builds a moment using the settings of the current calendar: timezone and language.
    // Accepts anything the vanilla moment() constructor accepts.
    
t.moment = function() {
        var 
mom;

        if (
options.timezone === 'local') {
            
mom FC.moment.apply(nullarguments);

            
// Force the moment to be local, because FC.moment doesn't guarantee it.
            
if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone
                
mom.local();
            }
        }
        else if (
options.timezone === 'UTC') {
            
mom FC.moment.utc.apply(nullarguments); // process as UTC
        
}
        else {
            
mom FC.moment.parseZone.apply(nullarguments); // let the input decide the zone
        
}

        if (
'_locale' in mom) { // moment 2.8 and above
            
mom._locale localeData;
        }
        else { 
// pre-moment-2.8
            
mom._lang localeData;
        }

        return 
mom;
    };


    
// Returns a boolean about whether or not the calendar knows how to calculate
    // the timezone offset of arbitrary dates in the current timezone.
    
t.getIsAmbigTimezone = function() {
        return 
options.timezone !== 'local' && options.timezone !== 'UTC';
    };


    
// Returns a copy of the given date in the current timezone. Has no effect on dates without times.
    
t.applyTimezone = function(date) {
        if (!
date.hasTime()) {
            return 
date.clone();
        }

        var 
zonedDate t.moment(date.toArray());
        var 
timeAdjust date.time() - zonedDate.time();
        var 
adjustedZonedDate;

        
// Safari sometimes has problems with this coersion when near DST. Adjust if necessary. (bug #2396)
        
if (timeAdjust) { // is the time result different than expected?
            
adjustedZonedDate zonedDate.clone().add(timeAdjust); // add milliseconds
            
if (date.time() - adjustedZonedDate.time() === 0) { // does it match perfectly now?
                
zonedDate adjustedZonedDate;
            }
        }

        return 
zonedDate;
    };


    
// Returns a moment for the current date, as defined by the client's computer or from the `now` option.
    // Will return an moment with an ambiguous timezone.
    
t.getNow = function() {
        var 
now options.now;
        if (
typeof now === 'function') {
            
now now();
        }
        return 
t.moment(now).stripZone();
    };


    
// Get an event's normalized end date. If not present, calculate it from the defaults.
    
t.getEventEnd = function(event) {
        if (
event.end) {
            return 
event.end.clone();
        }
        else {
            return 
t.getDefaultEventEnd(event.allDayevent.start);
        }
    };


    
// Given an event's allDay status and start date, return what its fallback end date should be.
    // TODO: rename to computeDefaultEventEnd
    
t.getDefaultEventEnd = function(allDayzonedStart) {
        var 
end zonedStart.clone();

        if (
allDay) {
            
end.stripTime().add(t.defaultAllDayEventDuration);
        }
        else {
            
end.add(t.defaultTimedEventDuration);
        }

        if (
t.getIsAmbigTimezone()) {
            
end.stripZone(); // we don't know what the tzo should be
        
}

        return 
end;
    };


    
// Produces a human-readable string for the given duration.
    // Side-effect: changes the locale of the given duration.
    
t.humanizeDuration = function(duration) {
        return (
duration.locale || duration.lang).call(durationoptions.lang// works moment-pre-2.8
            
.humanize();
    };


    
    
// Imports
    // -----------------------------------------------------------------------------------


    
EventManager.call(toptions);
    var 
isFetchNeeded t.isFetchNeeded;
    var 
fetchEvents t.fetchEvents;



    
// Locals
    // -----------------------------------------------------------------------------------


    
var _element element[0];
    var 
header;
    var 
headerElement;
    var 
content;
    var 
tm// for making theme classes
    
var currentView// NOTE: keep this in sync with this.view
    
var viewsByType = {}; // holds all instantiated view instances, current or not
    
var suggestedViewHeight;
    var 
windowResizeProxy// wraps the windowResize function
    
var ignoreWindowResize 0;
    var 
events = [];
    var 
date// unzoned
    
    
    
    // Main Rendering
    // -----------------------------------------------------------------------------------


    // compute the initial ambig-timezone date
    
if (options.defaultDate != null) {
        
date t.moment(options.defaultDate).stripZone();
    }
    else {
        
date t.getNow(); // getNow already returns unzoned
    
}
    
    
    function 
render() {
        if (!
content) {
            
initialRender();
        }
        else if (
elementVisible()) {
            
// mainly for the public API
            
calcSize();
            
renderView();
        }
    }
    
    
    function 
initialRender() {
        
tm options.theme 'ui' 'fc';
        
element.addClass('fc');

        if (
options.isRTL) {
            
element.addClass('fc-rtl');
        }
        else {
            
element.addClass('fc-ltr');
        }

        if (
options.theme) {
            
element.addClass('ui-widget');
        }
        else {
            
element.addClass('fc-unthemed');
        }

        
content = $("<div class='fc-view-container'/>").prependTo(element);

        
header t.header = new Header(toptions);
        
headerElement header.render();
        if (
headerElement) {
            
element.prepend(headerElement);
        }

        
renderView(options.defaultView);

        if (
options.handleWindowResize) {
            
windowResizeProxy debounce(windowResizeoptions.windowResizeDelay); // prevents rapid calls
            
$(window).resize(windowResizeProxy);
        }
    }
    
    
    function 
destroy() {

        if (
currentView) {
            
currentView.removeElement();

            
// NOTE: don't null-out currentView/t.view in case API methods are called after destroy.
            // It is still the "current" view, just not rendered.
        
}

        
header.removeElement();
        
content.remove();
        
element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');

        if (
windowResizeProxy) {
            $(
window).unbind('resize'windowResizeProxy);
        }
    }
    
    
    function 
elementVisible() {
        return 
element.is(':visible');
    }
    
    

    
// View Rendering
    // -----------------------------------------------------------------------------------


    // Renders a view because of a date change, view-type change, or for the first time.
    // If not given a viewType, keep the current view but render different dates.
    
function renderView(viewType) {
        
ignoreWindowResize++;

        
// if viewType is changing, remove the old view's rendering
        
if (currentView && viewType && currentView.type !== viewType) {
            
header.deactivateButton(currentView.type);
            
freezeContentHeight(); // prevent a scroll jump when view element is removed
            
currentView.removeElement();
            
currentView t.view null;
        }

        
// if viewType changed, or the view was never created, create a fresh view
        
if (!currentView && viewType) {
            
currentView t.view =
                
viewsByType[viewType] ||
                (
viewsByType[viewType] = t.instantiateView(viewType));

            
currentView.setElement(
                $(
"<div class='fc-view fc-" viewType "-view' />").appendTo(content)
            );
            
header.activateButton(viewType);
        }

        if (
currentView) {

            
// in case the view should render a period of time that is completely hidden
            
date currentView.massageCurrentDate(date);

            
// render or rerender the view
            
if (
                !
currentView.displaying ||
                !
date.isWithin(currentView.intervalStartcurrentView.intervalEnd// implicit date window change
            
) {
                if (
elementVisible()) {

                    
currentView.display(date); // will call freezeContentHeight
                    
unfreezeContentHeight(); // immediately unfreeze regardless of whether display is async

                    // need to do this after View::render, so dates are calculated
                    
updateHeaderTitle();
                    
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.displayEvents(events);
            
unfreezeContentHeight();
        }
    }


    function 
destroyEvents() {
        
freezeContentHeight();
        
currentView.clearEvents();
        
unfreezeContentHeight();
    }
    

    function 
getAndRenderEvents() {
        if (!
options.lazyFetching || isFetchNeeded(currentView.startcurrentView.end)) {
            
fetchAndRenderEvents();
        }
        else {
            
renderEvents();
        }
    }


    function 
fetchAndRenderEvents() {
        
fetchEvents(currentView.startcurrentView.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 updateHeaderTitle() {
        
header.updateTitle(currentView.title);
    }


    function 
updateTodayButton() {
        var 
now t.getNow();
        if (
now.isWithin(currentView.intervalStartcurrentView.intervalEnd)) {
            
header.disableButton('today');
        }
        else {
            
header.enableButton('today');
        }
    }
    


    
/* Selection
    -----------------------------------------------------------------------------*/
    

    // this public method receives start/end dates in any format, with any timezone
    
function select(zonedStartInputzonedEndInput) {
        
currentView.select(
            
t.buildSelectSpan.apply(targuments)
        );
    }
    

    function 
unselect() { // safe to be called before renderView
        
if (currentView) {
            
currentView.unselect();
        }
    }
    
    
    
    
/* Date
    -----------------------------------------------------------------------------*/
    
    
    
function prev() {
        
date currentView.computePrevDate(date);
        
renderView();
    }
    
    
    function 
next() {
        
date currentView.computeNextDate(date);
        
renderView();
    }
    
    
    function 
prevYear() {
        
date.add(-1'years');
        
renderView();
    }
    
    
    function 
nextYear() {
        
date.add(1'years');
        
renderView();
    }
    
    
    function 
today() {
        
date t.getNow();
        
renderView();
    }
    
    
    function 
gotoDate(zonedDateInput) {
        
date t.moment(zonedDateInput).stripZone();
        
renderView();
    }
    
    
    function 
incrementDate(delta) {
        
date.add(moment.duration(delta));
        
renderView();
    }


    
// Forces navigation to a view for the given date.
    // `viewType` can be a specific view name or a generic one like "week" or "day".
    
function zoomTo(newDateviewType) {
        var 
spec;

        
viewType viewType || 'day'// day is default zoom
        
spec t.getViewSpec(viewType) || t.getUnitViewSpec(viewType);

        
date newDate.clone();
        
renderView(spec spec.type null);
    }
    
    
    
// for external API
    
function getDate() {
        return 
t.applyTimezone(date); // infuse the calendar's timezone
    
}



    
/* Height "Freezing"
    -----------------------------------------------------------------------------*/
    // TODO: move this into the view

    
t.freezeContentHeight freezeContentHeight;
    
t.unfreezeContentHeight unfreezeContentHeight;


    function 
freezeContentHeight() {
        
content.css({
            
width'100%',
            
heightcontent.height(),
            
overflow'hidden'
        
});
    }


    function 
unfreezeContentHeight() {
        
content.css({
            
width'',
            
height'',
            
overflow''
        
});
    }
    
    
    
    
/* Misc
    -----------------------------------------------------------------------------*/
    

    
function getCalendar() {
        return 
t;
    }

    
    function 
getView() {
        return 
currentView;
    }
    
    
    function 
option(namevalue) {
        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(namethisObj) { // overrides the Emitter's trigger method :(
        
var args = Array.prototype.slice.call(arguments2);

        
thisObj thisObj || _element;
        
this.triggerWith(namethisObjargs); // Emitter's method

        
if (options[name]) {
            return 
options[name].apply(thisObjargs);
        }
    }

    
t.initialize();
}

;;

Calendar.defaults = {

    
titleRangeSeparator' u2014 '// emphasized dash
    
monthYearFormat'MMMM YYYY'// required for en. other languages rely on datepicker computable option

    
defaultTimedEventDuration'02:00:00',
    
defaultAllDayEventDuration: { days},
    
forceEventDurationfalse,
    
nextDayThreshold'09:00:00'// 9am

    // display
    
defaultView'month',
    
aspectRatio1.35,
    
header: {
        
left'title',
        
center'',
        
right'today prev,next'
    
},
    
weekendstrue,
    
weekNumbersfalse,

    
weekNumberTitle'W',
    
weekNumberCalculation'local',
    
    
//editable: false,

    //nowIndicator: false,

    
scrollTime'06:00:00',
    
    
// event ajax
    
lazyFetchingtrue,
    
startParam'start',
    
endParam'end',
    
timezoneParam'timezone',

    
timezonefalse,

    
//allDayDefault: undefined,

    // locale
    
isRTLfalse,
    
buttonText: {
        
prev"prev",
        
next"next",
        
prevYear"prev year",
        
nextYear"next year",
        
year'year'// TODO: locale files need to specify this
        
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
    
themefalse,
    
themeButtonIcons: {
        
prev'circle-triangle-w',
        
next'circle-triangle-e',
        
prevYear'seek-prev',
        
nextYear'seek-next'
    
},

    
//eventResizableFromStart: false,
    
dragOpacity.75,
    
dragRevertDuration500,
    
dragScrolltrue,
    
    
//selectable: false,
    
unselectAutotrue,
    
    
dropAccept'*',

    
eventOrder'title',

    
eventLimitfalse,
    
eventLimitText'more',
    
eventLimitClick'popover',
    
dayPopoverFormat'LL',
    
    
handleWindowResizetrue,
    
windowResizeDelay200 // milliseconds before an updateSize happens
    
};


Calendar.englishDefaults = { // used by lang.js
    
dayPopoverFormat'dddd, MMMM D'
};


Calendar.rtlDefaults = { // right-to-left defaults
    
header: { // TODO: smarter solution (first/center/last ?)
        
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 
langOptionHash FC.langs = {}; // initialize and expose


// TODO: document the structure and ordering of a FullCalendar lang file
// TODO: rename everything "lang" to "locale", like what the moment project did


// Initialize jQuery UI datepicker translations while using some of the translations
// Will set this as the default language for datepicker.
FC.datepickerLang = function(langCodedpLangCodedpOptions) {

    
// get the FullCalendar internal option hash for this language. create if necessary
    
var fcOptions langOptionHash[langCode] || (langOptionHash[langCode] = {});

    
// transfer some simple options from datepicker to fc
    
fcOptions.isRTL dpOptions.isRTL;
    
fcOptions.weekNumberTitle dpOptions.weekHeader;

    
// compute some more complex options from datepicker
    
$.each(dpComputableOptions, function(namefunc) {
        
fcOptions[name] = func(dpOptions);
    });

    
// is jQuery UI Datepicker is on the page?
    
if ($.datepicker) {

        
// Register the language data.
        // FullCalendar and MomentJS use language codes like "pt-br" but Datepicker
        // does it like "pt-BR" or if it doesn't have the language, maybe just "pt".
        // Make an alias so the language can be referenced either way.
        
$.datepicker.regional[dpLangCode] =
            $.
datepicker.regional[langCode] = // alias
                
dpOptions;

        
// Alias 'en' to the default language data. Do this every time.
        
$.datepicker.regional.en = $.datepicker.regional[''];

        
// Set as Datepicker's global defaults.
        
$.datepicker.setDefaults(dpOptions);
    }
};


// Sets FullCalendar-specific translations. Will set the language as the global default.
FC.lang = function(langCodenewFcOptions) {
    var 
fcOptions;
    var 
momOptions;

    
// get the FullCalendar internal option hash for this language. create if necessary
    
fcOptions langOptionHash[langCode] || (langOptionHash[langCode] = {});

    
// provided new options for this language? merge them in
    
if (newFcOptions) {
        
fcOptions langOptionHash[langCode] = mergeOptions([ fcOptionsnewFcOptions ]);
    }

    
// compute language options that weren't defined.
    // always do this. newFcOptions can be undefined when initializing from i18n file,
    // so no way to tell if this is an initialization or a default-setting.
    
momOptions getMomentLocaleData(langCode); // will fall back to en
    
$.each(momComputableOptions, function(namefunc) {
        if (
fcOptions[name] == null) {
            
fcOptions[name] = func(momOptionsfcOptions);
        }
    });

    
// set it as the default language for FullCalendar
    
Calendar.defaults.lang langCode;
};


// NOTE: can't guarantee any of these computations will run because not every language has datepicker
// configs, so make sure there are English fallbacks for these in the defaults file.
var dpComputableOptions = {

    
buttonText: function(dpOptions) {
        return {
            
// the translations sometimes wrongly contain HTML entities
            
prevstripHtmlEntities(dpOptions.prevText),
            
nextstripHtmlEntities(dpOptions.nextText),
            
todaystripHtmlEntities(dpOptions.currentText)
        };
    },

    
// Produces format strings like "MMMM YYYY" -> "September 2014"
    
monthYearFormat: function(dpOptions) {
        return 
dpOptions.showMonthAfterYear ?
            
'YYYY[' dpOptions.yearSuffix '] MMMM' :
            
'MMMM YYYY[' dpOptions.yearSuffix ']';
    }

};

var 
momComputableOptions = {

    
// Produces format strings like "ddd M/D" -> "Fri 9/15"
    
dayOfMonthFormat: function(momOptionsfcOptions) {
        var 
format momOptions.longDateFormat('l'); // for the format like "M/D/YYYY"

        // strip the year off the edge, as well as other misc non-whitespace chars
        
format format.replace(/^Y+[^ws]*|[^ws]*Y+$/g'');

        if (
fcOptions.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;
    },

    
// Produces format strings like "h:mma" -> "6:00pm"
    
mediumTimeFormat: function(momOptions) { // can't be called `timeFormat` because collides with option
        
return momOptions.longDateFormat('LT')
            .
replace(/s*a$/i'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
    
},

    
// Produces format strings like "h(:mm)a" -> "6pm" / "6:30pm"
    
smallTimeFormat: function(momOptions) {
        return 
momOptions.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
    
},

    
// Produces format strings like "h(:mm)t" -> "6p" / "6:30p"
    
extraSmallTimeFormat: function(momOptions) {
        return 
momOptions.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
    
},

    
// Produces format strings like "ha" / "H" -> "6pm" / "18"
    
hourFormat: function(momOptions) {
        return 
momOptions.longDateFormat('LT')
            .
replace(':mm''')
            .
replace(/(Wmm)$/, ''// like above, but for foreign langs
            
.replace(/s*a$/i'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
    
},

    
// Produces format strings like "h:mm" -> "6:30" (with no AM/PM)
    
noMeridiemTimeFormat: function(momOptions) {
        return 
momOptions.longDateFormat('LT')
            .
replace(/s*a$/i''); // remove trailing AM/PM
    
}

};


// options that should be computed off live calendar options (considers override options)
// TODO: best place for this? related to lang?
// TODO: flipping text based on isRTL is a bad idea because the CSS `direction` might want to handle it
var instanceComputableOptions = {

    
// Produces format strings for results like "Mo 16"
    
smallDayDateFormat: function(options) {
        return 
options.isRTL ?
            
'D dd' :
            
'dd D';
    },

    
// Produces format strings for results like "Wk 5"
    
weekFormat: function(options) {
        return 
options.isRTL ?
            
'w[ ' options.weekNumberTitle ']' :
            
'[' options.weekNumberTitle ' ]w';
    },

    
// Produces format strings for results like "Wk5"
    
smallWeekFormat: function(options) {
        return 
options.isRTL ?
            
'w[' options.weekNumberTitle ']' :
            
'[' options.weekNumberTitle ']w';
    }

};

function 
populateInstanceComputableOptions(options) {
    $.
each(instanceComputableOptions, function(namefunc) {
        if (
options[name] == null) {
            
options[name] = func(options);
        }
    });
}


// Returns moment's internal locale data. If doesn't exist, returns English.
// Works with moment-pre-2.8
function getMomentLocaleData(langCode) {
    var 
func moment.localeData || moment.langData;
    return 
func.call(momentlangCode) ||
        
func.call(moment'en'); // the newer localData could return null, so fall back to en
}


// Initialize English by forcing computation of moment-derived options.
// Also, sets it as the default.
FC.lang('en'Calendar.englishDefaults);

;;

/* Top toolbar area with buttons and title
----------------------------------------------------------------------------------------------------------------------*/
// TODO: rename all header-related things to "toolbar"

function Header(calendaroptions) {
    var 
this;
    
    
// exports
    
t.render render;
    
t.removeElement removeElement;
    
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 = $("<div class='fc-toolbar'/>")
                .
append(renderSection('left'))
                .
append(renderSection('right'))
                .
append(renderSection('center'))
                .
append('<div class="fc-clear"/>');

            return 
el;
        }
    }
    
    
    function 
removeElement() {
        
el.remove();
        
el = $();
    }
    
    
    function 
renderSection(position) {
        var 
sectionEl = $('<div class="fc-' position '"/>');
        var 
buttonStr options.header[position];

        if (
buttonStr) {
            $.
each(buttonStr.split(' '), function(i) {
                var 
groupChildren = $();
                var 
isOnlyButtons true;
                var 
groupEl;

                $.
each(this.split(','), function(jbuttonName) {
                    var 
customButtonProps;
                    var 
viewSpec;
                    var 
buttonClick;
                    var 
overrideText// text explicitly set by calendar's constructor options. overcomes icons
                    
var defaultText;
                    var 
themeIcon;
                    var 
normalIcon;
                    var 
innerHtml;
                    var 
classes;
                    var 
button// the element

                    
if (buttonName == 'title') {
                        
groupChildren groupChildren.add($('<h2>&nbsp;</h2>')); // we always want it to take up height
                        
isOnlyButtons false;
                    }
                    else {
                        if ((
customButtonProps = (calendar.options.customButtons || {})[buttonName])) {
                            
buttonClick = function(ev) {
                                if (
customButtonProps.click) {
                                    
customButtonProps.click.call(button[0], ev);
                                }
                            };
                            
overrideText ''// icons will override text
                            
defaultText customButtonProps.text;
                        }
                        else if ((
viewSpec calendar.getViewSpec(buttonName))) {
                            
buttonClick = function() {
                                
calendar.changeView(buttonName);
                            };
                            
viewsWithButtons.push(buttonName);
                            
overrideText viewSpec.buttonTextOverride;
                            
defaultText viewSpec.buttonTextDefault;
                        }
                        else if (
calendar[buttonName]) { // a calendar method
                            
buttonClick = function() {
                                
calendar[buttonName]();
                            };
                            
overrideText = (calendar.overrides.buttonText || {})[buttonName];
                            
defaultText options.buttonText[buttonName]; // everything else is considered default
                        
}

                        if (
buttonClick) {

                            
themeIcon =
                                
customButtonProps ?
                                    
customButtonProps.themeIcon :
                                    
options.themeButtonIcons[buttonName];

                            
normalIcon =
                                
customButtonProps ?
                                    
customButtonProps.icon :
                                    
options.buttonIcons[buttonName];

                            if (
overrideText) {
                                
innerHtml htmlEscape(overrideText);
                            }
                            else if (
themeIcon && options.theme) {
                                
innerHtml "<span class='ui-icon ui-icon-" themeIcon "'></span>";
                            }
                            else if (
normalIcon && !options.theme) {
                                
innerHtml "<span class='fc-icon fc-icon-" normalIcon "'></span>";
                            }
                            else {
                                
innerHtml htmlEscape(defaultText);
                            }

                            
classes = [
                                
'fc-' buttonName '-button',
                                
tm '-button',
                                
tm '-state-default'
                            
];

                            
button = $( // type="button" so that it doesn't submit a form
                                
'<button type="button" class="' classes.join(' ') + '">' +
                                    
innerHtml +
                                
'</button>'
                                
)
                                .
click(function(ev) {
                                    
// don't process clicks for disabled buttons
                                    
if (!button.hasClass(tm '-state-disabled')) {

                                        
buttonClick(ev);

                                        
// 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 = $('<div/>');
                    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',
    
cachefalse
};

var 
eventGUID 1;


function 
EventManager(options) { // assumed to be a calendar
    
var 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;
    
t.normalizeEventDates normalizeEventDates;
    
t.normalizeEventTimes normalizeEventTimes;
    
    
    
// imports
    
var reportEvents t.reportEvents;
    
    
    
// locals
    
var stickySource = { events: [] };
    var 
sources = [ stickySource ];
    var 
rangeStartrangeEnd;
    var 
currentFetchID 0;
    var 
pendingSourceCnt 0;
    var 
cache = []; // holds events that have already been expanded


    
$.each(
        (
options.events ? [ options.events ] : []).concat(options.eventSources || []),
        function(
isourceInput) {
            var 
source buildEventSource(sourceInput);
            if (
source) {
                
sources.push(source);
            }
        }
    );
    
    
    
    
/* Fetching
    -----------------------------------------------------------------------------*/


    // start and end are assumed to be unzoned
    
function isFetchNeeded(startend) {
        return !
rangeStart || // nothing has been fetched yet?
            
start rangeStart || end rangeEnd// is part of the new range outside of the old range?
    
}
    
    
    function 
fetchEvents(startend) {
        
rangeStart start;
        
rangeEnd end;
        
cache = [];
        var 
fetchID = ++currentFetchID;
        var 
len sources.length;
        
pendingSourceCnt len;
        for (var 
i=0i<leni++) {
            
fetchEventSource(sources[i], fetchID);
        }
    }
    
    
    function 
fetchEventSource(sourcefetchID) {
        
_fetchEventSource(source, function(eventInputs) {
            var 
isArraySource = $.isArray(source.events);
            var 
ieventInput;
            var 
abstractEvent;

            if (
fetchID == currentFetchID) {

                if (
eventInputs) {
                    for (
0eventInputs.lengthi++) {
                        
eventInput eventInputs[i];

                        if (
isArraySource) { // array sources have already been convert to Event Objects
                            
abstractEvent eventInput;
                        }
                        else {
                            
abstractEvent buildEventFromInput(eventInputsource);
                        }

                        if (
abstractEvent) { // not false (an invalid event)
                            
cache.push.apply(
                                
cache,
                                
expandEvent(abstractEvent// add individual expanded events to the cache
                            
);
                        }
                    }
                }

                
pendingSourceCnt--;
                if (!
pendingSourceCnt) {
                    
reportEvents(cache);
                }
            }
        });
    }
    
    
    function 
_fetchEventSource(sourcecallback) {
        var 
i;
        var 
fetchers FC.sourceFetchers;
        var 
res;

        for (
i=0i<fetchers.lengthi++) {
            
res fetchers[i].call(
                
t// this, the Calendar object
                
source,
                
rangeStart.clone(),
                
rangeEnd.clone(),
                
options.timezone,
                
callback
            
);

            if (
res === true) {
                
// the fetcher is in charge. made its own async request
                
return;
            }
            else if (
typeof res == 'object') {
                
// the fetcher returned a new source. process it
                
_fetchEventSource(rescallback);
                return;
            }
        }

        var 
events source.events;
        if (
events) {
            if ($.
isFunction(events)) {
                
t.pushLoading();
                
events.call(
                    
t// this, the Calendar object
                    
rangeStart.clone(),
                    
rangeEnd.clone(),
                    
options.timezone,
                    function(
events) {
                        
callback(events);
                        
t.popLoading();
                    }
                );
            }
            else if ($.
isArray(events)) {
                
callback(events);
            }
            else {
                
callback();
            }
        }else{
            var 
url source.url;
            if (
url) {
                var 
success source.success;
                var 
error source.error;
                var 
complete source.complete;

                
// retrieve any outbound GET/POST $.ajax data from the options
                
var customData;
                if ($.
isFunction(source.data)) {
                    
// supplied as a function that returns a key/value object
                    
customData source.data();
                }
                else {
                    
// supplied as a straight key/value object
                    
customData source.data;
                }

                
// use a copy of the custom data so we can modify the parameters
                // and not affect the passed-in object.
                
var data = $.extend({}, customData || {});

                var 
startParam firstDefined(source.startParamoptions.startParam);
                var 
endParam firstDefined(source.endParamoptions.endParam);
                var 
timezoneParam firstDefined(source.timezoneParamoptions.timezoneParam);

                if (
startParam) {
                    
data[startParam] = rangeStart.format();
                }
                if (
endParam) {
                    
data[endParam] = rangeEnd.format();
                }
                if (
options.timezone && options.timezone != 'local') {
                    
data[timezoneParam] = options.timezone;
                }

                
t.pushLoading();
                $.
ajax($.extend({}, ajaxDefaultssource, {
                    
datadata,
                    
success: function(events) {
                        
events events || [];
                        var 
res applyAll(successthisarguments);
                        if ($.
isArray(res)) {
                            
events res;
                        }
                        
callback(events);
                    },
                    
error: function() {
                        
applyAll(errorthisarguments);
                        
callback();
                    },
                    
complete: function() {
                        
applyAll(completethisarguments);
                        
t.popLoading();
                    }
                }));
            }else{
                
callback();
            }
        }
    }
    
    
    
    
/* Sources
    -----------------------------------------------------------------------------*/
    

    
function addEventSource(sourceInput) {
        var 
source buildEventSource(sourceInput);
        if (
source) {
            
sources.push(source);
            
pendingSourceCnt++;
            
fetchEventSource(sourcecurrentFetchID); // will eventually call reportEvents
        
}
    }


    function 
buildEventSource(sourceInput) { // will return undefined if invalid source
        
var normalizers FC.sourceNormalizers;
        var 
source;
        var 
i;

        if ($.
isFunction(sourceInput) || $.isArray(sourceInput)) {
            
source = { eventssourceInput };
        }
        else if (
typeof sourceInput === 'string') {
            
source = { urlsourceInput };
        }
        else if (
typeof sourceInput === 'object') {
            
source = $.extend({}, sourceInput); // shallow copy
        
}

        if (
source) {

            
// TODO: repeat code, same code for event classNames
            
if (source.className) {
                if (
typeof source.className === 'string') {
                    
source.className source.className.split(/s+/);
                }
                
// otherwise, assumed to be an array
            
}
            else {
                
source.className = [];
            }

            
// for array sources, we convert to standard Event Objects up front
            
if ($.isArray(source.events)) {
                
source.origArray source.events// for removeEventSource
                
source.events = $.map(source.events, function(eventInput) {
                    return 
buildEventFromInput(eventInputsource);
                });
            }

            for (
i=0i<normalizers.lengthi++) {
                
normalizers[i].call(tsource);
            }

            return 
source;
        }
    }


    function 
removeEventSource(source) {
        
sources = $.grep(sources, function(src) {
            return !
isSourcesEqual(srcsource);
        });
        
// remove all client events from that source
        
cache = $.grep(cache, function(e) {
            return !
isSourcesEqual(e.sourcesource);
        });
        
reportEvents(cache);
    }


    function 
isSourcesEqual(source1source2) {
        return 
source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2);
    }


    function 
getSourcePrimitive(source) {
        return (
            (
typeof source === 'object') ? // a normalized event source?
                
(source.origArray || source.googleCalendarId || source.url || source.events) : // get the primitive
                
null
        
) ||
        
source// the given argument *is* the primitive
    
}
    
    
    
    
/* Manipulation
    -----------------------------------------------------------------------------*/


    // Only ever called from the externally-facing API
    
function updateEvent(event) {

        
// massage start/end values, even if date string values
        
event.start t.moment(event.start);
        if (
event.end) {
            
event.end t.moment(event.end);
        }
        else {
            
event.end null;
        }

        
mutateEvent(eventgetMiscEventProps(event)); // will handle start/end/allDay normalization
        
reportEvents(cache); // reports event modifications (so we can redraw)
    
}


    
// Returns a hash of misc event properties that should be copied over to related events.
    
function getMiscEventProps(event) {
        var 
props = {};

        $.
each(event, function(nameval) {
            if (
isMiscEventPropName(name)) {
                if (
val !== undefined && isAtomic(val)) { // a defined non-object
                    
props[name] = val;
                }
            }
        });

        return 
props;
    }

    
// non-date-related, non-id-related, non-secret
    
function isMiscEventPropName(name) {
        return !/^
_|^(id|allDay|start|end)$/.test(name);
    }

    
    
// returns the expanded events that were created
    
function renderEvent(eventInputstick) {
        var 
abstractEvent buildEventFromInput(eventInput);
        var 
events;
        var 
ievent;

        if (
abstractEvent) { // not false (a valid input)
            
events expandEvent(abstractEvent);

            for (
0events.lengthi++) {
                
event events[i];

                if (!
event.source) {
                    if (
stick) {
                        
stickySource.events.push(event);
                        
event.source stickySource;
                    }
                    
cache.push(event);
                }
            }

            
reportEvents(cache);

            return 
events;
        }

        return [];
    }
    
    
    function 
removeEvents(filter) {
        var 
eventID;
        var 
i;

        if (
filter == null) { // null or undefined. remove all events
            
filter = function() { return true; }; // will always match
        
}
        else if (!$.
isFunction(filter)) { // an event ID
            
eventID filter '';
            
filter = function(event) {
                return 
event._id == eventID;
            };
        }

        
// Purge event(s) from our local cache
        
cache = $.grep(cachefiltertrue); // inverse=true

        // Remove events from array sources.
        // This works because they have been converted to official Event Objects up front.
        // (and as a result, event._id has been calculated).
        
for (i=0i<sources.lengthi++) {
            if ($.
isArray(sources[i].events)) {
                
sources[i].events = $.grep(sources[i].eventsfiltertrue);
            }
        }

        
reportEvents(cache);
    }
    
    
    function 
clientEvents(filter) {
        if ($.
isFunction(filter)) {
            return $.
grep(cachefilter);
        }
        else if (
filter != null) { // not null, not undefined. an event ID
            
filter += '';
            return $.
grep(cache, function(e) {
                return 
e._id == filter;
            });
        }
        return 
cache// else, return all
    
}
    
    
    
    
/* Event Normalization
    -----------------------------------------------------------------------------*/


    // Given a raw object with key/value properties, returns an "abstract" Event object.
    // An "abstract" event is an event that, if recurring, will not have been expanded yet.
    // Will return `false` when input is invalid.
    // `source` is optional
    
function buildEventFromInput(inputsource) {
        var 
out = {};
        var 
startend;
        var 
allDay;

        if (
options.eventDataTransform) {
            
input options.eventDataTransform(input);
        }
        if (
source && source.eventDataTransform) {
            
input source.eventDataTransform(input);
        }

        
// Copy all properties over to the resulting object.
        // The special-case properties will be copied over afterwards.
        
$.extend(outinput);

        if (
source) {
            
out.source source;
        }

        
out._id input._id || (input.id === undefined '_fc' eventGUID++ : input.id '');

        if (
input.className) {
            if (
typeof input.className == 'string') {
                
out.className input.className.split(/s+/);
            }
            else { 
// assumed to be an array
                
out.className input.className;
            }
        }
        else {
            
out.className = [];
        }

        
start input.start || input.date// "date" is an alias for "start"
        
end input.end;

        
// parse as a time (Duration) if applicable
        
if (isTimeString(start)) {
            
start moment.duration(start);
        }
        if (
isTimeString(end)) {
            
end moment.duration(end);
        }

        if (
input.dow || moment.isDuration(start) || moment.isDuration(end)) {

            
// the event is "abstract" (recurring) so don't calculate exact start/end dates just yet
            
out.start start moment.duration(start) : null// will be a Duration or null
            
out.end end moment.duration(end) : null// will be a Duration or null
            
out._recurring true// our internal marker
        
}
        else {

            if (
start) {
                
start t.moment(start);
                if (!
start.isValid()) {
                    return 
false;
                }
            }

            if (
end) {
                
end t.moment(end);
                if (!
end.isValid()) {
                    
end null// let defaults take over
                
}
            }

            
allDay input.allDay;
            if (
allDay === undefined) { // still undefined? fallback to default
                
allDay firstDefined(
                    
source source.allDayDefault undefined,
                    
options.allDayDefault
                
);
                
// still undefined? normalizeEventDates will calculate it
            
}

            
assignDatesToEvent(startendallDayout);
        }

        return 
out;
    }


    
// Normalizes and assigns the given dates to the given partially-formed event object.
    // NOTE: mutates the given start/end moments. does not make a copy.
    
function assignDatesToEvent(startendallDayevent) {
        
event.start start;
        
event.end end;
        
event.allDay allDay;
        
normalizeEventDates(event);
        
backupEventDates(event);
    }


    
// Ensures proper values for allDay/start/end. Accepts an Event object, or a plain object with event-ish properties.
    // NOTE: Will modify the given object.
    
function normalizeEventDates(eventProps) {

        
normalizeEventTimes(eventProps);

        if (
eventProps.end && !eventProps.end.isAfter(eventProps.start)) {
            
eventProps.end null;
        }

        if (!
eventProps.end) {
            if (
options.forceEventDuration) {
                
eventProps.end t.getDefaultEventEnd(eventProps.allDayeventProps.start);
            }
            else {
                
eventProps.end null;
            }
        }
    }


    
// Ensures the allDay property exists and the timeliness of the start/end dates are consistent
    
function normalizeEventTimes(eventProps) {
        if (
eventProps.allDay == null) {
            
eventProps.allDay = !(eventProps.start.hasTime() || (eventProps.end && eventProps.end.hasTime()));
        }

        if (
eventProps.allDay) {
            
eventProps.start.stripTime();
            if (
eventProps.end) {
                
// TODO: consider nextDayThreshold here? If so, will require a lot of testing and adjustment
                
eventProps.end.stripTime();
            }
        }
        else {
            if (!
eventProps.start.hasTime()) {
                
eventProps.start t.applyTimezone(eventProps.start.time(0)); // will assign a 00:00 time
            
}
            if (
eventProps.end && !eventProps.end.hasTime()) {
                
eventProps.end t.applyTimezone(eventProps.end.time(0)); // will assign a 00:00 time
            
}
        }
    }


    
// If the given event is a recurring event, break it down into an array of individual instances.
    // If not a recurring event, return an array with the single original event.
    // If given a falsy input (probably because of a failed buildEventFromInput call), returns an empty array.
    // HACK: can override the recurring window by providing custom rangeStart/rangeEnd (for businessHours).
    
function expandEvent(abstractEvent_rangeStart_rangeEnd) {
        var 
events = [];
        var 
dowHash;
        var 
dow;
        var 
i;
        var 
date;
        var 
startTimeendTime;
        var 
startend;
        var 
event;

        
_rangeStart _rangeStart || rangeStart;
        
_rangeEnd _rangeEnd || rangeEnd;

        if (
abstractEvent) {
            if (
abstractEvent._recurring) {

                
// make a boolean hash as to whether the event occurs on each day-of-week
                
if ((dow abstractEvent.dow)) {
                    
dowHash = {};
                    for (
0dow.lengthi++) {
                        
dowHash[dow[i]] = true;
                    }
                }

                
// iterate through every day in the current range
                
date _rangeStart.clone().stripTime(); // holds the date of the current day
                
while (date.isBefore(_rangeEnd)) {

                    if (!
dowHash || dowHash[date.day()]) { // if everyday, or this particular day-of-week

                        
startTime abstractEvent.start// the stored start and end properties are times (Durations)
                        
endTime abstractEvent.end// "
                        
start date.clone();
                        
end null;

                        if (
startTime) {
                            
start start.time(startTime);
                        }
                        if (
endTime) {
                            
end date.clone().time(endTime);
                        }

                        
event = $.extend({}, abstractEvent); // make a copy of the original
                        
assignDatesToEvent(
                            
startend,
                            !
startTime && !endTime// allDay?
                            
event
                        
);
                        
events.push(event);
                    }

                    
date.add(1'days');
                }
            }
            else {
                
events.push(abstractEvent); // return the original event. will be a one-item array
            
}
        }

        return 
events;
    }



    
/* Event Modification Math
    -----------------------------------------------------------------------------------------*/


    // Modifies an event and all related events by applying the given properties.
    // Special date-diffing logic is used for manipulation of dates.
    // If `props` does not contain start/end dates, the updated values are assumed to be the event's current start/end.
    // All date comparisons are done against the event's pristine _start and _end dates.
    // Returns an object with delta information and a function to undo all operations.
    // For making computations in a granularity greater than day/time, specify largeUnit.
    // NOTE: The given `newProps` might be mutated for normalization purposes.
    
function mutateEvent(eventnewPropslargeUnit) {
        var 
miscProps = {};
        var 
oldProps;
        var 
clearEnd;
        var 
startDelta;
        var 
endDelta;
        var 
durationDelta;
        var 
undoFunc;

        
// diffs the dates in the appropriate way, returning a duration
        
function diffDates(date1date0) { // date1 - date0
            
if (largeUnit) {
                return 
diffByUnit(date1date0largeUnit);
            }
            else if (
newProps.allDay) {
                return 
diffDay(date1date0);
            }
            else {
                return 
diffDayTime(date1date0);
            }
        }

        
newProps newProps || {};

        
// normalize new date-related properties
        
if (!newProps.start) {
            
newProps.start event.start.clone();
        }
        if (
newProps.end === undefined) {
            
newProps.end event.end event.end.clone() : null;
        }
        if (
newProps.allDay == null) { // is null or undefined?
            
newProps.allDay event.allDay;
        }
        
normalizeEventDates(newProps);

        
// create normalized versions of the original props to compare against
        // need a real end value, for diffing
        
oldProps = {
            
startevent._start.clone(),
            
endevent._end event._end.clone() : t.getDefaultEventEnd(event._allDayevent._start),
            
allDaynewProps.allDay // normalize the dates in the same regard as the new properties
        
};
        
normalizeEventDates(oldProps);

        
// need to clear the end date if explicitly changed to null
        
clearEnd event._end !== null && newProps.end === null;

        
// compute the delta for moving the start date
        
startDelta diffDates(newProps.startoldProps.start);

        
// compute the delta for moving the end date
        
if (newProps.end) {
            
endDelta diffDates(newProps.endoldProps.end);
            
durationDelta endDelta.subtract(startDelta);
        }
        else {
            
durationDelta null;
        }

        
// gather all non-date-related properties
        
$.each(newProps, function(nameval) {
            if (
isMiscEventPropName(name)) {
                if (
val !== undefined) {
                    
miscProps[name] = val;
                }
            }
        });

        
// apply the operations to the event and all related events
        
undoFunc mutateEvents(
            
clientEvents(event._id), // get events with this ID
            
clearEnd,
            
newProps.allDay,
            
startDelta,
            
durationDelta,
            
miscProps
        
);

        return {
            
dateDeltastartDelta,
            
durationDeltadurationDelta,
            
undoundoFunc
        
};
    }


    
// Modifies an array of events in the following ways (operations are in order):
    // - clear the event's `end`
    // - convert the event to allDay
    // - add `dateDelta` to the start and end
    // - add `durationDelta` to the event's duration
    // - assign `miscProps` to the event
    //
    // Returns a function that can be called to undo all the operations.
    //
    // TODO: don't use so many closures. possible memory issues when lots of events with same ID.
    //
    
function mutateEvents(eventsclearEndallDaydateDeltadurationDeltamiscProps) {
        var 
isAmbigTimezone t.getIsAmbigTimezone();
        var 
undoFunctions = [];

        
// normalize zero-length deltas to be null
        
if (dateDelta && !dateDelta.valueOf()) { dateDelta null; }
        if (
durationDelta && !durationDelta.valueOf()) { durationDelta null; }

        $.
each(events, function(ievent) {
            var 
oldProps;
            var 
newProps;

            
// build an object holding all the old values, both date-related and misc.
            // for the undo function.
            
oldProps = {
                
startevent.start.clone(),
                
endevent.end event.end.clone() : null,
                
allDayevent.allDay
            
};
            $.
each(miscProps, function(name) {
                
oldProps[name] = event[name];
            });

            
// new date-related properties. work off the original date snapshot.
            // ok to use references because they will be thrown away when backupEventDates is called.
            
newProps = {
                
startevent._start,
                
endevent._end,
                
allDayallDay // normalize the dates in the same regard as the new properties
            
};
            
normalizeEventDates(newProps); // massages start/end/allDay

            // strip or ensure the end date
            
if (clearEnd) {
                
newProps.end null;
            }
            else if (
durationDelta && !newProps.end) { // the duration translation requires an end date
                
newProps.end t.getDefaultEventEnd(newProps.allDaynewProps.start);
            }

            if (
dateDelta) {
                
newProps.start.add(dateDelta);
                if (
newProps.end) {
                    
newProps.end.add(dateDelta);
                }
            }

            if (
durationDelta) {
                
newProps.end.add(durationDelta); // end already ensured above
            
}

            
// if the dates have changed, and we know it is impossible to recompute the
            // timezone offsets, strip the zone.
            
if (
                
isAmbigTimezone &&
                !
newProps.allDay &&
                (
dateDelta || durationDelta)
            ) {
                
newProps.start.stripZone();
                if (
newProps.end) {
                    
newProps.end.stripZone();
                }
            }

            $.
extend(eventmiscPropsnewProps); // copy over misc props, then date-related props
            
backupEventDates(event); // regenerate internal _start/_end/_allDay

            
undoFunctions.push(function() {
                $.
extend(eventoldProps);
                
backupEventDates(event); // regenerate internal _start/_end/_allDay
            
});
        });

        return function() {
            for (var 
0undoFunctions.lengthi++) {
                
undoFunctions[i]();
            }
        };
    }


    
/* Business Hours
    -----------------------------------------------------------------------------------------*/

    
t.getBusinessHoursEvents getBusinessHoursEvents;


    
// Returns an array of events as to when the business hours occur in the given view.
    // Abuse of our event system :(
    
function getBusinessHoursEvents(wholeDay) {
        var 
optionVal options.businessHours;
        var 
defaultVal = {
            
className'fc-nonbusiness',
            
start'09:00',
            
end'17:00',
            
dow: [ 1234], // monday - friday
            
rendering'inverse-background'
        
};
        var 
view t.getView();
        var 
eventInput;

        if (
optionVal) { // `true` (which means "use the defaults") or an override object
            
eventInput = $.extend(
                {}, 
// copy to a new object in either case
                
defaultVal,
                
typeof optionVal === 'object' optionVal : {} // override the defaults
            
);
        }

        if (
eventInput) {

            
// if a whole-day series is requested, clear the start/end times
            
if (wholeDay) {
                
eventInput.start null;
                
eventInput.end null;
            }

            return 
expandEvent(
                
buildEventFromInput(eventInput),
                
view.start,
                
view.end
            
);
        }

        return [];
    }


    
/* Overlapping / Constraining
    -----------------------------------------------------------------------------------------*/

    
t.isEventSpanAllowed isEventSpanAllowed;
    
t.isExternalSpanAllowed isExternalSpanAllowed;
    
t.isSelectionSpanAllowed isSelectionSpanAllowed;


    
// Determines if the given event can be relocated to the given span (unzoned start/end with other misc data)
    
function isEventSpanAllowed(spanevent) {
        var 
source event.source || {};
        var 
constraint firstDefined(
            
event.constraint,
            
source.constraint,
            
options.eventConstraint
        
);
        var 
overlap firstDefined(
            
event.overlap,
            
source.overlap,
            
options.eventOverlap
        
);
        return 
isSpanAllowed(spanconstraintoverlapevent);
    }


    
// Determines if an external event can be relocated to the given span (unzoned start/end with other misc data)
    
function isExternalSpanAllowed(eventSpaneventLocationeventProps) {
        var 
eventInput;
        var 
event;

        
// note: very similar logic is in View's reportExternalDrop
        
if (eventProps) {
            
eventInput = $.extend({}, eventPropseventLocation);
            
event expandEvent(buildEventFromInput(eventInput))[0];
        }

        if (
event) {
            return 
isEventSpanAllowed(eventSpanevent);
        }
        else { 
// treat it as a selection

            
return isSelectionSpanAllowed(eventSpan);
        }
    }


    
// Determines the given span (unzoned start/end with other misc data) can be selected.
    
function isSelectionSpanAllowed(span) {
        return 
isSpanAllowed(spanoptions.selectConstraintoptions.selectOverlap);
    }


    
// Returns true if the given span (caused by an event drop/resize or a selection) is allowed to exist
    // according to the constraint/overlap settings.
    // `event` is not required if checking a selection.
    
function isSpanAllowed(spanconstraintoverlapevent) {
        var 
constraintEvents;
        var 
anyContainment;
        var 
peerEvents;
        var 
ipeerEvent;
        var 
peerOverlap;

        
// the range must be fully contained by at least one of produced constraint events
        
if (constraint != null) {

            
// not treated as an event! intermediate data structure
            // TODO: use ranges in the future
            
constraintEvents constraintToEvents(constraint);

            
anyContainment false;
            for (
0constraintEvents.lengthi++) {
                if (
eventContainsRange(constraintEvents[i], span)) {
                    
anyContainment true;
                    break;
                }
            }

            if (!
anyContainment) {
                return 
false;
            }
        }

        
peerEvents t.getPeerEvents(spanevent);

        for (
0peerEvents.lengthi++)  {
            
peerEvent peerEvents[i];

            
// there needs to be an actual intersection before disallowing anything
            
if (eventIntersectsRange(peerEventspan)) {

                
// evaluate overlap for the given range and short-circuit if necessary
                
if (overlap === false) {
                    return 
false;
                }
                
// if the event's overlap is a test function, pass the peer event in question as the first param
                
else if (typeof overlap === 'function' && !overlap(peerEventevent)) {
                    return 
false;
                }

                
// if we are computing if the given range is allowable for an event, consider the other event's
                // EventObject-specific or Source-specific `overlap` property
                
if (event) {
                    
peerOverlap firstDefined(
                        
peerEvent.overlap,
                        (
peerEvent.source || {}).overlap
                        
// we already considered the global `eventOverlap`
                    
);
                    if (
peerOverlap === false) {
                        return 
false;
                    }
                    
// if the peer event's overlap is a test function, pass the subject event as the first param
                    
if (typeof peerOverlap === 'function' && !peerOverlap(eventpeerEvent)) {
                        return 
false;
                    }
                }
            }
        }

        return 
true;
    }


    
// Given an event input from the API, produces an array of event objects. Possible event inputs:
    // 'businessHours'
    // An event ID (number or string)
    // An object with specific start/end dates or a recurring event (like what businessHours accepts)
    
function constraintToEvents(constraintInput) {

        if (
constraintInput === 'businessHours') {
            return 
getBusinessHoursEvents();
        }

        if (
typeof constraintInput === 'object') {
            return 
expandEvent(buildEventFromInput(constraintInput));
        }

        return 
clientEvents(constraintInput); // probably an ID
    
}


    
// Does the event's date range fully contain the given range?
    // start/end already assumed to have stripped zones :(
    
function eventContainsRange(eventrange) {
        var 
eventStart event.start.clone().stripZone();
        var 
eventEnd t.getEventEnd(event).stripZone();

        return 
range.start >= eventStart && range.end <= eventEnd;
    }


    
// Does the event's date range intersect with the given range?
    // start/end already assumed to have stripped zones :(
    
function eventIntersectsRange(eventrange) {
        var 
eventStart event.start.clone().stripZone();
        var 
eventEnd t.getEventEnd(event).stripZone();

        return 
range.start eventEnd && range.end eventStart;
    }


    
t.getEventCache = function() {
        return 
cache;
    };

}


// Returns a list of events that the given event should be compared against when being considered for a move to
// the specified span. Attached to the Calendar's prototype because EventManager is a mixin for a Calendar.
Calendar.prototype.getPeerEvents = function(spanevent) {
    var 
cache this.getEventCache();
    var 
peerEvents = [];
    var 
iotherEvent;

    for (
0cache.lengthi++) {
        
otherEvent cache[i];
        if (
            !
event ||
            
event._id !== otherEvent._id // don't compare the event to itself or other related [repeating] events
        
) {
            
peerEvents.push(otherEvent);
        }
    }

    return 
peerEvents;
};


// updates the "backup" properties, which are preserved in order to compute diffs later on.
function backupEventDates(event) {
    
event._allDay event.allDay;
    
event._start event.start.clone();
    
event._end event.end event.end.clone() : null;
}

;;

/* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells.
----------------------------------------------------------------------------------------------------------------------*/
// It is a manager for a DayGrid subcomponent, which does most of the heavy lifting.
// It is responsible for managing width/height.

var BasicView FC.BasicView View.extend({

    
dayGridClassDayGrid// class the dayGrid will be instantiated from (overridable by subclasses)
    
dayGridnull// the main subcomponent that does most of the heavy lifting

    
dayNumbersVisiblefalse// display day numbers on each day cell?
    
weekNumbersVisiblefalse// display week numbers along the side?

    
weekNumberWidthnull// width of all the week-number cells running down the side

    
headContainerElnull// div that hold's the dayGrid's rendered date header
    
headRowElnull// the fake row element of the day-of-week header


    
initialize: function() {
        
this.dayGrid this.instantiateDayGrid();
    },


    
// Generates the DayGrid object this view needs. Draws from this.dayGridClass
    
instantiateDayGrid: function() {
        
// generate a subclass on the fly with BasicView-specific behavior
        // TODO: cache this subclass
        
var subclass this.dayGridClass.extend(basicDayGridMethods);

        return new 
subclass(this);
    },


    
// Sets the display range and computes all necessary dates
    
setRange: function(range) {
        
View.prototype.setRange.call(thisrange); // call the super-method

        
this.dayGrid.breakOnWeeks = /year|month|week/.test(this.intervalUnit); // do before setRange
        
this.dayGrid.setRange(range);
    },


    
// Compute the value to feed into setRange. Overrides superclass.
    
computeRange: function(date) {
        var 
range View.prototype.computeRange.call(thisdate); // get value from the super-method

        // year and month views should be aligned with weeks. this is already done for week
        
if (/year|month/.test(range.intervalUnit)) {
            
range.start.startOf('week');
            
range.start this.skipHiddenDays(range.start);

            
// make end-of-week if not already
            
if (range.end.weekday()) {
                
range.end.add(1'week').startOf('week');
                
range.end this.skipHiddenDays(range.end, -1true); // exclusively move backwards
            
}
        }

        return 
range;
    },


    
// Renders the view into `this.el`, which should already be assigned
    
renderDates: function() {

        
this.dayNumbersVisible this.dayGrid.rowCnt 1// TODO: make grid responsible
        
this.weekNumbersVisible this.opt('weekNumbers');
        
this.dayGrid.numbersVisible this.dayNumbersVisible || this.weekNumbersVisible;

        
this.el.addClass('fc-basic-view').html(this.renderSkeletonHtml());
        
this.renderHead();

        
this.scrollerEl this.el.find('.fc-day-grid-container');

        
this.dayGrid.setElement(this.el.find('.fc-day-grid'));
        
this.dayGrid.renderDates(this.hasRigidRows());
    },


    
// render the day-of-week headers
    
renderHead: function() {
        
this.headContainerEl =
            
this.el.find('.fc-head-container')
                .
html(this.dayGrid.renderHeadHtml());
        
this.headRowEl this.headContainerEl.find('.fc-row');
    },


    
// Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering,
    // always completely kill the dayGrid's rendering.
    
unrenderDates: function() {
        
this.dayGrid.unrenderDates();
        
this.dayGrid.removeElement();
    },


    
renderBusinessHours: function() {
        
this.dayGrid.renderBusinessHours();
    },


    
// Builds the HTML skeleton for the view.
    // The day-grid component will render inside of a container defined by this HTML.
    
renderSkeletonHtml: function() {
        return 
'' +
            
'<table>' +
                
'<thead class="fc-head">' +
                    
'<tr>' +
                        
'<td class="fc-head-container ' this.widgetHeaderClass '"></td>' +
                    
'</tr>' +
                
'</thead>' +
                
'<tbody class="fc-body">' +
                    
'<tr>' +
                        
'<td class="' this.widgetContentClass '">' +
                            
'<div class="fc-day-grid-container">' +
                                
'<div class="fc-day-grid"/>' +
                            
'</div>' +
                        
'</td>' +
                    
'</tr>' +
                
'</tbody>' +
            
'</table>';
    },


    
// 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(totalHeightisAuto) {
        var 
eventLimit this.opt('eventLimit');
        var 
scrollerHeight;

        
// reset all heights to be natural
        
unsetScroller(this.scrollerEl);
        
uncompensateScroll(this.headRowEl);

        
this.dayGrid.removeSegPopover(); // 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(scrollerHeightisAuto);

        
// 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.scrollerElscrollerHeight)) { // using scrollbars?

            
compensateScroll(this.headRowElgetScrollbarWidths(this.scrollerEl));

            
// doing the scrollbar compensation might have created text overflow which created more height. redo
            
scrollerHeight this.computeScrollerHeight(totalHeight);
            
this.scrollerEl.height(scrollerHeight);
        }
    },


    
// Sets the height of just the DayGrid component in this view
    
setGridHeight: function(heightisAuto) {
        if (
isAuto) {
            
undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding
        
}
        else {
            
distributeHeight(this.dayGrid.rowElsheighttrue); // true = compensate for height-hogging rows
        
}
    },


    
/* Hit Areas
    ------------------------------------------------------------------------------------------------------------------*/
    // forward all hit-related method calls to dayGrid


    
prepareHits: function() {
        
this.dayGrid.prepareHits();
    },


    
releaseHits: function() {
        
this.dayGrid.releaseHits();
    },


    
queryHit: function(lefttop) {
        return 
this.dayGrid.queryHit(lefttop);
    },


    
getHitSpan: function(hit) {
        return 
this.dayGrid.getHitSpan(hit);
    },


    
getHitEl: function(hit) {
        return 
this.dayGrid.getHitEl(hit);
    },


    
/* 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
    
},


    
// Retrieves all segment objects that are rendered in the view
    
getEventSegs: function() {
        return 
this.dayGrid.getEventSegs();
    },


    
// Unrenders all event elements and clears internal segment data
    
unrenderEvents: function() {
        
this.dayGrid.unrenderEvents();

        
// 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
    
},


    
/* Dragging (for both events and external elements)
    ------------------------------------------------------------------------------------------------------------------*/


    // A returned value of `true` signals that a mock "helper" event has been rendered.
    
renderDrag: function(dropLocationseg) {
        return 
this.dayGrid.renderDrag(dropLocationseg);
    },


    
unrenderDrag: function() {
        
this.dayGrid.unrenderDrag();
    },


    
/* Selection
    ------------------------------------------------------------------------------------------------------------------*/


    // Renders a visual indication of a selection
    
renderSelection: function(span) {
        
this.dayGrid.renderSelection(span);
    },


    
// Unrenders a visual indications of a selection
    
unrenderSelection: function() {
        
this.dayGrid.unrenderSelection();
    }

});


// Methods that will customize the rendering behavior of the BasicView's dayGrid
var basicDayGridMethods = {


    
// Generates the HTML that will go before the day-of week header cells
    
renderHeadIntroHtml: function() {
        var 
view this.view;

        if (
view.weekNumbersVisible) {
            return 
'' +
                
'<th class="fc-week-number ' view.widgetHeaderClass '" ' view.weekNumberStyleAttr() + '>' +
                    
'<span>' // needed for matchCellWidths
                        
htmlEscape(view.opt('weekNumberTitle')) +
                    
'</span>' +
                
'</th>';
        }

        return 
'';
    },


    
// Generates the HTML that will go before content-skeleton cells that display the day/week numbers
    
renderNumberIntroHtml: function(row) {
        var 
view this.view;

        if (
view.weekNumbersVisible) {
            return 
'' +
                
'<td class="fc-week-number" ' view.weekNumberStyleAttr() + '>' +
                    
'<span>' // needed for matchCellWidths
                        
this.getCellDate(row0).format('w') +
                    
'</span>' +
                
'</td>';
        }

        return 
'';
    },


    
// Generates the HTML that goes before the day bg cells for each day-row
    
renderBgIntroHtml: function() {
        var 
view this.view;

        if (
view.weekNumbersVisible) {
            return 
'<td class="fc-week-number ' view.widgetContentClass '" ' +
                
view.weekNumberStyleAttr() + '></td>';
        }

        return 
'';
    },


    
// Generates the HTML that goes before every other type of row generated by DayGrid.
    // Affects helper-skeleton and highlight-skeleton rows.
    
renderIntroHtml: function() {
        var 
view this.view;

        if (
view.weekNumbersVisible) {
            return 
'<td class="fc-week-number" ' view.weekNumberStyleAttr() + '></td>';
        }

        return 
'';
    }

};

;;

/* A month view with day cells running in rows (one-per-week) and columns
----------------------------------------------------------------------------------------------------------------------*/

var MonthView FC.MonthView BasicView.extend({

    
// Produces information about what range to display
    
computeRange: function(date) {
        var 
range BasicView.prototype.computeRange.call(thisdate); // get value from super-method
        
var rowCnt;

        
// ensure 6 weeks
        
if (this.isFixedWeeks()) {
            
rowCnt Math.ceil(range.end.diff(range.start'weeks'true)); // could be partial weeks due to hiddenDays
            
range.end.add(rowCnt'weeks');
        }

        return 
range;
    },


    
// Overrides the default BasicView behavior to have special multi-week auto-height logic
    
setGridHeight: function(heightisAuto) {

        
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.rowElsheight, !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');
    }

});

;;

fcViews.basic = {
    
'class'BasicView
};

fcViews.basicDay = {
    
type'basic',
    
duration: { days}
};

fcViews.basicWeek = {
    
type'basic',
    
duration: { weeks}
};

fcViews.month = {
    
'class'MonthView,
    
duration: { months}, // important for prev/next
    
defaults: {
        
fixedWeekCounttrue
    
}
};
;;

/* 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.

var AgendaView FC.AgendaView View.extend({

    
timeGridClassTimeGrid// class used to instantiate the timeGrid. subclasses can override
    
timeGridnull// the main time-grid subcomponent of this view

    
dayGridClassDayGrid// class used to instantiate the dayGrid. subclasses can override
    
dayGridnull// the "all-day" subcomponent. if all-day is turned off, this will be null

    
axisWidthnull// the width of the time axis running down the side

    
headContainerElnull// div that hold's the timeGrid's rendered date header
    
noScrollRowElsnull// 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 <hr> underneath
    
bottomRuleElnull,
    
bottomRuleHeightnull,


    
initialize: function() {
        
this.timeGrid this.instantiateTimeGrid();

        if (
this.opt('allDaySlot')) { // should we display the "all-day" area?
            
this.dayGrid this.instantiateDayGrid(); // the all-day subcomponent of this view
        
}
    },


    
// Instantiates the TimeGrid object this view needs. Draws from this.timeGridClass
    
instantiateTimeGrid: function() {
        var 
subclass this.timeGridClass.extend(agendaTimeGridMethods);

        return new 
subclass(this);
    },


    
// Instantiates the DayGrid object this view might need. Draws from this.dayGridClass
    
instantiateDayGrid: function() {
        var 
subclass this.dayGridClass.extend(agendaDayGridMethods);

        return new 
subclass(this);
    },


    
/* Rendering
    ------------------------------------------------------------------------------------------------------------------*/


    // Sets the display range and computes all necessary dates
    
setRange: function(range) {
        
View.prototype.setRange.call(thisrange); // call the super-method

        
this.timeGrid.setRange(range);
        if (
this.dayGrid) {
            
this.dayGrid.setRange(range);
        }
    },


    
// Renders the view into `this.el`, which has already been assigned
    
renderDates: function() {

        
this.el.addClass('fc-agenda-view').html(this.renderSkeletonHtml());
        
this.renderHead();

        
// the element that wraps the time-grid that will probably scroll
        
this.scrollerEl this.el.find('.fc-time-grid-container');

        
this.timeGrid.setElement(this.el.find('.fc-time-grid'));
        
this.timeGrid.renderDates();

        
// the <hr> that sometimes displays under the time-grid
        
this.bottomRuleEl = $('<hr class="fc-divider ' this.widgetHeaderClass '"/>')
            .
appendTo(this.timeGrid.el); // inject it into the time-grid

        
if (this.dayGrid) {
            
this.dayGrid.setElement(this.el.find('.fc-day-grid'));
            
this.dayGrid.renderDates();

            
// have the day-grid extend it's coordinate area over the <hr> 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
    
},


    
// render the day-of-week headers
    
renderHead: function() {
        
this.headContainerEl =
            
this.el.find('.fc-head-container')
                .
html(this.timeGrid.renderHeadHtml());
    },


    
// Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering,
    // always completely kill each grid's rendering.
    
unrenderDates: function() {
        
this.timeGrid.unrenderDates();
        
this.timeGrid.removeElement();

        if (
this.dayGrid) {
            
this.dayGrid.unrenderDates();
            
this.dayGrid.removeElement();
        }
    },


    
// Builds the HTML skeleton for the view.
    // The day-grid and time-grid components will render inside containers defined by this HTML.
    
renderSkeletonHtml: function() {
        return 
'' +
            
'<table>' +
                
'<thead class="fc-head">' +
                    
'<tr>' +
                        
'<td class="fc-head-container ' this.widgetHeaderClass '"></td>' +
                    
'</tr>' +
                
'</thead>' +
                
'<tbody class="fc-body">' +
                    
'<tr>' +
                        
'<td class="' this.widgetContentClass '">' +
                            (
this.dayGrid ?
                                
'<div class="fc-day-grid"/>' +
                                
'<hr class="fc-divider ' this.widgetHeaderClass '"/>' :
                                
''
                                
) +
                            
'<div class="fc-time-grid-container">' +
                                
'<div class="fc-time-grid"/>' +
                            
'</div>' +
                        
'</td>' +
                    
'</tr>' +
                
'</tbody>' +
            
'</table>';
    },


    
// 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 
'';
    },


    
/* Business Hours
    ------------------------------------------------------------------------------------------------------------------*/


    
renderBusinessHours: function() {
        
this.timeGrid.renderBusinessHours();

        if (
this.dayGrid) {
            
this.dayGrid.renderBusinessHours();
        }
    },


    
unrenderBusinessHours: function() {
        
this.timeGrid.unrenderBusinessHours();

        if (
this.dayGrid) {
            
this.dayGrid.unrenderBusinessHours();
        }
    },


    
/* Now Indicator
    ------------------------------------------------------------------------------------------------------------------*/


    
getNowIndicatorUnit: function() {
        return 
this.timeGrid.getNowIndicatorUnit();
    },


    
renderNowIndicator: function(date) {
        
this.timeGrid.renderNowIndicator(date);
    },


    
unrenderNowIndicator: function() {
        
this.timeGrid.unrenderNowIndicator();
    },


    
/* Dimensions
    ------------------------------------------------------------------------------------------------------------------*/


    
updateSize: function(isResize) {
        
this.timeGrid.updateSize(isResize);

        
View.prototype.updateSize.call(thisisResize); // call the super-method
    
},


    
// 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(totalHeightisAuto) {
        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 <hr> 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.removeSegPopover(); // 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.scrollerElscrollerHeight)) { // using scrollbars?

                // make the all-day and header rows lines up
                
compensateScroll(this.noScrollRowElsgetScrollbarWidths(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);
            }
            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 <hr> goes outside
                
this.bottomRuleEl.show();
            }
        }
    },


    
// Computes the initial pre-configured scroll state prior to allowing the user to change it
    
computeInitialScroll: function() {
        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
        
}

        return 
top;
    },


    
/* Hit Areas
    ------------------------------------------------------------------------------------------------------------------*/
    // forward all hit-related method calls to the grids (dayGrid might not be defined)


    
prepareHits: function() {
        
this.timeGrid.prepareHits();
        if (
this.dayGrid) {
            
this.dayGrid.prepareHits();
        }
    },


    
releaseHits: function() {
        
this.timeGrid.releaseHits();
        if (
this.dayGrid) {
            
this.dayGrid.releaseHits();
        }
    },


    
queryHit: function(lefttop) {
        var 
hit this.timeGrid.queryHit(lefttop);

        if (!
hit && this.dayGrid) {
            
hit this.dayGrid.queryHit(lefttop);
        }

        return 
hit;
    },


    
getHitSpan: function(hit) {
        
// TODO: hit.component is set as a hack to identify where the hit came from
        
return hit.component.getHitSpan(hit);
    },


    
getHitEl: function(hit) {
        
// TODO: hit.component is set as a hack to identify where the hit came from
        
return hit.component.getHitEl(hit);
    },


    
/* 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 (0events.lengthi++) {
            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();
    },


    
// Retrieves all segment objects that are rendered in the view
    
getEventSegs: function() {
        return 
this.timeGrid.getEventSegs().concat(
            
this.dayGrid this.dayGrid.getEventSegs() : []
        );
    },


    
// Unrenders all event elements and clears internal segment data
    
unrenderEvents: function() {

        
// unrender the events in the subcomponents
        
this.timeGrid.unrenderEvents();
        if (
this.dayGrid) {
            
this.dayGrid.unrenderEvents();
        }

        
// 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
    
},


    
/* Dragging (for events and external elements)
    ------------------------------------------------------------------------------------------------------------------*/


    // A returned value of `true` signals that a mock "helper" event has been rendered.
    
renderDrag: function(dropLocationseg) {
        if (
dropLocation.start.hasTime()) {
            return 
this.timeGrid.renderDrag(dropLocationseg);
        }
        else if (
this.dayGrid) {
            return 
this.dayGrid.renderDrag(dropLocationseg);
        }
    },


    
unrenderDrag: function() {
        
this.timeGrid.unrenderDrag();
        if (
this.dayGrid) {
            
this.dayGrid.unrenderDrag();
        }
    },


    
/* Selection
    ------------------------------------------------------------------------------------------------------------------*/


    // Renders a visual indication of a selection
    
renderSelection: function(span) {
        if (
span.start.hasTime() || span.end.hasTime()) {
            
this.timeGrid.renderSelection(span);
        }
        else if (
this.dayGrid) {
            
this.dayGrid.renderSelection(span);
        }
    },


    
// Unrenders a visual indications of a selection
    
unrenderSelection: function() {
        
this.timeGrid.unrenderSelection();
        if (
this.dayGrid) {
            
this.dayGrid.unrenderSelection();
        }
    }

});


// Methods that will customize the rendering behavior of the AgendaView's timeGrid
// TODO: move into TimeGrid
var agendaTimeGridMethods = {


    
// Generates the HTML that will go before the day-of week header cells
    
renderHeadIntroHtml: function() {
        var 
view this.view;
        var 
weekText;

        if (
view.opt('weekNumbers')) {
            
weekText this.start.format(view.opt('smallWeekFormat'));

            return 
'' +
                
'<th class="fc-axis fc-week-number ' view.widgetHeaderClass '" ' view.axisStyleAttr() + '>' +
                    
'<span>' // needed for matchCellWidths
                        
htmlEscape(weekText) +
                    
'</span>' +
                
'</th>';
        }
        else {
            return 
'<th class="fc-axis ' view.widgetHeaderClass '" ' view.axisStyleAttr() + '></th>';
        }
    },


    
// Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column.
    
renderBgIntroHtml: function() {
        var 
view this.view;

        return 
'<td class="fc-axis ' view.widgetContentClass '" ' view.axisStyleAttr() + '></td>';
    },


    
// 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.
    
renderIntroHtml: function() {
        var 
view this.view;

        return 
'<td class="fc-axis" ' view.axisStyleAttr() + '></td>';
    }

};


// Methods that will customize the rendering behavior of the AgendaView's dayGrid
var agendaDayGridMethods = {


    
// Generates the HTML that goes before the all-day cells
    
renderBgIntroHtml: function() {
        var 
view this.view;

        return 
'' +
            
'<td class="fc-axis ' view.widgetContentClass '" ' view.axisStyleAttr() + '>' +
                
'<span>' // needed for matchCellWidths
                    
(view.opt('allDayHtml') || htmlEscape(view.opt('allDayText'))) +
                
'</span>' +
            
'</td>';
    },


    
// 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.
    
renderIntroHtml: function() {
        var 
view this.view;

        return 
'<td class="fc-axis" ' view.axisStyleAttr() + '></td>';
    }

};

;;

var 
AGENDA_ALL_DAY_EVENT_LIMIT 5;

// potential nice values for the slot-duration and interval-duration
// from largest to smallest
var AGENDA_STOCK_SUB_DURATIONS = [
    { 
hours},
    { 
minutes30 },
    { 
minutes15 },
    { 
seconds30 },
    { 
seconds15 }
];

fcViews.agenda = {
    
'class'AgendaView,
    
defaults: {
        
allDaySlottrue,
        
allDayText'all-day',
        
slotDuration'00:30:00',
        
minTime'00:00:00',
        
maxTime'24:00:00',
        
slotEventOverlaptrue // a bad name. confused with overlap/constraint system
    
}
};

fcViews.agendaDay = {
    
type'agenda',
    
duration: { days}
};

fcViews.agendaWeek = {
    
type'agenda',
    
duration: { weeks}
};
;;

return 
FC// export for Node/CommonJS
});
?>
Онлайн: 0
Реклама