From c9cbd2f4f6cb432c6558bd88d783b356f0311a09 Mon Sep 17 00:00:00 2001 From: Mario Vavti Date: Fri, 30 Dec 2016 10:18:05 +0100 Subject: update fullcalendar to version 3.1 --- library/fullcalendar/fullcalendar.js | 2645 +++++++++++++++++++++------------- 1 file changed, 1670 insertions(+), 975 deletions(-) (limited to 'library/fullcalendar/fullcalendar.js') 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 '' + - '"); + } + else { + el.empty(); + } + el.append(renderSection('left')) + .append(renderSection('right')) + .append(renderSection('center')) + .append('
'); + } + else { + removeElement(); + } + } - return cache[viewType] || (cache[viewType] = this.buildViewSpec(viewType)); - }, + function removeElement() { + if (el) { + el.remove(); + el = t.el = null; + } + } - // Given a duration singular unit, like "week" or "day", finds a matching view spec. - // Preference is given to views that have corresponding buttons. - getUnitViewSpec: function(unit) { - var viewTypes; - var i; - var spec; - if ($.inArray(unit, intervalUnits) != -1) { + function renderSection(position) { + var sectionEl = $('
'); + var buttonStr = toolbarOptions.layout[position]; - // put views that have buttons first. there will be duplicates, but oh well - viewTypes = this.header.getViewsWithButtons(); - $.each(FC.views, function(viewType) { // all views - viewTypes.push(viewType); - }); + if (buttonStr) { + $.each(buttonStr.split(' '), function(i) { + var groupChildren = $(); + var isOnlyButtons = true; + var groupEl; - for (i = 0; i < viewTypes.length; i++) { - spec = this.getViewSpec(viewTypes[i]); - if (spec) { - if (spec.singleUnit == unit) { - return spec; + $.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($('

 

')); // 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) { - // Builds an object with information on how to create a given view - buildViewSpec: function(requestedViewType) { - var viewOverrides = this.overrides.views || {}; - var specChain = []; // for the view. lowest to highest priority - var defaultsChain = []; // for the view. lowest to highest priority - var overridesChain = []; // for the view. lowest to highest priority - var viewType = requestedViewType; - var spec; // for the view - var overrides; // for the view - var duration; - var unit; + themeIcon = + customButtonProps ? + customButtonProps.themeIcon : + calendar.options.themeButtonIcons[buttonName]; - // iterate from the specific view definition to a more general one until we hit an actual View class - while (viewType) { - spec = fcViews[viewType]; - overrides = viewOverrides[viewType]; - viewType = null; // clear. might repopulate for another iteration + normalIcon = + customButtonProps ? + customButtonProps.icon : + calendar.options.buttonIcons[buttonName]; - if (typeof spec === 'function') { // TODO: deprecate - spec = { 'class': spec }; - } + if (overrideText) { + innerHtml = htmlEscape(overrideText); + } + else if (themeIcon && calendar.options.theme) { + innerHtml = ""; + } + else if (normalIcon && !calendar.options.theme) { + innerHtml = ""; + } + else { + innerHtml = htmlEscape(defaultText); + } - if (spec) { - specChain.unshift(spec); - defaultsChain.unshift(spec.defaults || {}); - duration = duration || spec.duration; - viewType = viewType || spec.type; - } + classes = [ + 'fc-' + buttonName + '-button', + tm + '-button', + tm + '-state-default' + ]; - if (overrides) { - overridesChain.unshift(overrides); // view-specific option hashes have options at zero-level - duration = duration || overrides.duration; - viewType = viewType || overrides.type; - } - } + button = $( // type="button" so that it doesn't submit a form + '' + ) + .click(function(ev) { + // don't process clicks for disabled buttons + if (!button.hasClass(tm + '-state-disabled')) { - spec = mergeProps(specChain); - spec.type = requestedViewType; - if (!spec['class']) { - return false; - } + buttonClick(ev); - if (duration) { - duration = moment.duration(duration); - if (duration.valueOf()) { // valid? - spec.duration = duration; - unit = computeIntervalUnit(duration); + // 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 + } + ); - // view is a single-unit duration, like "week" or "day" - // incorporate options for this. lowest priority - if (duration.as(unit) === 1) { - spec.singleUnit = unit; - overridesChain.unshift(viewOverrides[unit] || {}); + groupChildren = groupChildren.add(button); + } + } + }); + + if (isOnlyButtons) { + groupChildren + .first().addClass(tm + '-corner-left').end() + .last().addClass(tm + '-corner-right').end(); } - } - } - spec.defaults = mergeOptions(defaultsChain); - spec.overrides = mergeOptions(overridesChain); + if (groupChildren.length > 1) { + groupEl = $('
'); + if (isOnlyButtons) { + groupEl.addClass('fc-button-group'); + } + groupEl.append(groupChildren); + sectionEl.append(groupEl); + } + else { + sectionEl.append(groupChildren); // 1 or 0 children + } + }); + } - this.buildViewSpecOptions(spec); - this.buildViewSpecButtonText(spec, requestedViewType); + return sectionEl; + } - return spec; - }, + function updateTitle(text) { + if (el) { + el.find('h2').text(text); + } + } - // Builds and assigns a view spec's options object from its already-assigned defaults and overrides - buildViewSpecOptions: function(spec) { - spec.options = mergeOptions([ // lowest to highest priority - Calendar.defaults, // global defaults - spec.defaults, // view's defaults (from ViewSubclass.defaults) - this.dirDefaults, - this.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) - this.dynamicOverrides // dynamically set via setter. highest precedence - ]); - populateInstanceComputableOptions(spec.options); - }, + function activateButton(buttonName) { + if (el) { + el.find('.fc-' + buttonName + '-button') + .addClass(tm + '-state-active'); + } + } - // Computes and assigns a view spec's buttonText-related options - buildViewSpecButtonText: function(spec, requestedViewType) { - // given an options object with a possible `buttonText` hash, lookup the buttonText for the - // requested view, falling back to a generic unit entry like "week" or "day" - function queryButtonText(options) { - var buttonText = options.buttonText || {}; - return buttonText[requestedViewType] || - // 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); + function deactivateButton(buttonName) { + if (el) { + el.find('.fc-' + buttonName + '-button') + .removeClass(tm + '-state-active'); } + } - // 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.localeDefaults) || - queryButtonText(this.dirDefaults) || + 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({ + + dirDefaults: null, // option defaults related to LTR or RTL + 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 + header: null, + footer: null, + loadingLevel: 0, // number of simultaneous loading tasks + + + // a lot of this class' OOP logic is scoped within this constructor function, + // but in the future, write individual methods on the prototype. + constructor: Calendar_constructor, + + + // Subclasses can override this for initialization logic after the constructor has been called + initialize: function() { + }, + + + // 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; + + 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( // 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.localeDefaults = localeDefaults; + this.options = mergeOptions([ // merge defaults and overrides. lowest to highest precedence + Calendar.defaults, // global defaults + dirDefaults, + localeDefaults, + this.overrides, + this.dynamicOverrides + ]); + populateInstanceComputableOptions(this.options); // fill in gaps with computed options + }, + + + // Gets information about how to create a view. Will use a cache. + getViewSpec: function(viewType) { + var cache = this.viewSpecCache; + + return cache[viewType] || (cache[viewType] = this.buildViewSpec(viewType)); + }, + + + // Given a duration singular unit, like "week" or "day", finds a matching view spec. + // Preference is given to views that have corresponding buttons. + getUnitViewSpec: function(unit) { + var viewTypes; + var i; + var spec; + + if ($.inArray(unit, intervalUnits) != -1) { + + // put views that have buttons first. there will be duplicates, but oh well + viewTypes = this.header.getViewsWithButtons(); // TODO: include footer as well? + $.each(FC.views, function(viewType) { // all views + viewTypes.push(viewType); + }); + + for (i = 0; i < viewTypes.length; i++) { + spec = this.getViewSpec(viewTypes[i]); + if (spec) { + if (spec.singleUnit == unit) { + return spec; + } + } + } + } + }, + + + // Builds an object with information on how to create a given view + buildViewSpec: function(requestedViewType) { + var viewOverrides = this.overrides.views || {}; + var specChain = []; // for the view. lowest to highest priority + var defaultsChain = []; // for the view. lowest to highest priority + var overridesChain = []; // for the view. lowest to highest priority + var viewType = requestedViewType; + var spec; // for the view + var overrides; // for the view + var duration; + var unit; + + // iterate from the specific view definition to a more general one until we hit an actual View class + while (viewType) { + spec = fcViews[viewType]; + overrides = viewOverrides[viewType]; + viewType = null; // clear. might repopulate for another iteration + + if (typeof spec === 'function') { // TODO: deprecate + spec = { 'class': spec }; + } + + if (spec) { + specChain.unshift(spec); + defaultsChain.unshift(spec.defaults || {}); + duration = duration || spec.duration; + viewType = viewType || spec.type; + } + + if (overrides) { + overridesChain.unshift(overrides); // view-specific option hashes have options at zero-level + duration = duration || overrides.duration; + viewType = viewType || overrides.type; + } + } + + spec = mergeProps(specChain); + spec.type = requestedViewType; + if (!spec['class']) { + return false; + } + + if (duration) { + duration = moment.duration(duration); + if (duration.valueOf()) { // valid? + spec.duration = duration; + unit = computeIntervalUnit(duration); + + // view is a single-unit duration, like "week" or "day" + // incorporate options for this. lowest priority + if (duration.as(unit) === 1) { + spec.singleUnit = unit; + overridesChain.unshift(viewOverrides[unit] || {}); + } + } + } + + spec.defaults = mergeOptions(defaultsChain); + spec.overrides = mergeOptions(overridesChain); + + this.buildViewSpecOptions(spec); + this.buildViewSpecButtonText(spec, requestedViewType); + + return spec; + }, + + + // Builds and assigns a view spec's options object from its already-assigned defaults and overrides + buildViewSpecOptions: function(spec) { + spec.options = mergeOptions([ // lowest to highest priority + Calendar.defaults, // global defaults + spec.defaults, // view's defaults (from ViewSubclass.defaults) + this.dirDefaults, + this.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) + this.dynamicOverrides // dynamically set via setter. highest precedence + ]); + populateInstanceComputableOptions(spec.options); + }, + + + // Computes and assigns a view spec's buttonText-related options + buildViewSpecButtonText: function(spec, requestedViewType) { + + // given an options object with a possible `buttonText` hash, lookup the buttonText for the + // requested view, falling back to a generic unit entry like "week" or "day" + function queryButtonText(options) { + var buttonText = options.buttonText || {}; + return buttonText[requestedViewType] || + // 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.localeDefaults) || + queryButtonText(this.dirDefaults) || spec.defaults.buttonText || // a single string. from ViewSubclass.defaults queryButtonText(Calendar.defaults) || (spec.duration ? this.humanizeDuration(spec.duration) : null) || // like "3 days" @@ -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 = $("
").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( $("
").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); + } + + currentView.setDate(date, forcedScroll); - // need to do this after View::render, so dates are calculated - updateHeaderTitle(); - updateTodayButton(); + 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', @@ -10430,392 +11301,123 @@ var dpComputableOptions = { // the translations sometimes wrongly contain HTML entities prev: stripHtmlEntities(dpOptions.prevText), next: stripHtmlEntities(dpOptions.nextText), - today: stripHtmlEntities(dpOptions.currentText) - }; - }, - - // Produces format strings like "MMMM YYYY" -> "September 2014" - monthYearFormat: function(dpOptions) { - return dpOptions.showMonthAfterYear ? - 'YYYY[' + dpOptions.yearSuffix + '] MMMM' : - 'MMMM YYYY[' + dpOptions.yearSuffix + ']'; - } - -}; - -var momComputableOptions = { - - // Produces format strings like "ddd M/D" -> "Fri 9/15" - dayOfMonthFormat: function(momOptions, fcOptions) { - var format = momOptions.longDateFormat('l'); // for the format like "M/D/YYYY" - - // strip the year off the edge, as well as other misc non-whitespace chars - format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, ''); - - if (fcOptions.isRTL) { - format += ' ddd'; // for RTL, add day-of-week to end - } - else { - format = 'ddd ' + format; // for LTR, add day-of-week to beginning - } - return format; - }, - - // Produces format strings like "h:mma" -> "6:00pm" - mediumTimeFormat: function(momOptions) { // can't be called `timeFormat` because collides with option - return momOptions.longDateFormat('LT') - .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand - }, - - // Produces format strings like "h(:mm)a" -> "6pm" / "6:30pm" - smallTimeFormat: function(momOptions) { - return momOptions.longDateFormat('LT') - .replace(':mm', '(:mm)') - .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales - .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand - }, - - // Produces format strings like "h(:mm)t" -> "6p" / "6:30p" - extraSmallTimeFormat: function(momOptions) { - return momOptions.longDateFormat('LT') - .replace(':mm', '(:mm)') - .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales - .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand - }, - - // Produces format strings like "ha" / "H" -> "6pm" / "18" - hourFormat: function(momOptions) { - return momOptions.longDateFormat('LT') - .replace(':mm', '') - .replace(/(\Wmm)$/, '') // like above, but for foreign locales - .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand - }, - - // Produces format strings like "h:mm" -> "6:30" (with no AM/PM) - noMeridiemTimeFormat: function(momOptions) { - return momOptions.longDateFormat('LT') - .replace(/\s*a$/i, ''); // remove trailing AM/PM - } - -}; - - -// options that should be computed off live calendar options (considers override options) -// TODO: best place for this? related to locale? -// TODO: flipping text based on isRTL is a bad idea because the CSS `direction` might want to handle it -var instanceComputableOptions = { - - // Produces format strings for results like "Mo 16" - smallDayDateFormat: function(options) { - return options.isRTL ? - 'D dd' : - 'dd D'; - }, - - // Produces format strings for results like "Wk 5" - weekFormat: function(options) { - return options.isRTL ? - 'w[ ' + options.weekNumberTitle + ']' : - '[' + options.weekNumberTitle + ' ]w'; - }, - - // Produces format strings for results like "Wk5" - smallWeekFormat: function(options) { - return options.isRTL ? - 'w[' + options.weekNumberTitle + ']' : - '[' + options.weekNumberTitle + ']w'; - } - -}; - -function populateInstanceComputableOptions(options) { - $.each(instanceComputableOptions, function(name, func) { - if (options[name] == null) { - options[name] = func(options); - } - }); -} - - -// Returns moment's internal locale data. If doesn't exist, returns English. -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.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 = $("
"); - } - else { - el.empty(); - } - el.append(renderSection('left')) - .append(renderSection('right')) - .append(renderSection('center')) - .append('
'); - } - else { - removeElement(); - } - } - - - function removeElement() { - if (el) { - el.remove(); - el = t.el = null; - } - } - - - function renderSection(position) { - var sectionEl = $('
'); - 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($('

 

')); // 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 = ""; - } - else if (normalIcon && !options.theme) { - innerHtml = ""; - } - else { - innerHtml = htmlEscape(defaultText); - } - - classes = [ - 'fc-' + buttonName + '-button', - tm + '-button', - tm + '-state-default' - ]; - - button = $( // type="button" so that it doesn't submit a form - '' - ) - .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 = $('
'); - 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'); - } + today: stripHtmlEntities(dpOptions.currentText) + }; + }, + + // Produces format strings like "MMMM YYYY" -> "September 2014" + monthYearFormat: function(dpOptions) { + return dpOptions.showMonthAfterYear ? + 'YYYY[' + dpOptions.yearSuffix + '] MMMM' : + 'MMMM YYYY[' + dpOptions.yearSuffix + ']'; } - - - function disableButton(buttonName) { - if (el) { - el.find('.fc-' + buttonName + '-button') - .prop('disabled', true) - .addClass(tm + '-state-disabled'); + +}; + +var momComputableOptions = { + + // Produces format strings like "ddd M/D" -> "Fri 9/15" + dayOfMonthFormat: function(momOptions, fcOptions) { + var format = momOptions.longDateFormat('l'); // for the format like "M/D/YYYY" + + // strip the year off the edge, as well as other misc non-whitespace chars + format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, ''); + + if (fcOptions.isRTL) { + format += ' ddd'; // for RTL, add day-of-week to end } - } - - - function enableButton(buttonName) { - if (el) { - el.find('.fc-' + buttonName + '-button') - .prop('disabled', false) - .removeClass(tm + '-state-disabled'); + else { + format = 'ddd ' + format; // for LTR, add day-of-week to beginning } + return format; + }, + + // Produces format strings like "h:mma" -> "6:00pm" + mediumTimeFormat: function(momOptions) { // can't be called `timeFormat` because collides with option + return momOptions.longDateFormat('LT') + .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand + }, + + // Produces format strings like "h(:mm)a" -> "6pm" / "6:30pm" + smallTimeFormat: function(momOptions) { + return momOptions.longDateFormat('LT') + .replace(':mm', '(:mm)') + .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales + .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand + }, + + // Produces format strings like "h(:mm)t" -> "6p" / "6:30p" + extraSmallTimeFormat: function(momOptions) { + return momOptions.longDateFormat('LT') + .replace(':mm', '(:mm)') + .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales + .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand + }, + + // Produces format strings like "ha" / "H" -> "6pm" / "18" + hourFormat: function(momOptions) { + return momOptions.longDateFormat('LT') + .replace(':mm', '') + .replace(/(\Wmm)$/, '') // like above, but for foreign locales + .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand + }, + + // Produces format strings like "h:mm" -> "6:30" (with no AM/PM) + noMeridiemTimeFormat: function(momOptions) { + return momOptions.longDateFormat('LT') + .replace(/\s*a$/i, ''); // remove trailing AM/PM } +}; - function getViewsWithButtons() { - return viewsWithButtons; + +// options that should be computed off live calendar options (considers override options) +// 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 = { + + // Produces format strings for results like "Mo 16" + smallDayDateFormat: function(options) { + return options.isRTL ? + 'D dd' : + 'dd D'; + }, + + // Produces format strings for results like "Wk 5" + weekFormat: function(options) { + return options.isRTL ? + 'w[ ' + options.weekNumberTitle + ']' : + '[' + options.weekNumberTitle + ' ]w'; + }, + + // Produces format strings for results like "Wk5" + smallWeekFormat: function(options) { + return options.isRTL ? + 'w[' + options.weekNumberTitle + ']' : + '[' + options.weekNumberTitle + ']w'; } +}; + +function populateInstanceComputableOptions(options) { + $.each(instanceComputableOptions, function(name, func) { + if (options[name] == null) { + options[name] = func(options); + } + }); +} + + +// Returns moment's internal locale data. If doesn't exist, returns English. +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.locale('en', Calendar.englishDefaults); + ;; FC.sourceNormalizers = []; @@ -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); + + for (j = 0; j < renderableEvents.length; j++) { + event = renderableEvents[j]; - if (!event.source) { - if (stick) { - stickySource.events.push(event); - event.source = stickySource; + 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); }, -- cgit v1.2.3