aboutsummaryrefslogtreecommitdiffstats
path: root/library/fullcalendar/fullcalendar.js
diff options
context:
space:
mode:
Diffstat (limited to 'library/fullcalendar/fullcalendar.js')
-rw-r--r--library/fullcalendar/fullcalendar.js2348
1 files changed, 1460 insertions, 888 deletions
diff --git a/library/fullcalendar/fullcalendar.js b/library/fullcalendar/fullcalendar.js
index 2460eb5e7..33c0c6a38 100644
--- a/library/fullcalendar/fullcalendar.js
+++ b/library/fullcalendar/fullcalendar.js
@@ -1,5 +1,5 @@
/*!
- * FullCalendar v2.8.0
+ * FullCalendar v3.0.0
* Docs & License: http://fullcalendar.io/
* (c) 2016 Adam Shaw
*/
@@ -19,8 +19,8 @@
;;
var FC = $.fullCalendar = {
- version: "2.8.0",
- internalApiVersion: 4
+ version: "3.0.0",
+ internalApiVersion: 6
};
var fcViews = FC.views = {};
@@ -71,56 +71,6 @@ 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
@@ -247,7 +197,7 @@ function undistributeHeight(els) {
function matchCellWidths(els) {
var maxInnerWidth = 0;
- els.find('> span').each(function(i, innerEl) {
+ els.find('> *').each(function(i, innerEl) {
var innerWidth = $(innerEl).outerWidth();
if (innerWidth > maxInnerWidth) {
maxInnerWidth = innerWidth;
@@ -628,7 +578,8 @@ function flexibleCompare(a, b) {
----------------------------------------------------------------------------------------------------------------------*/
-// Computes the intersection of the two ranges. Returns undefined if no intersection.
+// Computes the intersection of the two ranges. Will return fresh date clones in a range.
+// 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) {
@@ -908,22 +859,6 @@ function copyOwnProps(src, dest) {
}
-// 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);
}
@@ -989,6 +924,21 @@ function cssToStr(cssProps) {
}
+// Given an object hash of HTML attribute names to values,
+// generates a string that can be injected between < > in HTML
+function attrsToStr(attrs) {
+ var parts = [];
+
+ $.each(attrs, function(name, val) {
+ if (val != null) {
+ parts.push(name + '="' + htmlEscape(val) + '"');
+ }
+ });
+
+ return parts.join(' ');
+}
+
+
function capitaliseFirstLetter(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
@@ -1070,14 +1020,24 @@ function syncThen(promise, thenFunc) {
;;
+/*
+GENERAL NOTE on moments throughout the *entire rest* of the codebase:
+All moments are assumed to be ambiguously-zoned unless otherwise noted,
+with the NOTABLE EXCEOPTION of start/end dates that live on *Event Objects*.
+Ambiguously-TIMED moments are assumed to be ambiguously-zoned by nature.
+*/
+
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
+
+// tell momentjs to transfer these properties upon clone
+var momentProperties = moment.momentProperties;
+momentProperties.push('_fullCalendar');
+momentProperties.push('_ambigTime');
+momentProperties.push('_ambigZone');
// Creating
@@ -1123,12 +1083,8 @@ function makeMoment(args, parseAsUTC, parseZone) {
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
+ if (moment.isMoment(input) || isNativeDate(input) || input === undefined) {
+ mom = moment.apply(null, args);
}
else { // "parsing" is required
isAmbigTime = false;
@@ -1169,12 +1125,7 @@ function makeMoment(args, parseAsUTC, parseZone) {
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.utcOffset(input); // if not a valid zone, will assign UTC
}
}
}
@@ -1185,21 +1136,6 @@ function makeMoment(args, parseAsUTC, parseZone) {
}
-// 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
// -------------------------------------------------------------------------------------------------
@@ -1207,8 +1143,7 @@ newMomentProto.clone = function() {
// 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;
+ var weekCalc = this._locale._fullCalendar_weekCalc;
if (input == null && typeof weekCalc === 'function') { // custom function only works for getter
return weekCalc(this);
@@ -1275,19 +1210,21 @@ newMomentProto.time = function(time) {
// 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
+ this.utc(true); // keepLocalTime=true (for keeping *date* value)
- // 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
+ // set time to zero
+ this.set({
+ hours: 0,
+ minutes: 0,
+ seconds: 0,
+ ms: 0
+ });
// 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.
+ // which clears all ambig flags.
this._ambigTime = true;
this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset
}
@@ -1307,24 +1244,20 @@ newMomentProto.hasTime = function() {
// 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;
+ var 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
+ this.utc(true); // keepLocalTime=true (for keeping date and time values)
// 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.
+ // which clears the ambig flags.
this._ambigZone = true;
}
@@ -1337,32 +1270,26 @@ newMomentProto.hasZone = function() {
};
-// 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;
+// implicitly marks a zone
+newMomentProto.local = function(keepLocalTime) {
- oldMomentProto.local.apply(this, arguments);
+ // for when converting from ambiguously-zoned to local,
+ // keep the time values when converting from UTC -> local
+ oldMomentProto.local.call(this, this._ambigZone || keepLocalTime);
// 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);
+newMomentProto.utc = function(keepLocalTime) {
+
+ oldMomentProto.utc.call(this, keepLocalTime);
// ensure non-ambiguous
// this probably already happened via utc() -> utcOffset(), but don't rely on Moment's internals
@@ -1373,28 +1300,18 @@ newMomentProto.utc = function() {
};
-// 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) {
+// implicitly marks a zone (will probably get called upon .utc() and .local())
+newMomentProto.utcOffset = 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);
- };
+ 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.utcOffset.apply(this, arguments);
+};
// Formatting
@@ -1423,156 +1340,6 @@ newMomentProto.toISOString = function() {
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
@@ -1653,7 +1420,7 @@ function formatRange(date1, date2, formatStr, separator, isRTL) {
date1 = FC.moment.parseZone(date1);
date2 = FC.moment.parseZone(date2);
- localeData = (date1.localeData || date1.lang).call(date1); // works with moment-pre-2.8
+ localeData = date1.localeData();
// Expand localized format strings, like "LL" -> "MMMM D YYYY"
formatStr = localeData.longDateFormat(formatStr) || formatStr;
@@ -1858,7 +1625,6 @@ function extendClass(superClass, members) {
// copy each member variable/method onto the the subclass's prototype
copyOwnProps(members, subClass.prototype);
- copyNativeMethods(members, subClass.prototype); // hack for IE8
// copy over all class variables/methods to the subclass, such as `extend` and `mixin`
copyOwnProps(superClass, subClass);
@@ -1868,7 +1634,7 @@ function extendClass(superClass, members) {
function mixIntoClass(theClass, members) {
- copyOwnProps(members, theClass.prototype); // TODO: copyNativeMethods?
+ copyOwnProps(members, theClass.prototype);
}
;;
@@ -2263,17 +2029,6 @@ var CoordCache = FC.CoordCache = Class.extend({
},
- // 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 = [];
@@ -2313,42 +2068,36 @@ var CoordCache = FC.CoordCache = Class.extend({
// 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.
+ // If no intersection is made, 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 (i = 0; i < len; i++) {
- if (leftOffset >= lefts[i] && leftOffset < rights[i]) {
- return i;
- }
+ for (i = 0; i < len; i++) {
+ 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.
+ // If no intersection is made, 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 (i = 0; i < len; i++) {
- if (topOffset >= tops[i] && topOffset < bottoms[i]) {
- return i;
- }
+ for (i = 0; i < len; i++) {
+ if (topOffset >= tops[i] && topOffset < bottoms[i]) {
+ return i;
}
}
},
@@ -2424,6 +2173,32 @@ var CoordCache = FC.CoordCache = Class.extend({
getHeight: function(topIndex) {
this.ensureBuilt();
return this.bottoms[topIndex] - this.tops[topIndex];
+ },
+
+
+ // Bounding Rect
+ // TODO: decouple this from CoordCache
+
+ // 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);
+ }
+ },
+
+ isPointInBounds: function(leftOffset, topOffset) {
+ return this.isLeftInBounds(leftOffset) && this.isTopInBounds(topOffset);
+ },
+
+ isLeftInBounds: function(leftOffset) {
+ return !this.boundingRect || (leftOffset >= this.boundingRect.left && leftOffset < this.boundingRect.right);
+ },
+
+ isTopInBounds: function(topOffset) {
+ return !this.boundingRect || (topOffset >= this.boundingRect.top && topOffset < this.boundingRect.bottom);
}
});
@@ -2437,10 +2212,7 @@ var CoordCache = FC.CoordCache = Class.extend({
var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMixin, {
options: null,
-
- // for IE8 bug-fighting behavior
subjectEl: null,
- subjectHref: null,
// coordinates of the initial mousedown
originX: null,
@@ -2631,7 +2403,6 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMix
handleDragStart: function(ev) {
this.trigger('dragStart', ev);
- this.initHrefHack();
},
@@ -2671,7 +2442,6 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMix
handleDragEnd: function(ev) {
this.trigger('dragEnd', ev);
- this.destroyHrefHack();
},
@@ -2747,33 +2517,6 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMix
},
- // <A> HREF Hack
- // -----------------------------------------------------------------------------------------------------------------
-
-
- initHrefHack: function() {
- var subjectEl = this.subjectEl;
-
- // 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');
- }
- },
-
-
- destroyHrefHack: function() {
- var subjectEl = this.subjectEl;
- var subjectHref = this.subjectHref;
-
- // restore a mousedown'd <a>'s href (for IE8 bug)
- setTimeout(function() { // must be outside of the click's execution
- if (subjectHref) {
- subjectEl.attr('href', subjectHref);
- }
- }, 0);
- },
-
-
// Utils
// -----------------------------------------------------------------------------------------------------------------
@@ -3259,11 +3002,11 @@ var MouseFollower = Class.extend(ListenerMixin, {
var _this = this;
var revertDuration = this.options.revertDuration;
- function complete() {
- this.isAnimating = false;
+ function complete() { // might be called by .animate(), which might change `this` context
+ _this.isAnimating = false;
_this.removeElement();
- this.top0 = this.left0 = null; // reset state for future updatePosition calls
+ _this.top0 = _this.left0 = null; // reset state for future updatePosition calls
if (callback) {
callback();
@@ -3297,7 +3040,6 @@ var MouseFollower = Class.extend(ListenerMixin, {
var el = this.el;
if (!el) {
- this.sourceEl.width(); // hack to force IE8 to compute correct bounding box
el = this.el = this.sourceEl.clone()
.addClass(this.options.additionalClass || '')
.css({
@@ -3342,7 +3084,6 @@ var MouseFollower = Class.extend(ListenerMixin, {
// 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;
@@ -3396,6 +3137,9 @@ var MouseFollower = Class.extend(ListenerMixin, {
var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
+ // self-config, overridable by subclasses
+ hasDayInteractions: true, // can user click/select ranges of time?
+
view: null, // a View object
isRTL: null, // shortcut to the view's isRTL option
@@ -3563,10 +3307,13 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
// Does other DOM-related initializations.
setElement: function(el) {
this.el = el;
- preventSelection(el);
- this.bindDayHandler('touchstart', this.dayTouchStart);
- this.bindDayHandler('mousedown', this.dayMousedown);
+ if (this.hasDayInteractions) {
+ preventSelection(el);
+
+ this.bindDayHandler('touchstart', this.dayTouchStart);
+ this.bindDayHandler('mousedown', this.dayMousedown);
+ }
// attach event-element-related handlers. in Grid.events
// same garbage collection note as above.
@@ -3583,8 +3330,12 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
// jQuery will take care of unregistering them when removeElement gets called.
this.el.on(name, 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)
+ !$(ev.target).is(
+ _this.segSelector + ',' + // directly on an event element
+ _this.segSelector + ' *,' + // within an event element
+ '.fc-more,' + // a "more.." link
+ 'a[data-goto]' // a clickable nav link
+ )
) {
return handler.call(_this, ev);
}
@@ -3683,6 +3434,7 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
scroll: view.opt('dragScroll'),
interactionStart: function() {
dayClickHit = dragListener.origHit; // for dayClick, where no dragging happens
+ selectionSpan = null;
},
dragStart: function() {
view.unselect(); // since we could be rendering a new selection, we want to clear any old one
@@ -3709,10 +3461,12 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
}
}
},
- hitOut: function() {
+ hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
dayClickHit = null;
selectionSpan = null;
_this.unrenderSelection();
+ },
+ hitDone: function() { // called after a hitOut OR before a dragEnd
enableCursor();
},
interactionEnd: function(ev, isCancelled) {
@@ -3731,7 +3485,6 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
// the selection will already have been rendered. just report it
view.reportSelection(selectionSpan, ev);
}
- enableCursor();
}
}
});
@@ -4034,6 +3787,9 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
Grid.mixin({
+ // self-config, overridable by subclasses
+ segSelector: '.fc-event-container > *', // what constitutes an event element?
+
mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing
isDraggingSeg: false, // is a segment being dragged? boolean
isResizingSeg: false, // is a segment being resized? boolean
@@ -4172,7 +3928,7 @@ Grid.mixin({
// Generates an array of classNames to be used for the default rendering of a background event.
- // Called by the fill system.
+ // Called by fillSegHtml.
bgEventSegClasses: function(seg) {
var event = seg.event;
var source = event.source || {};
@@ -4185,7 +3941,7 @@ Grid.mixin({
// Generates a semicolon-separated CSS string to be used for the default rendering of a background event.
- // Called by the fill system.
+ // Called by fillSegHtml.
bgEventSegCss: function(seg) {
return {
'background-color': this.getSegSkinCss(seg)['background-color']
@@ -4194,32 +3950,68 @@ Grid.mixin({
// Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system.
+ // Called by fillSegHtml.
businessHoursSegClasses: function(seg) {
return [ 'fc-nonbusiness', 'fc-bgevent' ];
},
+ /* Business Hours
+ ------------------------------------------------------------------------------------------------------------------*/
+
+
+ // Compute business hour segs for the grid's current date range.
+ // Caller must ask if whole-day business hours are needed.
+ buildBusinessHourSegs: function(wholeDay) {
+ var events = this.view.calendar.getCurrentBusinessHourEvents(wholeDay);
+
+ // HACK. Eventually refactor business hours "events" system.
+ // If no events are given, but businessHours is activated, this means the entire visible range should be
+ // marked as *not* business-hours, via inverse-background rendering.
+ if (
+ !events.length &&
+ this.view.calendar.options.businessHours // don't access view option. doesn't update with dynamic options
+ ) {
+ events = [
+ $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, {
+ start: this.view.end, // guaranteed out-of-range
+ end: this.view.end, // "
+ dow: null
+ })
+ ];
+ }
+
+ return this.eventsToSegs(events);
+ },
+
+
/* Handlers
------------------------------------------------------------------------------------------------------------------*/
- // Attaches event-element-related handlers to the container element and leverage bubbling
+ // Attaches event-element-related handlers for *all* rendered event segments of the view.
bindSegHandlers: function() {
- this.bindSegHandler('touchstart', this.handleSegTouchStart);
- this.bindSegHandler('touchend', this.handleSegTouchEnd);
- this.bindSegHandler('mouseenter', this.handleSegMouseover);
- this.bindSegHandler('mouseleave', this.handleSegMouseout);
- this.bindSegHandler('mousedown', this.handleSegMousedown);
- this.bindSegHandler('click', this.handleSegClick);
+ this.bindSegHandlersToEl(this.el);
+ },
+
+
+ // Attaches event-element-related handlers to an arbitrary container element. leverages bubbling.
+ bindSegHandlersToEl: function(el) {
+ this.bindSegHandlerToEl(el, 'touchstart', this.handleSegTouchStart);
+ this.bindSegHandlerToEl(el, 'touchend', this.handleSegTouchEnd);
+ this.bindSegHandlerToEl(el, 'mouseenter', this.handleSegMouseover);
+ this.bindSegHandlerToEl(el, 'mouseleave', this.handleSegMouseout);
+ this.bindSegHandlerToEl(el, 'mousedown', this.handleSegMousedown);
+ this.bindSegHandlerToEl(el, 'click', this.handleSegClick);
},
// Executes a handler for any a user-interaction on a segment.
// Handler gets called with (seg, ev), and with the `this` context of the Grid
- bindSegHandler: function(name, handler) {
+ bindSegHandlerToEl: function(el, name, handler) {
var _this = this;
- this.el.on(name, '.fc-event-container > *', function(ev) {
+ el.on(name, this.segSelector, 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
@@ -4231,7 +4023,10 @@ Grid.mixin({
handleSegClick: function(seg, ev) {
- return this.view.trigger('eventClick', seg.el[0], seg.event, ev); // can return `false` to cancel
+ var res = this.view.trigger('eventClick', seg.el[0], seg.event, ev); // can return `false` to cancel
+ if (res === false) {
+ ev.preventDefault();
+ }
},
@@ -4242,7 +4037,9 @@ Grid.mixin({
!this.mousedOverSeg
) {
this.mousedOverSeg = seg;
- seg.el.addClass('fc-allow-mouse-resize');
+ if (this.view.isEventResizable(seg.event)) {
+ seg.el.addClass('fc-allow-mouse-resize');
+ }
this.view.trigger('eventMouseover', seg.el[0], seg.event, ev);
}
},
@@ -4256,7 +4053,9 @@ Grid.mixin({
if (this.mousedOverSeg) {
seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment
this.mousedOverSeg = null;
- seg.el.removeClass('fc-allow-mouse-resize');
+ if (this.view.isEventResizable(seg.event)) {
+ seg.el.removeClass('fc-allow-mouse-resize');
+ }
this.view.trigger('eventMouseout', seg.el[0], seg.event, ev);
}
},
@@ -4353,6 +4152,7 @@ Grid.mixin({
subjectEl: el,
subjectCenter: true,
interactionStart: function(ev) {
+ seg.component = _this; // for renderDrag
isDragging = false;
mouseFollower = new MouseFollower(seg.el, {
additionalClass: 'fc-dragging',
@@ -4421,6 +4221,8 @@ Grid.mixin({
enableCursor();
},
interactionEnd: function(ev) {
+ delete seg.component; // prevent side effects
+
// do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
mouseFollower.stop(!dropLocation, function() {
if (isDragging) {
@@ -4508,11 +4310,7 @@ Grid.mixin({
}
// othewise, work off existing values
else {
- dropLocation = {
- start: event.start.clone(),
- end: event.end ? event.end.clone() : null,
- allDay: event.allDay // keep it the same
- };
+ dropLocation = pluckEventDateProps(event);
}
dropLocation.start.add(delta);
@@ -4538,11 +4336,7 @@ Grid.mixin({
var opacity = this.view.opt('dragOpacity');
if (opacity != null) {
- els.each(function(i, node) {
- // 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;
- });
+ els.css('opacity', opacity);
}
},
@@ -4708,8 +4502,11 @@ Grid.mixin({
disableCursor();
resizeLocation = null;
}
- // no change? (TODO: how does this work with timezones?)
- else if (resizeLocation.start.isSame(event.start) && resizeLocation.end.isSame(eventEnd)) {
+ // no change? (FYI, event dates might have zones)
+ else if (
+ resizeLocation.start.isSame(event.start.clone().stripZone()) &&
+ resizeLocation.end.isSame(eventEnd.clone().stripZone())
+ ) {
resizeLocation = null;
}
}
@@ -4862,15 +4659,11 @@ Grid.mixin({
// Generic utility for generating the HTML classNames for an event segment's element
getSegClasses: function(seg, isDraggable, isResizable) {
var view = this.view;
- 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 : []
- );
+ ].concat(this.getSegCustomClasses(seg));
if (isDraggable) {
classes.push('fc-draggable');
@@ -4880,7 +4673,7 @@ Grid.mixin({
}
// event is currently selected? attach a className.
- if (view.isEventSelected(event)) {
+ if (view.isEventSelected(seg.event)) {
classes.push('fc-selected');
}
@@ -4888,38 +4681,78 @@ Grid.mixin({
},
- // Utility for generating event skin-related CSS properties
- getSegSkinCss: function(seg) {
+ // List of classes that were defined by the caller of the API in some way
+ getSegCustomClasses: 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 [].concat(
+ event.className, // guaranteed to be an array
+ event.source ? event.source.className : []
+ );
+ },
+
+
+ // Utility for generating event skin-related CSS properties
+ getSegSkinCss: function(seg) {
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')
+ 'background-color': this.getSegBackgroundColor(seg),
+ 'border-color': this.getSegBorderColor(seg),
+ color: this.getSegTextColor(seg)
};
},
+ // Queries for caller-specified color, then falls back to default
+ getSegBackgroundColor: function(seg) {
+ return seg.event.backgroundColor ||
+ seg.event.color ||
+ this.getSegDefaultBackgroundColor(seg);
+ },
+
+
+ getSegDefaultBackgroundColor: function(seg) {
+ var source = seg.event.source || {};
+
+ return source.backgroundColor ||
+ source.color ||
+ this.view.opt('eventBackgroundColor') ||
+ this.view.opt('eventColor');
+ },
+
+
+ // Queries for caller-specified color, then falls back to default
+ getSegBorderColor: function(seg) {
+ return seg.event.borderColor ||
+ seg.event.color ||
+ this.getSegDefaultBorderColor(seg);
+ },
+
+
+ getSegDefaultBorderColor: function(seg) {
+ var source = seg.event.source || {};
+
+ return source.borderColor ||
+ source.color ||
+ this.view.opt('eventBorderColor') ||
+ this.view.opt('eventColor');
+ },
+
+
+ // Queries for caller-specified color, then falls back to default
+ getSegTextColor: function(seg) {
+ return seg.event.textColor ||
+ this.getSegDefaultTextColor(seg);
+ },
+
+
+ getSegDefaultTextColor: function(seg) {
+ var source = seg.event.source || {};
+
+ return source.textColor ||
+ this.view.opt('eventTextColor');
+ },
+
+
/* Converting events -> eventRange -> eventSpan -> eventSegs
------------------------------------------------------------------------------------------------------------------*/
@@ -4987,20 +4820,25 @@ Grid.mixin({
// 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 {
- start: event.start.clone().stripZone(),
- end: (
+ var calendar = this.view.calendar;
+ var start = event.start.clone().stripZone();
+ var end = (
event.end ?
event.end.clone() :
// derive the end from the start and allDay. compute allDay if necessary
- this.view.calendar.getDefaultEventEnd(
+ calendar.getDefaultEventEnd(
event.allDay != null ?
event.allDay :
!event.start.hasTime(),
event.start
)
- ).stripZone()
- };
+ ).stripZone();
+
+ // hack: dynamic locale change forgets to upate stored event localed
+ calendar.localizeMoment(start);
+ calendar.localizeMoment(end);
+
+ return { start: start, end: end };
},
@@ -5103,6 +4941,16 @@ Grid.mixin({
----------------------------------------------------------------------------------------------------------------------*/
+function pluckEventDateProps(event) {
+ return {
+ start: event.start.clone(),
+ end: event.end ? event.end.clone() : null,
+ allDay: event.allDay // keep it the same
+ };
+}
+FC.pluckEventDateProps = pluckEventDateProps;
+
+
function isBgEvent(event) { // returns true if background OR inverse-background
var rendering = getEventRendering(event);
return rendering === 'background' || rendering === 'inverse-background';
@@ -5493,7 +5341,7 @@ var DayTableMixin = FC.DayTableMixin = {
return '' +
'<th class="fc-day-header ' + view.widgetHeaderClass + ' fc-' + dayIDs[date.day()] + '"' +
- (this.rowCnt == 1 ?
+ (this.rowCnt === 1 ?
' data-date="' + date.format('YYYY-MM-DD') + '"' :
'') +
(colspan > 1 ?
@@ -5502,8 +5350,12 @@ var DayTableMixin = FC.DayTableMixin = {
(otherAttrs ?
' ' + otherAttrs :
'') +
- '>' +
- htmlEscape(date.format(this.colHeadFormat)) +
+ '>' +
+ // don't make a link if the heading could represent multiple days, or if there's only one day (forceOff)
+ view.buildGotoAnchorHtml(
+ { date: date, forceOff: this.rowCnt > 1 || this.colCnt === 1 },
+ htmlEscape(date.format(this.colHeadFormat)) // inner HTML
+ ) +
'</th>';
},
@@ -5656,13 +5508,16 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
renderBusinessHours: function() {
- var events = this.view.calendar.getBusinessHoursEvents(true); // wholeDay=true
- var segs = this.eventsToSegs(events);
-
+ var segs = this.buildBusinessHourSegs(true); // wholeDay=true
this.renderFill('businessHours', segs, 'bgevent');
},
+ unrenderBusinessHours: function() {
+ this.unrenderFill('businessHours');
+ },
+
+
// Generates the HTML for a single row, which is a div that wraps a table.
// `row` is the row number.
renderDayRowHtml: function(row, isRigid) {
@@ -5729,19 +5584,53 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
// 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 html = '';
var classes;
+ var weekCalcFirstDoW;
- if (!this.view.dayNumbersVisible) { // if there are week numbers but not day numbers
+ if (!this.view.dayNumbersVisible && !this.view.cellWeekNumbersVisible) {
+ // no numbers in day cell (week number must be along the side)
return '<td/>'; // will create an empty space above events :(
}
classes = this.getDayClasses(date);
- classes.unshift('fc-day-number');
+ classes.unshift('fc-day-top');
- return '' +
- '<td class="' + classes.join(' ') + '" data-date="' + date.format() + '">' +
- date.date() +
- '</td>';
+ if (this.view.cellWeekNumbersVisible) {
+ // To determine the day of week number change under ISO, we cannot
+ // rely on moment.js methods such as firstDayOfWeek() or weekday(),
+ // because they rely on the locale's dow (possibly overridden by
+ // our firstDay option), which may not be Monday. We cannot change
+ // dow, because that would affect the calendar start day as well.
+ if (date._locale._fullCalendar_weekCalc === 'ISO') {
+ weekCalcFirstDoW = 1; // Monday by ISO 8601 definition
+ }
+ else {
+ weekCalcFirstDoW = date._locale.firstDayOfWeek();
+ }
+ }
+
+ html += '<td class="' + classes.join(' ') + '" data-date="' + date.format() + '">';
+
+ if (this.view.cellWeekNumbersVisible && (date.day() == weekCalcFirstDoW)) {
+ html += this.view.buildGotoAnchorHtml(
+ { date: date, type: 'week' },
+ { 'class': 'fc-week-number' },
+ date.format('w') // inner HTML
+ );
+ }
+
+ if (this.view.dayNumbersVisible) {
+ html += this.view.buildGotoAnchorHtml(
+ date,
+ { 'class': 'fc-day-number' },
+ date.date() // inner HTML
+ );
+ }
+
+ html += '</td>';
+
+ return html;
},
@@ -5809,11 +5698,13 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
queryHit: function(leftOffset, topOffset) {
- var col = this.colCoordCache.getHorizontalIndex(leftOffset);
- var row = this.rowCoordCache.getVerticalIndex(topOffset);
+ if (this.colCoordCache.isLeftInBounds(leftOffset) && this.rowCoordCache.isTopInBounds(topOffset)) {
+ var col = this.colCoordCache.getHorizontalIndex(leftOffset);
+ var row = this.rowCoordCache.getVerticalIndex(topOffset);
- if (row != null && col != null) {
- return this.getCellHit(row, col);
+ if (row != null && col != null) {
+ return this.getCellHit(row, col);
+ }
}
},
@@ -5864,8 +5755,7 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
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) {
-
+ if (seg && seg.component !== this) {
return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements
}
},
@@ -6573,7 +6463,7 @@ DayGrid.mixin({
options = {
className: 'fc-more-popover',
content: this.renderSegPopoverContent(row, col, segs),
- parentEl: this.el,
+ parentEl: this.view.el, // attach to root of view. guarantees outside of scrollbars.
top: topEl.offset().top,
autoHide: true, // when the user clicks elsewhere, hide the popover
viewportConstrain: view.opt('popoverViewportConstrain'),
@@ -6596,6 +6486,10 @@ DayGrid.mixin({
this.segPopover = new Popover(options);
this.segPopover.show();
+
+ // the popover doesn't live within the grid's container element, and thus won't get the event
+ // delegated-handlers for free. attach event-related handlers to the popover.
+ this.bindSegHandlersToEl(this.segPopover.el);
},
@@ -6844,7 +6738,6 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
this.labelFormat =
input ||
- view.opt('axisFormat') || // deprecated
view.opt('smallTimeFormat'); // the computed default
input = view.opt('slotLabelInterval');
@@ -6905,27 +6798,30 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
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 {
- col: colIndex,
- snap: snapIndex,
- component: this, // needed unfortunately :(
- left: colCoordCache.getLeftOffset(colIndex),
- right: colCoordCache.getRightOffset(colIndex),
- top: snapTop,
- bottom: snapBottom
- };
+
+ if (colCoordCache.isLeftInBounds(leftOffset) && slatCoordCache.isTopInBounds(topOffset)) {
+ 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 {
+ col: colIndex,
+ snap: snapIndex,
+ component: this, // needed unfortunately :(
+ left: colCoordCache.getLeftOffset(colIndex),
+ right: colCoordCache.getRightOffset(colIndex),
+ top: snapTop,
+ bottom: snapBottom
+ };
+ }
}
},
@@ -7128,10 +7024,9 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
renderBusinessHours: function() {
- var events = this.view.calendar.getBusinessHoursEvents();
- var segs = this.eventsToSegs(events);
-
- this.renderBusinessSegs(segs);
+ this.renderBusinessSegs(
+ this.buildBusinessHourSegs()
+ );
},
@@ -8074,6 +7969,62 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
},
+ getAllDayHtml: function() {
+ return this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'));
+ },
+
+
+ /* Navigation
+ ------------------------------------------------------------------------------------------------------------------*/
+
+
+ // Generates HTML for an anchor to another view into the calendar.
+ // Will either generate an <a> tag or a non-clickable <span> tag, depending on enabled settings.
+ // `gotoOptions` can either be a moment input, or an object with the form:
+ // { date, type, forceOff }
+ // `type` is a view-type like "day" or "week". default value is "day".
+ // `attrs` and `innerHtml` are use to generate the rest of the HTML tag.
+ buildGotoAnchorHtml: function(gotoOptions, attrs, innerHtml) {
+ var date, type, forceOff;
+ var finalOptions;
+
+ if ($.isPlainObject(gotoOptions)) {
+ date = gotoOptions.date;
+ type = gotoOptions.type;
+ forceOff = gotoOptions.forceOff;
+ }
+ else {
+ date = gotoOptions; // a single moment input
+ }
+ date = FC.moment(date); // if a string, parse it
+
+ finalOptions = { // for serialization into the link
+ date: date.format('YYYY-MM-DD'),
+ type: type || 'day'
+ };
+
+ if (typeof attrs === 'string') {
+ innerHtml = attrs;
+ attrs = null;
+ }
+
+ attrs = attrs ? ' ' + attrsToStr(attrs) : ''; // will have a leading space
+ innerHtml = innerHtml || '';
+
+ if (!forceOff && this.opt('navLinks')) {
+ return '<a' + attrs +
+ ' data-goto="' + htmlEscape(JSON.stringify(finalOptions)) + '">' +
+ innerHtml +
+ '</a>';
+ }
+ else {
+ return '<span' + attrs + '>' +
+ innerHtml +
+ '</span>';
+ }
+ },
+
+
/* Rendering
------------------------------------------------------------------------------------------------------------------*/
@@ -8110,12 +8061,12 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
// 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) {
+ display: function(date, explicitScrollState) {
var _this = this;
- var scrollState = null;
+ var prevScrollState = null;
- if (this.displaying) {
- scrollState = this.queryScroll();
+ if (explicitScrollState != null && this.displaying) { // don't need prevScrollState if explicitScrollState
+ prevScrollState = this.queryScroll();
}
this.calendar.freezeContentHeight();
@@ -8124,7 +8075,17 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
return (
_this.displaying =
syncThen(_this.displayView(date), function() { // displayView might return a promise
- _this.forceScroll(_this.computeInitialScroll(scrollState));
+
+ // caller of display() wants a specific scroll state?
+ if (explicitScrollState != null) {
+ // we make an assumption that this is NOT the initial render,
+ // and thus don't need forceScroll (is inconveniently asynchronous)
+ _this.setScroll(explicitScrollState);
+ }
+ else {
+ _this.forceScroll(_this.computeInitialScroll(prevScrollState));
+ }
+
_this.calendar.unfreezeContentHeight();
_this.triggerRender();
})
@@ -8559,14 +8520,24 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
// Computes if the given event is allowed to be dragged by the user
isEventDraggable: function(event) {
- var source = event.source || {};
+ return this.isEventStartEditable(event);
+ },
+
+ isEventStartEditable: function(event) {
return firstDefined(
event.startEditable,
- source.startEditable,
+ (event.source || {}).startEditable,
this.opt('eventStartEditable'),
+ this.isEventGenerallyEditable(event)
+ );
+ },
+
+
+ isEventGenerallyEditable: function(event) {
+ return firstDefined(
event.editable,
- source.editable,
+ (event.source || {}).editable,
this.opt('editable')
);
},
@@ -9066,8 +9037,9 @@ var Scroller = FC.Scroller = Class.extend({
var Calendar = FC.Calendar = Class.extend({
dirDefaults: null, // option defaults related to LTR or RTL
- langDefaults: null, // option defaults related to current locale
+ localeDefaults: null, // option defaults related to current locale
overrides: null, // option overrides given to the fullCalendar constructor
+ dynamicOverrides: null, // options set with dynamic setter method. higher precedence than view overrides.
options: null, // all defaults combined with overrides
viewSpecCache: null, // cache of view definitions
view: null, // current View object
@@ -9085,41 +9057,40 @@ var Calendar = FC.Calendar = Class.extend({
},
- // Initializes `this.options` and other important options-related objects
- initOptions: function(overrides) {
- var lang, langDefaults;
+ // Computes the flattened options hash for the calendar and assigns to `this.options`.
+ // Assumes this.overrides and this.dynamicOverrides have already been initialized.
+ populateOptionsHash: function() {
+ var locale, localeDefaults;
var isRTL, dirDefaults;
- // 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] || {};
+ locale = firstDefined( // explicit locale option given?
+ this.dynamicOverrides.locale,
+ this.overrides.locale
+ );
+ localeDefaults = localeOptionHash[locale];
+ if (!localeDefaults) { // explicit locale option not given or invalid?
+ locale = Calendar.defaults.locale;
+ localeDefaults = localeOptionHash[locale] || {};
}
- isRTL = firstDefined(
- overrides.isRTL,
- langDefaults.isRTL,
+ isRTL = firstDefined( // based on options computed so far, is direction RTL?
+ this.dynamicOverrides.isRTL,
+ this.overrides.isRTL,
+ localeDefaults.isRTL,
Calendar.defaults.isRTL
);
dirDefaults = isRTL ? Calendar.rtlDefaults : {};
this.dirDefaults = dirDefaults;
- this.langDefaults = langDefaults;
- this.overrides = overrides;
+ this.localeDefaults = localeDefaults;
this.options = mergeOptions([ // merge defaults and overrides. lowest to highest precedence
Calendar.defaults, // global defaults
dirDefaults,
- langDefaults,
- overrides
+ localeDefaults,
+ this.overrides,
+ this.dynamicOverrides
]);
- populateInstanceComputableOptions(this.options);
-
- this.viewSpecCache = {}; // somewhat unrelated
+ populateInstanceComputableOptions(this.options); // fill in gaps with computed options
},
@@ -9231,9 +9202,10 @@ var Calendar = FC.Calendar = Class.extend({
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.localeDefaults, // 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)
+ spec.overrides, // view's overrides (view-specific options)
+ this.dynamicOverrides // dynamically set via setter. highest precedence
]);
populateInstanceComputableOptions(spec.options);
},
@@ -9247,17 +9219,21 @@ var Calendar = FC.Calendar = Class.extend({
function queryButtonText(options) {
var buttonText = options.buttonText || {};
return buttonText[requestedViewType] ||
+ // view can decide to look up a certain key
+ (spec.buttonTextKey ? buttonText[spec.buttonTextKey] : null) ||
+ // a key like "month"
(spec.singleUnit ? buttonText[spec.singleUnit] : null);
}
// highest to lowest priority
spec.buttonTextOverride =
+ queryButtonText(this.dynamicOverrides) ||
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.localeDefaults) ||
queryButtonText(this.dirDefaults) ||
spec.defaults.buttonText || // a single string. from ViewSubclass.defaults
queryButtonText(Calendar.defaults) ||
@@ -9324,10 +9300,6 @@ function Calendar_constructor(element, overrides) {
var t = this;
- t.initOptions(overrides || {});
- var options = this.options;
-
-
// Exports
// -----------------------------------------------------------------------------------
@@ -9352,67 +9324,95 @@ function Calendar_constructor(element, overrides) {
t.getDate = getDate;
t.getCalendar = getCalendar;
t.getView = getView;
- t.option = option;
+ t.option = option; // getter/setter method
t.trigger = trigger;
+ // Options
+ // -----------------------------------------------------------------------------------
+
+ t.dynamicOverrides = {};
+ t.viewSpecCache = {};
+ t.optionHandlers = {}; // for Calendar.options.js
+ t.overrides = $.extend({}, overrides); // make a copy
- // Language-data Internals
+ t.populateOptionsHash(); // sets this.options
+
+
+
+ // Locale-data Internals
// -----------------------------------------------------------------------------------
- // Apply overrides to the current language's data
+ // Apply overrides to the current locale's data
+ var localeData;
- var localeData = createObject( // make a cheap copy
- getMomentLocaleData(options.lang) // will fall back to en
- );
+ // Called immediately, and when any of the options change.
+ // Happens before any internal objects rebuild or rerender, because this is very core.
+ t.bindOptions([
+ 'locale', 'monthNames', 'monthNamesShort', 'dayNames', 'dayNamesShort', 'firstDay', 'weekNumberCalculation'
+ ], function(locale, monthNames, monthNamesShort, dayNames, dayNamesShort, firstDay, weekNumberCalculation) {
- 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;
- }
+ // normalize
+ if (weekNumberCalculation === 'iso') {
+ weekNumberCalculation = 'ISO'; // normalize
+ }
+
+ localeData = createObject( // make a cheap copy
+ getMomentLocaleData(locale) // will fall back to en
+ );
+
+ if (monthNames) {
+ localeData._months = monthNames;
+ }
+ if (monthNamesShort) {
+ localeData._monthsShort = monthNamesShort;
+ }
+ if (dayNames) {
+ localeData._weekdays = dayNames;
+ }
+ if (dayNamesShort) {
+ localeData._weekdaysShort = dayNamesShort;
+ }
- // assign a normalized value, to be used by our .week() moment extension
- localeData._fullCalendar_weekCalc = (function(weekCalc) {
- if (typeof weekCalc === 'function') {
- return weekCalc;
+ if (firstDay == null && weekNumberCalculation === 'ISO') {
+ firstDay = 1;
}
- else if (weekCalc === 'local') {
- return weekCalc;
+ if (firstDay != null) {
+ var _week = createObject(localeData._week); // _week: { dow: # }
+ _week.dow = firstDay;
+ localeData._week = _week;
}
- else if (weekCalc === 'iso' || weekCalc === 'ISO') {
- return 'ISO';
+
+ if ( // whitelist certain kinds of input
+ weekNumberCalculation === 'ISO' ||
+ weekNumberCalculation === 'local' ||
+ typeof weekNumberCalculation === 'function'
+ ) {
+ localeData._fullCalendar_weekCalc = weekNumberCalculation; // moment-ext will know what to do with it
}
- })(options.weekNumberCalculation);
+ // If the internal current date object already exists, move to new locale.
+ // We do NOT need to do this technique for event dates, because this happens when converting to "segments".
+ if (date) {
+ localizeMoment(date); // sets to localeData
+ }
+ });
// Calendar-specific Date Utilities
// -----------------------------------------------------------------------------------
- t.defaultAllDayEventDuration = moment.duration(options.defaultAllDayEventDuration);
- t.defaultTimedEventDuration = moment.duration(options.defaultTimedEventDuration);
+ t.defaultAllDayEventDuration = moment.duration(t.options.defaultAllDayEventDuration);
+ t.defaultTimedEventDuration = moment.duration(t.options.defaultTimedEventDuration);
- // Builds a moment using the settings of the current calendar: timezone and language.
+ // Builds a moment using the settings of the current calendar: timezone and locale.
// Accepts anything the vanilla moment() constructor accepts.
t.moment = function() {
var mom;
- if (options.timezone === 'local') {
+ if (t.options.timezone === 'local') {
mom = FC.moment.apply(null, arguments);
// Force the moment to be local, because FC.moment doesn't guarantee it.
@@ -9420,28 +9420,30 @@ function Calendar_constructor(element, overrides) {
mom.local();
}
}
- else if (options.timezone === 'UTC') {
+ else if (t.options.timezone === 'UTC') {
mom = FC.moment.utc.apply(null, arguments); // process as UTC
}
else {
mom = FC.moment.parseZone.apply(null, arguments); // let the input decide the zone
}
- if ('_locale' in mom) { // moment 2.8 and above
- mom._locale = localeData;
- }
- else { // pre-moment-2.8
- mom._lang = localeData;
- }
+ localizeMoment(mom);
return mom;
};
+ // Updates the given moment's locale settings to the current calendar locale settings.
+ function localizeMoment(mom) {
+ mom._locale = localeData;
+ }
+ t.localizeMoment = localizeMoment;
+
+
// 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';
+ return t.options.timezone !== 'local' && t.options.timezone !== 'UTC';
};
@@ -9470,7 +9472,7 @@ function Calendar_constructor(element, overrides) {
// 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;
+ var now = t.options.now;
if (typeof now === 'function') {
now = now();
}
@@ -9512,8 +9514,7 @@ function Calendar_constructor(element, overrides) {
// 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(duration, options.lang) // works moment-pre-2.8
- .humanize();
+ return duration.locale(t.options.locale).humanize();
};
@@ -9522,7 +9523,7 @@ function Calendar_constructor(element, overrides) {
// -----------------------------------------------------------------------------------
- EventManager.call(t, options);
+ EventManager.call(t);
var isFetchNeeded = t.isFetchNeeded;
var fetchEvents = t.fetchEvents;
var fetchEventSources = t.fetchEventSources;
@@ -9535,7 +9536,6 @@ function Calendar_constructor(element, overrides) {
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
@@ -9553,8 +9553,8 @@ function Calendar_constructor(element, overrides) {
// compute the initial ambig-timezone date
- if (options.defaultDate != null) {
- date = t.moment(options.defaultDate).stripZone();
+ if (t.options.defaultDate != null) {
+ date = t.moment(t.options.defaultDate).stripZone();
}
else {
date = t.getNow(); // getNow already returns unzoned
@@ -9574,38 +9574,64 @@ function Calendar_constructor(element, overrides) {
function initialRender() {
- tm = options.theme ? 'ui' : 'fc';
element.addClass('fc');
- if (options.isRTL) {
- element.addClass('fc-rtl');
- }
- else {
- element.addClass('fc-ltr');
- }
+ // event delegation for nav links
+ element.on('click.fc', 'a[data-goto]', function(ev) {
+ var anchorEl = $(this);
+ var gotoOptions = anchorEl.data('goto'); // will automatically parse JSON
+ var date = t.moment(gotoOptions.date);
+ var viewType = gotoOptions.type;
- if (options.theme) {
- element.addClass('ui-widget');
- }
- else {
- element.addClass('fc-unthemed');
- }
+ // property like "navLinkDayClick". might be a string or a function
+ var customAction = currentView.opt('navLink' + capitaliseFirstLetter(viewType) + 'Click');
+
+ if (typeof customAction === 'function') {
+ customAction(date, ev);
+ }
+ else {
+ if (typeof customAction === 'string') {
+ viewType = customAction;
+ }
+ zoomTo(date, viewType);
+ }
+ });
+
+ // called immediately, and upon option change
+ t.bindOption('theme', function(theme) {
+ tm = theme ? 'ui' : 'fc'; // affects a larger scope
+ element.toggleClass('ui-widget', theme);
+ element.toggleClass('fc-unthemed', !theme);
+ });
+
+ // called immediately, and upon option change.
+ // HACK: locale often affects isRTL, so we explicitly listen to that too.
+ t.bindOptions([ 'isRTL', 'locale' ], function(isRTL) {
+ element.toggleClass('fc-ltr', !isRTL);
+ element.toggleClass('fc-rtl', isRTL);
+ });
content = $("<div class='fc-view-container'/>").prependTo(element);
- header = t.header = new Header(t, options);
- headerElement = header.render();
- if (headerElement) {
- element.prepend(headerElement);
- }
+ header = t.header = new Header(t);
+ renderHeader();
- renderView(options.defaultView);
+ renderView(t.options.defaultView);
- if (options.handleWindowResize) {
- windowResizeProxy = debounce(windowResize, options.windowResizeDelay); // prevents rapid calls
+ if (t.options.handleWindowResize) {
+ windowResizeProxy = debounce(windowResize, t.options.windowResizeDelay); // prevents rapid calls
$(window).resize(windowResizeProxy);
}
}
+
+
+ // can be called repeatedly and Header will rerender
+ function renderHeader() {
+ header.render();
+ if (header.el) {
+ element.prepend(header.el);
+ }
+ }
function destroy() {
@@ -9621,6 +9647,8 @@ function Calendar_constructor(element, overrides) {
content.remove();
element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');
+ element.off('.fc'); // unbind nav link handlers
+
if (windowResizeProxy) {
$(window).unbind('resize', windowResizeProxy);
}
@@ -9639,15 +9667,14 @@ function Calendar_constructor(element, overrides) {
// 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) {
+ // Accepts an optional scroll state to restore to.
+ function renderView(viewType, explicitScrollState) {
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;
+ clearView();
}
// if viewType changed, or the view was never created, create a fresh view
@@ -9670,11 +9697,14 @@ function Calendar_constructor(element, overrides) {
// render or rerender the view
if (
!currentView.displaying ||
- !date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change
+ !( // NOT within interval range signals an implicit date window change
+ date >= currentView.intervalStart &&
+ date < currentView.intervalEnd
+ )
) {
if (elementVisible()) {
- currentView.display(date); // will call freezeContentHeight
+ currentView.display(date, explicitScrollState); // will call freezeContentHeight
unfreezeContentHeight(); // immediately unfreeze regardless of whether display is async
// need to do this after View::render, so dates are calculated
@@ -9690,6 +9720,32 @@ function Calendar_constructor(element, overrides) {
ignoreWindowResize--;
}
+
+ // Unrenders the current view and reflects this change in the Header.
+ // Unregsiters the `currentView`, but does not remove from viewByType hash.
+ function clearView() {
+ header.deactivateButton(currentView.type);
+ currentView.removeElement();
+ currentView = t.view = null;
+ }
+
+
+ // Destroys the view, including the view object. Then, re-instantiates it and renders it.
+ // Maintains the same scroll state.
+ // TODO: maintain any other user-manipulated state.
+ function reinitView() {
+ ignoreWindowResize++;
+ freezeContentHeight();
+
+ var viewType = currentView.type;
+ var scrollState = currentView.queryScroll();
+ clearView();
+ renderView(viewType, scrollState);
+
+ unfreezeContentHeight();
+ ignoreWindowResize--;
+ }
+
// Resizing
@@ -9705,7 +9761,7 @@ function Calendar_constructor(element, overrides) {
t.isHeightAuto = function() {
- return options.contentHeight === 'auto' || options.height === 'auto';
+ return t.options.contentHeight === 'auto' || t.options.height === 'auto';
};
@@ -9733,16 +9789,33 @@ function Calendar_constructor(element, overrides) {
function _calcSize() { // assumes elementVisible
- if (typeof options.contentHeight === 'number') { // exists and not 'auto'
- suggestedViewHeight = options.contentHeight;
+ var contentHeightInput = t.options.contentHeight;
+ var heightInput = t.options.height;
+
+ if (typeof contentHeightInput === 'number') { // exists and not 'auto'
+ suggestedViewHeight = contentHeightInput;
+ }
+ else if (typeof contentHeightInput === 'function') { // exists and is a function
+ suggestedViewHeight = contentHeightInput();
}
- else if (typeof options.height === 'number') { // exists and not 'auto'
- suggestedViewHeight = options.height - (headerElement ? headerElement.outerHeight(true) : 0);
+ else if (typeof heightInput === 'number') { // exists and not 'auto'
+ suggestedViewHeight = heightInput - queryHeaderHeight();
+ }
+ else if (typeof heightInput === 'function') { // exists and is a function
+ suggestedViewHeight = heightInput() - queryHeaderHeight();
+ }
+ else if (heightInput === 'parent') { // set to height of parent element
+ suggestedViewHeight = element.parent().height() - queryHeaderHeight();
}
else {
- suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5));
+ suggestedViewHeight = Math.round(content.width() / Math.max(t.options.aspectRatio, .5));
}
}
+
+
+ function queryHeaderHeight() {
+ return header.el ? header.el.outerHeight(true) : 0; // includes margin
+ }
function windowResize(ev) {
@@ -9785,7 +9858,7 @@ function Calendar_constructor(element, overrides) {
function getAndRenderEvents() {
- if (!options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) {
+ if (!t.options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) {
fetchAndRenderEvents();
}
else {
@@ -9826,7 +9899,8 @@ function Calendar_constructor(element, overrides) {
function updateTodayButton() {
var now = t.getNow();
- if (now.isWithin(currentView.intervalStart, currentView.intervalEnd)) {
+
+ if (now >= currentView.intervalStart && now < currentView.intervalEnd) {
header.disableButton('today');
}
else {
@@ -9964,14 +10038,70 @@ function Calendar_constructor(element, overrides) {
function option(name, value) {
- if (value === undefined) {
- return options[name];
+ var newOptionHash;
+
+ if (typeof name === 'string') {
+ if (value === undefined) { // getter
+ return t.options[name];
+ }
+ else { // setter for individual option
+ newOptionHash = {};
+ newOptionHash[name] = value;
+ setOptions(newOptionHash);
+ }
}
- if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') {
- options[name] = value;
- updateSize(true); // true = allow recalculation of height
+ else if (typeof name === 'object') { // compound setter with object input
+ setOptions(name);
}
}
+
+
+ function setOptions(newOptionHash) {
+ var optionCnt = 0;
+ var optionName;
+
+ for (optionName in newOptionHash) {
+ t.dynamicOverrides[optionName] = newOptionHash[optionName];
+ }
+
+ t.viewSpecCache = {}; // the dynamic override invalidates the options in this cache, so just clear it
+ t.populateOptionsHash(); // this.options needs to be recomputed after the dynamic override
+
+ // trigger handlers after this.options has been updated
+ for (optionName in newOptionHash) {
+ t.triggerOptionHandlers(optionName); // recall bindOption/bindOptions
+ optionCnt++;
+ }
+
+ // special-case handling of single option change.
+ // if only one option change, `optionName` will be its name.
+ if (optionCnt === 1) {
+ if (optionName === 'height' || optionName === 'contentHeight' || optionName === 'aspectRatio') {
+ updateSize(true); // true = allow recalculation of height
+ return;
+ }
+ else if (optionName === 'defaultDate') {
+ return; // can't change date this way. use gotoDate instead
+ }
+ else if (optionName === 'businessHours') {
+ if (currentView) {
+ currentView.unrenderBusinessHours();
+ currentView.renderBusinessHours();
+ }
+ return;
+ }
+ else if (optionName === 'timezone') {
+ t.rezoneArrayEventSources();
+ refetchEvents();
+ return;
+ }
+ }
+
+ // catch-all. rerender the header and rebuild/rerender the current view
+ renderHeader();
+ viewsByType = {}; // even non-current views will be affected by this option change. do before rerender
+ reinitView();
+ }
function trigger(name, thisObj) { // overrides the Emitter's trigger method :(
@@ -9980,8 +10110,8 @@ function Calendar_constructor(element, overrides) {
thisObj = thisObj || _element;
this.triggerWith(name, thisObj, args); // Emitter's method
- if (options[name]) {
- return options[name].apply(thisObj, args);
+ if (t.options[name]) {
+ return t.options[name].apply(thisObj, args);
}
}
@@ -9989,11 +10119,75 @@ function Calendar_constructor(element, overrides) {
}
;;
+/*
+Options binding/triggering system.
+*/
+Calendar.mixin({
+
+ // A map of option names to arrays of handler objects. Initialized to {} in Calendar.
+ // Format for a handler object:
+ // {
+ // func // callback function to be called upon change
+ // names // option names whose values should be given to func
+ // }
+ optionHandlers: null,
+
+ // Calls handlerFunc immediately, and when the given option has changed.
+ // handlerFunc will be given the option value.
+ bindOption: function(optionName, handlerFunc) {
+ this.bindOptions([ optionName ], handlerFunc);
+ },
+
+ // Calls handlerFunc immediately, and when any of the given options change.
+ // handlerFunc will be given each option value as ordered function arguments.
+ bindOptions: function(optionNames, handlerFunc) {
+ var handlerObj = { func: handlerFunc, names: optionNames };
+ var i;
+
+ for (i = 0; i < optionNames.length; i++) {
+ this.registerOptionHandlerObj(optionNames[i], handlerObj);
+ }
+
+ this.triggerOptionHandlerObj(handlerObj);
+ },
+
+ // Puts the given handler object into the internal hash
+ registerOptionHandlerObj: function(optionName, handlerObj) {
+ (this.optionHandlers[optionName] || (this.optionHandlers[optionName] = []))
+ .push(handlerObj);
+ },
+
+ // Reports that the given option has changed, and calls all appropriate handlers.
+ triggerOptionHandlers: function(optionName) {
+ var handlerObjs = this.optionHandlers[optionName] || [];
+ var i;
+
+ for (i = 0; i < handlerObjs.length; i++) {
+ this.triggerOptionHandlerObj(handlerObjs[i]);
+ }
+ },
+
+ // Calls the callback for a specific handler object, passing in the appropriate arguments.
+ triggerOptionHandlerObj: function(handlerObj) {
+ var optionNames = handlerObj.names;
+ var optionValues = [];
+ var i;
+
+ for (i = 0; i < optionNames.length; i++) {
+ optionValues.push(this.options[optionNames[i]]);
+ }
+
+ handlerObj.func.apply(this, optionValues); // maintain the Calendar's `this` context
+ }
+
+});
+
+;;
Calendar.defaults = {
titleRangeSeparator: ' \u2013 ', // en dash
- monthYearFormat: 'MMMM YYYY', // required for en. other languages rely on datepicker computable option
+ monthYearFormat: 'MMMM YYYY', // required for en. other locales rely on datepicker computable option
defaultTimedEventDuration: '02:00:00',
defaultAllDayEventDuration: { days: 1 },
@@ -10050,6 +10244,8 @@ Calendar.defaults = {
prevYear: 'left-double-arrow',
nextYear: 'right-double-arrow'
},
+
+ allDayText: 'all-day',
// jquery-ui theming
theme: false,
@@ -10078,14 +10274,14 @@ Calendar.defaults = {
dayPopoverFormat: 'LL',
handleWindowResize: true,
- windowResizeDelay: 200, // milliseconds before an updateSize happens
+ windowResizeDelay: 100, // milliseconds before an updateSize happens
longPressDelay: 1000
};
-Calendar.englishDefaults = { // used by lang.js
+Calendar.englishDefaults = { // used by locale.js
dayPopoverFormat: 'dddd, MMMM D'
};
@@ -10112,19 +10308,18 @@ Calendar.rtlDefaults = { // right-to-left defaults
;;
-var langOptionHash = FC.langs = {}; // initialize and expose
+var localeOptionHash = FC.locales = {}; // 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
+// TODO: document the structure and ordering of a FullCalendar locale file
// Initialize jQuery UI datepicker translations while using some of the translations
-// Will set this as the default language for datepicker.
-FC.datepickerLang = function(langCode, dpLangCode, dpOptions) {
+// Will set this as the default locales for datepicker.
+FC.datepickerLocale = function(localeCode, dpLocaleCode, dpOptions) {
- // get the FullCalendar internal option hash for this language. create if necessary
- var fcOptions = langOptionHash[langCode] || (langOptionHash[langCode] = {});
+ // get the FullCalendar internal option hash for this locale. create if necessary
+ var fcOptions = localeOptionHash[localeCode] || (localeOptionHash[localeCode] = {});
// transfer some simple options from datepicker to fc
fcOptions.isRTL = dpOptions.isRTL;
@@ -10138,15 +10333,15 @@ FC.datepickerLang = function(langCode, dpLangCode, 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
+ // Register the locale data.
+ // FullCalendar and MomentJS use locale codes like "pt-br" but Datepicker
+ // does it like "pt-BR" or if it doesn't have the locale, maybe just "pt".
+ // Make an alias so the locale can be referenced either way.
+ $.datepicker.regional[dpLocaleCode] =
+ $.datepicker.regional[localeCode] = // alias
dpOptions;
- // Alias 'en' to the default language data. Do this every time.
+ // Alias 'en' to the default locale data. Do this every time.
$.datepicker.regional.en = $.datepicker.regional[''];
// Set as Datepicker's global defaults.
@@ -10155,35 +10350,35 @@ FC.datepickerLang = function(langCode, dpLangCode, dpOptions) {
};
-// Sets FullCalendar-specific translations. Will set the language as the global default.
-FC.lang = function(langCode, newFcOptions) {
+// Sets FullCalendar-specific translations. Will set the locales as the global default.
+FC.locale = function(localeCode, newFcOptions) {
var fcOptions;
var momOptions;
- // get the FullCalendar internal option hash for this language. create if necessary
- fcOptions = langOptionHash[langCode] || (langOptionHash[langCode] = {});
+ // get the FullCalendar internal option hash for this locale. create if necessary
+ fcOptions = localeOptionHash[localeCode] || (localeOptionHash[localeCode] = {});
- // provided new options for this language? merge them in
+ // provided new options for this locales? merge them in
if (newFcOptions) {
- fcOptions = langOptionHash[langCode] = mergeOptions([ fcOptions, newFcOptions ]);
+ fcOptions = localeOptionHash[localeCode] = mergeOptions([ fcOptions, newFcOptions ]);
}
- // compute language options that weren't defined.
+ // compute locale 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
+ momOptions = getMomentLocaleData(localeCode); // will fall back to en
$.each(momComputableOptions, function(name, func) {
if (fcOptions[name] == null) {
fcOptions[name] = func(momOptions, fcOptions);
}
});
- // set it as the default language for FullCalendar
- Calendar.defaults.lang = langCode;
+ // set it as the default locale for FullCalendar
+ Calendar.defaults.locale = localeCode;
};
-// NOTE: can't guarantee any of these computations will run because not every language has datepicker
+// NOTE: can't guarantee any of these computations will run because not every locale has datepicker
// configs, so make sure there are English fallbacks for these in the defaults file.
var dpComputableOptions = {
@@ -10233,7 +10428,7 @@ var momComputableOptions = {
smallTimeFormat: function(momOptions) {
return momOptions.longDateFormat('LT')
.replace(':mm', '(:mm)')
- .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs
+ .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales
.replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
},
@@ -10241,7 +10436,7 @@ var momComputableOptions = {
extraSmallTimeFormat: function(momOptions) {
return momOptions.longDateFormat('LT')
.replace(':mm', '(:mm)')
- .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs
+ .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales
.replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand
},
@@ -10249,7 +10444,7 @@ var momComputableOptions = {
hourFormat: function(momOptions) {
return momOptions.longDateFormat('LT')
.replace(':mm', '')
- .replace(/(\Wmm)$/, '') // like above, but for foreign langs
+ .replace(/(\Wmm)$/, '') // like above, but for foreign locales
.replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
},
@@ -10263,7 +10458,7 @@ var momComputableOptions = {
// options that should be computed off live calendar options (considers override options)
-// TODO: best place for this? related to lang?
+// TODO: best place for this? related to locale?
// TODO: flipping text based on isRTL is a bad idea because the CSS `direction` might want to handle it
var instanceComputableOptions = {
@@ -10300,17 +10495,14 @@ function populateInstanceComputableOptions(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(moment, langCode) ||
- func.call(moment, 'en'); // the newer localData could return null, so fall back to en
+function getMomentLocaleData(localeCode) {
+ return moment.localeData(localeCode) || moment.localeData('en');
}
// Initialize English by forcing computation of moment-derived options.
// Also, sets it as the default.
-FC.lang('en', Calendar.englishDefaults);
+FC.locale('en', Calendar.englishDefaults);
;;
@@ -10318,7 +10510,7 @@ FC.lang('en', Calendar.englishDefaults);
----------------------------------------------------------------------------------------------------------------------*/
// TODO: rename all header-related things to "toolbar"
-function Header(calendar, options) {
+function Header(calendar) {
var t = this;
// exports
@@ -10330,38 +10522,50 @@ function Header(calendar, options) {
t.disableButton = disableButton;
t.enableButton = enableButton;
t.getViewsWithButtons = getViewsWithButtons;
+ t.el = null; // mirrors local `el`
// locals
- var el = $();
+ var el;
var viewsWithButtons = [];
var tm;
+ // can be called repeatedly and will rerender
function render() {
+ var options = calendar.options;
var sections = options.header;
tm = options.theme ? 'ui' : 'fc';
if (sections) {
- el = $("<div class='fc-toolbar'/>")
- .append(renderSection('left'))
+ if (!el) {
+ el = this.el = $("<div class='fc-toolbar'/>");
+ }
+ else {
+ el.empty();
+ }
+ el.append(renderSection('left'))
.append(renderSection('right'))
.append(renderSection('center'))
.append('<div class="fc-clear"/>');
-
- return el;
+ }
+ else {
+ removeElement();
}
}
function removeElement() {
- el.remove();
- el = $();
+ if (el) {
+ el.remove();
+ el = t.el = null;
+ }
}
function renderSection(position) {
var sectionEl = $('<div class="fc-' + position + '"/>');
+ var options = calendar.options;
var buttonStr = options.header[position];
if (buttonStr) {
@@ -10387,7 +10591,7 @@ function Header(calendar, options) {
isOnlyButtons = false;
}
else {
- if ((customButtonProps = (calendar.options.customButtons || {})[buttonName])) {
+ if ((customButtonProps = (options.customButtons || {})[buttonName])) {
buttonClick = function(ev) {
if (customButtonProps.click) {
customButtonProps.click.call(button[0], ev);
@@ -10523,33 +10727,43 @@ function Header(calendar, options) {
function updateTitle(text) {
- el.find('h2').text(text);
+ if (el) {
+ el.find('h2').text(text);
+ }
}
function activateButton(buttonName) {
- el.find('.fc-' + buttonName + '-button')
- .addClass(tm + '-state-active');
+ if (el) {
+ el.find('.fc-' + buttonName + '-button')
+ .addClass(tm + '-state-active');
+ }
}
function deactivateButton(buttonName) {
- el.find('.fc-' + buttonName + '-button')
- .removeClass(tm + '-state-active');
+ if (el) {
+ el.find('.fc-' + buttonName + '-button')
+ .removeClass(tm + '-state-active');
+ }
}
function disableButton(buttonName) {
- el.find('.fc-' + buttonName + '-button')
- .prop('disabled', true)
- .addClass(tm + '-state-disabled');
+ if (el) {
+ el.find('.fc-' + buttonName + '-button')
+ .prop('disabled', true)
+ .addClass(tm + '-state-disabled');
+ }
}
function enableButton(buttonName) {
- el.find('.fc-' + buttonName + '-button')
- .prop('disabled', false)
- .removeClass(tm + '-state-disabled');
+ if (el) {
+ el.find('.fc-' + buttonName + '-button')
+ .prop('disabled', false)
+ .removeClass(tm + '-state-disabled');
+ }
}
@@ -10572,7 +10786,7 @@ var ajaxDefaults = {
var eventGUID = 1;
-function EventManager(options) { // assumed to be a calendar
+function EventManager() { // assumed to be a calendar
var t = this;
@@ -10609,7 +10823,7 @@ function EventManager(options) { // assumed to be a calendar
$.each(
- (options.events ? [ options.events ] : []).concat(options.eventSources || []),
+ (t.options.events ? [ t.options.events ] : []).concat(t.options.eventSources || []),
function(i, sourceInput) {
var source = buildEventSource(sourceInput);
if (source) {
@@ -10743,7 +10957,7 @@ function EventManager(options) { // assumed to be a calendar
source,
rangeStart.clone(),
rangeEnd.clone(),
- options.timezone,
+ t.options.timezone,
callback
);
@@ -10766,7 +10980,7 @@ function EventManager(options) { // assumed to be a calendar
t, // this, the Calendar object
rangeStart.clone(),
rangeEnd.clone(),
- options.timezone,
+ t.options.timezone,
function(events) {
callback(events);
t.popLoading();
@@ -10801,9 +11015,9 @@ function EventManager(options) { // assumed to be a calendar
// and not affect the passed-in object.
var data = $.extend({}, customData || {});
- var startParam = firstDefined(source.startParam, options.startParam);
- var endParam = firstDefined(source.endParam, options.endParam);
- var timezoneParam = firstDefined(source.timezoneParam, options.timezoneParam);
+ var startParam = firstDefined(source.startParam, t.options.startParam);
+ var endParam = firstDefined(source.endParam, t.options.endParam);
+ var timezoneParam = firstDefined(source.timezoneParam, t.options.timezoneParam);
if (startParam) {
data[startParam] = rangeStart.format();
@@ -10811,8 +11025,8 @@ function EventManager(options) { // assumed to be a calendar
if (endParam) {
data[endParam] = rangeEnd.format();
}
- if (options.timezone && options.timezone != 'local') {
- data[timezoneParam] = options.timezone;
+ if (t.options.timezone && t.options.timezone != 'local') {
+ data[timezoneParam] = t.options.timezone;
}
t.pushLoading();
@@ -11144,7 +11358,7 @@ function EventManager(options) { // assumed to be a calendar
reportEvents(cache);
}
-
+
function clientEvents(filter) {
if ($.isFunction(filter)) {
@@ -11158,7 +11372,33 @@ function EventManager(options) { // assumed to be a calendar
}
return cache; // else, return all
}
-
+
+
+ // Makes sure all array event sources have their internal event objects
+ // converted over to the Calendar's current timezone.
+ t.rezoneArrayEventSources = function() {
+ var i;
+ var events;
+ var j;
+
+ for (i = 0; i < sources.length; i++) {
+ events = sources[i].events;
+ if ($.isArray(events)) {
+
+ for (j = 0; j < events.length; j++) {
+ rezoneEventDates(events[j]);
+ }
+ }
+ }
+ };
+
+ function rezoneEventDates(event) {
+ event.start = t.moment(event.start);
+ if (event.end) {
+ event.end = t.moment(event.end);
+ }
+ backupEventDates(event);
+ }
/* Event Normalization
@@ -11174,8 +11414,8 @@ function EventManager(options) { // assumed to be a calendar
var start, end;
var allDay;
- if (options.eventDataTransform) {
- input = options.eventDataTransform(input);
+ if (t.options.eventDataTransform) {
+ input = t.options.eventDataTransform(input);
}
if (source && source.eventDataTransform) {
input = source.eventDataTransform(input);
@@ -11241,7 +11481,7 @@ function EventManager(options) { // assumed to be a calendar
if (allDay === undefined) { // still undefined? fallback to default
allDay = firstDefined(
source ? source.allDayDefault : undefined,
- options.allDayDefault
+ t.options.allDayDefault
);
// still undefined? normalizeEventDates will calculate it
}
@@ -11253,6 +11493,7 @@ function EventManager(options) { // assumed to be a calendar
return out;
}
+ t.buildEventFromInput = buildEventFromInput;
// Normalizes and assigns the given dates to the given partially-formed event object.
@@ -11277,7 +11518,7 @@ function EventManager(options) { // assumed to be a calendar
}
if (!eventProps.end) {
- if (options.forceEventDuration) {
+ if (t.options.forceEventDuration) {
eventProps.end = t.getDefaultEventEnd(eventProps.allDay, eventProps.start);
}
else {
@@ -11376,6 +11617,7 @@ function EventManager(options) { // assumed to be a calendar
return events;
}
+ t.expandEvent = expandEvent;
@@ -11569,125 +11811,134 @@ function EventManager(options) { // assumed to be a calendar
}
- /* Business Hours
- -----------------------------------------------------------------------------------------*/
+ t.getEventCache = function() {
+ return cache;
+ };
- 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: [ 1, 2, 3, 4, 5 ], // monday - friday
- rendering: 'inverse-background'
- };
- var view = t.getView();
- var eventInput;
+// hook for external libs to manipulate event properties upon creation.
+// should manipulate the event in-place.
+Calendar.prototype.normalizeEvent = function(event) {
+};
- 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) {
+// Does the given span (start, end, and other location information)
+// fully contain the other?
+Calendar.prototype.spanContainsSpan = function(outerSpan, innerSpan) {
+ var eventStart = outerSpan.start.clone().stripZone();
+ var eventEnd = this.getEventEnd(outerSpan).stripZone();
- // if a whole-day series is requested, clear the start/end times
- if (wholeDay) {
- eventInput.start = null;
- eventInput.end = null;
- }
+ return innerSpan.start >= eventStart && innerSpan.end <= eventEnd;
+};
- return expandEvent(
- buildEventFromInput(eventInput),
- view.start,
- view.end
- );
- }
- return [];
+// 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(span, event) {
+ var cache = this.getEventCache();
+ var peerEvents = [];
+ var i, otherEvent;
+
+ for (i = 0; i < cache.length; i++) {
+ 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;
+};
- /* Overlapping / Constraining
- -----------------------------------------------------------------------------------------*/
- t.isEventSpanAllowed = isEventSpanAllowed;
- t.isExternalSpanAllowed = isExternalSpanAllowed;
- t.isSelectionSpanAllowed = isSelectionSpanAllowed;
+// 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;
+}
- // Determines if the given event can be relocated to the given span (unzoned start/end with other misc data)
- function isEventSpanAllowed(span, event) {
- var source = event.source || {};
- var constraint = firstDefined(
- event.constraint,
- source.constraint,
- options.eventConstraint
- );
- var overlap = firstDefined(
- event.overlap,
- source.overlap,
- options.eventOverlap
- );
- return isSpanAllowed(span, constraint, overlap, event);
- }
+/* Overlapping / Constraining
+-----------------------------------------------------------------------------------------*/
- // Determines if an external event can be relocated to the given span (unzoned start/end with other misc data)
- function isExternalSpanAllowed(eventSpan, eventLocation, eventProps) {
- var eventInput;
- var event;
+// Determines if the given event can be relocated to the given span (unzoned start/end with other misc data)
+Calendar.prototype.isEventSpanAllowed = function(span, event) {
+ var source = event.source || {};
- // note: very similar logic is in View's reportExternalDrop
- if (eventProps) {
- eventInput = $.extend({}, eventProps, eventLocation);
- event = expandEvent(buildEventFromInput(eventInput))[0];
- }
+ var constraint = firstDefined(
+ event.constraint,
+ source.constraint,
+ this.options.eventConstraint
+ );
- if (event) {
- return isEventSpanAllowed(eventSpan, event);
- }
- else { // treat it as a selection
+ var overlap = firstDefined(
+ event.overlap,
+ source.overlap,
+ this.options.eventOverlap
+ );
- return isSelectionSpanAllowed(eventSpan);
- }
+ return this.isSpanAllowed(span, constraint, overlap, event) &&
+ (!this.options.eventAllow || this.options.eventAllow(span, event) !== false);
+};
+
+
+// Determines if an external event can be relocated to the given span (unzoned start/end with other misc data)
+Calendar.prototype.isExternalSpanAllowed = function(eventSpan, eventLocation, eventProps) {
+ var eventInput;
+ var event;
+
+ // note: very similar logic is in View's reportExternalDrop
+ if (eventProps) {
+ eventInput = $.extend({}, eventProps, eventLocation);
+ event = this.expandEvent(
+ this.buildEventFromInput(eventInput)
+ )[0];
}
+ if (event) {
+ return this.isEventSpanAllowed(eventSpan, event);
+ }
+ else { // treat it as a selection
- // Determines the given span (unzoned start/end with other misc data) can be selected.
- function isSelectionSpanAllowed(span) {
- return isSpanAllowed(span, options.selectConstraint, options.selectOverlap);
+ return this.isSelectionSpanAllowed(eventSpan);
}
+};
- // 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(span, constraint, overlap, event) {
- var constraintEvents;
- var anyContainment;
- var peerEvents;
- var i, peerEvent;
- var peerOverlap;
+// Determines the given span (unzoned start/end with other misc data) can be selected.
+Calendar.prototype.isSelectionSpanAllowed = function(span) {
+ return this.isSpanAllowed(span, this.options.selectConstraint, this.options.selectOverlap) &&
+ (!this.options.selectAllow || this.options.selectAllow(span) !== false);
+};
+
- // the range must be fully contained by at least one of produced constraint events
- if (constraint != null) {
+// 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.
+Calendar.prototype.isSpanAllowed = function(span, constraint, overlap, event) {
+ var constraintEvents;
+ var anyContainment;
+ var peerEvents;
+ var i, peerEvent;
+ var peerOverlap;
- // not treated as an event! intermediate data structure
- // TODO: use ranges in the future
- constraintEvents = constraintToEvents(constraint);
+ // 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 = this.constraintToEvents(constraint);
+ if (constraintEvents) { // not invalid
anyContainment = false;
for (i = 0; i < constraintEvents.length; i++) {
- if (eventContainsRange(constraintEvents[i], span)) {
+ if (this.spanContainsSpan(constraintEvents[i], span)) {
anyContainment = true;
break;
}
@@ -11697,126 +11948,150 @@ function EventManager(options) { // assumed to be a calendar
return false;
}
}
+ }
- peerEvents = t.getPeerEvents(span, event);
+ peerEvents = this.getPeerEvents(span, event);
- for (i = 0; i < peerEvents.length; i++) {
- peerEvent = peerEvents[i];
+ for (i = 0; i < peerEvents.length; i++) {
+ peerEvent = peerEvents[i];
- // there needs to be an actual intersection before disallowing anything
- if (eventIntersectsRange(peerEvent, span)) {
+ // there needs to be an actual intersection before disallowing anything
+ if (this.eventIntersectsRange(peerEvent, span)) {
- // evaluate overlap for the given range and short-circuit if necessary
- if (overlap === false) {
+ // 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(peerEvent, event)) {
+ 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 event's overlap is a test function, pass the peer event in question as the first param
- else if (typeof overlap === 'function' && !overlap(peerEvent, event)) {
+ // if the peer event's overlap is a test function, pass the subject event as the first param
+ if (typeof peerOverlap === 'function' && !peerOverlap(event, peerEvent)) {
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(event, peerEvent)) {
- return false;
- }
- }
}
}
-
- return true;
}
+ 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();
- }
+// 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)
+Calendar.prototype.constraintToEvents = function(constraintInput) {
- if (typeof constraintInput === 'object') {
- return expandEvent(buildEventFromInput(constraintInput));
- }
+ if (constraintInput === 'businessHours') {
+ return this.getCurrentBusinessHourEvents();
+ }
- return clientEvents(constraintInput); // probably an ID
+ if (typeof constraintInput === 'object') {
+ if (constraintInput.start != null) { // needs to be event-like input
+ return this.expandEvent(this.buildEventFromInput(constraintInput));
+ }
+ else {
+ return null; // invalid
+ }
}
+ return this.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(event, range) {
- 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 :(
+Calendar.prototype.eventIntersectsRange = function(event, range) {
+ var eventStart = event.start.clone().stripZone();
+ var eventEnd = this.getEventEnd(event).stripZone();
+ return range.start < eventEnd && range.end > eventStart;
+};
- // Does the event's date range intersect with the given range?
- // start/end already assumed to have stripped zones :(
- function eventIntersectsRange(event, range) {
- var eventStart = event.start.clone().stripZone();
- var eventEnd = t.getEventEnd(event).stripZone();
- return range.start < eventEnd && range.end > eventStart;
- }
+/* Business Hours
+-----------------------------------------------------------------------------------------*/
+var BUSINESS_HOUR_EVENT_DEFAULTS = {
+ id: '_fcBusinessHours', // will relate events from different calls to expandEvent
+ start: '09:00',
+ end: '17:00',
+ dow: [ 1, 2, 3, 4, 5 ], // monday - friday
+ rendering: 'inverse-background'
+ // classNames are defined in businessHoursSegClasses
+};
- t.getEventCache = function() {
- return cache;
- };
+// Return events objects for business hours within the current view.
+// Abuse of our event system :(
+Calendar.prototype.getCurrentBusinessHourEvents = function(wholeDay) {
+ return this.computeBusinessHourEvents(wholeDay, this.options.businessHours);
+};
-}
+// Given a raw input value from options, return events objects for business hours within the current view.
+Calendar.prototype.computeBusinessHourEvents = function(wholeDay, input) {
+ if (input === true) {
+ return this.expandBusinessHourEvents(wholeDay, [ {} ]);
+ }
+ else if ($.isPlainObject(input)) {
+ return this.expandBusinessHourEvents(wholeDay, [ input ]);
+ }
+ else if ($.isArray(input)) {
+ return this.expandBusinessHourEvents(wholeDay, input, true);
+ }
+ else {
+ return [];
+ }
+};
+// inputs expected to be an array of objects.
+// if ignoreNoDow is true, will ignore entries that don't specify a day-of-week (dow) key.
+Calendar.prototype.expandBusinessHourEvents = function(wholeDay, inputs, ignoreNoDow) {
+ var view = this.getView();
+ var events = [];
+ var i, input;
-// hook for external libs to manipulate event properties upon creation.
-// should manipulate the event in-place.
-Calendar.prototype.normalizeEvent = function(event) {
-};
+ for (i = 0; i < inputs.length; i++) {
+ input = inputs[i];
+ if (ignoreNoDow && !input.dow) {
+ continue;
+ }
-// 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(span, event) {
- var cache = this.getEventCache();
- var peerEvents = [];
- var i, otherEvent;
+ // give defaults. will make a copy
+ input = $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, input);
- for (i = 0; i < cache.length; i++) {
- otherEvent = cache[i];
- if (
- !event ||
- event._id !== otherEvent._id // don't compare the event to itself or other related [repeating] events
- ) {
- peerEvents.push(otherEvent);
+ // if a whole-day series is requested, clear the start/end times
+ if (wholeDay) {
+ input.start = null;
+ input.end = null;
}
+
+ events.push.apply(events, // append
+ this.expandEvent(
+ this.buildEventFromInput(input),
+ view.start,
+ view.end
+ )
+ );
}
- return peerEvents;
+ return events;
};
-
-// 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.
@@ -11832,7 +12107,8 @@ var BasicView = FC.BasicView = View.extend({
dayGrid: null, // the main subcomponent that does most of the heavy lifting
dayNumbersVisible: false, // display day numbers on each day cell?
- weekNumbersVisible: false, // display week numbers along the side?
+ colWeekNumbersVisible: false, // display week numbers along the side?
+ cellWeekNumbersVisible: false, // display week numbers in day cell?
weekNumberWidth: null, // width of all the week-number cells running down the side
@@ -11893,8 +12169,18 @@ var BasicView = FC.BasicView = View.extend({
renderDates: function() {
this.dayNumbersVisible = this.dayGrid.rowCnt > 1; // TODO: make grid responsible
- this.weekNumbersVisible = this.opt('weekNumbers');
- this.dayGrid.numbersVisible = this.dayNumbersVisible || this.weekNumbersVisible;
+ if (this.opt('weekNumbers')) {
+ if (this.opt('weekNumbersWithinDays')) {
+ this.cellWeekNumbersVisible = true;
+ this.colWeekNumbersVisible = false;
+ }
+ else {
+ this.cellWeekNumbersVisible = false;
+ this.colWeekNumbersVisible = true;
+ };
+ }
+ this.dayGrid.numbersVisible = this.dayNumbersVisible ||
+ this.cellWeekNumbersVisible || this.colWeekNumbersVisible;
this.el.addClass('fc-basic-view').html(this.renderSkeletonHtml());
this.renderHead();
@@ -11932,6 +12218,11 @@ var BasicView = FC.BasicView = View.extend({
},
+ unrenderBusinessHours: function() {
+ this.dayGrid.unrenderBusinessHours();
+ },
+
+
// Builds the HTML skeleton for the view.
// The day-grid component will render inside of a container defined by this HTML.
renderSkeletonHtml: function() {
@@ -11973,7 +12264,7 @@ var BasicView = FC.BasicView = View.extend({
// Refreshes the horizontal dimensions of the view
updateWidth: function() {
- if (this.weekNumbersVisible) {
+ if (this.colWeekNumbersVisible) {
// Make sure all week number cells running down the side have the same width.
// Record the width for cells created later.
this.weekNumberWidth = matchCellWidths(
@@ -12114,9 +12405,8 @@ var BasicView = FC.BasicView = View.extend({
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
+ // we DON'T need to call updateHeight() because
+ // a renderEvents() call always happens after this, which will eventually call updateHeight()
},
@@ -12161,7 +12451,7 @@ var basicDayGridMethods = {
renderHeadIntroHtml: function() {
var view = this.view;
- if (view.weekNumbersVisible) {
+ if (view.colWeekNumbersVisible) {
return '' +
'<th class="fc-week-number ' + view.widgetHeaderClass + '" ' + view.weekNumberStyleAttr() + '>' +
'<span>' + // needed for matchCellWidths
@@ -12177,13 +12467,15 @@ var basicDayGridMethods = {
// Generates the HTML that will go before content-skeleton cells that display the day/week numbers
renderNumberIntroHtml: function(row) {
var view = this.view;
+ var weekStart = this.getCellDate(row, 0);
- if (view.weekNumbersVisible) {
+ if (view.colWeekNumbersVisible) {
return '' +
'<td class="fc-week-number" ' + view.weekNumberStyleAttr() + '>' +
- '<span>' + // needed for matchCellWidths
- this.getCellDate(row, 0).format('w') +
- '</span>' +
+ view.buildGotoAnchorHtml( // aside from link, important for matchCellWidths
+ { date: weekStart, type: 'week', forceOff: this.colCnt === 1 },
+ weekStart.format('w') // inner HTML
+ ) +
'</td>';
}
@@ -12195,7 +12487,7 @@ var basicDayGridMethods = {
renderBgIntroHtml: function() {
var view = this.view;
- if (view.weekNumbersVisible) {
+ if (view.colWeekNumbersVisible) {
return '<td class="fc-week-number ' + view.widgetContentClass + '" ' +
view.weekNumberStyleAttr() + '></td>';
}
@@ -12209,7 +12501,7 @@ var basicDayGridMethods = {
renderIntroHtml: function() {
var view = this.view;
- if (view.weekNumbersVisible) {
+ if (view.colWeekNumbersVisible) {
return '<td class="fc-week-number" ' + view.weekNumberStyleAttr() + '></td>';
}
@@ -12243,8 +12535,6 @@ var MonthView = FC.MonthView = BasicView.extend({
// Overrides the default BasicView behavior to have special multi-week auto-height logic
setGridHeight: function(height, isAuto) {
- isAuto = isAuto || this.opt('weekMode') === 'variable'; // LEGACY: weekMode is deprecated
-
// if auto, make the height of each row the height that it would be if there were 6 weeks
if (isAuto) {
height *= this.rowCnt / 6;
@@ -12255,11 +12545,6 @@ var MonthView = FC.MonthView = BasicView.extend({
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');
}
@@ -12689,9 +12974,8 @@ var AgendaView = FC.AgendaView = View.extend({
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
+ // we DON'T need to call updateHeight() because
+ // a renderEvents() call always happens after this, which will eventually call updateHeight()
},
@@ -12759,9 +13043,10 @@ var agendaTimeGridMethods = {
return '' +
'<th class="fc-axis fc-week-number ' + view.widgetHeaderClass + '" ' + view.axisStyleAttr() + '>' +
- '<span>' + // needed for matchCellWidths
- htmlEscape(weekText) +
- '</span>' +
+ view.buildGotoAnchorHtml( // aside from link, important for matchCellWidths
+ { date: this.start, type: 'week', forceOff: this.colCnt > 1 },
+ htmlEscape(weekText) // inner HTML
+ ) +
'</th>';
}
else {
@@ -12800,7 +13085,7 @@ var agendaDayGridMethods = {
return '' +
'<td class="fc-axis ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' +
'<span>' + // needed for matchCellWidths
- (view.opt('allDayHtml') || htmlEscape(view.opt('allDayText'))) +
+ view.getAllDayHtml() +
'</span>' +
'</td>';
},
@@ -12834,7 +13119,6 @@ fcViews.agenda = {
'class': AgendaView,
defaults: {
allDaySlot: true,
- allDayText: 'all-day',
slotDuration: '00:30:00',
minTime: '00:00:00',
maxTime: '24:00:00',
@@ -12853,5 +13137,293 @@ fcViews.agendaWeek = {
};
;;
-return FC; // export for Node/CommonJS
+/*
+Responsible for the scroller, and forwarding event-related actions into the "grid"
+*/
+var ListView = View.extend({
+
+ grid: null,
+ scroller: null,
+
+ initialize: function() {
+ this.grid = new ListViewGrid(this);
+ this.scroller = new Scroller({
+ overflowX: 'hidden',
+ overflowY: 'auto'
+ });
+ },
+
+ setRange: function(range) {
+ View.prototype.setRange.call(this, range); // super
+
+ this.grid.setRange(range); // needs to process range-related options
+ },
+
+ renderSkeleton: function() {
+ this.el.addClass(
+ 'fc-list-view ' +
+ this.widgetContentClass
+ );
+
+ this.scroller.render();
+ this.scroller.el.appendTo(this.el);
+
+ this.grid.setElement(this.scroller.scrollEl);
+ },
+
+ unrenderSkeleton: function() {
+ this.scroller.destroy(); // will remove the Grid too
+ },
+
+ setHeight: function(totalHeight, isAuto) {
+ this.scroller.setHeight(this.computeScrollerHeight(totalHeight));
+ },
+
+ computeScrollerHeight: function(totalHeight) {
+ return totalHeight -
+ subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
+ },
+
+ renderEvents: function(events) {
+ this.grid.renderEvents(events);
+ },
+
+ unrenderEvents: function() {
+ this.grid.unrenderEvents();
+ },
+
+ isEventResizable: function(event) {
+ return false;
+ },
+
+ isEventDraggable: function(event) {
+ return false;
+ }
+
+});
+
+/*
+Responsible for event rendering and user-interaction.
+Its "el" is the inner-content of the above view's scroller.
+*/
+var ListViewGrid = Grid.extend({
+
+ segSelector: '.fc-list-item', // which elements accept event actions
+ hasDayInteractions: false, // no day selection or day clicking
+
+ // slices by day
+ spanToSegs: function(span) {
+ var view = this.view;
+ var dayStart = view.start.clone();
+ var dayEnd;
+ var seg;
+ var segs = [];
+
+ while (dayStart < view.end) {
+ dayEnd = dayStart.clone().add(1, 'day');
+ seg = intersectRanges(span, {
+ start: dayStart,
+ end: dayEnd
+ });
+ if (seg) {
+ segs.push(seg);
+ }
+ dayStart = dayEnd;
+ }
+
+ return segs;
+ },
+
+ // like "4:00am"
+ computeEventTimeFormat: function() {
+ return this.view.opt('mediumTimeFormat');
+ },
+
+ // for events with a url, the whole <tr> should be clickable,
+ // but it's impossible to wrap with an <a> tag. simulate this.
+ handleSegClick: function(seg, ev) {
+ var url;
+
+ Grid.prototype.handleSegClick.apply(this, arguments); // super. might prevent the default action
+
+ // not clicking on or within an <a> with an href
+ if (!$(ev.target).closest('a[href]').length) {
+ url = seg.event.url;
+ if (url && !ev.isDefaultPrevented()) { // jsEvent not cancelled in handler
+ window.location.href = url; // simulate link click
+ }
+ }
+ },
+
+ // returns list of foreground segs that were actually rendered
+ renderFgSegs: function(segs) {
+ segs = this.renderFgSegEls(segs); // might filter away hidden events
+
+ if (!segs.length) {
+ this.renderEmptyMessage();
+ return segs;
+ }
+ else {
+ return this.renderSegList(segs);
+ }
+ },
+
+ renderEmptyMessage: function() {
+ this.el.html(
+ '<div class="fc-list-empty-wrap2">' + // TODO: try less wraps
+ '<div class="fc-list-empty-wrap1">' +
+ '<div class="fc-list-empty">' +
+ htmlEscape(this.view.opt('noEventsMessage')) +
+ '</div>' +
+ '</div>' +
+ '</div>'
+ );
+ },
+
+ // render the event segments in the view. returns the mutated array.
+ renderSegList: function(segs) {
+ var tableEl = $('<table class="fc-list-table"><tbody/></table>');
+ var tbodyEl = tableEl.find('tbody');
+ var i, seg;
+ var dayDate;
+
+ this.sortEventSegs(segs);
+
+ for (i = 0; i < segs.length; i++) {
+ seg = segs[i];
+
+ // append a day header
+ if (!dayDate || !seg.start.isSame(dayDate, 'day')) {
+ dayDate = seg.start.clone().stripTime();
+ tbodyEl.append(this.dayHeaderHtml(dayDate));
+ }
+
+ tbodyEl.append(seg.el); // append event row
+ }
+
+ this.el.empty().append(tableEl);
+
+ return segs; // return the sorted list
+ },
+
+ // generates the HTML for the day headers that live amongst the event rows
+ dayHeaderHtml: function(dayDate) {
+ var view = this.view;
+ var mainFormat = view.opt('listDayFormat');
+ var altFormat = view.opt('listDayAltFormat');
+
+ return '<tr class="fc-list-heading" data-date="' + dayDate.format('YYYY-MM-DD') + '">' +
+ '<td class="' + view.widgetHeaderClass + '" colspan="3">' +
+ (mainFormat ?
+ view.buildGotoAnchorHtml(
+ dayDate,
+ { 'class': 'fc-list-heading-main' },
+ htmlEscape(dayDate.format(mainFormat)) // inner HTML
+ ) :
+ '') +
+ (altFormat ?
+ view.buildGotoAnchorHtml(
+ dayDate,
+ { 'class': 'fc-list-heading-alt' },
+ htmlEscape(dayDate.format(altFormat)) // inner HTML
+ ) :
+ '') +
+ '</td>' +
+ '</tr>';
+ },
+
+ // generates the HTML for a single event row
+ fgSegHtml: function(seg) {
+ var view = this.view;
+ var classes = [ 'fc-list-item' ].concat(this.getSegCustomClasses(seg));
+ var bgColor = this.getSegBackgroundColor(seg);
+ var event = seg.event;
+ var url = event.url;
+ var timeHtml;
+
+ if (!seg.start.hasTime()) {
+ if (this.displayEventTime) {
+ timeHtml = view.getAllDayHtml();
+ }
+ }
+ else {
+ timeHtml = htmlEscape(this.getEventTimeText(event)); // might return empty
+ }
+
+ if (url) {
+ classes.push('fc-has-url');
+ }
+
+ return '<tr class="' + classes.join(' ') + '">' +
+ (timeHtml ?
+ '<td class="fc-list-item-time ' + view.widgetContentClass + '">' +
+ timeHtml +
+ '</td>' :
+ '') +
+ '<td class="fc-list-item-marker ' + view.widgetContentClass + '">' +
+ '<span class="fc-event-dot"' +
+ (bgColor ?
+ ' style="background-color:' + bgColor + '"' :
+ '') +
+ '></span>' +
+ '</td>' +
+ '<td class="fc-list-item-title ' + view.widgetContentClass + '">' +
+ '<a' + (url ? ' href="' + htmlEscape(url) + '"' : '') + '>' +
+ htmlEscape(seg.event.title) +
+ '</a>' +
+ '</td>' +
+ '</tr>';
+ }
+
+});
+
+;;
+
+fcViews.list = {
+ 'class': ListView,
+ buttonTextKey: 'list', // what to lookup in locale files
+ defaults: {
+ buttonText: 'list', // text to display for English
+ listTime: true, // show the time column?
+ listDayFormat: 'LL', // like "January 1, 2016"
+ noEventsMessage: 'No events to display'
+ }
+};
+
+fcViews.listDay = {
+ type: 'list',
+ duration: { days: 1 },
+ defaults: {
+ listDayFormat: 'dddd' // day-of-week is all we need. full date is probably in header
+ }
+};
+
+fcViews.listWeek = {
+ type: 'list',
+ duration: { weeks: 1 },
+ defaults: {
+ listDayFormat: 'dddd', // day-of-week is more important
+ listDayAltFormat: 'LL'
+ }
+};
+
+fcViews.listMonth = {
+ type: 'list',
+ duration: { month: 1 },
+ defaults: {
+ listDayAltFormat: 'dddd' // day-of-week is nice-to-have
+ }
+};
+
+fcViews.listYear = {
+ type: 'list',
+ duration: { year: 1 },
+ defaults: {
+ listDayAltFormat: 'dddd' // day-of-week is nice-to-have
+ }
+};
+
+;;
+
+return FC; // export for Node/CommonJS
}); \ No newline at end of file