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.js2111
1 files changed, 1403 insertions, 708 deletions
diff --git a/library/fullcalendar/fullcalendar.js b/library/fullcalendar/fullcalendar.js
index 3c2c380bc..b7371e25f 100644
--- a/library/fullcalendar/fullcalendar.js
+++ b/library/fullcalendar/fullcalendar.js
@@ -1,5 +1,5 @@
/*!
- * FullCalendar v3.0.1
+ * FullCalendar v3.1.0
* Docs & License: http://fullcalendar.io/
* (c) 2016 Adam Shaw
*/
@@ -19,8 +19,8 @@
;;
var FC = $.fullCalendar = {
- version: "3.0.1",
- internalApiVersion: 6
+ version: "3.1.0",
+ internalApiVersion: 7
};
var fcViews = FC.views = {};
@@ -53,13 +53,14 @@ $.fn.fullCalendar = function(options) {
calendar.render();
}
});
-
+
return res;
};
var complexOptions = [ // names of options that are objects whose properties should be combined
'header',
+ 'footer',
'buttonText',
'buttonIcons',
'themeButtonIcons'
@@ -848,6 +849,7 @@ function createObject(proto) {
f.prototype = proto;
return new f();
}
+FC.createObject = createObject;
function copyOwnProps(src, dest) {
@@ -1004,20 +1006,6 @@ function debounce(func, wait, immediate) {
};
}
-
-// HACK around jQuery's now A+ promises: execute callback synchronously if already resolved.
-// thenFunc shouldn't accept args.
-// similar to whenResources in Scheduler plugin.
-function syncThen(promise, thenFunc) {
- // not a promise, or an already-resolved promise?
- if (!promise || !promise.then || promise.state() === 'resolved') {
- return $.when(thenFunc()); // resolve immediately
- }
- else if (thenFunc) {
- return promise.then(thenFunc);
- }
-}
-
;;
/*
@@ -1681,6 +1669,201 @@ function mixIntoClass(theClass, members) {
}
;;
+/*
+Wrap jQuery's Deferred Promise object to be slightly more Promise/A+ compliant.
+With the added non-standard feature of synchronously executing handlers on resolved promises,
+which doesn't always happen otherwise (esp with nested .then handlers!?),
+so, this makes things a lot easier, esp because jQuery 3 changed the synchronicity for Deferred objects.
+
+TODO: write tests and more comments
+*/
+
+function Promise(executor) {
+ var deferred = $.Deferred();
+ var promise = deferred.promise();
+
+ if (typeof executor === 'function') {
+ executor(
+ function(value) { // resolve
+ if (Promise.immediate) {
+ promise._value = value;
+ }
+ deferred.resolve(value);
+ },
+ function() { // reject
+ deferred.reject();
+ }
+ );
+ }
+
+ if (Promise.immediate) {
+ var origThen = promise.then;
+
+ promise.then = function(onFulfilled, onRejected) {
+ var state = promise.state();
+
+ if (state === 'resolved') {
+ if (typeof onFulfilled === 'function') {
+ return Promise.resolve(onFulfilled(promise._value));
+ }
+ }
+ else if (state === 'rejected') {
+ if (typeof onRejected === 'function') {
+ onRejected();
+ return promise; // already rejected
+ }
+ }
+
+ return origThen.call(promise, onFulfilled, onRejected);
+ };
+ }
+
+ return promise; // instanceof Promise will break :( TODO: make Promise a real class
+}
+
+FC.Promise = Promise;
+
+Promise.immediate = true;
+
+
+Promise.resolve = function(value) {
+ if (value && typeof value.resolve === 'function') {
+ return value.promise();
+ }
+ if (value && typeof value.then === 'function') {
+ return value;
+ }
+ else {
+ var deferred = $.Deferred().resolve(value);
+ var promise = deferred.promise();
+
+ if (Promise.immediate) {
+ var origThen = promise.then;
+
+ promise._value = value;
+
+ promise.then = function(onFulfilled, onRejected) {
+ if (typeof onFulfilled === 'function') {
+ return Promise.resolve(onFulfilled(value));
+ }
+ return origThen.call(promise, onFulfilled, onRejected);
+ };
+ }
+
+ return promise;
+ }
+};
+
+
+Promise.reject = function() {
+ return $.Deferred().reject().promise();
+};
+
+
+Promise.all = function(inputs) {
+ var hasAllValues = false;
+ var values;
+ var i, input;
+
+ if (Promise.immediate) {
+ hasAllValues = true;
+ values = [];
+
+ for (i = 0; i < inputs.length; i++) {
+ input = inputs[i];
+
+ if (input && typeof input.state === 'function' && input.state() === 'resolved' && ('_value' in input)) {
+ values.push(input._value);
+ }
+ else if (input && typeof input.then === 'function') {
+ hasAllValues = false;
+ break;
+ }
+ else {
+ values.push(input);
+ }
+ }
+ }
+
+ if (hasAllValues) {
+ return Promise.resolve(values);
+ }
+ else {
+ return $.when.apply($.when, inputs).then(function() {
+ return $.when($.makeArray(arguments));
+ });
+ }
+};
+
+;;
+
+// TODO: write tests and clean up code
+
+function TaskQueue(debounceWait) {
+ var q = []; // array of runFuncs
+
+ function addTask(taskFunc) {
+ return new Promise(function(resolve) {
+
+ // should run this function when it's taskFunc's turn to run.
+ // responsible for popping itself off the queue.
+ var runFunc = function() {
+ Promise.resolve(taskFunc()) // result might be async, coerce to promise
+ .then(resolve) // resolve TaskQueue::push's promise, for the caller. will receive result of taskFunc.
+ .then(function() {
+ q.shift(); // pop itself off
+
+ // run the next task, if any
+ if (q.length) {
+ q[0]();
+ }
+ });
+ };
+
+ // always put the task at the end of the queue, BEFORE running the task
+ q.push(runFunc);
+
+ // if it's the only task in the queue, run immediately
+ if (q.length === 1) {
+ runFunc();
+ }
+ });
+ }
+
+ this.add = // potentially debounce, for the public method
+ typeof debounceWait === 'number' ?
+ debounce(addTask, debounceWait) :
+ addTask; // if not a number (null/undefined/false), no debounce at all
+
+ this.addQuickly = addTask; // guaranteed no debounce
+}
+
+FC.TaskQueue = TaskQueue;
+
+/*
+q = new TaskQueue();
+
+function work(i) {
+ return q.push(function() {
+ trigger();
+ console.log('work' + i);
+ });
+}
+
+var cnt = 0;
+
+function trigger() {
+ if (cnt < 5) {
+ cnt++;
+ work(cnt);
+ }
+}
+
+work(9);
+*/
+
+;;
+
var EmitterMixin = FC.EmitterMixin = {
// jQuery-ification via $(this) allows a non-DOM object to have
@@ -1688,7 +1871,18 @@ var EmitterMixin = FC.EmitterMixin = {
on: function(types, handler) {
+ $(this).on(types, this._prepareIntercept(handler));
+ return this; // for chaining
+ },
+
+
+ one: function(types, handler) {
+ $(this).one(types, this._prepareIntercept(handler));
+ return this; // for chaining
+ },
+
+ _prepareIntercept: function(handler) {
// handlers are always called with an "event" object as their first param.
// sneak the `this` context and arguments into the extra parameter object
// and forward them on to the original handler.
@@ -1708,9 +1902,7 @@ var EmitterMixin = FC.EmitterMixin = {
}
intercept.guid = handler.guid;
- $(this).on(types, intercept);
-
- return this; // for chaining
+ return intercept;
},
@@ -2039,9 +2231,15 @@ var CoordCache = FC.CoordCache = Class.extend({
// Queries the els for coordinates and stores them.
// Call this method before using and of the get* methods below.
build: function() {
- var offsetParentEl = this.forcedOffsetParentEl || this.els.eq(0).offsetParent();
+ var offsetParentEl = this.forcedOffsetParentEl;
+ if (!offsetParentEl && this.els.length > 0) {
+ offsetParentEl = this.els.eq(0).offsetParent();
+ }
+
+ this.origin = offsetParentEl ?
+ offsetParentEl.offset() :
+ null;
- this.origin = offsetParentEl.offset();
this.boundingRect = this.queryBoundingRect();
if (this.isHorizontal) {
@@ -2224,12 +2422,19 @@ 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.
+ // Returns null if there are no elements
queryBoundingRect: function() {
- var scrollParentEl = getScrollParent(this.els.eq(0));
+ var scrollParentEl;
+
+ if (this.els.length > 0) {
+ scrollParentEl = getScrollParent(this.els.eq(0));
- if (!scrollParentEl.is(document)) {
- return getClientRect(scrollParentEl);
+ if (!scrollParentEl.is(document)) {
+ return getClientRect(scrollParentEl);
+ }
}
+
+ return null;
},
isPointInBounds: function(leftOffset, topOffset) {
@@ -3448,6 +3653,7 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
dayTouchStart: function(ev) {
var view = this.view;
+ var selectLongPressDelay = view.opt('selectLongPressDelay');
// HACK to prevent a user's clickaway for unselecting a range or an event
// from causing a dayClick.
@@ -3455,8 +3661,12 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
this.tempIgnoreMouse();
}
+ if (selectLongPressDelay == null) {
+ selectLongPressDelay = view.opt('longPressDelay'); // fallback
+ }
+
this.dayDragListener.startInteraction(ev, {
- delay: this.view.opt('longPressDelay')
+ delay: selectLongPressDelay
});
},
@@ -3793,7 +4003,7 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
// Computes HTML classNames for a single-day element
- getDayClasses: function(date) {
+ getDayClasses: function(date, noThemeHighlight) {
var view = this.view;
var today = view.calendar.getNow();
var classes = [ 'fc-' + dayIDs[date.day()] ];
@@ -3806,10 +4016,11 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
}
if (date.isSame(today, 'day')) {
- classes.push(
- 'fc-today',
- view.highlightStateClass
- );
+ classes.push('fc-today');
+
+ if (noThemeHighlight !== true) {
+ classes.push(view.highlightStateClass);
+ }
}
else if (date < today) {
classes.push('fc-past');
@@ -4005,16 +4216,33 @@ Grid.mixin({
// 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);
+ // If no `businessHours` configuration value is specified, assumes the calendar default.
+ buildBusinessHourSegs: function(wholeDay, businessHours) {
+ return this.eventsToSegs(
+ this.buildBusinessHourEvents(wholeDay, businessHours)
+ );
+ },
+
+
+ // Compute business hour *events* for the grid's current date range.
+ // Caller must ask if whole-day business hours are needed.
+ // If no `businessHours` configuration value is specified, assumes the calendar default.
+ buildBusinessHourEvents: function(wholeDay, businessHours) {
+ var calendar = this.view.calendar;
+ var events;
+
+ if (businessHours == null) {
+ // fallback
+ // access from calendawr. don't access from view. doesn't update with dynamic options.
+ businessHours = calendar.options.businessHours;
+ }
+
+ events = calendar.computeBusinessHourEvents(wholeDay, businessHours);
// 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
- ) {
+ if (!events.length && businessHours) {
events = [
$.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, {
start: this.view.end, // guaranteed out-of-range
@@ -4024,7 +4252,7 @@ Grid.mixin({
];
}
- return this.eventsToSegs(events);
+ return events;
},
@@ -4066,7 +4294,7 @@ Grid.mixin({
handleSegClick: function(seg, ev) {
- var res = this.view.trigger('eventClick', seg.el[0], seg.event, ev); // can return `false` to cancel
+ var res = this.view.publiclyTrigger('eventClick', seg.el[0], seg.event, ev); // can return `false` to cancel
if (res === false) {
ev.preventDefault();
}
@@ -4083,7 +4311,7 @@ Grid.mixin({
if (this.view.isEventResizable(seg.event)) {
seg.el.addClass('fc-allow-mouse-resize');
}
- this.view.trigger('eventMouseover', seg.el[0], seg.event, ev);
+ this.view.publiclyTrigger('eventMouseover', seg.el[0], seg.event, ev);
}
},
@@ -4099,7 +4327,7 @@ Grid.mixin({
if (this.view.isEventResizable(seg.event)) {
seg.el.removeClass('fc-allow-mouse-resize');
}
- this.view.trigger('eventMouseout', seg.el[0], seg.event, ev);
+ this.view.publiclyTrigger('eventMouseout', seg.el[0], seg.event, ev);
}
},
@@ -4124,6 +4352,7 @@ Grid.mixin({
var isResizable = view.isEventResizable(event);
var isResizing = false;
var dragListener;
+ var eventLongPressDelay;
if (isSelected && isResizable) {
// only allow resizing of the event is selected
@@ -4132,12 +4361,17 @@ Grid.mixin({
if (!isResizing && (isDraggable || isResizable)) { // allowed to be selected?
+ eventLongPressDelay = view.opt('eventLongPressDelay');
+ if (eventLongPressDelay == null) {
+ eventLongPressDelay = view.opt('longPressDelay'); // fallback
+ }
+
dragListener = isDraggable ?
this.buildSegDragListener(seg) :
this.buildSegSelectListener(seg); // seg isn't draggable, but still needs to be selected
dragListener.startInteraction(ev, { // won't start if already started
- delay: isSelected ? 0 : this.view.opt('longPressDelay') // do delay if not already selected
+ delay: isSelected ? 0 : eventLongPressDelay // do delay if not already selected
});
}
@@ -4270,11 +4504,15 @@ Grid.mixin({
mouseFollower.stop(!dropLocation, function() {
if (isDragging) {
view.unrenderDrag();
- view.showEvent(event);
_this.segDragStop(seg, ev);
}
+
if (dropLocation) {
- view.reportEventDrop(event, dropLocation, this.largeUnit, el, ev);
+ // no need to re-show original, will rerender all anyways. esp important if eventRenderWait
+ view.reportEventDrop(event, dropLocation, _this.largeUnit, el, ev);
+ }
+ else {
+ view.showEvent(event);
}
});
_this.segDragListener = null;
@@ -4316,14 +4554,14 @@ Grid.mixin({
// Called before event segment dragging starts
segDragStart: function(seg, ev) {
this.isDraggingSeg = true;
- this.view.trigger('eventDragStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
+ this.view.publiclyTrigger('eventDragStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
},
// Called after event segment dragging stops
segDragStop: function(seg, ev) {
this.isDraggingSeg = false;
- this.view.trigger('eventDragStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
+ this.view.publiclyTrigger('eventDragStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
},
@@ -4561,18 +4799,23 @@ Grid.mixin({
},
hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
resizeLocation = null;
+ view.showEvent(event); // for when out-of-bounds. show original
},
hitDone: function() { // resets the rendering to show the original event
_this.unrenderEventResize();
- view.showEvent(event);
enableCursor();
},
interactionEnd: function(ev) {
if (isDragging) {
_this.segResizeStop(seg, ev);
}
+
if (resizeLocation) { // valid date to resize to?
- view.reportEventResize(event, resizeLocation, this.largeUnit, el, ev);
+ // no need to re-show original, will rerender all anyways. esp important if eventRenderWait
+ view.reportEventResize(event, resizeLocation, _this.largeUnit, el, ev);
+ }
+ else {
+ view.showEvent(event);
}
_this.segResizeListener = null;
}
@@ -4585,14 +4828,14 @@ Grid.mixin({
// Called before event segment resizing starts
segResizeStart: function(seg, ev) {
this.isResizingSeg = true;
- this.view.trigger('eventResizeStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
+ this.view.publiclyTrigger('eventResizeStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
},
// Called after event segment resizing stops
segResizeStop: function(seg, ev) {
this.isResizingSeg = false;
- this.view.trigger('eventResizeStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
+ this.view.publiclyTrigger('eventResizeStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
},
@@ -5143,7 +5386,7 @@ var DayTableMixin = FC.DayTableMixin = {
this.dayIndices = dayIndices;
this.daysPerRow = daysPerRow;
this.rowCnt = rowCnt;
-
+
this.updateDayTableCols();
},
@@ -5381,9 +5624,25 @@ var DayTableMixin = FC.DayTableMixin = {
// (colspan should be no different)
renderHeadDateCellHtml: function(date, colspan, otherAttrs) {
var view = this.view;
+ var classNames = [
+ 'fc-day-header',
+ view.widgetHeaderClass
+ ];
+
+ // if only one row of days, the classNames on the header can represent the specific days beneath
+ if (this.rowCnt === 1) {
+ classNames = classNames.concat(
+ // includes the day-of-week class
+ // noThemeHighlight=true (don't highlight the header)
+ this.getDayClasses(date, true)
+ );
+ }
+ else {
+ classNames.push('fc-' + dayIDs[date.day()]); // only add the day-of-week class
+ }
return '' +
- '<th class="fc-day-header ' + view.widgetHeaderClass + ' fc-' + dayIDs[date.day()] + '"' +
+ '<th class="' + classNames.join(' ') + '"' +
(this.rowCnt === 1 ?
' data-date="' + date.format('YYYY-MM-DD') + '"' :
'') +
@@ -5534,7 +5793,7 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
// trigger dayRender with each cell's element
for (row = 0; row < rowCnt; row++) {
for (col = 0; col < colCnt; col++) {
- view.trigger(
+ view.publiclyTrigger(
'dayRender',
null,
this.getCellDate(row, col),
@@ -6469,7 +6728,7 @@ DayGrid.mixin({
if (typeof clickOption === 'function') {
// the returned value can be an atomic option
- clickOption = view.trigger('eventLimitClick', null, {
+ clickOption = view.publiclyTrigger('eventLimitClick', null, {
date: date,
dayEl: dayEl,
moreEl: moreEl,
@@ -6512,6 +6771,14 @@ DayGrid.mixin({
viewportConstrain: view.opt('popoverViewportConstrain'),
hide: function() {
// kill everything when the popover is hidden
+ // notify events to be removed
+ if (_this.popoverSegs) {
+ var seg;
+ for (var i = 0; i < _this.popoverSegs.length; ++i) {
+ seg = _this.popoverSegs[i];
+ view.publiclyTrigger('eventDestroy', seg.event, seg.event, seg.el);
+ }
+ }
_this.segPopover.removeElement();
_this.segPopover = null;
_this.popoverSegs = null;
@@ -7789,9 +8056,14 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
options: null, // hash containing all options. already merged with view-specific-options
el: null, // the view's containing element. set by Calendar
- displaying: null, // a promise representing the state of rendering. null if no render requested
- isSkeletonRendered: false,
+ isDateSet: false,
+ isDateRendered: false,
+ dateRenderQueue: null,
+
+ isEventsBound: false,
+ isEventsSet: false,
isEventsRendered: false,
+ eventRenderQueue: null,
// range the view is actually displaying (moments)
start: null,
@@ -7841,6 +8113,9 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder'));
+ this.dateRenderQueue = new TaskQueue();
+ this.eventRenderQueue = new TaskQueue(this.opt('eventRenderWait'));
+
this.initialize();
},
@@ -7858,10 +8133,10 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
// Triggers handlers that are view-related. Modifies args before passing to calendar.
- trigger: function(name, thisObj) { // arguments beyond thisObj are passed along
+ publiclyTrigger: function(name, thisObj) { // arguments beyond thisObj are passed along
var calendar = this.calendar;
- return calendar.trigger.apply(
+ return calendar.publiclyTrigger.apply(
calendar,
[name, thisObj || this].concat(
Array.prototype.slice.call(arguments, 2), // arguments beyond thisObj
@@ -7871,16 +8146,33 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
},
- /* Dates
- ------------------------------------------------------------------------------------------------------------------*/
+ // Returns a proxy of the given promise that will be rejected if the given event fires
+ // before the promise resolves.
+ rejectOn: function(eventName, promise) {
+ var _this = this;
+ return new Promise(function(resolve, reject) {
+ _this.one(eventName, reject);
- // Updates all internal dates to center around the given current unzoned date.
- setDate: function(date) {
- this.setRange(this.computeRange(date));
+ function cleanup() {
+ _this.off(eventName, reject);
+ }
+
+ promise.then(function(res) { // success
+ cleanup();
+ resolve(res);
+ }, function() { // failure
+ cleanup();
+ reject();
+ });
+ });
},
+ /* Date Computation
+ ------------------------------------------------------------------------------------------------------------------*/
+
+
// Updates all internal dates for displaying the given unzoned range.
setRange: function(range) {
$.extend(this, range); // assigns every property to this object's member variables
@@ -7963,6 +8255,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
// Sets the view's title property to the most updated computed value
updateTitle: function() {
this.title = this.computeTitle();
+ this.calendar.setToolbarsTitle(this.title);
},
@@ -8068,164 +8361,219 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
},
- /* Rendering
- ------------------------------------------------------------------------------------------------------------------*/
+ // Rendering Non-date-related Content
+ // -----------------------------------------------------------------------------------------------------------------
- // Sets the container element that the view should render inside of.
- // Does other DOM-related initializations.
+ // Sets the container element that the view should render inside of, does global DOM-related initializations,
+ // and renders all the non-date-related content inside.
setElement: function(el) {
this.el = el;
this.bindGlobalHandlers();
+ this.renderSkeleton();
},
// Removes the view's container element from the DOM, clearing any content beforehand.
// Undoes any other DOM-related attachments.
removeElement: function() {
- this.clear(); // clears all content
-
- // clean up the skeleton
- if (this.isSkeletonRendered) {
- this.unrenderSkeleton();
- this.isSkeletonRendered = false;
- }
+ this.unsetDate();
+ this.unrenderSkeleton();
this.unbindGlobalHandlers();
this.el.remove();
-
// NOTE: don't null-out this.el in case the View was destroyed within an API callback.
// We don't null-out the View's other jQuery element references upon destroy,
// so we shouldn't kill this.el either.
},
- // Does everything necessary to display the view centered around the given unzoned date.
- // Does every type of rendering EXCEPT rendering events.
- // Is asychronous and returns a promise.
- display: function(date, explicitScrollState) {
- var _this = this;
- var prevScrollState = null;
+ // Renders the basic structure of the view before any content is rendered
+ renderSkeleton: function() {
+ // subclasses should implement
+ },
+
- if (explicitScrollState != null && this.displaying) { // don't need prevScrollState if explicitScrollState
- prevScrollState = this.queryScroll();
+ // Unrenders the basic structure of the view
+ unrenderSkeleton: function() {
+ // subclasses should implement
+ },
+
+
+ // Date Setting/Unsetting
+ // -----------------------------------------------------------------------------------------------------------------
+
+
+ setDate: function(date) {
+ var isReset = this.isDateSet;
+
+ this.isDateSet = true;
+ this.handleDate(date, isReset);
+ this.trigger(isReset ? 'dateReset' : 'dateSet', date);
+ },
+
+
+ unsetDate: function() {
+ if (this.isDateSet) {
+ this.isDateSet = false;
+ this.handleDateUnset();
+ this.trigger('dateUnset');
}
+ },
- this.calendar.freezeContentHeight();
- return syncThen(this.clear(), function() { // clear the content first
- return (
- _this.displaying =
- syncThen(_this.displayView(date), function() { // displayView might return a promise
+ // Date Handling
+ // -----------------------------------------------------------------------------------------------------------------
- // 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();
- })
- );
+ handleDate: function(date, isReset) {
+ var _this = this;
+
+ this.unbindEvents(); // will do nothing if not already bound
+ this.requestDateRender(date).then(function() {
+ // wish we could start earlier, but setRange/computeRange needs to execute first
+ _this.bindEvents(); // will request events
});
},
- // Does everything necessary to clear the content of the view.
- // Clears dates and events. Does not clear the skeleton.
- // Is asychronous and returns a promise.
- clear: function() {
+ handleDateUnset: function() {
+ this.unbindEvents();
+ this.requestDateUnrender();
+ },
+
+
+ // Date Render Queuing
+ // -----------------------------------------------------------------------------------------------------------------
+
+
+ // if date not specified, uses current
+ requestDateRender: function(date) {
var _this = this;
- var displaying = this.displaying;
- if (displaying) { // previously displayed, or in the process of being displayed?
- return syncThen(displaying, function() { // wait for the display to finish
- _this.displaying = null;
- _this.clearEvents();
- return _this.clearView(); // might return a promise. chain it
- });
- }
- else {
- return $.when(); // an immediately-resolved promise
- }
+ return this.dateRenderQueue.add(function() {
+ return _this.executeDateRender(date);
+ });
},
- // Displays the view's non-event content, such as date-related content or anything required by events.
- // Renders the view's non-content skeleton if necessary.
- // Can be asynchronous and return a promise.
- displayView: function(date) {
- if (!this.isSkeletonRendered) {
- this.renderSkeleton();
- this.isSkeletonRendered = true;
- }
+ requestDateUnrender: function() {
+ var _this = this;
+
+ return this.dateRenderQueue.add(function() {
+ return _this.executeDateUnrender();
+ });
+ },
+
+
+ // Date High-level Rendering
+ // -----------------------------------------------------------------------------------------------------------------
+
+
+ // if date not specified, uses current
+ executeDateRender: function(date) {
+ var _this = this;
+
+ // if rendering a new date, reset scroll to initial state (scrollTime)
if (date) {
- this.setDate(date);
+ this.captureInitialScroll();
}
- if (this.render) {
- this.render(); // TODO: deprecate
+ else {
+ this.captureScroll(); // a rerender of the current date
}
- this.renderDates();
- this.updateSize();
- this.renderBusinessHours(); // might need coordinates, so should go after updateSize()
- this.startNowIndicator();
+
+ this.freezeHeight();
+
+ return this.executeDateUnrender().then(function() {
+
+ if (date) {
+ _this.setRange(_this.computeRange(date));
+ }
+
+ if (_this.render) {
+ _this.render(); // TODO: deprecate
+ }
+
+ _this.renderDates();
+ _this.updateSize();
+ _this.renderBusinessHours(); // might need coordinates, so should go after updateSize()
+ _this.startNowIndicator();
+
+ _this.thawHeight();
+ _this.releaseScroll();
+
+ _this.isDateRendered = true;
+ _this.onDateRender();
+ _this.trigger('dateRender');
+ });
},
- // Unrenders the view content that was rendered in displayView.
- // Can be asynchronous and return a promise.
- clearView: function() {
- this.unselect();
- this.stopNowIndicator();
- this.triggerUnrender();
- this.unrenderBusinessHours();
- this.unrenderDates();
- if (this.destroy) {
- this.destroy(); // TODO: deprecate
+ executeDateUnrender: function() {
+ var _this = this;
+
+ if (_this.isDateRendered) {
+ return this.requestEventsUnrender().then(function() {
+
+ _this.unselect();
+ _this.stopNowIndicator();
+ _this.triggerUnrender();
+ _this.unrenderBusinessHours();
+ _this.unrenderDates();
+
+ if (_this.destroy) {
+ _this.destroy(); // TODO: deprecate
+ }
+
+ _this.isDateRendered = false;
+ _this.trigger('dateUnrender');
+ });
+ }
+ else {
+ return Promise.resolve();
}
},
- // Renders the basic structure of the view before any content is rendered
- renderSkeleton: function() {
- // subclasses should implement
- },
+ // Date Rendering Triggers
+ // -----------------------------------------------------------------------------------------------------------------
- // Unrenders the basic structure of the view
- unrenderSkeleton: function() {
- // subclasses should implement
+ onDateRender: function() {
+ this.triggerRender();
},
- // Renders the view's date-related content.
- // Assumes setRange has already been called and the skeleton has already been rendered.
+ // Date Low-level Rendering
+ // -----------------------------------------------------------------------------------------------------------------
+
+
+ // date-cell content only
renderDates: function() {
// subclasses should implement
},
- // Unrenders the view's date-related content
+ // date-cell content only
unrenderDates: function() {
// subclasses should override
},
+ // Misc view rendering utils
+ // -------------------------
+
+
// Signals that the view's content has been rendered
triggerRender: function() {
- this.trigger('viewRender', this, this, this.el);
+ this.publiclyTrigger('viewRender', this, this, this.el);
},
// Signals that the view's content is about to be unrendered
triggerUnrender: function() {
- this.trigger('viewDestroy', this, this, this.el);
+ this.publiclyTrigger('viewDestroy', this, this, this.el);
},
@@ -8362,10 +8710,9 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
// Refreshes anything dependant upon sizing of the container element of the grid
updateSize: function(isResize) {
- var scrollState;
if (isResize) {
- scrollState = this.queryScroll();
+ this.captureScroll();
}
this.updateHeight(isResize);
@@ -8373,7 +8720,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
this.updateNowIndicator();
if (isResize) {
- this.setScroll(scrollState);
+ this.releaseScroll();
}
},
@@ -8406,72 +8753,294 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
------------------------------------------------------------------------------------------------------------------*/
- // Computes the initial pre-configured scroll state prior to allowing the user to change it.
- // Given the scroll state from the previous rendering. If first time rendering, given null.
- computeInitialScroll: function(previousScrollState) {
- return 0;
+ capturedScroll: null,
+ capturedScrollDepth: 0,
+
+
+ captureScroll: function() {
+ if (!(this.capturedScrollDepth++)) {
+ this.capturedScroll = this.isDateRendered ? this.queryScroll() : {}; // require a render first
+ return true; // root?
+ }
+ return false;
},
- // Retrieves the view's current natural scroll state. Can return an arbitrary format.
- queryScroll: function() {
- // subclasses must implement
+ captureInitialScroll: function(forcedScroll) {
+ if (this.captureScroll()) { // root?
+ this.capturedScroll.isInitial = true;
+
+ if (forcedScroll) {
+ $.extend(this.capturedScroll, forcedScroll);
+ }
+ else {
+ this.capturedScroll.isComputed = true;
+ }
+ }
},
- // Sets the view's scroll state. Will accept the same format computeInitialScroll and queryScroll produce.
- setScroll: function(scrollState) {
- // subclasses must implement
+ releaseScroll: function() {
+ var scroll = this.capturedScroll;
+ var isRoot = this.discardScroll();
+
+ if (scroll.isComputed) {
+ if (isRoot) {
+ // only compute initial scroll if it will actually be used (is the root capture)
+ $.extend(scroll, this.computeInitialScroll());
+ }
+ else {
+ scroll = null; // scroll couldn't be computed. don't apply it to the DOM
+ }
+ }
+
+ if (scroll) {
+ // we act immediately on a releaseScroll operation, as opposed to captureScroll.
+ // if capture/release wraps a render operation that screws up the scroll,
+ // we still want to restore it a good state after, regardless of depth.
+
+ if (scroll.isInitial) {
+ this.hardSetScroll(scroll); // outsmart how browsers set scroll on initial DOM
+ }
+ else {
+ this.setScroll(scroll);
+ }
+ }
+ },
+
+
+ discardScroll: function() {
+ if (!(--this.capturedScrollDepth)) {
+ this.capturedScroll = null;
+ return true; // root?
+ }
+ return false;
+ },
+
+
+ computeInitialScroll: function() {
+ return {};
+ },
+
+
+ queryScroll: function() {
+ return {};
},
- // Sets the scroll state, making sure to overcome any predefined scroll value the browser has in mind
- forceScroll: function(scrollState) {
+ hardSetScroll: function(scroll) {
var _this = this;
+ var exec = function() { _this.setScroll(scroll); };
+ exec();
+ setTimeout(exec, 0); // to surely clear the browser's initial scroll for the DOM
+ },
- this.setScroll(scrollState);
- setTimeout(function() {
- _this.setScroll(scrollState);
- }, 0);
+
+ setScroll: function(scroll) {
},
- /* Event Elements / Segments
+ /* Height Freezing
------------------------------------------------------------------------------------------------------------------*/
- // Does everything necessary to display the given events onto the current view
- displayEvents: function(events) {
- var scrollState = this.queryScroll();
+ freezeHeight: function() {
+ this.calendar.freezeContentHeight();
+ },
+
+
+ thawHeight: function() {
+ this.calendar.thawContentHeight();
+ },
+
+
+ // Event Binding/Unbinding
+ // -----------------------------------------------------------------------------------------------------------------
+
+
+ bindEvents: function() {
+ var _this = this;
+
+ if (!this.isEventsBound) {
+ this.isEventsBound = true;
+ this.rejectOn('eventsUnbind', this.requestEvents()).then(function(events) { // TODO: test rejection
+ _this.listenTo(_this.calendar, 'eventsReset', _this.setEvents);
+ _this.setEvents(events);
+ });
+ }
+ },
+
+
+ unbindEvents: function() {
+ if (this.isEventsBound) {
+ this.isEventsBound = false;
+ this.stopListeningTo(this.calendar, 'eventsReset');
+ this.unsetEvents();
+ this.trigger('eventsUnbind');
+ }
+ },
+
+
+ // Event Setting/Unsetting
+ // -----------------------------------------------------------------------------------------------------------------
+
- this.clearEvents();
- this.renderEvents(events);
- this.isEventsRendered = true;
- this.setScroll(scrollState);
- this.triggerEventRender();
+ setEvents: function(events) {
+ var isReset = this.isEventSet;
+
+ this.isEventsSet = true;
+ this.handleEvents(events, isReset);
+ this.trigger(isReset ? 'eventsReset' : 'eventsSet', events);
+ },
+
+
+ unsetEvents: function() {
+ if (this.isEventsSet) {
+ this.isEventsSet = false;
+ this.handleEventsUnset();
+ this.trigger('eventsUnset');
+ }
},
- // Does everything necessary to clear the view's currently-rendered events
- clearEvents: function() {
- var scrollState;
+ whenEventsSet: function() {
+ var _this = this;
+ if (this.isEventsSet) {
+ return Promise.resolve(this.getCurrentEvents());
+ }
+ else {
+ return new Promise(function(resolve) {
+ _this.one('eventsSet', resolve);
+ });
+ }
+ },
+
+
+ // Event Handling
+ // -----------------------------------------------------------------------------------------------------------------
+
+
+ handleEvents: function(events, isReset) {
+ this.requestEventsRender(events);
+ },
+
+
+ handleEventsUnset: function() {
+ this.requestEventsUnrender();
+ },
+
+
+ // Event Render Queuing
+ // -----------------------------------------------------------------------------------------------------------------
+
+
+ // assumes any previous event renders have been cleared already
+ requestEventsRender: function(events) {
+ var _this = this;
+
+ return this.eventRenderQueue.add(function() { // might not return a promise if debounced!? bad
+ return _this.executeEventsRender(events);
+ });
+ },
+
+
+ requestEventsUnrender: function() {
+ var _this = this;
+
+ if (this.isEventsRendered) {
+ return this.eventRenderQueue.addQuickly(function() {
+ return _this.executeEventsUnrender();
+ });
+ }
+ else {
+ return Promise.resolve();
+ }
+ },
+
+
+ requestCurrentEventsRender: function() {
+ if (this.isEventsSet) {
+ this.requestEventsRender(this.getCurrentEvents());
+ }
+ else {
+ return Promise.reject();
+ }
+ },
+
+
+ // Event High-level Rendering
+ // -----------------------------------------------------------------------------------------------------------------
+
+
+ executeEventsRender: function(events) {
+ var _this = this;
+
+ this.captureScroll();
+ this.freezeHeight();
+
+ return this.executeEventsUnrender().then(function() {
+ _this.renderEvents(events);
+
+ _this.thawHeight();
+ _this.releaseScroll();
+
+ _this.isEventsRendered = true;
+ _this.onEventsRender();
+ _this.trigger('eventsRender');
+ });
+ },
+
+
+ executeEventsUnrender: function() {
if (this.isEventsRendered) {
+ this.onBeforeEventsUnrender();
- // TODO: optimize: if we know this is part of a displayEvents call, don't queryScroll/setScroll
- scrollState = this.queryScroll();
+ this.captureScroll();
+ this.freezeHeight();
- this.triggerEventUnrender();
if (this.destroyEvents) {
this.destroyEvents(); // TODO: deprecate
}
+
this.unrenderEvents();
- this.setScroll(scrollState);
+
+ this.thawHeight();
+ this.releaseScroll();
+
this.isEventsRendered = false;
+ this.trigger('eventsUnrender');
}
+
+ return Promise.resolve(); // always synchronous
+ },
+
+
+ // Event Rendering Triggers
+ // -----------------------------------------------------------------------------------------------------------------
+
+
+ // Signals that all events have been rendered
+ onEventsRender: function() {
+ this.renderedEventSegEach(function(seg) {
+ this.publiclyTrigger('eventAfterRender', seg.event, seg.event, seg.el);
+ });
+ this.publiclyTrigger('eventAfterAllRender');
+ },
+
+
+ // Signals that all event elements are about to be removed
+ onBeforeEventsUnrender: function() {
+ this.renderedEventSegEach(function(seg) {
+ this.publiclyTrigger('eventDestroy', seg.event, seg.event, seg.el);
+ });
},
+ // Event Low-level Rendering
+ // -----------------------------------------------------------------------------------------------------------------
+
+
// Renders the events onto the view.
renderEvents: function(events) {
// subclasses should implement
@@ -8484,27 +9053,28 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
},
- // Signals that all events have been rendered
- triggerEventRender: function() {
- this.renderedEventSegEach(function(seg) {
- this.trigger('eventAfterRender', seg.event, seg.event, seg.el);
- });
- this.trigger('eventAfterAllRender');
+ // Event Data Access
+ // -----------------------------------------------------------------------------------------------------------------
+
+
+ requestEvents: function() {
+ return this.calendar.requestEvents(this.start, this.end);
},
- // Signals that all event elements are about to be removed
- triggerEventUnrender: function() {
- this.renderedEventSegEach(function(seg) {
- this.trigger('eventDestroy', seg.event, seg.event, seg.el);
- });
+ getCurrentEvents: function() {
+ return this.calendar.getPrunedEventCache();
},
+ // Event Rendering Utils
+ // -----------------------------------------------------------------------------------------------------------------
+
+
// Given an event and the default element used for rendering, returns the element that should actually be used.
// Basically runs events and elements through the eventRender hook.
resolveEventEl: function(event, el) {
- var custom = this.trigger('eventRender', event, event, el);
+ var custom = this.publiclyTrigger('eventRender', event, event, el);
if (custom === false) { // means don't render at all
el = null;
@@ -8603,7 +9173,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
// Triggers event-drop handlers that have subscribed via the API
triggerEventDrop: function(event, dateDelta, undoFunc, el, ev) {
- this.trigger('eventDrop', el[0], event, dateDelta, undoFunc, ev, {}); // {} = jqui dummy
+ this.publiclyTrigger('eventDrop', el[0], event, dateDelta, undoFunc, ev, {}); // {} = jqui dummy
},
@@ -8633,10 +9203,10 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
triggerExternalDrop: function(event, dropLocation, el, ev, ui) {
// trigger 'drop' regardless of whether element represents an event
- this.trigger('drop', el[0], dropLocation.start, ev, ui);
+ this.publiclyTrigger('drop', el[0], dropLocation.start, ev, ui);
if (event) {
- this.trigger('eventReceive', null, event); // signal an external event landed
+ this.publiclyTrigger('eventReceive', null, event); // signal an external event landed
}
},
@@ -8706,7 +9276,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
// Triggers event-resize handlers that have subscribed via the API
triggerEventResize: function(event, durationDelta, undoFunc, el, ev) {
- this.trigger('eventResize', el[0], event, durationDelta, undoFunc, ev, {}); // {} = jqui dummy
+ this.publiclyTrigger('eventResize', el[0], event, durationDelta, undoFunc, ev, {}); // {} = jqui dummy
},
@@ -8738,7 +9308,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
// Triggers handlers to 'select'
triggerSelect: function(span, ev) {
- this.trigger(
+ this.publiclyTrigger(
'select',
null,
this.calendar.applyTimezone(span.start), // convert to calendar's tz for external API
@@ -8757,7 +9327,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
this.destroySelection(); // TODO: deprecate
}
this.unrenderSelection();
- this.trigger('unselect', null, ev);
+ this.publiclyTrigger('unselect', null, ev);
}
},
@@ -8849,7 +9419,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
// Triggers handlers to 'dayClick'
// Span has start/end of the clicked area. Only the start is useful.
triggerDayClick: function(span, dayEl, ev) {
- this.trigger(
+ this.publiclyTrigger(
'dayClick',
dayEl,
this.calendar.applyTimezone(span.start), // convert to calendar's timezone for external API
@@ -9076,6 +9646,295 @@ var Scroller = FC.Scroller = Class.extend({
});
;;
+function Iterator(items) {
+ this.items = items || [];
+}
+
+
+/* Calls a method on every item passing the arguments through */
+Iterator.prototype.proxyCall = function(methodName) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ var results = [];
+
+ this.items.forEach(function(item) {
+ results.push(item[methodName].apply(item, args));
+ });
+
+ return results;
+};
+
+;;
+
+/* Toolbar with buttons and title
+----------------------------------------------------------------------------------------------------------------------*/
+
+function Toolbar(calendar, toolbarOptions) {
+ var t = this;
+
+ // exports
+ t.setToolbarOptions = setToolbarOptions;
+ t.render = render;
+ t.removeElement = removeElement;
+ t.updateTitle = updateTitle;
+ t.activateButton = activateButton;
+ t.deactivateButton = deactivateButton;
+ t.disableButton = disableButton;
+ t.enableButton = enableButton;
+ t.getViewsWithButtons = getViewsWithButtons;
+ t.el = null; // mirrors local `el`
+
+ // locals
+ var el;
+ var viewsWithButtons = [];
+ var tm;
+
+ // method to update toolbar-specific options, not calendar-wide options
+ function setToolbarOptions(newToolbarOptions) {
+ toolbarOptions = newToolbarOptions;
+ }
+
+ // can be called repeatedly and will rerender
+ function render() {
+ var sections = toolbarOptions.layout;
+
+ tm = calendar.options.theme ? 'ui' : 'fc';
+
+ if (sections) {
+ if (!el) {
+ el = this.el = $("<div class='fc-toolbar "+ toolbarOptions.extraClasses + "'/>");
+ }
+ else {
+ el.empty();
+ }
+ el.append(renderSection('left'))
+ .append(renderSection('right'))
+ .append(renderSection('center'))
+ .append('<div class="fc-clear"/>');
+ }
+ else {
+ removeElement();
+ }
+ }
+
+
+ function removeElement() {
+ if (el) {
+ el.remove();
+ el = t.el = null;
+ }
+ }
+
+
+ function renderSection(position) {
+ var sectionEl = $('<div class="fc-' + position + '"/>');
+ var buttonStr = toolbarOptions.layout[position];
+
+ if (buttonStr) {
+ $.each(buttonStr.split(' '), function(i) {
+ var groupChildren = $();
+ var isOnlyButtons = true;
+ var groupEl;
+
+ $.each(this.split(','), function(j, buttonName) {
+ var customButtonProps;
+ var viewSpec;
+ var buttonClick;
+ var overrideText; // text explicitly set by calendar's constructor options. overcomes icons
+ var defaultText;
+ var themeIcon;
+ var normalIcon;
+ var innerHtml;
+ var classes;
+ var button; // the element
+
+ if (buttonName == 'title') {
+ groupChildren = groupChildren.add($('<h2>&nbsp;</h2>')); // we always want it to take up height
+ isOnlyButtons = false;
+ }
+ else {
+ if ((customButtonProps = (calendar.options.customButtons || {})[buttonName])) {
+ buttonClick = function(ev) {
+ if (customButtonProps.click) {
+ customButtonProps.click.call(button[0], ev);
+ }
+ };
+ overrideText = ''; // icons will override text
+ defaultText = customButtonProps.text;
+ }
+ else if ((viewSpec = calendar.getViewSpec(buttonName))) {
+ buttonClick = function() {
+ calendar.changeView(buttonName);
+ };
+ viewsWithButtons.push(buttonName);
+ overrideText = viewSpec.buttonTextOverride;
+ defaultText = viewSpec.buttonTextDefault;
+ }
+ else if (calendar[buttonName]) { // a calendar method
+ buttonClick = function() {
+ calendar[buttonName]();
+ };
+ overrideText = (calendar.overrides.buttonText || {})[buttonName];
+ defaultText = calendar.options.buttonText[buttonName]; // everything else is considered default
+ }
+
+ if (buttonClick) {
+
+ themeIcon =
+ customButtonProps ?
+ customButtonProps.themeIcon :
+ calendar.options.themeButtonIcons[buttonName];
+
+ normalIcon =
+ customButtonProps ?
+ customButtonProps.icon :
+ calendar.options.buttonIcons[buttonName];
+
+ if (overrideText) {
+ innerHtml = htmlEscape(overrideText);
+ }
+ else if (themeIcon && calendar.options.theme) {
+ innerHtml = "<span class='ui-icon ui-icon-" + themeIcon + "'></span>";
+ }
+ else if (normalIcon && !calendar.options.theme) {
+ innerHtml = "<span class='fc-icon fc-icon-" + normalIcon + "'></span>";
+ }
+ else {
+ innerHtml = htmlEscape(defaultText);
+ }
+
+ classes = [
+ 'fc-' + buttonName + '-button',
+ tm + '-button',
+ tm + '-state-default'
+ ];
+
+ button = $( // type="button" so that it doesn't submit a form
+ '<button type="button" class="' + classes.join(' ') + '">' +
+ innerHtml +
+ '</button>'
+ )
+ .click(function(ev) {
+ // don't process clicks for disabled buttons
+ if (!button.hasClass(tm + '-state-disabled')) {
+
+ buttonClick(ev);
+
+ // after the click action, if the button becomes the "active" tab, or disabled,
+ // it should never have a hover class, so remove it now.
+ if (
+ button.hasClass(tm + '-state-active') ||
+ button.hasClass(tm + '-state-disabled')
+ ) {
+ button.removeClass(tm + '-state-hover');
+ }
+ }
+ })
+ .mousedown(function() {
+ // the *down* effect (mouse pressed in).
+ // only on buttons that are not the "active" tab, or disabled
+ button
+ .not('.' + tm + '-state-active')
+ .not('.' + tm + '-state-disabled')
+ .addClass(tm + '-state-down');
+ })
+ .mouseup(function() {
+ // undo the *down* effect
+ button.removeClass(tm + '-state-down');
+ })
+ .hover(
+ function() {
+ // the *hover* effect.
+ // only on buttons that are not the "active" tab, or disabled
+ button
+ .not('.' + tm + '-state-active')
+ .not('.' + tm + '-state-disabled')
+ .addClass(tm + '-state-hover');
+ },
+ function() {
+ // undo the *hover* effect
+ button
+ .removeClass(tm + '-state-hover')
+ .removeClass(tm + '-state-down'); // if mouseleave happens before mouseup
+ }
+ );
+
+ groupChildren = groupChildren.add(button);
+ }
+ }
+ });
+
+ if (isOnlyButtons) {
+ groupChildren
+ .first().addClass(tm + '-corner-left').end()
+ .last().addClass(tm + '-corner-right').end();
+ }
+
+ if (groupChildren.length > 1) {
+ groupEl = $('<div/>');
+ if (isOnlyButtons) {
+ groupEl.addClass('fc-button-group');
+ }
+ groupEl.append(groupChildren);
+ sectionEl.append(groupEl);
+ }
+ else {
+ sectionEl.append(groupChildren); // 1 or 0 children
+ }
+ });
+ }
+
+ return sectionEl;
+ }
+
+
+ function updateTitle(text) {
+ if (el) {
+ el.find('h2').text(text);
+ }
+ }
+
+
+ function activateButton(buttonName) {
+ if (el) {
+ el.find('.fc-' + buttonName + '-button')
+ .addClass(tm + '-state-active');
+ }
+ }
+
+
+ function deactivateButton(buttonName) {
+ if (el) {
+ el.find('.fc-' + buttonName + '-button')
+ .removeClass(tm + '-state-active');
+ }
+ }
+
+
+ function disableButton(buttonName) {
+ if (el) {
+ el.find('.fc-' + buttonName + '-button')
+ .prop('disabled', true)
+ .addClass(tm + '-state-disabled');
+ }
+ }
+
+
+ function enableButton(buttonName) {
+ if (el) {
+ el.find('.fc-' + buttonName + '-button')
+ .prop('disabled', false)
+ .removeClass(tm + '-state-disabled');
+ }
+ }
+
+
+ function getViewsWithButtons() {
+ return viewsWithButtons;
+ }
+
+}
+
+;;
var Calendar = FC.Calendar = Class.extend({
@@ -9087,6 +9946,7 @@ var Calendar = FC.Calendar = Class.extend({
viewSpecCache: null, // cache of view definitions
view: null, // current View object
header: null,
+ footer: null,
loadingLevel: 0, // number of simultaneous loading tasks
@@ -9155,7 +10015,7 @@ var Calendar = FC.Calendar = Class.extend({
if ($.inArray(unit, intervalUnits) != -1) {
// put views that have buttons first. there will be duplicates, but oh well
- viewTypes = this.header.getViewsWithButtons();
+ viewTypes = this.header.getViewsWithButtons(); // TODO: include footer as well?
$.each(FC.views, function(viewType) { // all views
viewTypes.push(viewType);
});
@@ -9302,7 +10162,7 @@ var Calendar = FC.Calendar = Class.extend({
// Should be called when any type of async data fetching begins
pushLoading: function() {
if (!(this.loadingLevel++)) {
- this.trigger('loading', null, true, this.view);
+ this.publiclyTrigger('loading', null, true, this.view);
}
},
@@ -9310,7 +10170,7 @@ var Calendar = FC.Calendar = Class.extend({
// Should be called when any type of async data fetching completes
popLoading: function() {
if (!(--this.loadingLevel)) {
- this.trigger('loading', null, false, this.view);
+ this.publiclyTrigger('loading', null, false, this.view);
}
},
@@ -9348,11 +10208,7 @@ function Calendar_constructor(element, overrides) {
t.render = render;
t.destroy = destroy;
- t.refetchEvents = refetchEvents;
- t.refetchEventSources = refetchEventSources;
- t.reportEvents = reportEvents;
- t.reportEventChange = reportEventChange;
- t.rerenderEvents = renderEvents; // `renderEvents` serves as a rerender. an API method
+ t.rerenderEvents = rerenderEvents;
t.changeView = renderView; // `renderView` will switch to another view
t.select = select;
t.unselect = unselect;
@@ -9368,7 +10224,7 @@ function Calendar_constructor(element, overrides) {
t.getCalendar = getCalendar;
t.getView = getView;
t.option = option; // getter/setter method
- t.trigger = trigger;
+ t.publiclyTrigger = publiclyTrigger;
// Options
@@ -9561,15 +10417,12 @@ function Calendar_constructor(element, overrides) {
};
-
+
// Imports
// -----------------------------------------------------------------------------------
EventManager.call(t);
- var isFetchNeeded = t.isFetchNeeded;
- var fetchEvents = t.fetchEvents;
- var fetchEventSources = t.fetchEventSources;
@@ -9578,7 +10431,9 @@ function Calendar_constructor(element, overrides) {
var _element = element[0];
+ var toolbarsManager;
var header;
+ var footer;
var content;
var tm; // for making theme classes
var currentView; // NOTE: keep this in sync with this.view
@@ -9586,11 +10441,10 @@ function Calendar_constructor(element, overrides) {
var suggestedViewHeight;
var windowResizeProxy; // wraps the windowResize function
var ignoreWindowResize = 0;
- var events = [];
var date; // unzoned
-
-
-
+
+
+
// Main Rendering
// -----------------------------------------------------------------------------------
@@ -9602,8 +10456,8 @@ function Calendar_constructor(element, overrides) {
else {
date = t.getNow(); // getNow already returns unzoned
}
-
-
+
+
function render() {
if (!content) {
initialRender();
@@ -9614,8 +10468,8 @@ function Calendar_constructor(element, overrides) {
renderView();
}
}
-
-
+
+
function initialRender() {
element.addClass('fc');
@@ -9656,9 +10510,14 @@ function Calendar_constructor(element, overrides) {
content = $("<div class='fc-view-container'/>").prependTo(element);
- header = t.header = new Header(t);
- renderHeader();
+ var toolbars = buildToolbars();
+ toolbarsManager = new Iterator(toolbars);
+
+ header = t.header = toolbars[0];
+ footer = t.footer = toolbars[1];
+ renderHeader();
+ renderFooter();
renderView(t.options.defaultView);
if (t.options.handleWindowResize) {
@@ -9668,15 +10527,6 @@ function Calendar_constructor(element, overrides) {
}
- // can be called repeatedly and Header will rerender
- function renderHeader() {
- header.render();
- if (header.el) {
- element.prepend(header.el);
- }
- }
-
-
function destroy() {
if (currentView) {
@@ -9686,7 +10536,7 @@ function Calendar_constructor(element, overrides) {
// It is still the "current" view, just not rendered.
}
- header.removeElement();
+ toolbarsManager.proxyCall('removeElement');
content.remove();
element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');
@@ -9696,13 +10546,13 @@ function Calendar_constructor(element, overrides) {
$(window).unbind('resize', windowResizeProxy);
}
}
-
-
+
+
function elementVisible() {
return element.is(':visible');
}
-
-
+
+
// View Rendering
// -----------------------------------------------------------------------------------
@@ -9711,11 +10561,13 @@ 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.
// Accepts an optional scroll state to restore to.
- function renderView(viewType, explicitScrollState) {
+ function renderView(viewType, forcedScroll) {
ignoreWindowResize++;
+ var needsClearView = currentView && viewType && currentView.type !== viewType;
+
// if viewType is changing, remove the old view's rendering
- if (currentView && viewType && currentView.type !== viewType) {
+ if (needsClearView) {
freezeContentHeight(); // prevent a scroll jump when view element is removed
clearView();
}
@@ -9729,7 +10581,7 @@ function Calendar_constructor(element, overrides) {
currentView.setElement(
$("<div class='fc-view fc-" + viewType + "-view' />").appendTo(content)
);
- header.activateButton(viewType);
+ toolbarsManager.proxyCall('activateButton', viewType);
}
if (currentView) {
@@ -9739,7 +10591,7 @@ function Calendar_constructor(element, overrides) {
// render or rerender the view
if (
- !currentView.displaying ||
+ !currentView.isDateSet ||
!( // NOT within interval range signals an implicit date window change
date >= currentView.intervalStart &&
date < currentView.intervalEnd
@@ -9747,19 +10599,27 @@ function Calendar_constructor(element, overrides) {
) {
if (elementVisible()) {
- currentView.display(date, explicitScrollState); // will call freezeContentHeight
- unfreezeContentHeight(); // immediately unfreeze regardless of whether display is async
+ if (forcedScroll) {
+ currentView.captureInitialScroll(forcedScroll);
+ }
- // need to do this after View::render, so dates are calculated
- updateHeaderTitle();
- updateTodayButton();
+ currentView.setDate(date, forcedScroll);
+
+ if (forcedScroll) {
+ currentView.releaseScroll();
+ }
- getAndRenderEvents();
+ // need to do this after View::render, so dates are calculated
+ // NOTE: view updates title text proactively
+ updateToolbarsTodayButton();
}
}
}
- unfreezeContentHeight(); // undo any lone freezeContentHeight calls
+ if (needsClearView) {
+ thawContentHeight();
+ }
+
ignoreWindowResize--;
}
@@ -9767,7 +10627,7 @@ function Calendar_constructor(element, overrides) {
// 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);
+ toolbarsManager.proxyCall('deactivateButton', currentView.type);
currentView.removeElement();
currentView = t.view = null;
}
@@ -9783,13 +10643,14 @@ function Calendar_constructor(element, overrides) {
var viewType = currentView.type;
var scrollState = currentView.queryScroll();
clearView();
+ calcSize();
renderView(viewType, scrollState);
- unfreezeContentHeight();
+ thawContentHeight();
ignoreWindowResize--;
}
-
+
// Resizing
// -----------------------------------------------------------------------------------
@@ -9806,8 +10667,8 @@ function Calendar_constructor(element, overrides) {
t.isHeightAuto = function() {
return t.options.contentHeight === 'auto' || t.options.height === 'auto';
};
-
-
+
+
function updateSize(shouldRecalc) {
if (elementVisible()) {
@@ -9829,8 +10690,8 @@ function Calendar_constructor(element, overrides) {
_calcSize();
}
}
-
-
+
+
function _calcSize() { // assumes elementVisible
var contentHeightInput = t.options.contentHeight;
var heightInput = t.options.height;
@@ -9842,13 +10703,13 @@ function Calendar_constructor(element, overrides) {
suggestedViewHeight = contentHeightInput();
}
else if (typeof heightInput === 'number') { // exists and not 'auto'
- suggestedViewHeight = heightInput - queryHeaderHeight();
+ suggestedViewHeight = heightInput - queryToolbarsHeight();
}
else if (typeof heightInput === 'function') { // exists and is a function
- suggestedViewHeight = heightInput() - queryHeaderHeight();
+ suggestedViewHeight = heightInput() - queryToolbarsHeight();
}
else if (heightInput === 'parent') { // set to height of parent element
- suggestedViewHeight = element.parent().height() - queryHeaderHeight();
+ suggestedViewHeight = element.parent().height() - queryToolbarsHeight();
}
else {
suggestedViewHeight = Math.round(content.width() / Math.max(t.options.aspectRatio, .5));
@@ -9856,11 +10717,14 @@ function Calendar_constructor(element, overrides) {
}
- function queryHeaderHeight() {
- return header.el ? header.el.outerHeight(true) : 0; // includes margin
+ function queryToolbarsHeight() {
+ return toolbarsManager.items.reduce(function(accumulator, toolbar) {
+ var toolbarHeight = toolbar.el ? toolbar.el.outerHeight(true) : 0; // includes margin
+ return accumulator + toolbarHeight;
+ }, 0);
}
-
-
+
+
function windowResize(ev) {
if (
!ignoreWindowResize &&
@@ -9868,94 +10732,93 @@ function Calendar_constructor(element, overrides) {
currentView.start // view has already been rendered
) {
if (updateSize(true)) {
- currentView.trigger('windowResize', _element);
+ currentView.publiclyTrigger('windowResize', _element);
}
}
}
-
-
-
- /* Event Fetching/Rendering
- -----------------------------------------------------------------------------*/
- // TODO: going forward, most of this stuff should be directly handled by the view
- function refetchEvents() { // can be called as an API method
- fetchAndRenderEvents();
- }
-
- // TODO: move this into EventManager?
- function refetchEventSources(matchInputs) {
- fetchEventSources(t.getEventSourcesByMatchArray(matchInputs));
- }
+ /* Event Rendering
+ -----------------------------------------------------------------------------*/
- function renderEvents() { // destroys old events if previously rendered
+ function rerenderEvents() { // API method. destroys old events if previously rendered.
if (elementVisible()) {
- freezeContentHeight();
- currentView.displayEvents(events);
- unfreezeContentHeight();
+ t.reportEventChange(); // will re-trasmit events to the view, causing a rerender
}
}
-
- function getAndRenderEvents() {
- if (!t.options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) {
- fetchAndRenderEvents();
- }
- else {
- renderEvents();
- }
- }
- function fetchAndRenderEvents() {
- fetchEvents(currentView.start, currentView.end);
- // ... will call reportEvents
- // ... which will call renderEvents
- }
+ /* Toolbars
+ -----------------------------------------------------------------------------*/
-
- // called when event data arrives
- function reportEvents(_events) {
- events = _events;
- renderEvents();
+
+ function buildToolbars() {
+ return [
+ new Toolbar(t, computeHeaderOptions()),
+ new Toolbar(t, computeFooterOptions())
+ ];
}
- // called when a single event's data has been changed
- function reportEventChange() {
- renderEvents();
+ function computeHeaderOptions() {
+ return {
+ extraClasses: 'fc-header-toolbar',
+ layout: t.options.header
+ };
}
+ function computeFooterOptions() {
+ return {
+ extraClasses: 'fc-footer-toolbar',
+ layout: t.options.footer
+ };
+ }
- /* Header Updating
- -----------------------------------------------------------------------------*/
+
+ // can be called repeatedly and Header will rerender
+ function renderHeader() {
+ header.setToolbarOptions(computeHeaderOptions());
+ header.render();
+ if (header.el) {
+ element.prepend(header.el);
+ }
+ }
- function updateHeaderTitle() {
- header.updateTitle(currentView.title);
+ // can be called repeatedly and Footer will rerender
+ function renderFooter() {
+ footer.setToolbarOptions(computeFooterOptions());
+ footer.render();
+ if (footer.el) {
+ element.append(footer.el);
+ }
}
- function updateTodayButton() {
- var now = t.getNow();
+ t.setToolbarsTitle = function(title) {
+ toolbarsManager.proxyCall('updateTitle', title);
+ };
+
+ function updateToolbarsTodayButton() {
+ var now = t.getNow();
if (now >= currentView.intervalStart && now < currentView.intervalEnd) {
- header.disableButton('today');
+ toolbarsManager.proxyCall('disableButton', 'today');
}
else {
- header.enableButton('today');
+ toolbarsManager.proxyCall('enableButton', 'today');
}
}
-
+
/* Selection
-----------------------------------------------------------------------------*/
-
+
// this public method receives start/end dates in any format, with any timezone
function select(zonedStartInput, zonedEndInput) {
@@ -9963,56 +10826,56 @@ function Calendar_constructor(element, overrides) {
t.buildSelectSpan.apply(t, arguments)
);
}
-
+
function unselect() { // safe to be called before renderView
if (currentView) {
currentView.unselect();
}
}
-
-
-
+
+
+
/* Date
-----------------------------------------------------------------------------*/
-
-
+
+
function prev() {
date = currentView.computePrevDate(date);
renderView();
}
-
-
+
+
function next() {
date = currentView.computeNextDate(date);
renderView();
}
-
-
+
+
function prevYear() {
date.add(-1, 'years');
renderView();
}
-
-
+
+
function nextYear() {
date.add(1, 'years');
renderView();
}
-
-
+
+
function today() {
date = t.getNow();
renderView();
}
-
-
+
+
function gotoDate(zonedDateInput) {
date = t.moment(zonedDateInput).stripZone();
renderView();
}
-
-
+
+
function incrementDate(delta) {
date.add(moment.duration(delta));
renderView();
@@ -10030,8 +10893,8 @@ function Calendar_constructor(element, overrides) {
date = newDate.clone();
renderView(spec ? spec.type : null);
}
-
-
+
+
// for external API
function getDate() {
return t.applyTimezone(date); // infuse the calendar's timezone
@@ -10041,45 +10904,51 @@ function Calendar_constructor(element, overrides) {
/* Height "Freezing"
-----------------------------------------------------------------------------*/
- // TODO: move this into the view
+
t.freezeContentHeight = freezeContentHeight;
- t.unfreezeContentHeight = unfreezeContentHeight;
+ t.thawContentHeight = thawContentHeight;
+
+ var freezeContentHeightDepth = 0;
function freezeContentHeight() {
- content.css({
- width: '100%',
- height: content.height(),
- overflow: 'hidden'
- });
+ if (!(freezeContentHeightDepth++)) {
+ content.css({
+ width: '100%',
+ height: content.height(),
+ overflow: 'hidden'
+ });
+ }
}
- function unfreezeContentHeight() {
- content.css({
- width: '',
- height: '',
- overflow: ''
- });
+ function thawContentHeight() {
+ if (!(--freezeContentHeightDepth)) {
+ content.css({
+ width: '',
+ height: '',
+ overflow: ''
+ });
+ }
}
-
-
-
+
+
+
/* Misc
-----------------------------------------------------------------------------*/
-
+
function getCalendar() {
return t;
}
-
+
function getView() {
return currentView;
}
-
-
+
+
function option(name, value) {
var newOptionHash;
@@ -10135,19 +11004,20 @@ function Calendar_constructor(element, overrides) {
}
else if (optionName === 'timezone') {
t.rezoneArrayEventSources();
- refetchEvents();
+ t.refetchEvents();
return;
}
}
- // catch-all. rerender the header and rebuild/rerender the current view
+ // catch-all. rerender the header and footer and rebuild/rerender the current view
renderHeader();
+ renderFooter();
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 :(
+
+
+ function publiclyTrigger(name, thisObj) {
var args = Array.prototype.slice.call(arguments, 2);
thisObj = thisObj || _element;
@@ -10310,6 +11180,7 @@ Calendar.defaults = {
dropAccept: '*',
eventOrder: 'title',
+ //eventRenderWait: null,
eventLimit: false,
eventLimitText: 'more',
@@ -10549,275 +11420,6 @@ FC.locale('en', Calendar.englishDefaults);
;;
-/* Top toolbar area with buttons and title
-----------------------------------------------------------------------------------------------------------------------*/
-// TODO: rename all header-related things to "toolbar"
-
-function Header(calendar) {
- var t = this;
-
- // exports
- t.render = render;
- t.removeElement = removeElement;
- t.updateTitle = updateTitle;
- t.activateButton = activateButton;
- t.deactivateButton = deactivateButton;
- t.disableButton = disableButton;
- t.enableButton = enableButton;
- t.getViewsWithButtons = getViewsWithButtons;
- t.el = null; // mirrors local `el`
-
- // locals
- 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) {
- 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"/>');
- }
- else {
- removeElement();
- }
- }
-
-
- function removeElement() {
- 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) {
- $.each(buttonStr.split(' '), function(i) {
- var groupChildren = $();
- var isOnlyButtons = true;
- var groupEl;
-
- $.each(this.split(','), function(j, buttonName) {
- var customButtonProps;
- var viewSpec;
- var buttonClick;
- var overrideText; // text explicitly set by calendar's constructor options. overcomes icons
- var defaultText;
- var themeIcon;
- var normalIcon;
- var innerHtml;
- var classes;
- var button; // the element
-
- if (buttonName == 'title') {
- groupChildren = groupChildren.add($('<h2>&nbsp;</h2>')); // we always want it to take up height
- isOnlyButtons = false;
- }
- else {
- if ((customButtonProps = (options.customButtons || {})[buttonName])) {
- buttonClick = function(ev) {
- if (customButtonProps.click) {
- customButtonProps.click.call(button[0], ev);
- }
- };
- overrideText = ''; // icons will override text
- defaultText = customButtonProps.text;
- }
- else if ((viewSpec = calendar.getViewSpec(buttonName))) {
- buttonClick = function() {
- calendar.changeView(buttonName);
- };
- viewsWithButtons.push(buttonName);
- overrideText = viewSpec.buttonTextOverride;
- defaultText = viewSpec.buttonTextDefault;
- }
- else if (calendar[buttonName]) { // a calendar method
- buttonClick = function() {
- calendar[buttonName]();
- };
- overrideText = (calendar.overrides.buttonText || {})[buttonName];
- defaultText = options.buttonText[buttonName]; // everything else is considered default
- }
-
- if (buttonClick) {
-
- themeIcon =
- customButtonProps ?
- customButtonProps.themeIcon :
- options.themeButtonIcons[buttonName];
-
- normalIcon =
- customButtonProps ?
- customButtonProps.icon :
- options.buttonIcons[buttonName];
-
- if (overrideText) {
- innerHtml = htmlEscape(overrideText);
- }
- else if (themeIcon && options.theme) {
- innerHtml = "<span class='ui-icon ui-icon-" + themeIcon + "'></span>";
- }
- else if (normalIcon && !options.theme) {
- innerHtml = "<span class='fc-icon fc-icon-" + normalIcon + "'></span>";
- }
- else {
- innerHtml = htmlEscape(defaultText);
- }
-
- classes = [
- 'fc-' + buttonName + '-button',
- tm + '-button',
- tm + '-state-default'
- ];
-
- button = $( // type="button" so that it doesn't submit a form
- '<button type="button" class="' + classes.join(' ') + '">' +
- innerHtml +
- '</button>'
- )
- .click(function(ev) {
- // don't process clicks for disabled buttons
- if (!button.hasClass(tm + '-state-disabled')) {
-
- buttonClick(ev);
-
- // after the click action, if the button becomes the "active" tab, or disabled,
- // it should never have a hover class, so remove it now.
- if (
- button.hasClass(tm + '-state-active') ||
- button.hasClass(tm + '-state-disabled')
- ) {
- button.removeClass(tm + '-state-hover');
- }
- }
- })
- .mousedown(function() {
- // the *down* effect (mouse pressed in).
- // only on buttons that are not the "active" tab, or disabled
- button
- .not('.' + tm + '-state-active')
- .not('.' + tm + '-state-disabled')
- .addClass(tm + '-state-down');
- })
- .mouseup(function() {
- // undo the *down* effect
- button.removeClass(tm + '-state-down');
- })
- .hover(
- function() {
- // the *hover* effect.
- // only on buttons that are not the "active" tab, or disabled
- button
- .not('.' + tm + '-state-active')
- .not('.' + tm + '-state-disabled')
- .addClass(tm + '-state-hover');
- },
- function() {
- // undo the *hover* effect
- button
- .removeClass(tm + '-state-hover')
- .removeClass(tm + '-state-down'); // if mouseleave happens before mouseup
- }
- );
-
- groupChildren = groupChildren.add(button);
- }
- }
- });
-
- if (isOnlyButtons) {
- groupChildren
- .first().addClass(tm + '-corner-left').end()
- .last().addClass(tm + '-corner-right').end();
- }
-
- if (groupChildren.length > 1) {
- groupEl = $('<div/>');
- if (isOnlyButtons) {
- groupEl.addClass('fc-button-group');
- }
- groupEl.append(groupChildren);
- sectionEl.append(groupEl);
- }
- else {
- sectionEl.append(groupChildren); // 1 or 0 children
- }
- });
- }
-
- return sectionEl;
- }
-
-
- function updateTitle(text) {
- if (el) {
- el.find('h2').text(text);
- }
- }
-
-
- function activateButton(buttonName) {
- if (el) {
- el.find('.fc-' + buttonName + '-button')
- .addClass(tm + '-state-active');
- }
- }
-
-
- function deactivateButton(buttonName) {
- if (el) {
- el.find('.fc-' + buttonName + '-button')
- .removeClass(tm + '-state-active');
- }
- }
-
-
- function disableButton(buttonName) {
- if (el) {
- el.find('.fc-' + buttonName + '-button')
- .prop('disabled', true)
- .addClass(tm + '-state-disabled');
- }
- }
-
-
- function enableButton(buttonName) {
- if (el) {
- el.find('.fc-' + buttonName + '-button')
- .prop('disabled', false)
- .removeClass(tm + '-state-disabled');
- }
- }
-
-
- function getViewsWithButtons() {
- return viewsWithButtons;
- }
-
-}
-
-;;
-
FC.sourceNormalizers = [];
FC.sourceFetchers = [];
@@ -10831,38 +11433,39 @@ var eventGUID = 1;
function EventManager() { // assumed to be a calendar
var t = this;
-
-
+
+
// exports
+ t.requestEvents = requestEvents;
+ t.reportEventChange = reportEventChange;
t.isFetchNeeded = isFetchNeeded;
t.fetchEvents = fetchEvents;
t.fetchEventSources = fetchEventSources;
+ t.refetchEvents = refetchEvents;
+ t.refetchEventSources = refetchEventSources;
t.getEventSources = getEventSources;
t.getEventSourceById = getEventSourceById;
- t.getEventSourcesByMatchArray = getEventSourcesByMatchArray;
- t.getEventSourcesByMatch = getEventSourcesByMatch;
t.addEventSource = addEventSource;
t.removeEventSource = removeEventSource;
t.removeEventSources = removeEventSources;
t.updateEvent = updateEvent;
+ t.updateEvents = updateEvents;
t.renderEvent = renderEvent;
+ t.renderEvents = renderEvents;
t.removeEvents = removeEvents;
t.clientEvents = clientEvents;
t.mutateEvent = mutateEvent;
t.normalizeEventDates = normalizeEventDates;
t.normalizeEventTimes = normalizeEventTimes;
-
-
- // imports
- var reportEvents = t.reportEvents;
-
-
+
+
// locals
var stickySource = { events: [] };
var sources = [ stickySource ];
var rangeStart, rangeEnd;
var pendingSourceCnt = 0; // outstanding fetch requests, max one per source
var cache = []; // holds events that have already been expanded
+ var prunedCache; // like cache, but only events that intersect with rangeStart/rangeEnd
$.each(
@@ -10874,9 +11477,55 @@ function EventManager() { // assumed to be a calendar
}
}
);
-
-
-
+
+
+
+ function requestEvents(start, end) {
+ if (!t.options.lazyFetching || isFetchNeeded(start, end)) {
+ return fetchEvents(start, end);
+ }
+ else {
+ return Promise.resolve(prunedCache);
+ }
+ }
+
+
+ function reportEventChange() {
+ prunedCache = filterEventsWithinRange(cache);
+ t.trigger('eventsReset', prunedCache);
+ }
+
+
+ function filterEventsWithinRange(events) {
+ var filteredEvents = [];
+ var i, event;
+
+ for (i = 0; i < events.length; i++) {
+ event = events[i];
+
+ if (
+ event.start.clone().stripZone() < rangeEnd &&
+ t.getEventEnd(event).stripZone() > rangeStart
+ ) {
+ filteredEvents.push(event);
+ }
+ }
+
+ return filteredEvents;
+ }
+
+
+ t.getEventCache = function() {
+ return cache;
+ };
+
+
+ t.getPrunedEventCache = function() {
+ return prunedCache;
+ };
+
+
+
/* Fetching
-----------------------------------------------------------------------------*/
@@ -10886,12 +11535,24 @@ function EventManager() { // assumed to be a calendar
return !rangeStart || // nothing has been fetched yet?
start < rangeStart || end > rangeEnd; // is part of the new range outside of the old range?
}
-
-
+
+
function fetchEvents(start, end) {
rangeStart = start;
rangeEnd = end;
- fetchEventSources(sources, 'reset');
+ return refetchEvents();
+ }
+
+
+ // poorly named. fetches all sources with current `rangeStart` and `rangeEnd`.
+ function refetchEvents() {
+ return fetchEventSources(sources, 'reset');
+ }
+
+
+ // poorly named. fetches a subset of event sources.
+ function refetchEventSources(matchInputs) {
+ return fetchEventSources(getEventSourcesByMatchArray(matchInputs));
}
@@ -10921,9 +11582,17 @@ function EventManager() { // assumed to be a calendar
for (i = 0; i < specificSources.length; i++) {
source = specificSources[i];
-
tryFetchEventSource(source, source._fetchId);
}
+
+ if (pendingSourceCnt) {
+ return new Promise(function(resolve) {
+ t.one('eventsReceived', resolve); // will send prunedCache
+ });
+ }
+ else { // executed all synchronously, or no sources at all
+ return Promise.resolve(prunedCache);
+ }
}
@@ -10956,7 +11625,7 @@ function EventManager() { // assumed to be a calendar
}
if (abstractEvent) { // not false (an invalid event)
- cache.push.apply(
+ cache.push.apply( // append
cache,
expandEvent(abstractEvent) // add individual expanded events to the cache
);
@@ -10984,11 +11653,12 @@ function EventManager() { // assumed to be a calendar
function decrementPendingSourceCnt() {
pendingSourceCnt--;
if (!pendingSourceCnt) {
- reportEvents(cache);
+ reportEventChange(cache); // updates prunedCache
+ t.trigger('eventsReceived', prunedCache);
}
}
-
-
+
+
function _fetchEventSource(source, callback) {
var i;
var fetchers = FC.sourceFetchers;
@@ -11097,9 +11767,9 @@ function EventManager() { // assumed to be a calendar
}
}
}
-
-
-
+
+
+
/* Sources
-----------------------------------------------------------------------------*/
@@ -11108,7 +11778,7 @@ function EventManager() { // assumed to be a calendar
var source = buildEventSource(sourceInput);
if (source) {
sources.push(source);
- fetchEventSources([ source ], 'add'); // will eventually call reportEvents
+ fetchEventSources([ source ], 'add'); // will eventually call reportEventChange
}
}
@@ -11204,7 +11874,7 @@ function EventManager() { // assumed to be a calendar
cache = excludeEventsBySources(cache, targetSources);
}
- reportEvents(cache);
+ reportEventChange();
}
@@ -11298,27 +11968,39 @@ function EventManager() { // assumed to be a calendar
return true; // keep
});
}
-
-
-
+
+
+
/* Manipulation
-----------------------------------------------------------------------------*/
// Only ever called from the externally-facing API
function updateEvent(event) {
+ updateEvents([ event ]);
+ }
- // massage start/end values, even if date string values
- event.start = t.moment(event.start);
- if (event.end) {
- event.end = t.moment(event.end);
- }
- else {
- event.end = null;
+
+ // Only ever called from the externally-facing API
+ function updateEvents(events) {
+ var i, event;
+
+ for (i = 0; i < events.length; i++) {
+ event = events[i];
+
+ // massage start/end values, even if date string values
+ event.start = t.moment(event.start);
+ if (event.end) {
+ event.end = t.moment(event.end);
+ }
+ else {
+ event.end = null;
+ }
+
+ mutateEvent(event, getMiscEventProps(event)); // will handle start/end/allDay normalization
}
- mutateEvent(event, getMiscEventProps(event)); // will handle start/end/allDay normalization
- reportEvents(cache); // reports event modifications (so we can redraw)
+ reportEventChange(); // reports event modifications (so we can redraw)
}
@@ -11342,37 +12024,50 @@ function EventManager() { // assumed to be a calendar
return !/^_|^(id|allDay|start|end)$/.test(name);
}
-
+
// returns the expanded events that were created
function renderEvent(eventInput, stick) {
- var abstractEvent = buildEventFromInput(eventInput);
- var events;
- var i, event;
+ return renderEvents([ eventInput ], stick);
+ }
- if (abstractEvent) { // not false (a valid input)
- events = expandEvent(abstractEvent);
- for (i = 0; i < events.length; i++) {
- event = events[i];
+ // returns the expanded events that were created
+ function renderEvents(eventInputs, stick) {
+ var renderedEvents = [];
+ var renderableEvents;
+ var abstractEvent;
+ var i, j, event;
+
+ for (i = 0; i < eventInputs.length; i++) {
+ abstractEvent = buildEventFromInput(eventInputs[i]);
+
+ if (abstractEvent) { // not false (a valid input)
+ renderableEvents = expandEvent(abstractEvent);
- if (!event.source) {
- if (stick) {
- stickySource.events.push(event);
- event.source = stickySource;
+ for (j = 0; j < renderableEvents.length; j++) {
+ event = renderableEvents[j];
+
+ if (!event.source) {
+ if (stick) {
+ stickySource.events.push(event);
+ event.source = stickySource;
+ }
+ cache.push(event);
}
- cache.push(event);
}
- }
- reportEvents(cache);
+ renderedEvents = renderedEvents.concat(renderableEvents);
+ }
+ }
- return events;
+ if (renderedEvents.length) { // any new events rendered?
+ reportEventChange();
}
- return [];
+ return renderedEvents;
}
-
-
+
+
function removeEvents(filter) {
var eventID;
var i;
@@ -11399,10 +12094,10 @@ function EventManager() { // assumed to be a calendar
}
}
- reportEvents(cache);
+ reportEventChange();
}
-
+
function clientEvents(filter) {
if ($.isFunction(filter)) {
return $.grep(cache, filter);
@@ -11442,8 +12137,8 @@ function EventManager() { // assumed to be a calendar
}
backupEventDates(event);
}
-
-
+
+
/* Event Normalization
-----------------------------------------------------------------------------*/
@@ -11853,11 +12548,6 @@ function EventManager() { // assumed to be a calendar
};
}
-
- t.getEventCache = function() {
- return cache;
- };
-
}
@@ -12386,13 +13076,18 @@ var BasicView = FC.BasicView = View.extend({
------------------------------------------------------------------------------------------------------------------*/
+ computeInitialScroll: function() {
+ return { top: 0 };
+ },
+
+
queryScroll: function() {
- return this.scroller.getScrollTop();
+ return { top: this.scroller.getScrollTop() };
},
- setScroll: function(top) {
- this.scroller.setScrollTop(top);
+ setScroll: function(scroll) {
+ this.scroller.setScrollTop(scroll.top);
},
@@ -12909,17 +13604,17 @@ var AgendaView = FC.AgendaView = View.extend({
top++; // to overcome top border that slots beyond the first have. looks better
}
- return top;
+ return { top: top };
},
queryScroll: function() {
- return this.scroller.getScrollTop();
+ return { top: this.scroller.getScrollTop() };
},
- setScroll: function(top) {
- this.scroller.setScrollTop(top);
+ setScroll: function(scroll) {
+ this.scroller.setScrollTop(scroll.top);
},