gentelella
Version:
Gentellela Admin is a free to use Bootstrap admin template
1,737 lines (1,383 loc) • 354 kB
JavaScript
/*!
* FullCalendar v2.7.1
* Docs & License: http://fullcalendar.io/
* (c) 2016 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(jQuery, moment);
}
})(function($, moment) {
;;
var FC = $.fullCalendar = {
version: "2.7.1",
internalApiVersion: 3
};
var fcViews = FC.views = {};
FC.isTouch = 'ontouchstart' in document;
$.fn.fullCalendar = function(options) {
var args = Array.prototype.slice.call(arguments, 1); // for a possible method call
var res = this; // what this function will return (this jQuery object by default)
this.each(function(i, _element) { // loop each DOM element involved
var element = $(_element);
var calendar = element.data('fullCalendar'); // get the existing calendar object (if any)
var singleRes; // the returned value of this single method call
// a method call
if (typeof options === 'string') {
if (calendar && $.isFunction(calendar[options])) {
singleRes = calendar[options].apply(calendar, args);
if (!i) {
res = singleRes; // record the first method call result
}
if (options === 'destroy') { // for the destroy method, must remove Calendar object data
element.removeData('fullCalendar');
}
}
}
// a new calendar initialization
else if (!calendar) { // don't initialize twice
calendar = new Calendar(element, options);
element.data('fullCalendar', calendar);
calendar.render();
}
});
return res;
};
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(optionObjs, complexOptions);
}
// 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 = { views: input.views || {} }; // the output. ensure a `views` hash
var subObj;
// iterate through all option override properties (except `views`)
$.each(input, function(name, val) {
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(name, complexOptions) == -1 // 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(subName, subVal) {
// 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(rowEls, scrollbarWidths) {
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(els, availableHeight, shouldRedistribute) {
// *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(i, el) {
var minOffset = i === els.length - 1 ? 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(i, el) {
var minOffset = i === flexEls.length - 1 ? 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(i, innerEl) {
var innerWidth = $(innerEl).outerWidth();
if (innerWidth > maxInnerWidth) {
maxInnerWidth = innerWidth;
}
});
maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance
els.width(maxInnerWidth);
return maxInnerWidth;
}
// Given one element that resides inside another,
// Subtracts the height of the inner element from the outer element.
function subtractInnerElHeight(outerEl, innerEl) {
var both = outerEl.add(innerEl);
var diff;
// effin' IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked
both.css({
position: 'relative', // cause a reflow, which will force fresh dimension recalculation
left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll
});
diff = outerEl.outerHeight() - innerEl.outerHeight(); // grab the dimensions
both.css({ position: '', left: '' }); // undo hack
return diff;
}
/* Element Geom 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).
// Origin is optional.
function getOuterRect(el, origin) {
var offset = el.offset();
var left = offset.left - (origin ? origin.left : 0);
var top = offset.top - (origin ? origin.top : 0);
return {
left: left,
right: left + el.outerWidth(),
top: top,
bottom: 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).
// Origin is optional.
// NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
function getClientRect(el, origin) {
var offset = el.offset();
var scrollbarWidths = getScrollbarWidths(el);
var left = offset.left + getCssFloat(el, 'border-left-width') + scrollbarWidths.left - (origin ? origin.left : 0);
var top = offset.top + getCssFloat(el, 'border-top-width') + scrollbarWidths.top - (origin ? origin.top : 0);
return {
left: left,
right: left + el[0].clientWidth, // clientWidth includes padding but NOT scrollbars
top: top,
bottom: top + 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).
// Origin is optional.
function getContentRect(el, origin) {
var offset = el.offset(); // just outside of border, margin not included
var left = offset.left + getCssFloat(el, 'border-left-width') + getCssFloat(el, 'padding-left') -
(origin ? origin.left : 0);
var top = offset.top + getCssFloat(el, 'border-top-width') + getCssFloat(el, 'padding-top') -
(origin ? origin.top : 0);
return {
left: left,
right: left + el.width(),
top: top,
bottom: top + 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 = {
left: 0,
right: 0,
top: 0,
bottom: el.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,
left: 0,
border: 0,
padding: 0,
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(el, prop) {
return parseFloat(el.css(prop)) || 0;
}
/* Mouse / Touch Utilities
----------------------------------------------------------------------------------------------------------------------*/
FC.preventDefault = preventDefault;
// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
function isPrimaryMouseButton(ev) {
return ev.which == 1 && !ev.ctrlKey;
}
function getEvX(ev) {
if (ev.pageX !== undefined) {
return ev.pageX;
}
var touches = ev.originalEvent.touches;
if (touches) {
return touches[0].pageX;
}
}
function getEvY(ev) {
if (ev.pageY !== undefined) {
return ev.pageY;
}
var touches = ev.originalEvent.touches;
if (touches) {
return touches[0].pageY;
}
}
function getEvIsTouch(ev) {
return /^touch/.test(ev.type);
}
function preventSelection(el) {
el.addClass('fc-unselectable')
.on('selectstart', preventDefault);
}
// Stops a mouse/touch event from doing it's native browser action
function preventDefault(ev) {
ev.preventDefault();
}
/* General Geometry Utils
----------------------------------------------------------------------------------------------------------------------*/
FC.intersectRects = intersectRects;
// Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false
function intersectRects(rect1, rect2) {
var res = {
left: Math.max(rect1.left, rect2.left),
right: Math.min(rect1.right, rect2.right),
top: Math.max(rect1.top, rect2.top),
bottom: Math.min(rect1.bottom, rect2.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(point, rect) {
return {
left: Math.min(Math.max(point.left, rect.left), rect.right),
top: Math.min(Math.max(point.top, rect.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(point1, point2) {
return {
left: point1.left - point2.left,
top: point1.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 i, token;
if (typeof input === 'string') {
tokens = input.split(/\s*,\s*/);
}
else if (typeof input === 'function') {
tokens = [ input ];
}
else if ($.isArray(input)) {
tokens = input;
}
for (i = 0; i < tokens.length; i++) {
token = tokens[i];
if (typeof token === 'string') {
specs.push(
token.charAt(0) == '-' ?
{ field: token.substring(1), order: -1 } :
{ field: token, order: 1 }
);
}
else if (typeof token === 'function') {
specs.push({ func: token });
}
}
return specs;
}
function compareByFieldSpecs(obj1, obj2, fieldSpecs) {
var i;
var cmp;
for (i = 0; i < fieldSpecs.length; i++) {
cmp = compareByFieldSpec(obj1, obj2, fieldSpecs[i]);
if (cmp) {
return cmp;
}
}
return 0;
}
function compareByFieldSpec(obj1, obj2, fieldSpec) {
if (fieldSpec.func) {
return fieldSpec.func(obj1, obj2);
}
return flexibleCompare(obj1[fieldSpec.field], obj2[fieldSpec.field]) *
(fieldSpec.order || 1);
}
function flexibleCompare(a, b) {
if (!a && !b) {
return 0;
}
if (b == null) {
return -1;
}
if (a == null) {
return 1;
}
if ($.type(a) === 'string' || $.type(b) === 'string') {
return String(a).localeCompare(String(b));
}
return a - 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(subjectRange, constraintRange) {
var subjectStart = subjectRange.start;
var subjectEnd = subjectRange.end;
var constraintStart = constraintRange.start;
var constraintEnd = constraintRange.end;
var segStart, segEnd;
var isStart, isEnd;
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 {
start: segStart,
end: segEnd,
isStart: isStart,
isEnd: isEnd
};
}
}
/* 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(a, b) {
return moment.duration({
days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'),
ms: a.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(a, b) {
return moment.duration({
days: a.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(a, b, unit) {
return moment.duration(
Math.round(a.diff(b, unit, true)), // 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(start, end) {
var i, unit;
var val;
for (i = 0; i < intervalUnits.length; i++) {
unit = intervalUnits[i];
val = computeRangeAs(unit, start, end);
if (val >= 1 && 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(unit, start, end) {
if (end != null) { // given start, end
return end.diff(start, unit, true);
}
else if (moment.isDuration(start)) { // given duration
return start.as(unit);
}
else { // given { start, end } range object
return start.end.diff(start.start, unit, true);
}
}
// Intelligently divides a range (specified by a start/end params) by a duration
function divideRangeByDuration(start, end, dur) {
var months;
if (durationHasTime(dur)) {
return (end - start) / dur;
}
months = dur.asMonths();
if (Math.abs(months) >= 1 && 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(dur1, dur2) {
var months1, months2;
if (durationHasTime(dur1) || durationHasTime(dur2)) {
return dur1 / dur2;
}
months1 = dur1.asMonths();
months2 = dur2.asMonths();
if (
Math.abs(months1) >= 1 && isInt(months1) &&
Math.abs(months2) >= 1 && isInt(months2)
) {
return months1 / months2;
}
return dur1.asDays() / dur2.asDays();
}
// Intelligently multiplies a duration by a number
function multiplyDuration(dur, n) {
var months;
if (durationHasTime(dur)) {
return moment.duration(dur * n);
}
months = dur.asMonths();
if (Math.abs(months) >= 1 && isInt(months)) {
return moment.duration({ months: months * n });
}
return moment.duration({ days: dur.asDays() * n });
}
// 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(console, arguments);
}
};
FC.warn = function() {
var console = window.console;
if (console && console.warn) {
return console.warn.apply(console, arguments);
}
else {
return FC.log.apply(FC, arguments);
}
};
/* 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(propObjs, complexProps) {
var dest = {};
var i, name;
var complexObjs;
var j, val;
var props;
if (complexProps) {
for (i = 0; i < complexProps.length; i++) {
name = complexProps[i];
complexObjs = [];
// collect the trailing object values, stopping when a non-object is discovered
for (j = propObjs.length - 1; j >= 0; j--) {
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 (i = propObjs.length - 1; i >= 0; i--) {
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 f = function() {};
f.prototype = proto;
return new f();
}
function copyOwnProps(src, dest) {
for (var name in src) {
if (hasOwnProp(src, name)) {
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(src, dest) {
var names = [ 'constructor', 'toString', 'valueOf' ];
var i, name;
for (i = 0; i < names.length; i++) {
name = names[i];
if (src[name] !== Object.prototype[name]) {
dest[name] = src[name];
}
}
}
function hasOwnProp(obj, name) {
return hasOwnPropMethod.call(obj, name);
}
// Is the given value a non-object non-function value?
function isAtomic(val) {
return /undefined|null|boolean|number|string/.test($.type(val));
}
function applyAll(functions, thisObj, args) {
if ($.isFunction(functions)) {
functions = [ functions ];
}
if (functions) {
var i;
var ret;
for (i=0; i<functions.length; i++) {
ret = functions[i].apply(thisObj, args) || ret;
}
return ret;
}
}
function firstDefined() {
for (var i=0; i<arguments.length; i++) {
if (arguments[i] !== undefined) {
return arguments[i];
}
}
}
function htmlEscape(s) {
return (s + '').replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/'/g, ''')
.replace(/"/g, '"')
.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. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
// https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714
function debounce(func, wait, immediate) {
var timeout, args, context, timestamp, result;
var later = function() {
var last = +new Date() - timestamp;
if (last < wait) {
timeout = setTimeout(later, wait - last);
}
else {
timeout = null;
if (!immediate) {
result = func.apply(context, args);
context = args = null;
}
}
};
return function() {
context = this;
args = arguments;
timestamp = +new Date();
var callNow = immediate && !timeout;
if (!timeout) {
timeout = setTimeout(later, wait);
}
if (callNow) {
result = func.apply(context, args);
context = args = null;
}
return result;
};
}
;;
var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/;
var ambigTimeOrZoneRegex =
/^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/;
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=0; leftI<chunks.length; leftI++) {
chunkStr = formatSimilarChunk(date1, date2, unzonedDate1, unzonedDate2, chunks[leftI]);
if (chunkStr === false) {
break;
}
leftStr += chunkStr;
}
// Similarly, start at the rightmost side of the formatting string and move left
for (rightI=chunks.length-1; rightI>leftI; rightI--) {
chunkStr = formatSimilarChunk(date1, date2, unzonedDate1, unzonedDate2, chunks[rightI]);
if (chunkStr === false) {
break;
}
rightStr = chunkStr + rightStr;
}
// The area in the middle is different for both of the dates.
// Collect them distinctly so we can jam them together later.
for (middleI=leftI; middleI<=rightI; middleI++) {
middleStr1 += formatDateWithChunk(date1, chunks[middleI]);
middleStr2 += formatDateWithChunk(date2, chunks[middleI]);
}
if (middleStr1 || middleStr2) {
if (isRTL) {
middleStr = middleStr2 + separator + middleStr1;
}
else {
middleStr = middleStr1 + separator + middleStr2;
}
}
return leftStr + middleStr + rightStr;
}
var similarUnitMap = {
Y: 'year',
M: 'month',
D: 'day', // day of month
d: 'day', // day of week
// prevents a separator between anything time-related...
A: 'second', // AM/PM
a: 'second', // am/pm
T: 'second', // A/P
t: 'second', // a/p
H: 'second', // hour (24)
h: 'second', // hour (12)
m: 'second', // minute
s: 'second' // second
};
// TODO: week maybe?
// Given a formatting chunk, and given that both dates are similar in the regard the
// formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`.
function formatSimilarChunk(date1, date2, unzonedDate1, unzonedDate2, chunk) {
var token;
var unit;
if (typeof chunk === 'string') { // a literal string
return chunk;
}
else if ((token = chunk.token)) {
unit = similarUnitMap[token.charAt(0)];
// are the dates the same for this unit of measurement?
// use the unzoned dates for this calculation because unreliable when near DST (bug #2396)
if (unit && unzonedDate1.isSame(unzonedDate2, unit)) {
return oldMomentFormat(date1, token); // would be the same if we used `date2`
// BTW, don't support custom tokens
}
}
return false; // the chunk is NOT the same for the two dates
// BTW, don't support splitting on non-zero areas
}
// Chunking Utils
// -----------------------------------------------------------------------