fullcalendar.io-rails 3.0.1 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (26) hide show
  1. checksums.yaml +4 -4
  2. data/fullcalendar.io-rails.gemspec +1 -1
  3. data/lib/fullcalendar.io/rails/version.rb +1 -1
  4. data/vendor/assets/javascripts/fullcalendar.js +1670 -975
  5. data/vendor/assets/javascripts/fullcalendar/gcal.js +1 -1
  6. data/vendor/assets/javascripts/fullcalendar/locale-all.js +5 -5
  7. data/vendor/assets/javascripts/fullcalendar/locale/af.js +1 -0
  8. data/vendor/assets/javascripts/fullcalendar/locale/ar-dz.js +1 -0
  9. data/vendor/assets/javascripts/fullcalendar/locale/ar-ly.js +1 -0
  10. data/vendor/assets/javascripts/fullcalendar/locale/ar-sa.js +1 -1
  11. data/vendor/assets/javascripts/fullcalendar/locale/ca.js +1 -1
  12. data/vendor/assets/javascripts/fullcalendar/locale/gl.js +1 -1
  13. data/vendor/assets/javascripts/fullcalendar/locale/hr.js +1 -1
  14. data/vendor/assets/javascripts/fullcalendar/locale/hu.js +1 -1
  15. data/vendor/assets/javascripts/fullcalendar/locale/kk.js +1 -0
  16. data/vendor/assets/javascripts/fullcalendar/locale/lt.js +1 -1
  17. data/vendor/assets/javascripts/fullcalendar/locale/nl-be.js +1 -0
  18. data/vendor/assets/javascripts/fullcalendar/locale/nl.js +1 -1
  19. data/vendor/assets/javascripts/fullcalendar/locale/pl.js +1 -1
  20. data/vendor/assets/javascripts/fullcalendar/locale/sl.js +1 -1
  21. data/vendor/assets/javascripts/fullcalendar/locale/sr-cyrl.js +1 -1
  22. data/vendor/assets/javascripts/fullcalendar/locale/sr.js +1 -1
  23. data/vendor/assets/javascripts/fullcalendar/locale/th.js +1 -1
  24. data/vendor/assets/stylesheets/fullcalendar.css +9 -2
  25. data/vendor/assets/stylesheets/fullcalendar.print.css +1 -1
  26. metadata +79 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 11590cdad9631910cc61d4e7c0a4e295b7a47a89
4
- data.tar.gz: 79c00bec42488a518f494f7f2ecea97eee7208be
3
+ metadata.gz: 360b2048acb86aa79d0aac78fc0ca49be99591bd
4
+ data.tar.gz: b511e90f3ace317e3ef591ebe25f32c1e12ebcc8
5
5
  SHA512:
6
- metadata.gz: ef8ed02bf8ef14eb098da768b9a3c55424972f096559be6736d66ebd303fa8578e26bc55549e18e8df32c8f274e8908a2453ffe5dd3e566259f7c841be13c45c
7
- data.tar.gz: ce5fe2797bcda5659fadb3801655ed95f027c1205f7997bd767e83b3c91c1d438c7803345d88c708b9b1ffa3fe940877351854cefcf2527b8ea02548638e8969
6
+ metadata.gz: 533d93407cd7579f11255710338b4e80e45278cb690a826cac9eb311b98e250883bfc610f48df0936831daf4df8747967324bb293a4c3e0e6ef282875ce186e6
7
+ data.tar.gz: eba2626f57e3e8f9132d29956fc97175c6d953595ac9c802aa263715ca0da7e96764937b68703ad5bfc356f006f9c35904dc85fe5d8e1b3a93a04ca0a2e7c843
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
20
20
  spec.test_files = Dir["spec/**/*"]
21
21
 
22
22
  spec.add_runtime_dependency 'jquery-rails', '>= 3.1.0', '< 5.0'
23
- spec.add_runtime_dependency 'momentjs-rails', '~> 2.11', '>= 2.11.0'
23
+ spec.add_runtime_dependency 'momentjs-rails', '~> 2.15'
24
24
  spec.add_development_dependency "bundler", "~> 1.7"
25
25
  spec.add_development_dependency "rake", "~> 10.0"
26
26
  spec.add_development_dependency 'rails', '4.2.5'
@@ -1,5 +1,5 @@
1
1
  module Fullcalendario
2
2
  module Rails
3
- VERSION = "3.0.1"
3
+ VERSION = "3.1.0"
4
4
  end
5
5
  end
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * FullCalendar v3.0.1
2
+ * FullCalendar v3.1.0
3
3
  * Docs & License: http://fullcalendar.io/
4
4
  * (c) 2016 Adam Shaw
5
5
  */
@@ -19,8 +19,8 @@
19
19
  ;;
20
20
 
21
21
  var FC = $.fullCalendar = {
22
- version: "3.0.1",
23
- internalApiVersion: 6
22
+ version: "3.1.0",
23
+ internalApiVersion: 7
24
24
  };
25
25
  var fcViews = FC.views = {};
26
26
 
@@ -53,13 +53,14 @@ $.fn.fullCalendar = function(options) {
53
53
  calendar.render();
54
54
  }
55
55
  });
56
-
56
+
57
57
  return res;
58
58
  };
59
59
 
60
60
 
61
61
  var complexOptions = [ // names of options that are objects whose properties should be combined
62
62
  'header',
63
+ 'footer',
63
64
  'buttonText',
64
65
  'buttonIcons',
65
66
  'themeButtonIcons'
@@ -848,6 +849,7 @@ function createObject(proto) {
848
849
  f.prototype = proto;
849
850
  return new f();
850
851
  }
852
+ FC.createObject = createObject;
851
853
 
852
854
 
853
855
  function copyOwnProps(src, dest) {
@@ -1004,20 +1006,6 @@ function debounce(func, wait, immediate) {
1004
1006
  };
1005
1007
  }
1006
1008
 
1007
-
1008
- // HACK around jQuery's now A+ promises: execute callback synchronously if already resolved.
1009
- // thenFunc shouldn't accept args.
1010
- // similar to whenResources in Scheduler plugin.
1011
- function syncThen(promise, thenFunc) {
1012
- // not a promise, or an already-resolved promise?
1013
- if (!promise || !promise.then || promise.state() === 'resolved') {
1014
- return $.when(thenFunc()); // resolve immediately
1015
- }
1016
- else if (thenFunc) {
1017
- return promise.then(thenFunc);
1018
- }
1019
- }
1020
-
1021
1009
  ;;
1022
1010
 
1023
1011
  /*
@@ -1681,6 +1669,201 @@ function mixIntoClass(theClass, members) {
1681
1669
  }
1682
1670
  ;;
1683
1671
 
1672
+ /*
1673
+ Wrap jQuery's Deferred Promise object to be slightly more Promise/A+ compliant.
1674
+ With the added non-standard feature of synchronously executing handlers on resolved promises,
1675
+ which doesn't always happen otherwise (esp with nested .then handlers!?),
1676
+ so, this makes things a lot easier, esp because jQuery 3 changed the synchronicity for Deferred objects.
1677
+
1678
+ TODO: write tests and more comments
1679
+ */
1680
+
1681
+ function Promise(executor) {
1682
+ var deferred = $.Deferred();
1683
+ var promise = deferred.promise();
1684
+
1685
+ if (typeof executor === 'function') {
1686
+ executor(
1687
+ function(value) { // resolve
1688
+ if (Promise.immediate) {
1689
+ promise._value = value;
1690
+ }
1691
+ deferred.resolve(value);
1692
+ },
1693
+ function() { // reject
1694
+ deferred.reject();
1695
+ }
1696
+ );
1697
+ }
1698
+
1699
+ if (Promise.immediate) {
1700
+ var origThen = promise.then;
1701
+
1702
+ promise.then = function(onFulfilled, onRejected) {
1703
+ var state = promise.state();
1704
+
1705
+ if (state === 'resolved') {
1706
+ if (typeof onFulfilled === 'function') {
1707
+ return Promise.resolve(onFulfilled(promise._value));
1708
+ }
1709
+ }
1710
+ else if (state === 'rejected') {
1711
+ if (typeof onRejected === 'function') {
1712
+ onRejected();
1713
+ return promise; // already rejected
1714
+ }
1715
+ }
1716
+
1717
+ return origThen.call(promise, onFulfilled, onRejected);
1718
+ };
1719
+ }
1720
+
1721
+ return promise; // instanceof Promise will break :( TODO: make Promise a real class
1722
+ }
1723
+
1724
+ FC.Promise = Promise;
1725
+
1726
+ Promise.immediate = true;
1727
+
1728
+
1729
+ Promise.resolve = function(value) {
1730
+ if (value && typeof value.resolve === 'function') {
1731
+ return value.promise();
1732
+ }
1733
+ if (value && typeof value.then === 'function') {
1734
+ return value;
1735
+ }
1736
+ else {
1737
+ var deferred = $.Deferred().resolve(value);
1738
+ var promise = deferred.promise();
1739
+
1740
+ if (Promise.immediate) {
1741
+ var origThen = promise.then;
1742
+
1743
+ promise._value = value;
1744
+
1745
+ promise.then = function(onFulfilled, onRejected) {
1746
+ if (typeof onFulfilled === 'function') {
1747
+ return Promise.resolve(onFulfilled(value));
1748
+ }
1749
+ return origThen.call(promise, onFulfilled, onRejected);
1750
+ };
1751
+ }
1752
+
1753
+ return promise;
1754
+ }
1755
+ };
1756
+
1757
+
1758
+ Promise.reject = function() {
1759
+ return $.Deferred().reject().promise();
1760
+ };
1761
+
1762
+
1763
+ Promise.all = function(inputs) {
1764
+ var hasAllValues = false;
1765
+ var values;
1766
+ var i, input;
1767
+
1768
+ if (Promise.immediate) {
1769
+ hasAllValues = true;
1770
+ values = [];
1771
+
1772
+ for (i = 0; i < inputs.length; i++) {
1773
+ input = inputs[i];
1774
+
1775
+ if (input && typeof input.state === 'function' && input.state() === 'resolved' && ('_value' in input)) {
1776
+ values.push(input._value);
1777
+ }
1778
+ else if (input && typeof input.then === 'function') {
1779
+ hasAllValues = false;
1780
+ break;
1781
+ }
1782
+ else {
1783
+ values.push(input);
1784
+ }
1785
+ }
1786
+ }
1787
+
1788
+ if (hasAllValues) {
1789
+ return Promise.resolve(values);
1790
+ }
1791
+ else {
1792
+ return $.when.apply($.when, inputs).then(function() {
1793
+ return $.when($.makeArray(arguments));
1794
+ });
1795
+ }
1796
+ };
1797
+
1798
+ ;;
1799
+
1800
+ // TODO: write tests and clean up code
1801
+
1802
+ function TaskQueue(debounceWait) {
1803
+ var q = []; // array of runFuncs
1804
+
1805
+ function addTask(taskFunc) {
1806
+ return new Promise(function(resolve) {
1807
+
1808
+ // should run this function when it's taskFunc's turn to run.
1809
+ // responsible for popping itself off the queue.
1810
+ var runFunc = function() {
1811
+ Promise.resolve(taskFunc()) // result might be async, coerce to promise
1812
+ .then(resolve) // resolve TaskQueue::push's promise, for the caller. will receive result of taskFunc.
1813
+ .then(function() {
1814
+ q.shift(); // pop itself off
1815
+
1816
+ // run the next task, if any
1817
+ if (q.length) {
1818
+ q[0]();
1819
+ }
1820
+ });
1821
+ };
1822
+
1823
+ // always put the task at the end of the queue, BEFORE running the task
1824
+ q.push(runFunc);
1825
+
1826
+ // if it's the only task in the queue, run immediately
1827
+ if (q.length === 1) {
1828
+ runFunc();
1829
+ }
1830
+ });
1831
+ }
1832
+
1833
+ this.add = // potentially debounce, for the public method
1834
+ typeof debounceWait === 'number' ?
1835
+ debounce(addTask, debounceWait) :
1836
+ addTask; // if not a number (null/undefined/false), no debounce at all
1837
+
1838
+ this.addQuickly = addTask; // guaranteed no debounce
1839
+ }
1840
+
1841
+ FC.TaskQueue = TaskQueue;
1842
+
1843
+ /*
1844
+ q = new TaskQueue();
1845
+
1846
+ function work(i) {
1847
+ return q.push(function() {
1848
+ trigger();
1849
+ console.log('work' + i);
1850
+ });
1851
+ }
1852
+
1853
+ var cnt = 0;
1854
+
1855
+ function trigger() {
1856
+ if (cnt < 5) {
1857
+ cnt++;
1858
+ work(cnt);
1859
+ }
1860
+ }
1861
+
1862
+ work(9);
1863
+ */
1864
+
1865
+ ;;
1866
+
1684
1867
  var EmitterMixin = FC.EmitterMixin = {
1685
1868
 
1686
1869
  // jQuery-ification via $(this) allows a non-DOM object to have
@@ -1688,7 +1871,18 @@ var EmitterMixin = FC.EmitterMixin = {
1688
1871
 
1689
1872
 
1690
1873
  on: function(types, handler) {
1874
+ $(this).on(types, this._prepareIntercept(handler));
1875
+ return this; // for chaining
1876
+ },
1877
+
1878
+
1879
+ one: function(types, handler) {
1880
+ $(this).one(types, this._prepareIntercept(handler));
1881
+ return this; // for chaining
1882
+ },
1691
1883
 
1884
+
1885
+ _prepareIntercept: function(handler) {
1692
1886
  // handlers are always called with an "event" object as their first param.
1693
1887
  // sneak the `this` context and arguments into the extra parameter object
1694
1888
  // and forward them on to the original handler.
@@ -1708,9 +1902,7 @@ var EmitterMixin = FC.EmitterMixin = {
1708
1902
  }
1709
1903
  intercept.guid = handler.guid;
1710
1904
 
1711
- $(this).on(types, intercept);
1712
-
1713
- return this; // for chaining
1905
+ return intercept;
1714
1906
  },
1715
1907
 
1716
1908
 
@@ -2039,9 +2231,15 @@ var CoordCache = FC.CoordCache = Class.extend({
2039
2231
  // Queries the els for coordinates and stores them.
2040
2232
  // Call this method before using and of the get* methods below.
2041
2233
  build: function() {
2042
- var offsetParentEl = this.forcedOffsetParentEl || this.els.eq(0).offsetParent();
2234
+ var offsetParentEl = this.forcedOffsetParentEl;
2235
+ if (!offsetParentEl && this.els.length > 0) {
2236
+ offsetParentEl = this.els.eq(0).offsetParent();
2237
+ }
2238
+
2239
+ this.origin = offsetParentEl ?
2240
+ offsetParentEl.offset() :
2241
+ null;
2043
2242
 
2044
- this.origin = offsetParentEl.offset();
2045
2243
  this.boundingRect = this.queryBoundingRect();
2046
2244
 
2047
2245
  if (this.isHorizontal) {
@@ -2224,12 +2422,19 @@ var CoordCache = FC.CoordCache = Class.extend({
2224
2422
 
2225
2423
  // Compute and return what the elements' bounding rectangle is, from the user's perspective.
2226
2424
  // Right now, only returns a rectangle if constrained by an overflow:scroll element.
2425
+ // Returns null if there are no elements
2227
2426
  queryBoundingRect: function() {
2228
- var scrollParentEl = getScrollParent(this.els.eq(0));
2427
+ var scrollParentEl;
2428
+
2429
+ if (this.els.length > 0) {
2430
+ scrollParentEl = getScrollParent(this.els.eq(0));
2229
2431
 
2230
- if (!scrollParentEl.is(document)) {
2231
- return getClientRect(scrollParentEl);
2432
+ if (!scrollParentEl.is(document)) {
2433
+ return getClientRect(scrollParentEl);
2434
+ }
2232
2435
  }
2436
+
2437
+ return null;
2233
2438
  },
2234
2439
 
2235
2440
  isPointInBounds: function(leftOffset, topOffset) {
@@ -3448,6 +3653,7 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
3448
3653
 
3449
3654
  dayTouchStart: function(ev) {
3450
3655
  var view = this.view;
3656
+ var selectLongPressDelay = view.opt('selectLongPressDelay');
3451
3657
 
3452
3658
  // HACK to prevent a user's clickaway for unselecting a range or an event
3453
3659
  // from causing a dayClick.
@@ -3455,8 +3661,12 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
3455
3661
  this.tempIgnoreMouse();
3456
3662
  }
3457
3663
 
3664
+ if (selectLongPressDelay == null) {
3665
+ selectLongPressDelay = view.opt('longPressDelay'); // fallback
3666
+ }
3667
+
3458
3668
  this.dayDragListener.startInteraction(ev, {
3459
- delay: this.view.opt('longPressDelay')
3669
+ delay: selectLongPressDelay
3460
3670
  });
3461
3671
  },
3462
3672
 
@@ -3793,7 +4003,7 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
3793
4003
 
3794
4004
 
3795
4005
  // Computes HTML classNames for a single-day element
3796
- getDayClasses: function(date) {
4006
+ getDayClasses: function(date, noThemeHighlight) {
3797
4007
  var view = this.view;
3798
4008
  var today = view.calendar.getNow();
3799
4009
  var classes = [ 'fc-' + dayIDs[date.day()] ];
@@ -3806,10 +4016,11 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
3806
4016
  }
3807
4017
 
3808
4018
  if (date.isSame(today, 'day')) {
3809
- classes.push(
3810
- 'fc-today',
3811
- view.highlightStateClass
3812
- );
4019
+ classes.push('fc-today');
4020
+
4021
+ if (noThemeHighlight !== true) {
4022
+ classes.push(view.highlightStateClass);
4023
+ }
3813
4024
  }
3814
4025
  else if (date < today) {
3815
4026
  classes.push('fc-past');
@@ -4005,16 +4216,33 @@ Grid.mixin({
4005
4216
 
4006
4217
  // Compute business hour segs for the grid's current date range.
4007
4218
  // Caller must ask if whole-day business hours are needed.
4008
- buildBusinessHourSegs: function(wholeDay) {
4009
- var events = this.view.calendar.getCurrentBusinessHourEvents(wholeDay);
4219
+ // If no `businessHours` configuration value is specified, assumes the calendar default.
4220
+ buildBusinessHourSegs: function(wholeDay, businessHours) {
4221
+ return this.eventsToSegs(
4222
+ this.buildBusinessHourEvents(wholeDay, businessHours)
4223
+ );
4224
+ },
4225
+
4226
+
4227
+ // Compute business hour *events* for the grid's current date range.
4228
+ // Caller must ask if whole-day business hours are needed.
4229
+ // If no `businessHours` configuration value is specified, assumes the calendar default.
4230
+ buildBusinessHourEvents: function(wholeDay, businessHours) {
4231
+ var calendar = this.view.calendar;
4232
+ var events;
4233
+
4234
+ if (businessHours == null) {
4235
+ // fallback
4236
+ // access from calendawr. don't access from view. doesn't update with dynamic options.
4237
+ businessHours = calendar.options.businessHours;
4238
+ }
4239
+
4240
+ events = calendar.computeBusinessHourEvents(wholeDay, businessHours);
4010
4241
 
4011
4242
  // HACK. Eventually refactor business hours "events" system.
4012
4243
  // If no events are given, but businessHours is activated, this means the entire visible range should be
4013
4244
  // marked as *not* business-hours, via inverse-background rendering.
4014
- if (
4015
- !events.length &&
4016
- this.view.calendar.options.businessHours // don't access view option. doesn't update with dynamic options
4017
- ) {
4245
+ if (!events.length && businessHours) {
4018
4246
  events = [
4019
4247
  $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, {
4020
4248
  start: this.view.end, // guaranteed out-of-range
@@ -4024,7 +4252,7 @@ Grid.mixin({
4024
4252
  ];
4025
4253
  }
4026
4254
 
4027
- return this.eventsToSegs(events);
4255
+ return events;
4028
4256
  },
4029
4257
 
4030
4258
 
@@ -4066,7 +4294,7 @@ Grid.mixin({
4066
4294
 
4067
4295
 
4068
4296
  handleSegClick: function(seg, ev) {
4069
- var res = this.view.trigger('eventClick', seg.el[0], seg.event, ev); // can return `false` to cancel
4297
+ var res = this.view.publiclyTrigger('eventClick', seg.el[0], seg.event, ev); // can return `false` to cancel
4070
4298
  if (res === false) {
4071
4299
  ev.preventDefault();
4072
4300
  }
@@ -4083,7 +4311,7 @@ Grid.mixin({
4083
4311
  if (this.view.isEventResizable(seg.event)) {
4084
4312
  seg.el.addClass('fc-allow-mouse-resize');
4085
4313
  }
4086
- this.view.trigger('eventMouseover', seg.el[0], seg.event, ev);
4314
+ this.view.publiclyTrigger('eventMouseover', seg.el[0], seg.event, ev);
4087
4315
  }
4088
4316
  },
4089
4317
 
@@ -4099,7 +4327,7 @@ Grid.mixin({
4099
4327
  if (this.view.isEventResizable(seg.event)) {
4100
4328
  seg.el.removeClass('fc-allow-mouse-resize');
4101
4329
  }
4102
- this.view.trigger('eventMouseout', seg.el[0], seg.event, ev);
4330
+ this.view.publiclyTrigger('eventMouseout', seg.el[0], seg.event, ev);
4103
4331
  }
4104
4332
  },
4105
4333
 
@@ -4124,6 +4352,7 @@ Grid.mixin({
4124
4352
  var isResizable = view.isEventResizable(event);
4125
4353
  var isResizing = false;
4126
4354
  var dragListener;
4355
+ var eventLongPressDelay;
4127
4356
 
4128
4357
  if (isSelected && isResizable) {
4129
4358
  // only allow resizing of the event is selected
@@ -4132,12 +4361,17 @@ Grid.mixin({
4132
4361
 
4133
4362
  if (!isResizing && (isDraggable || isResizable)) { // allowed to be selected?
4134
4363
 
4364
+ eventLongPressDelay = view.opt('eventLongPressDelay');
4365
+ if (eventLongPressDelay == null) {
4366
+ eventLongPressDelay = view.opt('longPressDelay'); // fallback
4367
+ }
4368
+
4135
4369
  dragListener = isDraggable ?
4136
4370
  this.buildSegDragListener(seg) :
4137
4371
  this.buildSegSelectListener(seg); // seg isn't draggable, but still needs to be selected
4138
4372
 
4139
4373
  dragListener.startInteraction(ev, { // won't start if already started
4140
- delay: isSelected ? 0 : this.view.opt('longPressDelay') // do delay if not already selected
4374
+ delay: isSelected ? 0 : eventLongPressDelay // do delay if not already selected
4141
4375
  });
4142
4376
  }
4143
4377
 
@@ -4270,11 +4504,15 @@ Grid.mixin({
4270
4504
  mouseFollower.stop(!dropLocation, function() {
4271
4505
  if (isDragging) {
4272
4506
  view.unrenderDrag();
4273
- view.showEvent(event);
4274
4507
  _this.segDragStop(seg, ev);
4275
4508
  }
4509
+
4276
4510
  if (dropLocation) {
4277
- view.reportEventDrop(event, dropLocation, this.largeUnit, el, ev);
4511
+ // no need to re-show original, will rerender all anyways. esp important if eventRenderWait
4512
+ view.reportEventDrop(event, dropLocation, _this.largeUnit, el, ev);
4513
+ }
4514
+ else {
4515
+ view.showEvent(event);
4278
4516
  }
4279
4517
  });
4280
4518
  _this.segDragListener = null;
@@ -4316,14 +4554,14 @@ Grid.mixin({
4316
4554
  // Called before event segment dragging starts
4317
4555
  segDragStart: function(seg, ev) {
4318
4556
  this.isDraggingSeg = true;
4319
- this.view.trigger('eventDragStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
4557
+ this.view.publiclyTrigger('eventDragStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
4320
4558
  },
4321
4559
 
4322
4560
 
4323
4561
  // Called after event segment dragging stops
4324
4562
  segDragStop: function(seg, ev) {
4325
4563
  this.isDraggingSeg = false;
4326
- this.view.trigger('eventDragStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
4564
+ this.view.publiclyTrigger('eventDragStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
4327
4565
  },
4328
4566
 
4329
4567
 
@@ -4561,18 +4799,23 @@ Grid.mixin({
4561
4799
  },
4562
4800
  hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
4563
4801
  resizeLocation = null;
4802
+ view.showEvent(event); // for when out-of-bounds. show original
4564
4803
  },
4565
4804
  hitDone: function() { // resets the rendering to show the original event
4566
4805
  _this.unrenderEventResize();
4567
- view.showEvent(event);
4568
4806
  enableCursor();
4569
4807
  },
4570
4808
  interactionEnd: function(ev) {
4571
4809
  if (isDragging) {
4572
4810
  _this.segResizeStop(seg, ev);
4573
4811
  }
4812
+
4574
4813
  if (resizeLocation) { // valid date to resize to?
4575
- view.reportEventResize(event, resizeLocation, this.largeUnit, el, ev);
4814
+ // no need to re-show original, will rerender all anyways. esp important if eventRenderWait
4815
+ view.reportEventResize(event, resizeLocation, _this.largeUnit, el, ev);
4816
+ }
4817
+ else {
4818
+ view.showEvent(event);
4576
4819
  }
4577
4820
  _this.segResizeListener = null;
4578
4821
  }
@@ -4585,14 +4828,14 @@ Grid.mixin({
4585
4828
  // Called before event segment resizing starts
4586
4829
  segResizeStart: function(seg, ev) {
4587
4830
  this.isResizingSeg = true;
4588
- this.view.trigger('eventResizeStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
4831
+ this.view.publiclyTrigger('eventResizeStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
4589
4832
  },
4590
4833
 
4591
4834
 
4592
4835
  // Called after event segment resizing stops
4593
4836
  segResizeStop: function(seg, ev) {
4594
4837
  this.isResizingSeg = false;
4595
- this.view.trigger('eventResizeStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
4838
+ this.view.publiclyTrigger('eventResizeStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
4596
4839
  },
4597
4840
 
4598
4841
 
@@ -5143,7 +5386,7 @@ var DayTableMixin = FC.DayTableMixin = {
5143
5386
  this.dayIndices = dayIndices;
5144
5387
  this.daysPerRow = daysPerRow;
5145
5388
  this.rowCnt = rowCnt;
5146
-
5389
+
5147
5390
  this.updateDayTableCols();
5148
5391
  },
5149
5392
 
@@ -5381,9 +5624,25 @@ var DayTableMixin = FC.DayTableMixin = {
5381
5624
  // (colspan should be no different)
5382
5625
  renderHeadDateCellHtml: function(date, colspan, otherAttrs) {
5383
5626
  var view = this.view;
5627
+ var classNames = [
5628
+ 'fc-day-header',
5629
+ view.widgetHeaderClass
5630
+ ];
5631
+
5632
+ // if only one row of days, the classNames on the header can represent the specific days beneath
5633
+ if (this.rowCnt === 1) {
5634
+ classNames = classNames.concat(
5635
+ // includes the day-of-week class
5636
+ // noThemeHighlight=true (don't highlight the header)
5637
+ this.getDayClasses(date, true)
5638
+ );
5639
+ }
5640
+ else {
5641
+ classNames.push('fc-' + dayIDs[date.day()]); // only add the day-of-week class
5642
+ }
5384
5643
 
5385
5644
  return '' +
5386
- '<th class="fc-day-header ' + view.widgetHeaderClass + ' fc-' + dayIDs[date.day()] + '"' +
5645
+ '<th class="' + classNames.join(' ') + '"' +
5387
5646
  (this.rowCnt === 1 ?
5388
5647
  ' data-date="' + date.format('YYYY-MM-DD') + '"' :
5389
5648
  '') +
@@ -5534,7 +5793,7 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
5534
5793
  // trigger dayRender with each cell's element
5535
5794
  for (row = 0; row < rowCnt; row++) {
5536
5795
  for (col = 0; col < colCnt; col++) {
5537
- view.trigger(
5796
+ view.publiclyTrigger(
5538
5797
  'dayRender',
5539
5798
  null,
5540
5799
  this.getCellDate(row, col),
@@ -6469,7 +6728,7 @@ DayGrid.mixin({
6469
6728
 
6470
6729
  if (typeof clickOption === 'function') {
6471
6730
  // the returned value can be an atomic option
6472
- clickOption = view.trigger('eventLimitClick', null, {
6731
+ clickOption = view.publiclyTrigger('eventLimitClick', null, {
6473
6732
  date: date,
6474
6733
  dayEl: dayEl,
6475
6734
  moreEl: moreEl,
@@ -6512,6 +6771,14 @@ DayGrid.mixin({
6512
6771
  viewportConstrain: view.opt('popoverViewportConstrain'),
6513
6772
  hide: function() {
6514
6773
  // kill everything when the popover is hidden
6774
+ // notify events to be removed
6775
+ if (_this.popoverSegs) {
6776
+ var seg;
6777
+ for (var i = 0; i < _this.popoverSegs.length; ++i) {
6778
+ seg = _this.popoverSegs[i];
6779
+ view.publiclyTrigger('eventDestroy', seg.event, seg.event, seg.el);
6780
+ }
6781
+ }
6515
6782
  _this.segPopover.removeElement();
6516
6783
  _this.segPopover = null;
6517
6784
  _this.popoverSegs = null;
@@ -7789,9 +8056,14 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
7789
8056
  options: null, // hash containing all options. already merged with view-specific-options
7790
8057
  el: null, // the view's containing element. set by Calendar
7791
8058
 
7792
- displaying: null, // a promise representing the state of rendering. null if no render requested
7793
- isSkeletonRendered: false,
8059
+ isDateSet: false,
8060
+ isDateRendered: false,
8061
+ dateRenderQueue: null,
8062
+
8063
+ isEventsBound: false,
8064
+ isEventsSet: false,
7794
8065
  isEventsRendered: false,
8066
+ eventRenderQueue: null,
7795
8067
 
7796
8068
  // range the view is actually displaying (moments)
7797
8069
  start: null,
@@ -7841,6 +8113,9 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
7841
8113
 
7842
8114
  this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder'));
7843
8115
 
8116
+ this.dateRenderQueue = new TaskQueue();
8117
+ this.eventRenderQueue = new TaskQueue(this.opt('eventRenderWait'));
8118
+
7844
8119
  this.initialize();
7845
8120
  },
7846
8121
 
@@ -7858,10 +8133,10 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
7858
8133
 
7859
8134
 
7860
8135
  // Triggers handlers that are view-related. Modifies args before passing to calendar.
7861
- trigger: function(name, thisObj) { // arguments beyond thisObj are passed along
8136
+ publiclyTrigger: function(name, thisObj) { // arguments beyond thisObj are passed along
7862
8137
  var calendar = this.calendar;
7863
8138
 
7864
- return calendar.trigger.apply(
8139
+ return calendar.publiclyTrigger.apply(
7865
8140
  calendar,
7866
8141
  [name, thisObj || this].concat(
7867
8142
  Array.prototype.slice.call(arguments, 2), // arguments beyond thisObj
@@ -7871,16 +8146,33 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
7871
8146
  },
7872
8147
 
7873
8148
 
7874
- /* Dates
7875
- ------------------------------------------------------------------------------------------------------------------*/
8149
+ // Returns a proxy of the given promise that will be rejected if the given event fires
8150
+ // before the promise resolves.
8151
+ rejectOn: function(eventName, promise) {
8152
+ var _this = this;
7876
8153
 
8154
+ return new Promise(function(resolve, reject) {
8155
+ _this.one(eventName, reject);
7877
8156
 
7878
- // Updates all internal dates to center around the given current unzoned date.
7879
- setDate: function(date) {
7880
- this.setRange(this.computeRange(date));
8157
+ function cleanup() {
8158
+ _this.off(eventName, reject);
8159
+ }
8160
+
8161
+ promise.then(function(res) { // success
8162
+ cleanup();
8163
+ resolve(res);
8164
+ }, function() { // failure
8165
+ cleanup();
8166
+ reject();
8167
+ });
8168
+ });
7881
8169
  },
7882
8170
 
7883
8171
 
8172
+ /* Date Computation
8173
+ ------------------------------------------------------------------------------------------------------------------*/
8174
+
8175
+
7884
8176
  // Updates all internal dates for displaying the given unzoned range.
7885
8177
  setRange: function(range) {
7886
8178
  $.extend(this, range); // assigns every property to this object's member variables
@@ -7963,6 +8255,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
7963
8255
  // Sets the view's title property to the most updated computed value
7964
8256
  updateTitle: function() {
7965
8257
  this.title = this.computeTitle();
8258
+ this.calendar.setToolbarsTitle(this.title);
7966
8259
  },
7967
8260
 
7968
8261
 
@@ -8068,164 +8361,219 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8068
8361
  },
8069
8362
 
8070
8363
 
8071
- /* Rendering
8072
- ------------------------------------------------------------------------------------------------------------------*/
8364
+ // Rendering Non-date-related Content
8365
+ // -----------------------------------------------------------------------------------------------------------------
8073
8366
 
8074
8367
 
8075
- // Sets the container element that the view should render inside of.
8076
- // Does other DOM-related initializations.
8368
+ // Sets the container element that the view should render inside of, does global DOM-related initializations,
8369
+ // and renders all the non-date-related content inside.
8077
8370
  setElement: function(el) {
8078
8371
  this.el = el;
8079
8372
  this.bindGlobalHandlers();
8373
+ this.renderSkeleton();
8080
8374
  },
8081
8375
 
8082
8376
 
8083
8377
  // Removes the view's container element from the DOM, clearing any content beforehand.
8084
8378
  // Undoes any other DOM-related attachments.
8085
8379
  removeElement: function() {
8086
- this.clear(); // clears all content
8087
-
8088
- // clean up the skeleton
8089
- if (this.isSkeletonRendered) {
8090
- this.unrenderSkeleton();
8091
- this.isSkeletonRendered = false;
8092
- }
8380
+ this.unsetDate();
8381
+ this.unrenderSkeleton();
8093
8382
 
8094
8383
  this.unbindGlobalHandlers();
8095
8384
 
8096
8385
  this.el.remove();
8097
-
8098
8386
  // NOTE: don't null-out this.el in case the View was destroyed within an API callback.
8099
8387
  // We don't null-out the View's other jQuery element references upon destroy,
8100
8388
  // so we shouldn't kill this.el either.
8101
8389
  },
8102
8390
 
8103
8391
 
8104
- // Does everything necessary to display the view centered around the given unzoned date.
8105
- // Does every type of rendering EXCEPT rendering events.
8106
- // Is asychronous and returns a promise.
8107
- display: function(date, explicitScrollState) {
8108
- var _this = this;
8109
- var prevScrollState = null;
8392
+ // Renders the basic structure of the view before any content is rendered
8393
+ renderSkeleton: function() {
8394
+ // subclasses should implement
8395
+ },
8396
+
8397
+
8398
+ // Unrenders the basic structure of the view
8399
+ unrenderSkeleton: function() {
8400
+ // subclasses should implement
8401
+ },
8402
+
8403
+
8404
+ // Date Setting/Unsetting
8405
+ // -----------------------------------------------------------------------------------------------------------------
8406
+
8407
+
8408
+ setDate: function(date) {
8409
+ var isReset = this.isDateSet;
8410
+
8411
+ this.isDateSet = true;
8412
+ this.handleDate(date, isReset);
8413
+ this.trigger(isReset ? 'dateReset' : 'dateSet', date);
8414
+ },
8415
+
8110
8416
 
8111
- if (explicitScrollState != null && this.displaying) { // don't need prevScrollState if explicitScrollState
8112
- prevScrollState = this.queryScroll();
8417
+ unsetDate: function() {
8418
+ if (this.isDateSet) {
8419
+ this.isDateSet = false;
8420
+ this.handleDateUnset();
8421
+ this.trigger('dateUnset');
8113
8422
  }
8423
+ },
8114
8424
 
8115
- this.calendar.freezeContentHeight();
8116
8425
 
8117
- return syncThen(this.clear(), function() { // clear the content first
8118
- return (
8119
- _this.displaying =
8120
- syncThen(_this.displayView(date), function() { // displayView might return a promise
8426
+ // Date Handling
8427
+ // -----------------------------------------------------------------------------------------------------------------
8121
8428
 
8122
- // caller of display() wants a specific scroll state?
8123
- if (explicitScrollState != null) {
8124
- // we make an assumption that this is NOT the initial render,
8125
- // and thus don't need forceScroll (is inconveniently asynchronous)
8126
- _this.setScroll(explicitScrollState);
8127
- }
8128
- else {
8129
- _this.forceScroll(_this.computeInitialScroll(prevScrollState));
8130
- }
8131
8429
 
8132
- _this.calendar.unfreezeContentHeight();
8133
- _this.triggerRender();
8134
- })
8135
- );
8430
+ handleDate: function(date, isReset) {
8431
+ var _this = this;
8432
+
8433
+ this.unbindEvents(); // will do nothing if not already bound
8434
+ this.requestDateRender(date).then(function() {
8435
+ // wish we could start earlier, but setRange/computeRange needs to execute first
8436
+ _this.bindEvents(); // will request events
8136
8437
  });
8137
8438
  },
8138
8439
 
8139
8440
 
8140
- // Does everything necessary to clear the content of the view.
8141
- // Clears dates and events. Does not clear the skeleton.
8142
- // Is asychronous and returns a promise.
8143
- clear: function() {
8441
+ handleDateUnset: function() {
8442
+ this.unbindEvents();
8443
+ this.requestDateUnrender();
8444
+ },
8445
+
8446
+
8447
+ // Date Render Queuing
8448
+ // -----------------------------------------------------------------------------------------------------------------
8449
+
8450
+
8451
+ // if date not specified, uses current
8452
+ requestDateRender: function(date) {
8144
8453
  var _this = this;
8145
- var displaying = this.displaying;
8146
8454
 
8147
- if (displaying) { // previously displayed, or in the process of being displayed?
8148
- return syncThen(displaying, function() { // wait for the display to finish
8149
- _this.displaying = null;
8150
- _this.clearEvents();
8151
- return _this.clearView(); // might return a promise. chain it
8152
- });
8153
- }
8154
- else {
8155
- return $.when(); // an immediately-resolved promise
8156
- }
8455
+ return this.dateRenderQueue.add(function() {
8456
+ return _this.executeDateRender(date);
8457
+ });
8157
8458
  },
8158
8459
 
8159
8460
 
8160
- // Displays the view's non-event content, such as date-related content or anything required by events.
8161
- // Renders the view's non-content skeleton if necessary.
8162
- // Can be asynchronous and return a promise.
8163
- displayView: function(date) {
8164
- if (!this.isSkeletonRendered) {
8165
- this.renderSkeleton();
8166
- this.isSkeletonRendered = true;
8167
- }
8461
+ requestDateUnrender: function() {
8462
+ var _this = this;
8463
+
8464
+ return this.dateRenderQueue.add(function() {
8465
+ return _this.executeDateUnrender();
8466
+ });
8467
+ },
8468
+
8469
+
8470
+ // Date High-level Rendering
8471
+ // -----------------------------------------------------------------------------------------------------------------
8472
+
8473
+
8474
+ // if date not specified, uses current
8475
+ executeDateRender: function(date) {
8476
+ var _this = this;
8477
+
8478
+ // if rendering a new date, reset scroll to initial state (scrollTime)
8168
8479
  if (date) {
8169
- this.setDate(date);
8480
+ this.captureInitialScroll();
8170
8481
  }
8171
- if (this.render) {
8172
- this.render(); // TODO: deprecate
8482
+ else {
8483
+ this.captureScroll(); // a rerender of the current date
8173
8484
  }
8174
- this.renderDates();
8175
- this.updateSize();
8176
- this.renderBusinessHours(); // might need coordinates, so should go after updateSize()
8177
- this.startNowIndicator();
8485
+
8486
+ this.freezeHeight();
8487
+
8488
+ return this.executeDateUnrender().then(function() {
8489
+
8490
+ if (date) {
8491
+ _this.setRange(_this.computeRange(date));
8492
+ }
8493
+
8494
+ if (_this.render) {
8495
+ _this.render(); // TODO: deprecate
8496
+ }
8497
+
8498
+ _this.renderDates();
8499
+ _this.updateSize();
8500
+ _this.renderBusinessHours(); // might need coordinates, so should go after updateSize()
8501
+ _this.startNowIndicator();
8502
+
8503
+ _this.thawHeight();
8504
+ _this.releaseScroll();
8505
+
8506
+ _this.isDateRendered = true;
8507
+ _this.onDateRender();
8508
+ _this.trigger('dateRender');
8509
+ });
8178
8510
  },
8179
8511
 
8180
8512
 
8181
- // Unrenders the view content that was rendered in displayView.
8182
- // Can be asynchronous and return a promise.
8183
- clearView: function() {
8184
- this.unselect();
8185
- this.stopNowIndicator();
8186
- this.triggerUnrender();
8187
- this.unrenderBusinessHours();
8188
- this.unrenderDates();
8189
- if (this.destroy) {
8190
- this.destroy(); // TODO: deprecate
8513
+ executeDateUnrender: function() {
8514
+ var _this = this;
8515
+
8516
+ if (_this.isDateRendered) {
8517
+ return this.requestEventsUnrender().then(function() {
8518
+
8519
+ _this.unselect();
8520
+ _this.stopNowIndicator();
8521
+ _this.triggerUnrender();
8522
+ _this.unrenderBusinessHours();
8523
+ _this.unrenderDates();
8524
+
8525
+ if (_this.destroy) {
8526
+ _this.destroy(); // TODO: deprecate
8527
+ }
8528
+
8529
+ _this.isDateRendered = false;
8530
+ _this.trigger('dateUnrender');
8531
+ });
8532
+ }
8533
+ else {
8534
+ return Promise.resolve();
8191
8535
  }
8192
8536
  },
8193
8537
 
8194
8538
 
8195
- // Renders the basic structure of the view before any content is rendered
8196
- renderSkeleton: function() {
8197
- // subclasses should implement
8198
- },
8539
+ // Date Rendering Triggers
8540
+ // -----------------------------------------------------------------------------------------------------------------
8199
8541
 
8200
8542
 
8201
- // Unrenders the basic structure of the view
8202
- unrenderSkeleton: function() {
8203
- // subclasses should implement
8543
+ onDateRender: function() {
8544
+ this.triggerRender();
8204
8545
  },
8205
8546
 
8206
8547
 
8207
- // Renders the view's date-related content.
8208
- // Assumes setRange has already been called and the skeleton has already been rendered.
8548
+ // Date Low-level Rendering
8549
+ // -----------------------------------------------------------------------------------------------------------------
8550
+
8551
+
8552
+ // date-cell content only
8209
8553
  renderDates: function() {
8210
8554
  // subclasses should implement
8211
8555
  },
8212
8556
 
8213
8557
 
8214
- // Unrenders the view's date-related content
8558
+ // date-cell content only
8215
8559
  unrenderDates: function() {
8216
8560
  // subclasses should override
8217
8561
  },
8218
8562
 
8219
8563
 
8564
+ // Misc view rendering utils
8565
+ // -------------------------
8566
+
8567
+
8220
8568
  // Signals that the view's content has been rendered
8221
8569
  triggerRender: function() {
8222
- this.trigger('viewRender', this, this, this.el);
8570
+ this.publiclyTrigger('viewRender', this, this, this.el);
8223
8571
  },
8224
8572
 
8225
8573
 
8226
8574
  // Signals that the view's content is about to be unrendered
8227
8575
  triggerUnrender: function() {
8228
- this.trigger('viewDestroy', this, this, this.el);
8576
+ this.publiclyTrigger('viewDestroy', this, this, this.el);
8229
8577
  },
8230
8578
 
8231
8579
 
@@ -8362,10 +8710,9 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8362
8710
 
8363
8711
  // Refreshes anything dependant upon sizing of the container element of the grid
8364
8712
  updateSize: function(isResize) {
8365
- var scrollState;
8366
8713
 
8367
8714
  if (isResize) {
8368
- scrollState = this.queryScroll();
8715
+ this.captureScroll();
8369
8716
  }
8370
8717
 
8371
8718
  this.updateHeight(isResize);
@@ -8373,7 +8720,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8373
8720
  this.updateNowIndicator();
8374
8721
 
8375
8722
  if (isResize) {
8376
- this.setScroll(scrollState);
8723
+ this.releaseScroll();
8377
8724
  }
8378
8725
  },
8379
8726
 
@@ -8406,72 +8753,294 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8406
8753
  ------------------------------------------------------------------------------------------------------------------*/
8407
8754
 
8408
8755
 
8409
- // Computes the initial pre-configured scroll state prior to allowing the user to change it.
8410
- // Given the scroll state from the previous rendering. If first time rendering, given null.
8411
- computeInitialScroll: function(previousScrollState) {
8412
- return 0;
8756
+ capturedScroll: null,
8757
+ capturedScrollDepth: 0,
8758
+
8759
+
8760
+ captureScroll: function() {
8761
+ if (!(this.capturedScrollDepth++)) {
8762
+ this.capturedScroll = this.isDateRendered ? this.queryScroll() : {}; // require a render first
8763
+ return true; // root?
8764
+ }
8765
+ return false;
8413
8766
  },
8414
8767
 
8415
8768
 
8416
- // Retrieves the view's current natural scroll state. Can return an arbitrary format.
8417
- queryScroll: function() {
8418
- // subclasses must implement
8769
+ captureInitialScroll: function(forcedScroll) {
8770
+ if (this.captureScroll()) { // root?
8771
+ this.capturedScroll.isInitial = true;
8772
+
8773
+ if (forcedScroll) {
8774
+ $.extend(this.capturedScroll, forcedScroll);
8775
+ }
8776
+ else {
8777
+ this.capturedScroll.isComputed = true;
8778
+ }
8779
+ }
8419
8780
  },
8420
8781
 
8421
8782
 
8422
- // Sets the view's scroll state. Will accept the same format computeInitialScroll and queryScroll produce.
8423
- setScroll: function(scrollState) {
8424
- // subclasses must implement
8783
+ releaseScroll: function() {
8784
+ var scroll = this.capturedScroll;
8785
+ var isRoot = this.discardScroll();
8786
+
8787
+ if (scroll.isComputed) {
8788
+ if (isRoot) {
8789
+ // only compute initial scroll if it will actually be used (is the root capture)
8790
+ $.extend(scroll, this.computeInitialScroll());
8791
+ }
8792
+ else {
8793
+ scroll = null; // scroll couldn't be computed. don't apply it to the DOM
8794
+ }
8795
+ }
8796
+
8797
+ if (scroll) {
8798
+ // we act immediately on a releaseScroll operation, as opposed to captureScroll.
8799
+ // if capture/release wraps a render operation that screws up the scroll,
8800
+ // we still want to restore it a good state after, regardless of depth.
8801
+
8802
+ if (scroll.isInitial) {
8803
+ this.hardSetScroll(scroll); // outsmart how browsers set scroll on initial DOM
8804
+ }
8805
+ else {
8806
+ this.setScroll(scroll);
8807
+ }
8808
+ }
8809
+ },
8810
+
8811
+
8812
+ discardScroll: function() {
8813
+ if (!(--this.capturedScrollDepth)) {
8814
+ this.capturedScroll = null;
8815
+ return true; // root?
8816
+ }
8817
+ return false;
8818
+ },
8819
+
8820
+
8821
+ computeInitialScroll: function() {
8822
+ return {};
8425
8823
  },
8426
8824
 
8427
8825
 
8428
- // Sets the scroll state, making sure to overcome any predefined scroll value the browser has in mind
8429
- forceScroll: function(scrollState) {
8826
+ queryScroll: function() {
8827
+ return {};
8828
+ },
8829
+
8830
+
8831
+ hardSetScroll: function(scroll) {
8430
8832
  var _this = this;
8833
+ var exec = function() { _this.setScroll(scroll); };
8834
+ exec();
8835
+ setTimeout(exec, 0); // to surely clear the browser's initial scroll for the DOM
8836
+ },
8431
8837
 
8432
- this.setScroll(scrollState);
8433
- setTimeout(function() {
8434
- _this.setScroll(scrollState);
8435
- }, 0);
8838
+
8839
+ setScroll: function(scroll) {
8436
8840
  },
8437
8841
 
8438
8842
 
8439
- /* Event Elements / Segments
8843
+ /* Height Freezing
8440
8844
  ------------------------------------------------------------------------------------------------------------------*/
8441
8845
 
8442
8846
 
8443
- // Does everything necessary to display the given events onto the current view
8444
- displayEvents: function(events) {
8445
- var scrollState = this.queryScroll();
8847
+ freezeHeight: function() {
8848
+ this.calendar.freezeContentHeight();
8849
+ },
8850
+
8851
+
8852
+ thawHeight: function() {
8853
+ this.calendar.thawContentHeight();
8854
+ },
8855
+
8856
+
8857
+ // Event Binding/Unbinding
8858
+ // -----------------------------------------------------------------------------------------------------------------
8859
+
8860
+
8861
+ bindEvents: function() {
8862
+ var _this = this;
8863
+
8864
+ if (!this.isEventsBound) {
8865
+ this.isEventsBound = true;
8866
+ this.rejectOn('eventsUnbind', this.requestEvents()).then(function(events) { // TODO: test rejection
8867
+ _this.listenTo(_this.calendar, 'eventsReset', _this.setEvents);
8868
+ _this.setEvents(events);
8869
+ });
8870
+ }
8871
+ },
8872
+
8873
+
8874
+ unbindEvents: function() {
8875
+ if (this.isEventsBound) {
8876
+ this.isEventsBound = false;
8877
+ this.stopListeningTo(this.calendar, 'eventsReset');
8878
+ this.unsetEvents();
8879
+ this.trigger('eventsUnbind');
8880
+ }
8881
+ },
8882
+
8883
+
8884
+ // Event Setting/Unsetting
8885
+ // -----------------------------------------------------------------------------------------------------------------
8886
+
8887
+
8888
+ setEvents: function(events) {
8889
+ var isReset = this.isEventSet;
8890
+
8891
+ this.isEventsSet = true;
8892
+ this.handleEvents(events, isReset);
8893
+ this.trigger(isReset ? 'eventsReset' : 'eventsSet', events);
8894
+ },
8895
+
8896
+
8897
+ unsetEvents: function() {
8898
+ if (this.isEventsSet) {
8899
+ this.isEventsSet = false;
8900
+ this.handleEventsUnset();
8901
+ this.trigger('eventsUnset');
8902
+ }
8903
+ },
8904
+
8905
+
8906
+ whenEventsSet: function() {
8907
+ var _this = this;
8908
+
8909
+ if (this.isEventsSet) {
8910
+ return Promise.resolve(this.getCurrentEvents());
8911
+ }
8912
+ else {
8913
+ return new Promise(function(resolve) {
8914
+ _this.one('eventsSet', resolve);
8915
+ });
8916
+ }
8917
+ },
8918
+
8919
+
8920
+ // Event Handling
8921
+ // -----------------------------------------------------------------------------------------------------------------
8922
+
8923
+
8924
+ handleEvents: function(events, isReset) {
8925
+ this.requestEventsRender(events);
8926
+ },
8927
+
8928
+
8929
+ handleEventsUnset: function() {
8930
+ this.requestEventsUnrender();
8931
+ },
8932
+
8933
+
8934
+ // Event Render Queuing
8935
+ // -----------------------------------------------------------------------------------------------------------------
8936
+
8937
+
8938
+ // assumes any previous event renders have been cleared already
8939
+ requestEventsRender: function(events) {
8940
+ var _this = this;
8941
+
8942
+ return this.eventRenderQueue.add(function() { // might not return a promise if debounced!? bad
8943
+ return _this.executeEventsRender(events);
8944
+ });
8945
+ },
8446
8946
 
8447
- this.clearEvents();
8448
- this.renderEvents(events);
8449
- this.isEventsRendered = true;
8450
- this.setScroll(scrollState);
8451
- this.triggerEventRender();
8947
+
8948
+ requestEventsUnrender: function() {
8949
+ var _this = this;
8950
+
8951
+ if (this.isEventsRendered) {
8952
+ return this.eventRenderQueue.addQuickly(function() {
8953
+ return _this.executeEventsUnrender();
8954
+ });
8955
+ }
8956
+ else {
8957
+ return Promise.resolve();
8958
+ }
8452
8959
  },
8453
8960
 
8454
8961
 
8455
- // Does everything necessary to clear the view's currently-rendered events
8456
- clearEvents: function() {
8457
- var scrollState;
8962
+ requestCurrentEventsRender: function() {
8963
+ if (this.isEventsSet) {
8964
+ this.requestEventsRender(this.getCurrentEvents());
8965
+ }
8966
+ else {
8967
+ return Promise.reject();
8968
+ }
8969
+ },
8970
+
8971
+
8972
+ // Event High-level Rendering
8973
+ // -----------------------------------------------------------------------------------------------------------------
8974
+
8975
+
8976
+ executeEventsRender: function(events) {
8977
+ var _this = this;
8978
+
8979
+ this.captureScroll();
8980
+ this.freezeHeight();
8981
+
8982
+ return this.executeEventsUnrender().then(function() {
8983
+ _this.renderEvents(events);
8984
+
8985
+ _this.thawHeight();
8986
+ _this.releaseScroll();
8987
+
8988
+ _this.isEventsRendered = true;
8989
+ _this.onEventsRender();
8990
+ _this.trigger('eventsRender');
8991
+ });
8992
+ },
8993
+
8458
8994
 
8995
+ executeEventsUnrender: function() {
8459
8996
  if (this.isEventsRendered) {
8997
+ this.onBeforeEventsUnrender();
8460
8998
 
8461
- // TODO: optimize: if we know this is part of a displayEvents call, don't queryScroll/setScroll
8462
- scrollState = this.queryScroll();
8999
+ this.captureScroll();
9000
+ this.freezeHeight();
8463
9001
 
8464
- this.triggerEventUnrender();
8465
9002
  if (this.destroyEvents) {
8466
9003
  this.destroyEvents(); // TODO: deprecate
8467
9004
  }
9005
+
8468
9006
  this.unrenderEvents();
8469
- this.setScroll(scrollState);
9007
+
9008
+ this.thawHeight();
9009
+ this.releaseScroll();
9010
+
8470
9011
  this.isEventsRendered = false;
9012
+ this.trigger('eventsUnrender');
8471
9013
  }
9014
+
9015
+ return Promise.resolve(); // always synchronous
9016
+ },
9017
+
9018
+
9019
+ // Event Rendering Triggers
9020
+ // -----------------------------------------------------------------------------------------------------------------
9021
+
9022
+
9023
+ // Signals that all events have been rendered
9024
+ onEventsRender: function() {
9025
+ this.renderedEventSegEach(function(seg) {
9026
+ this.publiclyTrigger('eventAfterRender', seg.event, seg.event, seg.el);
9027
+ });
9028
+ this.publiclyTrigger('eventAfterAllRender');
9029
+ },
9030
+
9031
+
9032
+ // Signals that all event elements are about to be removed
9033
+ onBeforeEventsUnrender: function() {
9034
+ this.renderedEventSegEach(function(seg) {
9035
+ this.publiclyTrigger('eventDestroy', seg.event, seg.event, seg.el);
9036
+ });
8472
9037
  },
8473
9038
 
8474
9039
 
9040
+ // Event Low-level Rendering
9041
+ // -----------------------------------------------------------------------------------------------------------------
9042
+
9043
+
8475
9044
  // Renders the events onto the view.
8476
9045
  renderEvents: function(events) {
8477
9046
  // subclasses should implement
@@ -8484,27 +9053,28 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8484
9053
  },
8485
9054
 
8486
9055
 
8487
- // Signals that all events have been rendered
8488
- triggerEventRender: function() {
8489
- this.renderedEventSegEach(function(seg) {
8490
- this.trigger('eventAfterRender', seg.event, seg.event, seg.el);
8491
- });
8492
- this.trigger('eventAfterAllRender');
9056
+ // Event Data Access
9057
+ // -----------------------------------------------------------------------------------------------------------------
9058
+
9059
+
9060
+ requestEvents: function() {
9061
+ return this.calendar.requestEvents(this.start, this.end);
8493
9062
  },
8494
9063
 
8495
9064
 
8496
- // Signals that all event elements are about to be removed
8497
- triggerEventUnrender: function() {
8498
- this.renderedEventSegEach(function(seg) {
8499
- this.trigger('eventDestroy', seg.event, seg.event, seg.el);
8500
- });
9065
+ getCurrentEvents: function() {
9066
+ return this.calendar.getPrunedEventCache();
8501
9067
  },
8502
9068
 
8503
9069
 
9070
+ // Event Rendering Utils
9071
+ // -----------------------------------------------------------------------------------------------------------------
9072
+
9073
+
8504
9074
  // Given an event and the default element used for rendering, returns the element that should actually be used.
8505
9075
  // Basically runs events and elements through the eventRender hook.
8506
9076
  resolveEventEl: function(event, el) {
8507
- var custom = this.trigger('eventRender', event, event, el);
9077
+ var custom = this.publiclyTrigger('eventRender', event, event, el);
8508
9078
 
8509
9079
  if (custom === false) { // means don't render at all
8510
9080
  el = null;
@@ -8603,7 +9173,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8603
9173
 
8604
9174
  // Triggers event-drop handlers that have subscribed via the API
8605
9175
  triggerEventDrop: function(event, dateDelta, undoFunc, el, ev) {
8606
- this.trigger('eventDrop', el[0], event, dateDelta, undoFunc, ev, {}); // {} = jqui dummy
9176
+ this.publiclyTrigger('eventDrop', el[0], event, dateDelta, undoFunc, ev, {}); // {} = jqui dummy
8607
9177
  },
8608
9178
 
8609
9179
 
@@ -8633,10 +9203,10 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8633
9203
  triggerExternalDrop: function(event, dropLocation, el, ev, ui) {
8634
9204
 
8635
9205
  // trigger 'drop' regardless of whether element represents an event
8636
- this.trigger('drop', el[0], dropLocation.start, ev, ui);
9206
+ this.publiclyTrigger('drop', el[0], dropLocation.start, ev, ui);
8637
9207
 
8638
9208
  if (event) {
8639
- this.trigger('eventReceive', null, event); // signal an external event landed
9209
+ this.publiclyTrigger('eventReceive', null, event); // signal an external event landed
8640
9210
  }
8641
9211
  },
8642
9212
 
@@ -8706,7 +9276,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8706
9276
 
8707
9277
  // Triggers event-resize handlers that have subscribed via the API
8708
9278
  triggerEventResize: function(event, durationDelta, undoFunc, el, ev) {
8709
- this.trigger('eventResize', el[0], event, durationDelta, undoFunc, ev, {}); // {} = jqui dummy
9279
+ this.publiclyTrigger('eventResize', el[0], event, durationDelta, undoFunc, ev, {}); // {} = jqui dummy
8710
9280
  },
8711
9281
 
8712
9282
 
@@ -8738,7 +9308,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8738
9308
 
8739
9309
  // Triggers handlers to 'select'
8740
9310
  triggerSelect: function(span, ev) {
8741
- this.trigger(
9311
+ this.publiclyTrigger(
8742
9312
  'select',
8743
9313
  null,
8744
9314
  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, {
8757
9327
  this.destroySelection(); // TODO: deprecate
8758
9328
  }
8759
9329
  this.unrenderSelection();
8760
- this.trigger('unselect', null, ev);
9330
+ this.publiclyTrigger('unselect', null, ev);
8761
9331
  }
8762
9332
  },
8763
9333
 
@@ -8849,7 +9419,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8849
9419
  // Triggers handlers to 'dayClick'
8850
9420
  // Span has start/end of the clicked area. Only the start is useful.
8851
9421
  triggerDayClick: function(span, dayEl, ev) {
8852
- this.trigger(
9422
+ this.publiclyTrigger(
8853
9423
  'dayClick',
8854
9424
  dayEl,
8855
9425
  this.calendar.applyTimezone(span.start), // convert to calendar's timezone for external API
@@ -9076,208 +9646,498 @@ var Scroller = FC.Scroller = Class.extend({
9076
9646
  });
9077
9647
 
9078
9648
  ;;
9649
+ function Iterator(items) {
9650
+ this.items = items || [];
9651
+ }
9079
9652
 
9080
- var Calendar = FC.Calendar = Class.extend({
9081
9653
 
9082
- dirDefaults: null, // option defaults related to LTR or RTL
9083
- localeDefaults: null, // option defaults related to current locale
9084
- overrides: null, // option overrides given to the fullCalendar constructor
9085
- dynamicOverrides: null, // options set with dynamic setter method. higher precedence than view overrides.
9086
- options: null, // all defaults combined with overrides
9087
- viewSpecCache: null, // cache of view definitions
9088
- view: null, // current View object
9089
- header: null,
9090
- loadingLevel: 0, // number of simultaneous loading tasks
9654
+ /* Calls a method on every item passing the arguments through */
9655
+ Iterator.prototype.proxyCall = function(methodName) {
9656
+ var args = Array.prototype.slice.call(arguments, 1);
9657
+ var results = [];
9091
9658
 
9659
+ this.items.forEach(function(item) {
9660
+ results.push(item[methodName].apply(item, args));
9661
+ });
9092
9662
 
9093
- // a lot of this class' OOP logic is scoped within this constructor function,
9094
- // but in the future, write individual methods on the prototype.
9095
- constructor: Calendar_constructor,
9663
+ return results;
9664
+ };
9096
9665
 
9666
+ ;;
9097
9667
 
9098
- // Subclasses can override this for initialization logic after the constructor has been called
9099
- initialize: function() {
9100
- },
9668
+ /* Toolbar with buttons and title
9669
+ ----------------------------------------------------------------------------------------------------------------------*/
9101
9670
 
9671
+ function Toolbar(calendar, toolbarOptions) {
9672
+ var t = this;
9102
9673
 
9103
- // Computes the flattened options hash for the calendar and assigns to `this.options`.
9104
- // Assumes this.overrides and this.dynamicOverrides have already been initialized.
9105
- populateOptionsHash: function() {
9106
- var locale, localeDefaults;
9107
- var isRTL, dirDefaults;
9674
+ // exports
9675
+ t.setToolbarOptions = setToolbarOptions;
9676
+ t.render = render;
9677
+ t.removeElement = removeElement;
9678
+ t.updateTitle = updateTitle;
9679
+ t.activateButton = activateButton;
9680
+ t.deactivateButton = deactivateButton;
9681
+ t.disableButton = disableButton;
9682
+ t.enableButton = enableButton;
9683
+ t.getViewsWithButtons = getViewsWithButtons;
9684
+ t.el = null; // mirrors local `el`
9108
9685
 
9109
- locale = firstDefined( // explicit locale option given?
9110
- this.dynamicOverrides.locale,
9111
- this.overrides.locale
9112
- );
9113
- localeDefaults = localeOptionHash[locale];
9114
- if (!localeDefaults) { // explicit locale option not given or invalid?
9115
- locale = Calendar.defaults.locale;
9116
- localeDefaults = localeOptionHash[locale] || {};
9117
- }
9686
+ // locals
9687
+ var el;
9688
+ var viewsWithButtons = [];
9689
+ var tm;
9118
9690
 
9119
- isRTL = firstDefined( // based on options computed so far, is direction RTL?
9120
- this.dynamicOverrides.isRTL,
9121
- this.overrides.isRTL,
9122
- localeDefaults.isRTL,
9123
- Calendar.defaults.isRTL
9124
- );
9125
- dirDefaults = isRTL ? Calendar.rtlDefaults : {};
9691
+ // method to update toolbar-specific options, not calendar-wide options
9692
+ function setToolbarOptions(newToolbarOptions) {
9693
+ toolbarOptions = newToolbarOptions;
9694
+ }
9126
9695
 
9127
- this.dirDefaults = dirDefaults;
9128
- this.localeDefaults = localeDefaults;
9129
- this.options = mergeOptions([ // merge defaults and overrides. lowest to highest precedence
9130
- Calendar.defaults, // global defaults
9131
- dirDefaults,
9132
- localeDefaults,
9133
- this.overrides,
9134
- this.dynamicOverrides
9135
- ]);
9136
- populateInstanceComputableOptions(this.options); // fill in gaps with computed options
9137
- },
9696
+ // can be called repeatedly and will rerender
9697
+ function render() {
9698
+ var sections = toolbarOptions.layout;
9138
9699
 
9700
+ tm = calendar.options.theme ? 'ui' : 'fc';
9139
9701
 
9140
- // Gets information about how to create a view. Will use a cache.
9141
- getViewSpec: function(viewType) {
9142
- var cache = this.viewSpecCache;
9702
+ if (sections) {
9703
+ if (!el) {
9704
+ el = this.el = $("<div class='fc-toolbar "+ toolbarOptions.extraClasses + "'/>");
9705
+ }
9706
+ else {
9707
+ el.empty();
9708
+ }
9709
+ el.append(renderSection('left'))
9710
+ .append(renderSection('right'))
9711
+ .append(renderSection('center'))
9712
+ .append('<div class="fc-clear"/>');
9713
+ }
9714
+ else {
9715
+ removeElement();
9716
+ }
9717
+ }
9143
9718
 
9144
- return cache[viewType] || (cache[viewType] = this.buildViewSpec(viewType));
9145
- },
9146
9719
 
9720
+ function removeElement() {
9721
+ if (el) {
9722
+ el.remove();
9723
+ el = t.el = null;
9724
+ }
9725
+ }
9147
9726
 
9148
- // Given a duration singular unit, like "week" or "day", finds a matching view spec.
9149
- // Preference is given to views that have corresponding buttons.
9150
- getUnitViewSpec: function(unit) {
9151
- var viewTypes;
9152
- var i;
9153
- var spec;
9154
9727
 
9155
- if ($.inArray(unit, intervalUnits) != -1) {
9728
+ function renderSection(position) {
9729
+ var sectionEl = $('<div class="fc-' + position + '"/>');
9730
+ var buttonStr = toolbarOptions.layout[position];
9156
9731
 
9157
- // put views that have buttons first. there will be duplicates, but oh well
9158
- viewTypes = this.header.getViewsWithButtons();
9159
- $.each(FC.views, function(viewType) { // all views
9160
- viewTypes.push(viewType);
9161
- });
9732
+ if (buttonStr) {
9733
+ $.each(buttonStr.split(' '), function(i) {
9734
+ var groupChildren = $();
9735
+ var isOnlyButtons = true;
9736
+ var groupEl;
9162
9737
 
9163
- for (i = 0; i < viewTypes.length; i++) {
9164
- spec = this.getViewSpec(viewTypes[i]);
9165
- if (spec) {
9166
- if (spec.singleUnit == unit) {
9167
- return spec;
9738
+ $.each(this.split(','), function(j, buttonName) {
9739
+ var customButtonProps;
9740
+ var viewSpec;
9741
+ var buttonClick;
9742
+ var overrideText; // text explicitly set by calendar's constructor options. overcomes icons
9743
+ var defaultText;
9744
+ var themeIcon;
9745
+ var normalIcon;
9746
+ var innerHtml;
9747
+ var classes;
9748
+ var button; // the element
9749
+
9750
+ if (buttonName == 'title') {
9751
+ groupChildren = groupChildren.add($('<h2>&nbsp;</h2>')); // we always want it to take up height
9752
+ isOnlyButtons = false;
9168
9753
  }
9169
- }
9170
- }
9171
- }
9172
- },
9754
+ else {
9755
+ if ((customButtonProps = (calendar.options.customButtons || {})[buttonName])) {
9756
+ buttonClick = function(ev) {
9757
+ if (customButtonProps.click) {
9758
+ customButtonProps.click.call(button[0], ev);
9759
+ }
9760
+ };
9761
+ overrideText = ''; // icons will override text
9762
+ defaultText = customButtonProps.text;
9763
+ }
9764
+ else if ((viewSpec = calendar.getViewSpec(buttonName))) {
9765
+ buttonClick = function() {
9766
+ calendar.changeView(buttonName);
9767
+ };
9768
+ viewsWithButtons.push(buttonName);
9769
+ overrideText = viewSpec.buttonTextOverride;
9770
+ defaultText = viewSpec.buttonTextDefault;
9771
+ }
9772
+ else if (calendar[buttonName]) { // a calendar method
9773
+ buttonClick = function() {
9774
+ calendar[buttonName]();
9775
+ };
9776
+ overrideText = (calendar.overrides.buttonText || {})[buttonName];
9777
+ defaultText = calendar.options.buttonText[buttonName]; // everything else is considered default
9778
+ }
9173
9779
 
9780
+ if (buttonClick) {
9174
9781
 
9175
- // Builds an object with information on how to create a given view
9176
- buildViewSpec: function(requestedViewType) {
9177
- var viewOverrides = this.overrides.views || {};
9178
- var specChain = []; // for the view. lowest to highest priority
9179
- var defaultsChain = []; // for the view. lowest to highest priority
9180
- var overridesChain = []; // for the view. lowest to highest priority
9181
- var viewType = requestedViewType;
9182
- var spec; // for the view
9183
- var overrides; // for the view
9184
- var duration;
9185
- var unit;
9782
+ themeIcon =
9783
+ customButtonProps ?
9784
+ customButtonProps.themeIcon :
9785
+ calendar.options.themeButtonIcons[buttonName];
9186
9786
 
9187
- // iterate from the specific view definition to a more general one until we hit an actual View class
9188
- while (viewType) {
9189
- spec = fcViews[viewType];
9190
- overrides = viewOverrides[viewType];
9191
- viewType = null; // clear. might repopulate for another iteration
9787
+ normalIcon =
9788
+ customButtonProps ?
9789
+ customButtonProps.icon :
9790
+ calendar.options.buttonIcons[buttonName];
9192
9791
 
9193
- if (typeof spec === 'function') { // TODO: deprecate
9194
- spec = { 'class': spec };
9195
- }
9792
+ if (overrideText) {
9793
+ innerHtml = htmlEscape(overrideText);
9794
+ }
9795
+ else if (themeIcon && calendar.options.theme) {
9796
+ innerHtml = "<span class='ui-icon ui-icon-" + themeIcon + "'></span>";
9797
+ }
9798
+ else if (normalIcon && !calendar.options.theme) {
9799
+ innerHtml = "<span class='fc-icon fc-icon-" + normalIcon + "'></span>";
9800
+ }
9801
+ else {
9802
+ innerHtml = htmlEscape(defaultText);
9803
+ }
9196
9804
 
9197
- if (spec) {
9198
- specChain.unshift(spec);
9199
- defaultsChain.unshift(spec.defaults || {});
9200
- duration = duration || spec.duration;
9201
- viewType = viewType || spec.type;
9202
- }
9805
+ classes = [
9806
+ 'fc-' + buttonName + '-button',
9807
+ tm + '-button',
9808
+ tm + '-state-default'
9809
+ ];
9203
9810
 
9204
- if (overrides) {
9205
- overridesChain.unshift(overrides); // view-specific option hashes have options at zero-level
9206
- duration = duration || overrides.duration;
9207
- viewType = viewType || overrides.type;
9208
- }
9209
- }
9811
+ button = $( // type="button" so that it doesn't submit a form
9812
+ '<button type="button" class="' + classes.join(' ') + '">' +
9813
+ innerHtml +
9814
+ '</button>'
9815
+ )
9816
+ .click(function(ev) {
9817
+ // don't process clicks for disabled buttons
9818
+ if (!button.hasClass(tm + '-state-disabled')) {
9210
9819
 
9211
- spec = mergeProps(specChain);
9212
- spec.type = requestedViewType;
9213
- if (!spec['class']) {
9214
- return false;
9215
- }
9820
+ buttonClick(ev);
9216
9821
 
9217
- if (duration) {
9218
- duration = moment.duration(duration);
9219
- if (duration.valueOf()) { // valid?
9220
- spec.duration = duration;
9221
- unit = computeIntervalUnit(duration);
9822
+ // after the click action, if the button becomes the "active" tab, or disabled,
9823
+ // it should never have a hover class, so remove it now.
9824
+ if (
9825
+ button.hasClass(tm + '-state-active') ||
9826
+ button.hasClass(tm + '-state-disabled')
9827
+ ) {
9828
+ button.removeClass(tm + '-state-hover');
9829
+ }
9830
+ }
9831
+ })
9832
+ .mousedown(function() {
9833
+ // the *down* effect (mouse pressed in).
9834
+ // only on buttons that are not the "active" tab, or disabled
9835
+ button
9836
+ .not('.' + tm + '-state-active')
9837
+ .not('.' + tm + '-state-disabled')
9838
+ .addClass(tm + '-state-down');
9839
+ })
9840
+ .mouseup(function() {
9841
+ // undo the *down* effect
9842
+ button.removeClass(tm + '-state-down');
9843
+ })
9844
+ .hover(
9845
+ function() {
9846
+ // the *hover* effect.
9847
+ // only on buttons that are not the "active" tab, or disabled
9848
+ button
9849
+ .not('.' + tm + '-state-active')
9850
+ .not('.' + tm + '-state-disabled')
9851
+ .addClass(tm + '-state-hover');
9852
+ },
9853
+ function() {
9854
+ // undo the *hover* effect
9855
+ button
9856
+ .removeClass(tm + '-state-hover')
9857
+ .removeClass(tm + '-state-down'); // if mouseleave happens before mouseup
9858
+ }
9859
+ );
9222
9860
 
9223
- // view is a single-unit duration, like "week" or "day"
9224
- // incorporate options for this. lowest priority
9225
- if (duration.as(unit) === 1) {
9226
- spec.singleUnit = unit;
9227
- overridesChain.unshift(viewOverrides[unit] || {});
9861
+ groupChildren = groupChildren.add(button);
9862
+ }
9863
+ }
9864
+ });
9865
+
9866
+ if (isOnlyButtons) {
9867
+ groupChildren
9868
+ .first().addClass(tm + '-corner-left').end()
9869
+ .last().addClass(tm + '-corner-right').end();
9228
9870
  }
9229
- }
9230
- }
9231
9871
 
9232
- spec.defaults = mergeOptions(defaultsChain);
9233
- spec.overrides = mergeOptions(overridesChain);
9872
+ if (groupChildren.length > 1) {
9873
+ groupEl = $('<div/>');
9874
+ if (isOnlyButtons) {
9875
+ groupEl.addClass('fc-button-group');
9876
+ }
9877
+ groupEl.append(groupChildren);
9878
+ sectionEl.append(groupEl);
9879
+ }
9880
+ else {
9881
+ sectionEl.append(groupChildren); // 1 or 0 children
9882
+ }
9883
+ });
9884
+ }
9234
9885
 
9235
- this.buildViewSpecOptions(spec);
9236
- this.buildViewSpecButtonText(spec, requestedViewType);
9886
+ return sectionEl;
9887
+ }
9237
9888
 
9238
- return spec;
9239
- },
9240
9889
 
9890
+ function updateTitle(text) {
9891
+ if (el) {
9892
+ el.find('h2').text(text);
9893
+ }
9894
+ }
9241
9895
 
9242
- // Builds and assigns a view spec's options object from its already-assigned defaults and overrides
9243
- buildViewSpecOptions: function(spec) {
9244
- spec.options = mergeOptions([ // lowest to highest priority
9245
- Calendar.defaults, // global defaults
9246
- spec.defaults, // view's defaults (from ViewSubclass.defaults)
9247
- this.dirDefaults,
9248
- this.localeDefaults, // locale and dir take precedence over view's defaults!
9249
- this.overrides, // calendar's overrides (options given to constructor)
9250
- spec.overrides, // view's overrides (view-specific options)
9251
- this.dynamicOverrides // dynamically set via setter. highest precedence
9252
- ]);
9253
- populateInstanceComputableOptions(spec.options);
9254
- },
9255
9896
 
9897
+ function activateButton(buttonName) {
9898
+ if (el) {
9899
+ el.find('.fc-' + buttonName + '-button')
9900
+ .addClass(tm + '-state-active');
9901
+ }
9902
+ }
9256
9903
 
9257
- // Computes and assigns a view spec's buttonText-related options
9258
- buildViewSpecButtonText: function(spec, requestedViewType) {
9259
9904
 
9260
- // given an options object with a possible `buttonText` hash, lookup the buttonText for the
9261
- // requested view, falling back to a generic unit entry like "week" or "day"
9262
- function queryButtonText(options) {
9263
- var buttonText = options.buttonText || {};
9264
- return buttonText[requestedViewType] ||
9265
- // view can decide to look up a certain key
9266
- (spec.buttonTextKey ? buttonText[spec.buttonTextKey] : null) ||
9267
- // a key like "month"
9268
- (spec.singleUnit ? buttonText[spec.singleUnit] : null);
9905
+ function deactivateButton(buttonName) {
9906
+ if (el) {
9907
+ el.find('.fc-' + buttonName + '-button')
9908
+ .removeClass(tm + '-state-active');
9269
9909
  }
9910
+ }
9270
9911
 
9271
- // highest to lowest priority
9272
- spec.buttonTextOverride =
9273
- queryButtonText(this.dynamicOverrides) ||
9274
- queryButtonText(this.overrides) || // constructor-specified buttonText lookup hash takes precedence
9275
- spec.overrides.buttonText; // `buttonText` for view-specific options is a string
9276
9912
 
9277
- // highest to lowest priority. mirrors buildViewSpecOptions
9278
- spec.buttonTextDefault =
9279
- queryButtonText(this.localeDefaults) ||
9280
- queryButtonText(this.dirDefaults) ||
9913
+ function disableButton(buttonName) {
9914
+ if (el) {
9915
+ el.find('.fc-' + buttonName + '-button')
9916
+ .prop('disabled', true)
9917
+ .addClass(tm + '-state-disabled');
9918
+ }
9919
+ }
9920
+
9921
+
9922
+ function enableButton(buttonName) {
9923
+ if (el) {
9924
+ el.find('.fc-' + buttonName + '-button')
9925
+ .prop('disabled', false)
9926
+ .removeClass(tm + '-state-disabled');
9927
+ }
9928
+ }
9929
+
9930
+
9931
+ function getViewsWithButtons() {
9932
+ return viewsWithButtons;
9933
+ }
9934
+
9935
+ }
9936
+
9937
+ ;;
9938
+
9939
+ var Calendar = FC.Calendar = Class.extend({
9940
+
9941
+ dirDefaults: null, // option defaults related to LTR or RTL
9942
+ localeDefaults: null, // option defaults related to current locale
9943
+ overrides: null, // option overrides given to the fullCalendar constructor
9944
+ dynamicOverrides: null, // options set with dynamic setter method. higher precedence than view overrides.
9945
+ options: null, // all defaults combined with overrides
9946
+ viewSpecCache: null, // cache of view definitions
9947
+ view: null, // current View object
9948
+ header: null,
9949
+ footer: null,
9950
+ loadingLevel: 0, // number of simultaneous loading tasks
9951
+
9952
+
9953
+ // a lot of this class' OOP logic is scoped within this constructor function,
9954
+ // but in the future, write individual methods on the prototype.
9955
+ constructor: Calendar_constructor,
9956
+
9957
+
9958
+ // Subclasses can override this for initialization logic after the constructor has been called
9959
+ initialize: function() {
9960
+ },
9961
+
9962
+
9963
+ // Computes the flattened options hash for the calendar and assigns to `this.options`.
9964
+ // Assumes this.overrides and this.dynamicOverrides have already been initialized.
9965
+ populateOptionsHash: function() {
9966
+ var locale, localeDefaults;
9967
+ var isRTL, dirDefaults;
9968
+
9969
+ locale = firstDefined( // explicit locale option given?
9970
+ this.dynamicOverrides.locale,
9971
+ this.overrides.locale
9972
+ );
9973
+ localeDefaults = localeOptionHash[locale];
9974
+ if (!localeDefaults) { // explicit locale option not given or invalid?
9975
+ locale = Calendar.defaults.locale;
9976
+ localeDefaults = localeOptionHash[locale] || {};
9977
+ }
9978
+
9979
+ isRTL = firstDefined( // based on options computed so far, is direction RTL?
9980
+ this.dynamicOverrides.isRTL,
9981
+ this.overrides.isRTL,
9982
+ localeDefaults.isRTL,
9983
+ Calendar.defaults.isRTL
9984
+ );
9985
+ dirDefaults = isRTL ? Calendar.rtlDefaults : {};
9986
+
9987
+ this.dirDefaults = dirDefaults;
9988
+ this.localeDefaults = localeDefaults;
9989
+ this.options = mergeOptions([ // merge defaults and overrides. lowest to highest precedence
9990
+ Calendar.defaults, // global defaults
9991
+ dirDefaults,
9992
+ localeDefaults,
9993
+ this.overrides,
9994
+ this.dynamicOverrides
9995
+ ]);
9996
+ populateInstanceComputableOptions(this.options); // fill in gaps with computed options
9997
+ },
9998
+
9999
+
10000
+ // Gets information about how to create a view. Will use a cache.
10001
+ getViewSpec: function(viewType) {
10002
+ var cache = this.viewSpecCache;
10003
+
10004
+ return cache[viewType] || (cache[viewType] = this.buildViewSpec(viewType));
10005
+ },
10006
+
10007
+
10008
+ // Given a duration singular unit, like "week" or "day", finds a matching view spec.
10009
+ // Preference is given to views that have corresponding buttons.
10010
+ getUnitViewSpec: function(unit) {
10011
+ var viewTypes;
10012
+ var i;
10013
+ var spec;
10014
+
10015
+ if ($.inArray(unit, intervalUnits) != -1) {
10016
+
10017
+ // put views that have buttons first. there will be duplicates, but oh well
10018
+ viewTypes = this.header.getViewsWithButtons(); // TODO: include footer as well?
10019
+ $.each(FC.views, function(viewType) { // all views
10020
+ viewTypes.push(viewType);
10021
+ });
10022
+
10023
+ for (i = 0; i < viewTypes.length; i++) {
10024
+ spec = this.getViewSpec(viewTypes[i]);
10025
+ if (spec) {
10026
+ if (spec.singleUnit == unit) {
10027
+ return spec;
10028
+ }
10029
+ }
10030
+ }
10031
+ }
10032
+ },
10033
+
10034
+
10035
+ // Builds an object with information on how to create a given view
10036
+ buildViewSpec: function(requestedViewType) {
10037
+ var viewOverrides = this.overrides.views || {};
10038
+ var specChain = []; // for the view. lowest to highest priority
10039
+ var defaultsChain = []; // for the view. lowest to highest priority
10040
+ var overridesChain = []; // for the view. lowest to highest priority
10041
+ var viewType = requestedViewType;
10042
+ var spec; // for the view
10043
+ var overrides; // for the view
10044
+ var duration;
10045
+ var unit;
10046
+
10047
+ // iterate from the specific view definition to a more general one until we hit an actual View class
10048
+ while (viewType) {
10049
+ spec = fcViews[viewType];
10050
+ overrides = viewOverrides[viewType];
10051
+ viewType = null; // clear. might repopulate for another iteration
10052
+
10053
+ if (typeof spec === 'function') { // TODO: deprecate
10054
+ spec = { 'class': spec };
10055
+ }
10056
+
10057
+ if (spec) {
10058
+ specChain.unshift(spec);
10059
+ defaultsChain.unshift(spec.defaults || {});
10060
+ duration = duration || spec.duration;
10061
+ viewType = viewType || spec.type;
10062
+ }
10063
+
10064
+ if (overrides) {
10065
+ overridesChain.unshift(overrides); // view-specific option hashes have options at zero-level
10066
+ duration = duration || overrides.duration;
10067
+ viewType = viewType || overrides.type;
10068
+ }
10069
+ }
10070
+
10071
+ spec = mergeProps(specChain);
10072
+ spec.type = requestedViewType;
10073
+ if (!spec['class']) {
10074
+ return false;
10075
+ }
10076
+
10077
+ if (duration) {
10078
+ duration = moment.duration(duration);
10079
+ if (duration.valueOf()) { // valid?
10080
+ spec.duration = duration;
10081
+ unit = computeIntervalUnit(duration);
10082
+
10083
+ // view is a single-unit duration, like "week" or "day"
10084
+ // incorporate options for this. lowest priority
10085
+ if (duration.as(unit) === 1) {
10086
+ spec.singleUnit = unit;
10087
+ overridesChain.unshift(viewOverrides[unit] || {});
10088
+ }
10089
+ }
10090
+ }
10091
+
10092
+ spec.defaults = mergeOptions(defaultsChain);
10093
+ spec.overrides = mergeOptions(overridesChain);
10094
+
10095
+ this.buildViewSpecOptions(spec);
10096
+ this.buildViewSpecButtonText(spec, requestedViewType);
10097
+
10098
+ return spec;
10099
+ },
10100
+
10101
+
10102
+ // Builds and assigns a view spec's options object from its already-assigned defaults and overrides
10103
+ buildViewSpecOptions: function(spec) {
10104
+ spec.options = mergeOptions([ // lowest to highest priority
10105
+ Calendar.defaults, // global defaults
10106
+ spec.defaults, // view's defaults (from ViewSubclass.defaults)
10107
+ this.dirDefaults,
10108
+ this.localeDefaults, // locale and dir take precedence over view's defaults!
10109
+ this.overrides, // calendar's overrides (options given to constructor)
10110
+ spec.overrides, // view's overrides (view-specific options)
10111
+ this.dynamicOverrides // dynamically set via setter. highest precedence
10112
+ ]);
10113
+ populateInstanceComputableOptions(spec.options);
10114
+ },
10115
+
10116
+
10117
+ // Computes and assigns a view spec's buttonText-related options
10118
+ buildViewSpecButtonText: function(spec, requestedViewType) {
10119
+
10120
+ // given an options object with a possible `buttonText` hash, lookup the buttonText for the
10121
+ // requested view, falling back to a generic unit entry like "week" or "day"
10122
+ function queryButtonText(options) {
10123
+ var buttonText = options.buttonText || {};
10124
+ return buttonText[requestedViewType] ||
10125
+ // view can decide to look up a certain key
10126
+ (spec.buttonTextKey ? buttonText[spec.buttonTextKey] : null) ||
10127
+ // a key like "month"
10128
+ (spec.singleUnit ? buttonText[spec.singleUnit] : null);
10129
+ }
10130
+
10131
+ // highest to lowest priority
10132
+ spec.buttonTextOverride =
10133
+ queryButtonText(this.dynamicOverrides) ||
10134
+ queryButtonText(this.overrides) || // constructor-specified buttonText lookup hash takes precedence
10135
+ spec.overrides.buttonText; // `buttonText` for view-specific options is a string
10136
+
10137
+ // highest to lowest priority. mirrors buildViewSpecOptions
10138
+ spec.buttonTextDefault =
10139
+ queryButtonText(this.localeDefaults) ||
10140
+ queryButtonText(this.dirDefaults) ||
9281
10141
  spec.defaults.buttonText || // a single string. from ViewSubclass.defaults
9282
10142
  queryButtonText(Calendar.defaults) ||
9283
10143
  (spec.duration ? this.humanizeDuration(spec.duration) : null) || // like "3 days"
@@ -9302,7 +10162,7 @@ var Calendar = FC.Calendar = Class.extend({
9302
10162
  // Should be called when any type of async data fetching begins
9303
10163
  pushLoading: function() {
9304
10164
  if (!(this.loadingLevel++)) {
9305
- this.trigger('loading', null, true, this.view);
10165
+ this.publiclyTrigger('loading', null, true, this.view);
9306
10166
  }
9307
10167
  },
9308
10168
 
@@ -9310,7 +10170,7 @@ var Calendar = FC.Calendar = Class.extend({
9310
10170
  // Should be called when any type of async data fetching completes
9311
10171
  popLoading: function() {
9312
10172
  if (!(--this.loadingLevel)) {
9313
- this.trigger('loading', null, false, this.view);
10173
+ this.publiclyTrigger('loading', null, false, this.view);
9314
10174
  }
9315
10175
  },
9316
10176
 
@@ -9348,11 +10208,7 @@ function Calendar_constructor(element, overrides) {
9348
10208
 
9349
10209
  t.render = render;
9350
10210
  t.destroy = destroy;
9351
- t.refetchEvents = refetchEvents;
9352
- t.refetchEventSources = refetchEventSources;
9353
- t.reportEvents = reportEvents;
9354
- t.reportEventChange = reportEventChange;
9355
- t.rerenderEvents = renderEvents; // `renderEvents` serves as a rerender. an API method
10211
+ t.rerenderEvents = rerenderEvents;
9356
10212
  t.changeView = renderView; // `renderView` will switch to another view
9357
10213
  t.select = select;
9358
10214
  t.unselect = unselect;
@@ -9368,7 +10224,7 @@ function Calendar_constructor(element, overrides) {
9368
10224
  t.getCalendar = getCalendar;
9369
10225
  t.getView = getView;
9370
10226
  t.option = option; // getter/setter method
9371
- t.trigger = trigger;
10227
+ t.publiclyTrigger = publiclyTrigger;
9372
10228
 
9373
10229
 
9374
10230
  // Options
@@ -9561,15 +10417,12 @@ function Calendar_constructor(element, overrides) {
9561
10417
  };
9562
10418
 
9563
10419
 
9564
-
10420
+
9565
10421
  // Imports
9566
10422
  // -----------------------------------------------------------------------------------
9567
10423
 
9568
10424
 
9569
10425
  EventManager.call(t);
9570
- var isFetchNeeded = t.isFetchNeeded;
9571
- var fetchEvents = t.fetchEvents;
9572
- var fetchEventSources = t.fetchEventSources;
9573
10426
 
9574
10427
 
9575
10428
 
@@ -9578,7 +10431,9 @@ function Calendar_constructor(element, overrides) {
9578
10431
 
9579
10432
 
9580
10433
  var _element = element[0];
10434
+ var toolbarsManager;
9581
10435
  var header;
10436
+ var footer;
9582
10437
  var content;
9583
10438
  var tm; // for making theme classes
9584
10439
  var currentView; // NOTE: keep this in sync with this.view
@@ -9586,11 +10441,10 @@ function Calendar_constructor(element, overrides) {
9586
10441
  var suggestedViewHeight;
9587
10442
  var windowResizeProxy; // wraps the windowResize function
9588
10443
  var ignoreWindowResize = 0;
9589
- var events = [];
9590
10444
  var date; // unzoned
9591
-
9592
-
9593
-
10445
+
10446
+
10447
+
9594
10448
  // Main Rendering
9595
10449
  // -----------------------------------------------------------------------------------
9596
10450
 
@@ -9602,8 +10456,8 @@ function Calendar_constructor(element, overrides) {
9602
10456
  else {
9603
10457
  date = t.getNow(); // getNow already returns unzoned
9604
10458
  }
9605
-
9606
-
10459
+
10460
+
9607
10461
  function render() {
9608
10462
  if (!content) {
9609
10463
  initialRender();
@@ -9614,8 +10468,8 @@ function Calendar_constructor(element, overrides) {
9614
10468
  renderView();
9615
10469
  }
9616
10470
  }
9617
-
9618
-
10471
+
10472
+
9619
10473
  function initialRender() {
9620
10474
  element.addClass('fc');
9621
10475
 
@@ -9656,9 +10510,14 @@ function Calendar_constructor(element, overrides) {
9656
10510
 
9657
10511
  content = $("<div class='fc-view-container'/>").prependTo(element);
9658
10512
 
9659
- header = t.header = new Header(t);
9660
- renderHeader();
10513
+ var toolbars = buildToolbars();
10514
+ toolbarsManager = new Iterator(toolbars);
10515
+
10516
+ header = t.header = toolbars[0];
10517
+ footer = t.footer = toolbars[1];
9661
10518
 
10519
+ renderHeader();
10520
+ renderFooter();
9662
10521
  renderView(t.options.defaultView);
9663
10522
 
9664
10523
  if (t.options.handleWindowResize) {
@@ -9668,15 +10527,6 @@ function Calendar_constructor(element, overrides) {
9668
10527
  }
9669
10528
 
9670
10529
 
9671
- // can be called repeatedly and Header will rerender
9672
- function renderHeader() {
9673
- header.render();
9674
- if (header.el) {
9675
- element.prepend(header.el);
9676
- }
9677
- }
9678
-
9679
-
9680
10530
  function destroy() {
9681
10531
 
9682
10532
  if (currentView) {
@@ -9686,7 +10536,7 @@ function Calendar_constructor(element, overrides) {
9686
10536
  // It is still the "current" view, just not rendered.
9687
10537
  }
9688
10538
 
9689
- header.removeElement();
10539
+ toolbarsManager.proxyCall('removeElement');
9690
10540
  content.remove();
9691
10541
  element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');
9692
10542
 
@@ -9696,13 +10546,13 @@ function Calendar_constructor(element, overrides) {
9696
10546
  $(window).unbind('resize', windowResizeProxy);
9697
10547
  }
9698
10548
  }
9699
-
9700
-
10549
+
10550
+
9701
10551
  function elementVisible() {
9702
10552
  return element.is(':visible');
9703
10553
  }
9704
-
9705
-
10554
+
10555
+
9706
10556
 
9707
10557
  // View Rendering
9708
10558
  // -----------------------------------------------------------------------------------
@@ -9711,11 +10561,13 @@ function Calendar_constructor(element, overrides) {
9711
10561
  // Renders a view because of a date change, view-type change, or for the first time.
9712
10562
  // If not given a viewType, keep the current view but render different dates.
9713
10563
  // Accepts an optional scroll state to restore to.
9714
- function renderView(viewType, explicitScrollState) {
10564
+ function renderView(viewType, forcedScroll) {
9715
10565
  ignoreWindowResize++;
9716
10566
 
10567
+ var needsClearView = currentView && viewType && currentView.type !== viewType;
10568
+
9717
10569
  // if viewType is changing, remove the old view's rendering
9718
- if (currentView && viewType && currentView.type !== viewType) {
10570
+ if (needsClearView) {
9719
10571
  freezeContentHeight(); // prevent a scroll jump when view element is removed
9720
10572
  clearView();
9721
10573
  }
@@ -9729,7 +10581,7 @@ function Calendar_constructor(element, overrides) {
9729
10581
  currentView.setElement(
9730
10582
  $("<div class='fc-view fc-" + viewType + "-view' />").appendTo(content)
9731
10583
  );
9732
- header.activateButton(viewType);
10584
+ toolbarsManager.proxyCall('activateButton', viewType);
9733
10585
  }
9734
10586
 
9735
10587
  if (currentView) {
@@ -9739,7 +10591,7 @@ function Calendar_constructor(element, overrides) {
9739
10591
 
9740
10592
  // render or rerender the view
9741
10593
  if (
9742
- !currentView.displaying ||
10594
+ !currentView.isDateSet ||
9743
10595
  !( // NOT within interval range signals an implicit date window change
9744
10596
  date >= currentView.intervalStart &&
9745
10597
  date < currentView.intervalEnd
@@ -9747,19 +10599,27 @@ function Calendar_constructor(element, overrides) {
9747
10599
  ) {
9748
10600
  if (elementVisible()) {
9749
10601
 
9750
- currentView.display(date, explicitScrollState); // will call freezeContentHeight
9751
- unfreezeContentHeight(); // immediately unfreeze regardless of whether display is async
10602
+ if (forcedScroll) {
10603
+ currentView.captureInitialScroll(forcedScroll);
10604
+ }
10605
+
10606
+ currentView.setDate(date, forcedScroll);
9752
10607
 
9753
- // need to do this after View::render, so dates are calculated
9754
- updateHeaderTitle();
9755
- updateTodayButton();
10608
+ if (forcedScroll) {
10609
+ currentView.releaseScroll();
10610
+ }
9756
10611
 
9757
- getAndRenderEvents();
10612
+ // need to do this after View::render, so dates are calculated
10613
+ // NOTE: view updates title text proactively
10614
+ updateToolbarsTodayButton();
9758
10615
  }
9759
10616
  }
9760
10617
  }
9761
10618
 
9762
- unfreezeContentHeight(); // undo any lone freezeContentHeight calls
10619
+ if (needsClearView) {
10620
+ thawContentHeight();
10621
+ }
10622
+
9763
10623
  ignoreWindowResize--;
9764
10624
  }
9765
10625
 
@@ -9767,7 +10627,7 @@ function Calendar_constructor(element, overrides) {
9767
10627
  // Unrenders the current view and reflects this change in the Header.
9768
10628
  // Unregsiters the `currentView`, but does not remove from viewByType hash.
9769
10629
  function clearView() {
9770
- header.deactivateButton(currentView.type);
10630
+ toolbarsManager.proxyCall('deactivateButton', currentView.type);
9771
10631
  currentView.removeElement();
9772
10632
  currentView = t.view = null;
9773
10633
  }
@@ -9783,13 +10643,14 @@ function Calendar_constructor(element, overrides) {
9783
10643
  var viewType = currentView.type;
9784
10644
  var scrollState = currentView.queryScroll();
9785
10645
  clearView();
10646
+ calcSize();
9786
10647
  renderView(viewType, scrollState);
9787
10648
 
9788
- unfreezeContentHeight();
10649
+ thawContentHeight();
9789
10650
  ignoreWindowResize--;
9790
10651
  }
9791
10652
 
9792
-
10653
+
9793
10654
 
9794
10655
  // Resizing
9795
10656
  // -----------------------------------------------------------------------------------
@@ -9806,8 +10667,8 @@ function Calendar_constructor(element, overrides) {
9806
10667
  t.isHeightAuto = function() {
9807
10668
  return t.options.contentHeight === 'auto' || t.options.height === 'auto';
9808
10669
  };
9809
-
9810
-
10670
+
10671
+
9811
10672
  function updateSize(shouldRecalc) {
9812
10673
  if (elementVisible()) {
9813
10674
 
@@ -9829,8 +10690,8 @@ function Calendar_constructor(element, overrides) {
9829
10690
  _calcSize();
9830
10691
  }
9831
10692
  }
9832
-
9833
-
10693
+
10694
+
9834
10695
  function _calcSize() { // assumes elementVisible
9835
10696
  var contentHeightInput = t.options.contentHeight;
9836
10697
  var heightInput = t.options.height;
@@ -9842,13 +10703,13 @@ function Calendar_constructor(element, overrides) {
9842
10703
  suggestedViewHeight = contentHeightInput();
9843
10704
  }
9844
10705
  else if (typeof heightInput === 'number') { // exists and not 'auto'
9845
- suggestedViewHeight = heightInput - queryHeaderHeight();
10706
+ suggestedViewHeight = heightInput - queryToolbarsHeight();
9846
10707
  }
9847
10708
  else if (typeof heightInput === 'function') { // exists and is a function
9848
- suggestedViewHeight = heightInput() - queryHeaderHeight();
10709
+ suggestedViewHeight = heightInput() - queryToolbarsHeight();
9849
10710
  }
9850
10711
  else if (heightInput === 'parent') { // set to height of parent element
9851
- suggestedViewHeight = element.parent().height() - queryHeaderHeight();
10712
+ suggestedViewHeight = element.parent().height() - queryToolbarsHeight();
9852
10713
  }
9853
10714
  else {
9854
10715
  suggestedViewHeight = Math.round(content.width() / Math.max(t.options.aspectRatio, .5));
@@ -9856,11 +10717,14 @@ function Calendar_constructor(element, overrides) {
9856
10717
  }
9857
10718
 
9858
10719
 
9859
- function queryHeaderHeight() {
9860
- return header.el ? header.el.outerHeight(true) : 0; // includes margin
10720
+ function queryToolbarsHeight() {
10721
+ return toolbarsManager.items.reduce(function(accumulator, toolbar) {
10722
+ var toolbarHeight = toolbar.el ? toolbar.el.outerHeight(true) : 0; // includes margin
10723
+ return accumulator + toolbarHeight;
10724
+ }, 0);
9861
10725
  }
9862
-
9863
-
10726
+
10727
+
9864
10728
  function windowResize(ev) {
9865
10729
  if (
9866
10730
  !ignoreWindowResize &&
@@ -9868,94 +10732,93 @@ function Calendar_constructor(element, overrides) {
9868
10732
  currentView.start // view has already been rendered
9869
10733
  ) {
9870
10734
  if (updateSize(true)) {
9871
- currentView.trigger('windowResize', _element);
10735
+ currentView.publiclyTrigger('windowResize', _element);
9872
10736
  }
9873
10737
  }
9874
10738
  }
9875
-
9876
-
9877
-
9878
- /* Event Fetching/Rendering
9879
- -----------------------------------------------------------------------------*/
9880
- // TODO: going forward, most of this stuff should be directly handled by the view
9881
10739
 
9882
10740
 
9883
- function refetchEvents() { // can be called as an API method
9884
- fetchAndRenderEvents();
9885
- }
9886
-
9887
10741
 
9888
- // TODO: move this into EventManager?
9889
- function refetchEventSources(matchInputs) {
9890
- fetchEventSources(t.getEventSourcesByMatchArray(matchInputs));
9891
- }
10742
+ /* Event Rendering
10743
+ -----------------------------------------------------------------------------*/
9892
10744
 
9893
10745
 
9894
- function renderEvents() { // destroys old events if previously rendered
10746
+ function rerenderEvents() { // API method. destroys old events if previously rendered.
9895
10747
  if (elementVisible()) {
9896
- freezeContentHeight();
9897
- currentView.displayEvents(events);
9898
- unfreezeContentHeight();
10748
+ t.reportEventChange(); // will re-trasmit events to the view, causing a rerender
9899
10749
  }
9900
10750
  }
9901
-
9902
10751
 
9903
- function getAndRenderEvents() {
9904
- if (!t.options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) {
9905
- fetchAndRenderEvents();
9906
- }
9907
- else {
9908
- renderEvents();
9909
- }
9910
- }
9911
10752
 
9912
10753
 
9913
- function fetchAndRenderEvents() {
9914
- fetchEvents(currentView.start, currentView.end);
9915
- // ... will call reportEvents
9916
- // ... which will call renderEvents
9917
- }
10754
+ /* Toolbars
10755
+ -----------------------------------------------------------------------------*/
9918
10756
 
9919
-
9920
- // called when event data arrives
9921
- function reportEvents(_events) {
9922
- events = _events;
9923
- renderEvents();
10757
+
10758
+ function buildToolbars() {
10759
+ return [
10760
+ new Toolbar(t, computeHeaderOptions()),
10761
+ new Toolbar(t, computeFooterOptions())
10762
+ ];
9924
10763
  }
9925
10764
 
9926
10765
 
9927
- // called when a single event's data has been changed
9928
- function reportEventChange() {
9929
- renderEvents();
10766
+ function computeHeaderOptions() {
10767
+ return {
10768
+ extraClasses: 'fc-header-toolbar',
10769
+ layout: t.options.header
10770
+ };
9930
10771
  }
9931
10772
 
9932
10773
 
10774
+ function computeFooterOptions() {
10775
+ return {
10776
+ extraClasses: 'fc-footer-toolbar',
10777
+ layout: t.options.footer
10778
+ };
10779
+ }
9933
10780
 
9934
- /* Header Updating
9935
- -----------------------------------------------------------------------------*/
10781
+
10782
+ // can be called repeatedly and Header will rerender
10783
+ function renderHeader() {
10784
+ header.setToolbarOptions(computeHeaderOptions());
10785
+ header.render();
10786
+ if (header.el) {
10787
+ element.prepend(header.el);
10788
+ }
10789
+ }
9936
10790
 
9937
10791
 
9938
- function updateHeaderTitle() {
9939
- header.updateTitle(currentView.title);
10792
+ // can be called repeatedly and Footer will rerender
10793
+ function renderFooter() {
10794
+ footer.setToolbarOptions(computeFooterOptions());
10795
+ footer.render();
10796
+ if (footer.el) {
10797
+ element.append(footer.el);
10798
+ }
9940
10799
  }
9941
10800
 
9942
10801
 
9943
- function updateTodayButton() {
9944
- var now = t.getNow();
10802
+ t.setToolbarsTitle = function(title) {
10803
+ toolbarsManager.proxyCall('updateTitle', title);
10804
+ };
9945
10805
 
10806
+
10807
+ function updateToolbarsTodayButton() {
10808
+ var now = t.getNow();
9946
10809
  if (now >= currentView.intervalStart && now < currentView.intervalEnd) {
9947
- header.disableButton('today');
10810
+ toolbarsManager.proxyCall('disableButton', 'today');
9948
10811
  }
9949
10812
  else {
9950
- header.enableButton('today');
10813
+ toolbarsManager.proxyCall('enableButton', 'today');
9951
10814
  }
9952
10815
  }
9953
-
10816
+
9954
10817
 
9955
10818
 
9956
10819
  /* Selection
9957
10820
  -----------------------------------------------------------------------------*/
9958
-
10821
+
9959
10822
 
9960
10823
  // this public method receives start/end dates in any format, with any timezone
9961
10824
  function select(zonedStartInput, zonedEndInput) {
@@ -9963,56 +10826,56 @@ function Calendar_constructor(element, overrides) {
9963
10826
  t.buildSelectSpan.apply(t, arguments)
9964
10827
  );
9965
10828
  }
9966
-
10829
+
9967
10830
 
9968
10831
  function unselect() { // safe to be called before renderView
9969
10832
  if (currentView) {
9970
10833
  currentView.unselect();
9971
10834
  }
9972
10835
  }
9973
-
9974
-
9975
-
10836
+
10837
+
10838
+
9976
10839
  /* Date
9977
10840
  -----------------------------------------------------------------------------*/
9978
-
9979
-
10841
+
10842
+
9980
10843
  function prev() {
9981
10844
  date = currentView.computePrevDate(date);
9982
10845
  renderView();
9983
10846
  }
9984
-
9985
-
10847
+
10848
+
9986
10849
  function next() {
9987
10850
  date = currentView.computeNextDate(date);
9988
10851
  renderView();
9989
10852
  }
9990
-
9991
-
10853
+
10854
+
9992
10855
  function prevYear() {
9993
10856
  date.add(-1, 'years');
9994
10857
  renderView();
9995
10858
  }
9996
-
9997
-
10859
+
10860
+
9998
10861
  function nextYear() {
9999
10862
  date.add(1, 'years');
10000
10863
  renderView();
10001
10864
  }
10002
-
10003
-
10865
+
10866
+
10004
10867
  function today() {
10005
10868
  date = t.getNow();
10006
10869
  renderView();
10007
10870
  }
10008
-
10009
-
10871
+
10872
+
10010
10873
  function gotoDate(zonedDateInput) {
10011
10874
  date = t.moment(zonedDateInput).stripZone();
10012
10875
  renderView();
10013
10876
  }
10014
-
10015
-
10877
+
10878
+
10016
10879
  function incrementDate(delta) {
10017
10880
  date.add(moment.duration(delta));
10018
10881
  renderView();
@@ -10030,8 +10893,8 @@ function Calendar_constructor(element, overrides) {
10030
10893
  date = newDate.clone();
10031
10894
  renderView(spec ? spec.type : null);
10032
10895
  }
10033
-
10034
-
10896
+
10897
+
10035
10898
  // for external API
10036
10899
  function getDate() {
10037
10900
  return t.applyTimezone(date); // infuse the calendar's timezone
@@ -10041,45 +10904,51 @@ function Calendar_constructor(element, overrides) {
10041
10904
 
10042
10905
  /* Height "Freezing"
10043
10906
  -----------------------------------------------------------------------------*/
10044
- // TODO: move this into the view
10907
+
10045
10908
 
10046
10909
  t.freezeContentHeight = freezeContentHeight;
10047
- t.unfreezeContentHeight = unfreezeContentHeight;
10910
+ t.thawContentHeight = thawContentHeight;
10911
+
10912
+ var freezeContentHeightDepth = 0;
10048
10913
 
10049
10914
 
10050
10915
  function freezeContentHeight() {
10051
- content.css({
10052
- width: '100%',
10053
- height: content.height(),
10054
- overflow: 'hidden'
10055
- });
10916
+ if (!(freezeContentHeightDepth++)) {
10917
+ content.css({
10918
+ width: '100%',
10919
+ height: content.height(),
10920
+ overflow: 'hidden'
10921
+ });
10922
+ }
10056
10923
  }
10057
10924
 
10058
10925
 
10059
- function unfreezeContentHeight() {
10060
- content.css({
10061
- width: '',
10062
- height: '',
10063
- overflow: ''
10064
- });
10926
+ function thawContentHeight() {
10927
+ if (!(--freezeContentHeightDepth)) {
10928
+ content.css({
10929
+ width: '',
10930
+ height: '',
10931
+ overflow: ''
10932
+ });
10933
+ }
10065
10934
  }
10066
-
10067
-
10068
-
10935
+
10936
+
10937
+
10069
10938
  /* Misc
10070
10939
  -----------------------------------------------------------------------------*/
10071
-
10940
+
10072
10941
 
10073
10942
  function getCalendar() {
10074
10943
  return t;
10075
10944
  }
10076
10945
 
10077
-
10946
+
10078
10947
  function getView() {
10079
10948
  return currentView;
10080
10949
  }
10081
-
10082
-
10950
+
10951
+
10083
10952
  function option(name, value) {
10084
10953
  var newOptionHash;
10085
10954
 
@@ -10135,19 +11004,20 @@ function Calendar_constructor(element, overrides) {
10135
11004
  }
10136
11005
  else if (optionName === 'timezone') {
10137
11006
  t.rezoneArrayEventSources();
10138
- refetchEvents();
11007
+ t.refetchEvents();
10139
11008
  return;
10140
11009
  }
10141
11010
  }
10142
11011
 
10143
- // catch-all. rerender the header and rebuild/rerender the current view
11012
+ // catch-all. rerender the header and footer and rebuild/rerender the current view
10144
11013
  renderHeader();
11014
+ renderFooter();
10145
11015
  viewsByType = {}; // even non-current views will be affected by this option change. do before rerender
10146
11016
  reinitView();
10147
11017
  }
10148
-
10149
-
10150
- function trigger(name, thisObj) { // overrides the Emitter's trigger method :(
11018
+
11019
+
11020
+ function publiclyTrigger(name, thisObj) {
10151
11021
  var args = Array.prototype.slice.call(arguments, 2);
10152
11022
 
10153
11023
  thisObj = thisObj || _element;
@@ -10310,6 +11180,7 @@ Calendar.defaults = {
10310
11180
  dropAccept: '*',
10311
11181
 
10312
11182
  eventOrder: 'title',
11183
+ //eventRenderWait: null,
10313
11184
 
10314
11185
  eventLimit: false,
10315
11186
  eventLimitText: 'more',
@@ -10430,392 +11301,123 @@ var dpComputableOptions = {
10430
11301
  // the translations sometimes wrongly contain HTML entities
10431
11302
  prev: stripHtmlEntities(dpOptions.prevText),
10432
11303
  next: stripHtmlEntities(dpOptions.nextText),
10433
- today: stripHtmlEntities(dpOptions.currentText)
10434
- };
10435
- },
10436
-
10437
- // Produces format strings like "MMMM YYYY" -> "September 2014"
10438
- monthYearFormat: function(dpOptions) {
10439
- return dpOptions.showMonthAfterYear ?
10440
- 'YYYY[' + dpOptions.yearSuffix + '] MMMM' :
10441
- 'MMMM YYYY[' + dpOptions.yearSuffix + ']';
10442
- }
10443
-
10444
- };
10445
-
10446
- var momComputableOptions = {
10447
-
10448
- // Produces format strings like "ddd M/D" -> "Fri 9/15"
10449
- dayOfMonthFormat: function(momOptions, fcOptions) {
10450
- var format = momOptions.longDateFormat('l'); // for the format like "M/D/YYYY"
10451
-
10452
- // strip the year off the edge, as well as other misc non-whitespace chars
10453
- format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, '');
10454
-
10455
- if (fcOptions.isRTL) {
10456
- format += ' ddd'; // for RTL, add day-of-week to end
10457
- }
10458
- else {
10459
- format = 'ddd ' + format; // for LTR, add day-of-week to beginning
10460
- }
10461
- return format;
10462
- },
10463
-
10464
- // Produces format strings like "h:mma" -> "6:00pm"
10465
- mediumTimeFormat: function(momOptions) { // can't be called `timeFormat` because collides with option
10466
- return momOptions.longDateFormat('LT')
10467
- .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
10468
- },
10469
-
10470
- // Produces format strings like "h(:mm)a" -> "6pm" / "6:30pm"
10471
- smallTimeFormat: function(momOptions) {
10472
- return momOptions.longDateFormat('LT')
10473
- .replace(':mm', '(:mm)')
10474
- .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales
10475
- .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
10476
- },
10477
-
10478
- // Produces format strings like "h(:mm)t" -> "6p" / "6:30p"
10479
- extraSmallTimeFormat: function(momOptions) {
10480
- return momOptions.longDateFormat('LT')
10481
- .replace(':mm', '(:mm)')
10482
- .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales
10483
- .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand
10484
- },
10485
-
10486
- // Produces format strings like "ha" / "H" -> "6pm" / "18"
10487
- hourFormat: function(momOptions) {
10488
- return momOptions.longDateFormat('LT')
10489
- .replace(':mm', '')
10490
- .replace(/(\Wmm)$/, '') // like above, but for foreign locales
10491
- .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
10492
- },
10493
-
10494
- // Produces format strings like "h:mm" -> "6:30" (with no AM/PM)
10495
- noMeridiemTimeFormat: function(momOptions) {
10496
- return momOptions.longDateFormat('LT')
10497
- .replace(/\s*a$/i, ''); // remove trailing AM/PM
10498
- }
10499
-
10500
- };
10501
-
10502
-
10503
- // options that should be computed off live calendar options (considers override options)
10504
- // TODO: best place for this? related to locale?
10505
- // TODO: flipping text based on isRTL is a bad idea because the CSS `direction` might want to handle it
10506
- var instanceComputableOptions = {
10507
-
10508
- // Produces format strings for results like "Mo 16"
10509
- smallDayDateFormat: function(options) {
10510
- return options.isRTL ?
10511
- 'D dd' :
10512
- 'dd D';
10513
- },
10514
-
10515
- // Produces format strings for results like "Wk 5"
10516
- weekFormat: function(options) {
10517
- return options.isRTL ?
10518
- 'w[ ' + options.weekNumberTitle + ']' :
10519
- '[' + options.weekNumberTitle + ' ]w';
10520
- },
10521
-
10522
- // Produces format strings for results like "Wk5"
10523
- smallWeekFormat: function(options) {
10524
- return options.isRTL ?
10525
- 'w[' + options.weekNumberTitle + ']' :
10526
- '[' + options.weekNumberTitle + ']w';
10527
- }
10528
-
10529
- };
10530
-
10531
- function populateInstanceComputableOptions(options) {
10532
- $.each(instanceComputableOptions, function(name, func) {
10533
- if (options[name] == null) {
10534
- options[name] = func(options);
10535
- }
10536
- });
10537
- }
10538
-
10539
-
10540
- // Returns moment's internal locale data. If doesn't exist, returns English.
10541
- function getMomentLocaleData(localeCode) {
10542
- return moment.localeData(localeCode) || moment.localeData('en');
10543
- }
10544
-
10545
-
10546
- // Initialize English by forcing computation of moment-derived options.
10547
- // Also, sets it as the default.
10548
- FC.locale('en', Calendar.englishDefaults);
10549
-
10550
- ;;
10551
-
10552
- /* Top toolbar area with buttons and title
10553
- ----------------------------------------------------------------------------------------------------------------------*/
10554
- // TODO: rename all header-related things to "toolbar"
10555
-
10556
- function Header(calendar) {
10557
- var t = this;
10558
-
10559
- // exports
10560
- t.render = render;
10561
- t.removeElement = removeElement;
10562
- t.updateTitle = updateTitle;
10563
- t.activateButton = activateButton;
10564
- t.deactivateButton = deactivateButton;
10565
- t.disableButton = disableButton;
10566
- t.enableButton = enableButton;
10567
- t.getViewsWithButtons = getViewsWithButtons;
10568
- t.el = null; // mirrors local `el`
10569
-
10570
- // locals
10571
- var el;
10572
- var viewsWithButtons = [];
10573
- var tm;
10574
-
10575
-
10576
- // can be called repeatedly and will rerender
10577
- function render() {
10578
- var options = calendar.options;
10579
- var sections = options.header;
10580
-
10581
- tm = options.theme ? 'ui' : 'fc';
10582
-
10583
- if (sections) {
10584
- if (!el) {
10585
- el = this.el = $("<div class='fc-toolbar'/>");
10586
- }
10587
- else {
10588
- el.empty();
10589
- }
10590
- el.append(renderSection('left'))
10591
- .append(renderSection('right'))
10592
- .append(renderSection('center'))
10593
- .append('<div class="fc-clear"/>');
10594
- }
10595
- else {
10596
- removeElement();
10597
- }
10598
- }
10599
-
10600
-
10601
- function removeElement() {
10602
- if (el) {
10603
- el.remove();
10604
- el = t.el = null;
10605
- }
10606
- }
10607
-
10608
-
10609
- function renderSection(position) {
10610
- var sectionEl = $('<div class="fc-' + position + '"/>');
10611
- var options = calendar.options;
10612
- var buttonStr = options.header[position];
10613
-
10614
- if (buttonStr) {
10615
- $.each(buttonStr.split(' '), function(i) {
10616
- var groupChildren = $();
10617
- var isOnlyButtons = true;
10618
- var groupEl;
10619
-
10620
- $.each(this.split(','), function(j, buttonName) {
10621
- var customButtonProps;
10622
- var viewSpec;
10623
- var buttonClick;
10624
- var overrideText; // text explicitly set by calendar's constructor options. overcomes icons
10625
- var defaultText;
10626
- var themeIcon;
10627
- var normalIcon;
10628
- var innerHtml;
10629
- var classes;
10630
- var button; // the element
10631
-
10632
- if (buttonName == 'title') {
10633
- groupChildren = groupChildren.add($('<h2>&nbsp;</h2>')); // we always want it to take up height
10634
- isOnlyButtons = false;
10635
- }
10636
- else {
10637
- if ((customButtonProps = (options.customButtons || {})[buttonName])) {
10638
- buttonClick = function(ev) {
10639
- if (customButtonProps.click) {
10640
- customButtonProps.click.call(button[0], ev);
10641
- }
10642
- };
10643
- overrideText = ''; // icons will override text
10644
- defaultText = customButtonProps.text;
10645
- }
10646
- else if ((viewSpec = calendar.getViewSpec(buttonName))) {
10647
- buttonClick = function() {
10648
- calendar.changeView(buttonName);
10649
- };
10650
- viewsWithButtons.push(buttonName);
10651
- overrideText = viewSpec.buttonTextOverride;
10652
- defaultText = viewSpec.buttonTextDefault;
10653
- }
10654
- else if (calendar[buttonName]) { // a calendar method
10655
- buttonClick = function() {
10656
- calendar[buttonName]();
10657
- };
10658
- overrideText = (calendar.overrides.buttonText || {})[buttonName];
10659
- defaultText = options.buttonText[buttonName]; // everything else is considered default
10660
- }
10661
-
10662
- if (buttonClick) {
10663
-
10664
- themeIcon =
10665
- customButtonProps ?
10666
- customButtonProps.themeIcon :
10667
- options.themeButtonIcons[buttonName];
10668
-
10669
- normalIcon =
10670
- customButtonProps ?
10671
- customButtonProps.icon :
10672
- options.buttonIcons[buttonName];
10673
-
10674
- if (overrideText) {
10675
- innerHtml = htmlEscape(overrideText);
10676
- }
10677
- else if (themeIcon && options.theme) {
10678
- innerHtml = "<span class='ui-icon ui-icon-" + themeIcon + "'></span>";
10679
- }
10680
- else if (normalIcon && !options.theme) {
10681
- innerHtml = "<span class='fc-icon fc-icon-" + normalIcon + "'></span>";
10682
- }
10683
- else {
10684
- innerHtml = htmlEscape(defaultText);
10685
- }
10686
-
10687
- classes = [
10688
- 'fc-' + buttonName + '-button',
10689
- tm + '-button',
10690
- tm + '-state-default'
10691
- ];
10692
-
10693
- button = $( // type="button" so that it doesn't submit a form
10694
- '<button type="button" class="' + classes.join(' ') + '">' +
10695
- innerHtml +
10696
- '</button>'
10697
- )
10698
- .click(function(ev) {
10699
- // don't process clicks for disabled buttons
10700
- if (!button.hasClass(tm + '-state-disabled')) {
10701
-
10702
- buttonClick(ev);
10703
-
10704
- // after the click action, if the button becomes the "active" tab, or disabled,
10705
- // it should never have a hover class, so remove it now.
10706
- if (
10707
- button.hasClass(tm + '-state-active') ||
10708
- button.hasClass(tm + '-state-disabled')
10709
- ) {
10710
- button.removeClass(tm + '-state-hover');
10711
- }
10712
- }
10713
- })
10714
- .mousedown(function() {
10715
- // the *down* effect (mouse pressed in).
10716
- // only on buttons that are not the "active" tab, or disabled
10717
- button
10718
- .not('.' + tm + '-state-active')
10719
- .not('.' + tm + '-state-disabled')
10720
- .addClass(tm + '-state-down');
10721
- })
10722
- .mouseup(function() {
10723
- // undo the *down* effect
10724
- button.removeClass(tm + '-state-down');
10725
- })
10726
- .hover(
10727
- function() {
10728
- // the *hover* effect.
10729
- // only on buttons that are not the "active" tab, or disabled
10730
- button
10731
- .not('.' + tm + '-state-active')
10732
- .not('.' + tm + '-state-disabled')
10733
- .addClass(tm + '-state-hover');
10734
- },
10735
- function() {
10736
- // undo the *hover* effect
10737
- button
10738
- .removeClass(tm + '-state-hover')
10739
- .removeClass(tm + '-state-down'); // if mouseleave happens before mouseup
10740
- }
10741
- );
10742
-
10743
- groupChildren = groupChildren.add(button);
10744
- }
10745
- }
10746
- });
10747
-
10748
- if (isOnlyButtons) {
10749
- groupChildren
10750
- .first().addClass(tm + '-corner-left').end()
10751
- .last().addClass(tm + '-corner-right').end();
10752
- }
10753
-
10754
- if (groupChildren.length > 1) {
10755
- groupEl = $('<div/>');
10756
- if (isOnlyButtons) {
10757
- groupEl.addClass('fc-button-group');
10758
- }
10759
- groupEl.append(groupChildren);
10760
- sectionEl.append(groupEl);
10761
- }
10762
- else {
10763
- sectionEl.append(groupChildren); // 1 or 0 children
10764
- }
10765
- });
10766
- }
10767
-
10768
- return sectionEl;
10769
- }
10770
-
10771
-
10772
- function updateTitle(text) {
10773
- if (el) {
10774
- el.find('h2').text(text);
10775
- }
10776
- }
10777
-
10778
-
10779
- function activateButton(buttonName) {
10780
- if (el) {
10781
- el.find('.fc-' + buttonName + '-button')
10782
- .addClass(tm + '-state-active');
10783
- }
10784
- }
10785
-
10786
-
10787
- function deactivateButton(buttonName) {
10788
- if (el) {
10789
- el.find('.fc-' + buttonName + '-button')
10790
- .removeClass(tm + '-state-active');
10791
- }
11304
+ today: stripHtmlEntities(dpOptions.currentText)
11305
+ };
11306
+ },
11307
+
11308
+ // Produces format strings like "MMMM YYYY" -> "September 2014"
11309
+ monthYearFormat: function(dpOptions) {
11310
+ return dpOptions.showMonthAfterYear ?
11311
+ 'YYYY[' + dpOptions.yearSuffix + '] MMMM' :
11312
+ 'MMMM YYYY[' + dpOptions.yearSuffix + ']';
10792
11313
  }
10793
-
10794
-
10795
- function disableButton(buttonName) {
10796
- if (el) {
10797
- el.find('.fc-' + buttonName + '-button')
10798
- .prop('disabled', true)
10799
- .addClass(tm + '-state-disabled');
11314
+
11315
+ };
11316
+
11317
+ var momComputableOptions = {
11318
+
11319
+ // Produces format strings like "ddd M/D" -> "Fri 9/15"
11320
+ dayOfMonthFormat: function(momOptions, fcOptions) {
11321
+ var format = momOptions.longDateFormat('l'); // for the format like "M/D/YYYY"
11322
+
11323
+ // strip the year off the edge, as well as other misc non-whitespace chars
11324
+ format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, '');
11325
+
11326
+ if (fcOptions.isRTL) {
11327
+ format += ' ddd'; // for RTL, add day-of-week to end
10800
11328
  }
10801
- }
10802
-
10803
-
10804
- function enableButton(buttonName) {
10805
- if (el) {
10806
- el.find('.fc-' + buttonName + '-button')
10807
- .prop('disabled', false)
10808
- .removeClass(tm + '-state-disabled');
11329
+ else {
11330
+ format = 'ddd ' + format; // for LTR, add day-of-week to beginning
10809
11331
  }
11332
+ return format;
11333
+ },
11334
+
11335
+ // Produces format strings like "h:mma" -> "6:00pm"
11336
+ mediumTimeFormat: function(momOptions) { // can't be called `timeFormat` because collides with option
11337
+ return momOptions.longDateFormat('LT')
11338
+ .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
11339
+ },
11340
+
11341
+ // Produces format strings like "h(:mm)a" -> "6pm" / "6:30pm"
11342
+ smallTimeFormat: function(momOptions) {
11343
+ return momOptions.longDateFormat('LT')
11344
+ .replace(':mm', '(:mm)')
11345
+ .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales
11346
+ .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
11347
+ },
11348
+
11349
+ // Produces format strings like "h(:mm)t" -> "6p" / "6:30p"
11350
+ extraSmallTimeFormat: function(momOptions) {
11351
+ return momOptions.longDateFormat('LT')
11352
+ .replace(':mm', '(:mm)')
11353
+ .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales
11354
+ .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand
11355
+ },
11356
+
11357
+ // Produces format strings like "ha" / "H" -> "6pm" / "18"
11358
+ hourFormat: function(momOptions) {
11359
+ return momOptions.longDateFormat('LT')
11360
+ .replace(':mm', '')
11361
+ .replace(/(\Wmm)$/, '') // like above, but for foreign locales
11362
+ .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
11363
+ },
11364
+
11365
+ // Produces format strings like "h:mm" -> "6:30" (with no AM/PM)
11366
+ noMeridiemTimeFormat: function(momOptions) {
11367
+ return momOptions.longDateFormat('LT')
11368
+ .replace(/\s*a$/i, ''); // remove trailing AM/PM
10810
11369
  }
10811
11370
 
11371
+ };
10812
11372
 
10813
- function getViewsWithButtons() {
10814
- return viewsWithButtons;
11373
+
11374
+ // options that should be computed off live calendar options (considers override options)
11375
+ // TODO: best place for this? related to locale?
11376
+ // TODO: flipping text based on isRTL is a bad idea because the CSS `direction` might want to handle it
11377
+ var instanceComputableOptions = {
11378
+
11379
+ // Produces format strings for results like "Mo 16"
11380
+ smallDayDateFormat: function(options) {
11381
+ return options.isRTL ?
11382
+ 'D dd' :
11383
+ 'dd D';
11384
+ },
11385
+
11386
+ // Produces format strings for results like "Wk 5"
11387
+ weekFormat: function(options) {
11388
+ return options.isRTL ?
11389
+ 'w[ ' + options.weekNumberTitle + ']' :
11390
+ '[' + options.weekNumberTitle + ' ]w';
11391
+ },
11392
+
11393
+ // Produces format strings for results like "Wk5"
11394
+ smallWeekFormat: function(options) {
11395
+ return options.isRTL ?
11396
+ 'w[' + options.weekNumberTitle + ']' :
11397
+ '[' + options.weekNumberTitle + ']w';
10815
11398
  }
10816
11399
 
11400
+ };
11401
+
11402
+ function populateInstanceComputableOptions(options) {
11403
+ $.each(instanceComputableOptions, function(name, func) {
11404
+ if (options[name] == null) {
11405
+ options[name] = func(options);
11406
+ }
11407
+ });
11408
+ }
11409
+
11410
+
11411
+ // Returns moment's internal locale data. If doesn't exist, returns English.
11412
+ function getMomentLocaleData(localeCode) {
11413
+ return moment.localeData(localeCode) || moment.localeData('en');
10817
11414
  }
10818
11415
 
11416
+
11417
+ // Initialize English by forcing computation of moment-derived options.
11418
+ // Also, sets it as the default.
11419
+ FC.locale('en', Calendar.englishDefaults);
11420
+
10819
11421
  ;;
10820
11422
 
10821
11423
  FC.sourceNormalizers = [];
@@ -10831,38 +11433,39 @@ var eventGUID = 1;
10831
11433
 
10832
11434
  function EventManager() { // assumed to be a calendar
10833
11435
  var t = this;
10834
-
10835
-
11436
+
11437
+
10836
11438
  // exports
11439
+ t.requestEvents = requestEvents;
11440
+ t.reportEventChange = reportEventChange;
10837
11441
  t.isFetchNeeded = isFetchNeeded;
10838
11442
  t.fetchEvents = fetchEvents;
10839
11443
  t.fetchEventSources = fetchEventSources;
11444
+ t.refetchEvents = refetchEvents;
11445
+ t.refetchEventSources = refetchEventSources;
10840
11446
  t.getEventSources = getEventSources;
10841
11447
  t.getEventSourceById = getEventSourceById;
10842
- t.getEventSourcesByMatchArray = getEventSourcesByMatchArray;
10843
- t.getEventSourcesByMatch = getEventSourcesByMatch;
10844
11448
  t.addEventSource = addEventSource;
10845
11449
  t.removeEventSource = removeEventSource;
10846
11450
  t.removeEventSources = removeEventSources;
10847
11451
  t.updateEvent = updateEvent;
11452
+ t.updateEvents = updateEvents;
10848
11453
  t.renderEvent = renderEvent;
11454
+ t.renderEvents = renderEvents;
10849
11455
  t.removeEvents = removeEvents;
10850
11456
  t.clientEvents = clientEvents;
10851
11457
  t.mutateEvent = mutateEvent;
10852
11458
  t.normalizeEventDates = normalizeEventDates;
10853
11459
  t.normalizeEventTimes = normalizeEventTimes;
10854
-
10855
-
10856
- // imports
10857
- var reportEvents = t.reportEvents;
10858
-
10859
-
11460
+
11461
+
10860
11462
  // locals
10861
11463
  var stickySource = { events: [] };
10862
11464
  var sources = [ stickySource ];
10863
11465
  var rangeStart, rangeEnd;
10864
11466
  var pendingSourceCnt = 0; // outstanding fetch requests, max one per source
10865
11467
  var cache = []; // holds events that have already been expanded
11468
+ var prunedCache; // like cache, but only events that intersect with rangeStart/rangeEnd
10866
11469
 
10867
11470
 
10868
11471
  $.each(
@@ -10874,9 +11477,55 @@ function EventManager() { // assumed to be a calendar
10874
11477
  }
10875
11478
  }
10876
11479
  );
10877
-
10878
-
10879
-
11480
+
11481
+
11482
+
11483
+ function requestEvents(start, end) {
11484
+ if (!t.options.lazyFetching || isFetchNeeded(start, end)) {
11485
+ return fetchEvents(start, end);
11486
+ }
11487
+ else {
11488
+ return Promise.resolve(prunedCache);
11489
+ }
11490
+ }
11491
+
11492
+
11493
+ function reportEventChange() {
11494
+ prunedCache = filterEventsWithinRange(cache);
11495
+ t.trigger('eventsReset', prunedCache);
11496
+ }
11497
+
11498
+
11499
+ function filterEventsWithinRange(events) {
11500
+ var filteredEvents = [];
11501
+ var i, event;
11502
+
11503
+ for (i = 0; i < events.length; i++) {
11504
+ event = events[i];
11505
+
11506
+ if (
11507
+ event.start.clone().stripZone() < rangeEnd &&
11508
+ t.getEventEnd(event).stripZone() > rangeStart
11509
+ ) {
11510
+ filteredEvents.push(event);
11511
+ }
11512
+ }
11513
+
11514
+ return filteredEvents;
11515
+ }
11516
+
11517
+
11518
+ t.getEventCache = function() {
11519
+ return cache;
11520
+ };
11521
+
11522
+
11523
+ t.getPrunedEventCache = function() {
11524
+ return prunedCache;
11525
+ };
11526
+
11527
+
11528
+
10880
11529
  /* Fetching
10881
11530
  -----------------------------------------------------------------------------*/
10882
11531
 
@@ -10886,12 +11535,24 @@ function EventManager() { // assumed to be a calendar
10886
11535
  return !rangeStart || // nothing has been fetched yet?
10887
11536
  start < rangeStart || end > rangeEnd; // is part of the new range outside of the old range?
10888
11537
  }
10889
-
10890
-
11538
+
11539
+
10891
11540
  function fetchEvents(start, end) {
10892
11541
  rangeStart = start;
10893
11542
  rangeEnd = end;
10894
- fetchEventSources(sources, 'reset');
11543
+ return refetchEvents();
11544
+ }
11545
+
11546
+
11547
+ // poorly named. fetches all sources with current `rangeStart` and `rangeEnd`.
11548
+ function refetchEvents() {
11549
+ return fetchEventSources(sources, 'reset');
11550
+ }
11551
+
11552
+
11553
+ // poorly named. fetches a subset of event sources.
11554
+ function refetchEventSources(matchInputs) {
11555
+ return fetchEventSources(getEventSourcesByMatchArray(matchInputs));
10895
11556
  }
10896
11557
 
10897
11558
 
@@ -10921,9 +11582,17 @@ function EventManager() { // assumed to be a calendar
10921
11582
 
10922
11583
  for (i = 0; i < specificSources.length; i++) {
10923
11584
  source = specificSources[i];
10924
-
10925
11585
  tryFetchEventSource(source, source._fetchId);
10926
11586
  }
11587
+
11588
+ if (pendingSourceCnt) {
11589
+ return new Promise(function(resolve) {
11590
+ t.one('eventsReceived', resolve); // will send prunedCache
11591
+ });
11592
+ }
11593
+ else { // executed all synchronously, or no sources at all
11594
+ return Promise.resolve(prunedCache);
11595
+ }
10927
11596
  }
10928
11597
 
10929
11598
 
@@ -10956,7 +11625,7 @@ function EventManager() { // assumed to be a calendar
10956
11625
  }
10957
11626
 
10958
11627
  if (abstractEvent) { // not false (an invalid event)
10959
- cache.push.apply(
11628
+ cache.push.apply( // append
10960
11629
  cache,
10961
11630
  expandEvent(abstractEvent) // add individual expanded events to the cache
10962
11631
  );
@@ -10984,11 +11653,12 @@ function EventManager() { // assumed to be a calendar
10984
11653
  function decrementPendingSourceCnt() {
10985
11654
  pendingSourceCnt--;
10986
11655
  if (!pendingSourceCnt) {
10987
- reportEvents(cache);
11656
+ reportEventChange(cache); // updates prunedCache
11657
+ t.trigger('eventsReceived', prunedCache);
10988
11658
  }
10989
11659
  }
10990
-
10991
-
11660
+
11661
+
10992
11662
  function _fetchEventSource(source, callback) {
10993
11663
  var i;
10994
11664
  var fetchers = FC.sourceFetchers;
@@ -11097,9 +11767,9 @@ function EventManager() { // assumed to be a calendar
11097
11767
  }
11098
11768
  }
11099
11769
  }
11100
-
11101
-
11102
-
11770
+
11771
+
11772
+
11103
11773
  /* Sources
11104
11774
  -----------------------------------------------------------------------------*/
11105
11775
 
@@ -11108,7 +11778,7 @@ function EventManager() { // assumed to be a calendar
11108
11778
  var source = buildEventSource(sourceInput);
11109
11779
  if (source) {
11110
11780
  sources.push(source);
11111
- fetchEventSources([ source ], 'add'); // will eventually call reportEvents
11781
+ fetchEventSources([ source ], 'add'); // will eventually call reportEventChange
11112
11782
  }
11113
11783
  }
11114
11784
 
@@ -11204,7 +11874,7 @@ function EventManager() { // assumed to be a calendar
11204
11874
  cache = excludeEventsBySources(cache, targetSources);
11205
11875
  }
11206
11876
 
11207
- reportEvents(cache);
11877
+ reportEventChange();
11208
11878
  }
11209
11879
 
11210
11880
 
@@ -11298,27 +11968,39 @@ function EventManager() { // assumed to be a calendar
11298
11968
  return true; // keep
11299
11969
  });
11300
11970
  }
11301
-
11302
-
11303
-
11971
+
11972
+
11973
+
11304
11974
  /* Manipulation
11305
11975
  -----------------------------------------------------------------------------*/
11306
11976
 
11307
11977
 
11308
11978
  // Only ever called from the externally-facing API
11309
11979
  function updateEvent(event) {
11980
+ updateEvents([ event ]);
11981
+ }
11310
11982
 
11311
- // massage start/end values, even if date string values
11312
- event.start = t.moment(event.start);
11313
- if (event.end) {
11314
- event.end = t.moment(event.end);
11315
- }
11316
- else {
11317
- event.end = null;
11983
+
11984
+ // Only ever called from the externally-facing API
11985
+ function updateEvents(events) {
11986
+ var i, event;
11987
+
11988
+ for (i = 0; i < events.length; i++) {
11989
+ event = events[i];
11990
+
11991
+ // massage start/end values, even if date string values
11992
+ event.start = t.moment(event.start);
11993
+ if (event.end) {
11994
+ event.end = t.moment(event.end);
11995
+ }
11996
+ else {
11997
+ event.end = null;
11998
+ }
11999
+
12000
+ mutateEvent(event, getMiscEventProps(event)); // will handle start/end/allDay normalization
11318
12001
  }
11319
12002
 
11320
- mutateEvent(event, getMiscEventProps(event)); // will handle start/end/allDay normalization
11321
- reportEvents(cache); // reports event modifications (so we can redraw)
12003
+ reportEventChange(); // reports event modifications (so we can redraw)
11322
12004
  }
11323
12005
 
11324
12006
 
@@ -11342,37 +12024,50 @@ function EventManager() { // assumed to be a calendar
11342
12024
  return !/^_|^(id|allDay|start|end)$/.test(name);
11343
12025
  }
11344
12026
 
11345
-
12027
+
11346
12028
  // returns the expanded events that were created
11347
12029
  function renderEvent(eventInput, stick) {
11348
- var abstractEvent = buildEventFromInput(eventInput);
11349
- var events;
11350
- var i, event;
12030
+ return renderEvents([ eventInput ], stick);
12031
+ }
11351
12032
 
11352
- if (abstractEvent) { // not false (a valid input)
11353
- events = expandEvent(abstractEvent);
11354
12033
 
11355
- for (i = 0; i < events.length; i++) {
11356
- event = events[i];
12034
+ // returns the expanded events that were created
12035
+ function renderEvents(eventInputs, stick) {
12036
+ var renderedEvents = [];
12037
+ var renderableEvents;
12038
+ var abstractEvent;
12039
+ var i, j, event;
12040
+
12041
+ for (i = 0; i < eventInputs.length; i++) {
12042
+ abstractEvent = buildEventFromInput(eventInputs[i]);
12043
+
12044
+ if (abstractEvent) { // not false (a valid input)
12045
+ renderableEvents = expandEvent(abstractEvent);
12046
+
12047
+ for (j = 0; j < renderableEvents.length; j++) {
12048
+ event = renderableEvents[j];
11357
12049
 
11358
- if (!event.source) {
11359
- if (stick) {
11360
- stickySource.events.push(event);
11361
- event.source = stickySource;
12050
+ if (!event.source) {
12051
+ if (stick) {
12052
+ stickySource.events.push(event);
12053
+ event.source = stickySource;
12054
+ }
12055
+ cache.push(event);
11362
12056
  }
11363
- cache.push(event);
11364
12057
  }
11365
- }
11366
12058
 
11367
- reportEvents(cache);
12059
+ renderedEvents = renderedEvents.concat(renderableEvents);
12060
+ }
12061
+ }
11368
12062
 
11369
- return events;
12063
+ if (renderedEvents.length) { // any new events rendered?
12064
+ reportEventChange();
11370
12065
  }
11371
12066
 
11372
- return [];
12067
+ return renderedEvents;
11373
12068
  }
11374
-
11375
-
12069
+
12070
+
11376
12071
  function removeEvents(filter) {
11377
12072
  var eventID;
11378
12073
  var i;
@@ -11399,10 +12094,10 @@ function EventManager() { // assumed to be a calendar
11399
12094
  }
11400
12095
  }
11401
12096
 
11402
- reportEvents(cache);
12097
+ reportEventChange();
11403
12098
  }
11404
12099
 
11405
-
12100
+
11406
12101
  function clientEvents(filter) {
11407
12102
  if ($.isFunction(filter)) {
11408
12103
  return $.grep(cache, filter);
@@ -11442,8 +12137,8 @@ function EventManager() { // assumed to be a calendar
11442
12137
  }
11443
12138
  backupEventDates(event);
11444
12139
  }
11445
-
11446
-
12140
+
12141
+
11447
12142
  /* Event Normalization
11448
12143
  -----------------------------------------------------------------------------*/
11449
12144
 
@@ -11853,11 +12548,6 @@ function EventManager() { // assumed to be a calendar
11853
12548
  };
11854
12549
  }
11855
12550
 
11856
-
11857
- t.getEventCache = function() {
11858
- return cache;
11859
- };
11860
-
11861
12551
  }
11862
12552
 
11863
12553
 
@@ -12386,13 +13076,18 @@ var BasicView = FC.BasicView = View.extend({
12386
13076
  ------------------------------------------------------------------------------------------------------------------*/
12387
13077
 
12388
13078
 
13079
+ computeInitialScroll: function() {
13080
+ return { top: 0 };
13081
+ },
13082
+
13083
+
12389
13084
  queryScroll: function() {
12390
- return this.scroller.getScrollTop();
13085
+ return { top: this.scroller.getScrollTop() };
12391
13086
  },
12392
13087
 
12393
13088
 
12394
- setScroll: function(top) {
12395
- this.scroller.setScrollTop(top);
13089
+ setScroll: function(scroll) {
13090
+ this.scroller.setScrollTop(scroll.top);
12396
13091
  },
12397
13092
 
12398
13093
 
@@ -12909,17 +13604,17 @@ var AgendaView = FC.AgendaView = View.extend({
12909
13604
  top++; // to overcome top border that slots beyond the first have. looks better
12910
13605
  }
12911
13606
 
12912
- return top;
13607
+ return { top: top };
12913
13608
  },
12914
13609
 
12915
13610
 
12916
13611
  queryScroll: function() {
12917
- return this.scroller.getScrollTop();
13612
+ return { top: this.scroller.getScrollTop() };
12918
13613
  },
12919
13614
 
12920
13615
 
12921
- setScroll: function(top) {
12922
- this.scroller.setScrollTop(top);
13616
+ setScroll: function(scroll) {
13617
+ this.scroller.setScrollTop(scroll.top);
12923
13618
  },
12924
13619
 
12925
13620