fullcalendar-rails 3.0.0.0 → 3.1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/lib/fullcalendar-rails/version.rb +1 -1
  3. data/vendor/assets/javascripts/fullcalendar.js +1588 -811
  4. data/vendor/assets/javascripts/fullcalendar/gcal.js +1 -1
  5. data/vendor/assets/javascripts/fullcalendar/lang/af.js +1 -0
  6. data/vendor/assets/javascripts/fullcalendar/lang/ar-dz.js +1 -0
  7. data/vendor/assets/javascripts/fullcalendar/lang/ar-ly.js +1 -0
  8. data/vendor/assets/javascripts/fullcalendar/lang/ar-ma.js +0 -0
  9. data/vendor/assets/javascripts/fullcalendar/lang/ar-sa.js +1 -1
  10. data/vendor/assets/javascripts/fullcalendar/lang/ar-tn.js +0 -0
  11. data/vendor/assets/javascripts/fullcalendar/lang/ar.js +0 -0
  12. data/vendor/assets/javascripts/fullcalendar/lang/bg.js +0 -0
  13. data/vendor/assets/javascripts/fullcalendar/lang/ca.js +1 -1
  14. data/vendor/assets/javascripts/fullcalendar/lang/cs.js +0 -0
  15. data/vendor/assets/javascripts/fullcalendar/lang/da.js +0 -0
  16. data/vendor/assets/javascripts/fullcalendar/lang/de-at.js +1 -1
  17. data/vendor/assets/javascripts/fullcalendar/lang/de.js +1 -1
  18. data/vendor/assets/javascripts/fullcalendar/lang/el.js +0 -0
  19. data/vendor/assets/javascripts/fullcalendar/lang/en-au.js +0 -0
  20. data/vendor/assets/javascripts/fullcalendar/lang/en-ca.js +0 -0
  21. data/vendor/assets/javascripts/fullcalendar/lang/en-gb.js +0 -0
  22. data/vendor/assets/javascripts/fullcalendar/lang/en-ie.js +0 -0
  23. data/vendor/assets/javascripts/fullcalendar/lang/en-nz.js +0 -0
  24. data/vendor/assets/javascripts/fullcalendar/lang/es-do.js +0 -0
  25. data/vendor/assets/javascripts/fullcalendar/lang/es.js +0 -0
  26. data/vendor/assets/javascripts/fullcalendar/lang/eu.js +0 -0
  27. data/vendor/assets/javascripts/fullcalendar/lang/fa.js +0 -0
  28. data/vendor/assets/javascripts/fullcalendar/lang/fi.js +0 -0
  29. data/vendor/assets/javascripts/fullcalendar/lang/fr-ca.js +0 -0
  30. data/vendor/assets/javascripts/fullcalendar/lang/fr-ch.js +0 -0
  31. data/vendor/assets/javascripts/fullcalendar/lang/fr.js +0 -0
  32. data/vendor/assets/javascripts/fullcalendar/lang/gl.js +1 -1
  33. data/vendor/assets/javascripts/fullcalendar/lang/he.js +0 -0
  34. data/vendor/assets/javascripts/fullcalendar/lang/hi.js +0 -0
  35. data/vendor/assets/javascripts/fullcalendar/lang/hr.js +1 -1
  36. data/vendor/assets/javascripts/fullcalendar/lang/hu.js +1 -1
  37. data/vendor/assets/javascripts/fullcalendar/lang/id.js +0 -0
  38. data/vendor/assets/javascripts/fullcalendar/lang/is.js +0 -0
  39. data/vendor/assets/javascripts/fullcalendar/lang/it.js +0 -0
  40. data/vendor/assets/javascripts/fullcalendar/lang/ja.js +0 -0
  41. data/vendor/assets/javascripts/fullcalendar/lang/kk.js +1 -0
  42. data/vendor/assets/javascripts/fullcalendar/lang/ko.js +0 -0
  43. data/vendor/assets/javascripts/fullcalendar/lang/lb.js +0 -0
  44. data/vendor/assets/javascripts/fullcalendar/lang/lt.js +1 -1
  45. data/vendor/assets/javascripts/fullcalendar/lang/lv.js +0 -0
  46. data/vendor/assets/javascripts/fullcalendar/lang/mk.js +0 -0
  47. data/vendor/assets/javascripts/fullcalendar/lang/ms-my.js +0 -0
  48. data/vendor/assets/javascripts/fullcalendar/lang/ms.js +0 -0
  49. data/vendor/assets/javascripts/fullcalendar/lang/nb.js +0 -0
  50. data/vendor/assets/javascripts/fullcalendar/lang/nl-be.js +1 -0
  51. data/vendor/assets/javascripts/fullcalendar/lang/nl.js +1 -1
  52. data/vendor/assets/javascripts/fullcalendar/lang/nn.js +0 -0
  53. data/vendor/assets/javascripts/fullcalendar/lang/pl.js +1 -1
  54. data/vendor/assets/javascripts/fullcalendar/lang/pt-br.js +0 -0
  55. data/vendor/assets/javascripts/fullcalendar/lang/pt.js +0 -0
  56. data/vendor/assets/javascripts/fullcalendar/lang/ro.js +0 -0
  57. data/vendor/assets/javascripts/fullcalendar/lang/ru.js +0 -0
  58. data/vendor/assets/javascripts/fullcalendar/lang/sk.js +0 -0
  59. data/vendor/assets/javascripts/fullcalendar/lang/sl.js +1 -1
  60. data/vendor/assets/javascripts/fullcalendar/lang/sr-cyrl.js +1 -1
  61. data/vendor/assets/javascripts/fullcalendar/lang/sr.js +1 -1
  62. data/vendor/assets/javascripts/fullcalendar/lang/sv.js +0 -0
  63. data/vendor/assets/javascripts/fullcalendar/lang/th.js +1 -1
  64. data/vendor/assets/javascripts/fullcalendar/lang/tr.js +0 -0
  65. data/vendor/assets/javascripts/fullcalendar/lang/uk.js +0 -0
  66. data/vendor/assets/javascripts/fullcalendar/lang/vi.js +0 -0
  67. data/vendor/assets/javascripts/fullcalendar/lang/zh-cn.js +0 -0
  68. data/vendor/assets/javascripts/fullcalendar/lang/zh-tw.js +0 -0
  69. data/vendor/assets/javascripts/fullcalendar/locale-all.js +5 -5
  70. data/vendor/assets/stylesheets/fullcalendar.css +9 -2
  71. data/vendor/assets/stylesheets/fullcalendar.print.css +6 -6
  72. metadata +8 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8773252a35a176af5a30a696c6f5d850ad818da1
4
- data.tar.gz: 846cdb8578a403af18fd01d5a85dda750bbd6c74
3
+ metadata.gz: 101da80d21194928e277041d77169ca0c69db33c
4
+ data.tar.gz: b1d67a1680789432aff809f3323c9490fd29cbdb
5
5
  SHA512:
6
- metadata.gz: 90d7ab81c55d9fc97353af6c7f26f7b9c96b097fe8c9099c971f2f146617156ed5a7da127f87dd8afc20d4228ee607748fda8d5ba0a4e5160ef5634c874be802
7
- data.tar.gz: ba68426e2a511b8b7226036ab4524458820e841e1cff661b39fc4e7e4b27060500a44519f8be752016173af34a901e4b5cddaadb90939d7d25b8df4dbd0b316b
6
+ metadata.gz: 9fed2cf5ce778966801097161c8b1cae067ea4eaf943df6dea06bf5ecba9ffe1ae25ca618c4dae0bb62321d52ee73c6f2c3300aaee91e00261b7184c605ad93a
7
+ data.tar.gz: 21a423f82fc784a1c6095e94e82f0ab06d415acb760b4138d069939b6766fe6f1003437917bfd4c15edaac10c4f8a30e0f311b6f0ee31e3129bd3d3ca71719c2
@@ -1,5 +1,5 @@
1
1
  module Fullcalendar
2
2
  module Rails
3
- VERSION = "3.0.0.0"
3
+ VERSION = "3.1.0.0"
4
4
  end
5
5
  end
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * FullCalendar v3.0.0
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.0",
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
  /*
@@ -1574,6 +1562,49 @@ function chunkFormatString(formatStr) {
1574
1562
  return chunks;
1575
1563
  }
1576
1564
 
1565
+
1566
+ // Misc Utils
1567
+ // -------------------------------------------------------------------------------------------------
1568
+
1569
+
1570
+ // granularity only goes up until day
1571
+ // TODO: unify with similarUnitMap
1572
+ var tokenGranularities = {
1573
+ Y: { value: 1, unit: 'year' },
1574
+ M: { value: 2, unit: 'month' },
1575
+ W: { value: 3, unit: 'week' },
1576
+ w: { value: 3, unit: 'week' },
1577
+ D: { value: 4, unit: 'day' }, // day of month
1578
+ d: { value: 4, unit: 'day' } // day of week
1579
+ };
1580
+
1581
+ // returns a unit string, either 'year', 'month', 'day', or null
1582
+ // for the most granular formatting token in the string.
1583
+ FC.queryMostGranularFormatUnit = function(formatStr) {
1584
+ var chunks = getFormatStringChunks(formatStr);
1585
+ var i, chunk;
1586
+ var candidate;
1587
+ var best;
1588
+
1589
+ for (i = 0; i < chunks.length; i++) {
1590
+ chunk = chunks[i];
1591
+ if (chunk.token) {
1592
+ candidate = tokenGranularities[chunk.token.charAt(0)];
1593
+ if (candidate) {
1594
+ if (!best || candidate.value > best.value) {
1595
+ best = candidate;
1596
+ }
1597
+ }
1598
+ }
1599
+ }
1600
+
1601
+ if (best) {
1602
+ return best.unit;
1603
+ }
1604
+
1605
+ return null;
1606
+ };
1607
+
1577
1608
  ;;
1578
1609
 
1579
1610
  FC.Class = Class; // export
@@ -1638,6 +1669,201 @@ function mixIntoClass(theClass, members) {
1638
1669
  }
1639
1670
  ;;
1640
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
+
1641
1867
  var EmitterMixin = FC.EmitterMixin = {
1642
1868
 
1643
1869
  // jQuery-ification via $(this) allows a non-DOM object to have
@@ -1645,7 +1871,18 @@ var EmitterMixin = FC.EmitterMixin = {
1645
1871
 
1646
1872
 
1647
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
+ },
1883
+
1648
1884
 
1885
+ _prepareIntercept: function(handler) {
1649
1886
  // handlers are always called with an "event" object as their first param.
1650
1887
  // sneak the `this` context and arguments into the extra parameter object
1651
1888
  // and forward them on to the original handler.
@@ -1665,9 +1902,7 @@ var EmitterMixin = FC.EmitterMixin = {
1665
1902
  }
1666
1903
  intercept.guid = handler.guid;
1667
1904
 
1668
- $(this).on(types, intercept);
1669
-
1670
- return this; // for chaining
1905
+ return intercept;
1671
1906
  },
1672
1907
 
1673
1908
 
@@ -1996,9 +2231,15 @@ var CoordCache = FC.CoordCache = Class.extend({
1996
2231
  // Queries the els for coordinates and stores them.
1997
2232
  // Call this method before using and of the get* methods below.
1998
2233
  build: function() {
1999
- 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;
2000
2242
 
2001
- this.origin = offsetParentEl.offset();
2002
2243
  this.boundingRect = this.queryBoundingRect();
2003
2244
 
2004
2245
  if (this.isHorizontal) {
@@ -2181,12 +2422,19 @@ var CoordCache = FC.CoordCache = Class.extend({
2181
2422
 
2182
2423
  // Compute and return what the elements' bounding rectangle is, from the user's perspective.
2183
2424
  // Right now, only returns a rectangle if constrained by an overflow:scroll element.
2425
+ // Returns null if there are no elements
2184
2426
  queryBoundingRect: function() {
2185
- var scrollParentEl = getScrollParent(this.els.eq(0));
2427
+ var scrollParentEl;
2186
2428
 
2187
- if (!scrollParentEl.is(document)) {
2188
- return getClientRect(scrollParentEl);
2429
+ if (this.els.length > 0) {
2430
+ scrollParentEl = getScrollParent(this.els.eq(0));
2431
+
2432
+ if (!scrollParentEl.is(document)) {
2433
+ return getClientRect(scrollParentEl);
2434
+ }
2189
2435
  }
2436
+
2437
+ return null;
2190
2438
  },
2191
2439
 
2192
2440
  isPointInBounds: function(leftOffset, topOffset) {
@@ -3405,6 +3653,7 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
3405
3653
 
3406
3654
  dayTouchStart: function(ev) {
3407
3655
  var view = this.view;
3656
+ var selectLongPressDelay = view.opt('selectLongPressDelay');
3408
3657
 
3409
3658
  // HACK to prevent a user's clickaway for unselecting a range or an event
3410
3659
  // from causing a dayClick.
@@ -3412,8 +3661,12 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
3412
3661
  this.tempIgnoreMouse();
3413
3662
  }
3414
3663
 
3664
+ if (selectLongPressDelay == null) {
3665
+ selectLongPressDelay = view.opt('longPressDelay'); // fallback
3666
+ }
3667
+
3415
3668
  this.dayDragListener.startInteraction(ev, {
3416
- delay: this.view.opt('longPressDelay')
3669
+ delay: selectLongPressDelay
3417
3670
  });
3418
3671
  },
3419
3672
 
@@ -3750,7 +4003,7 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
3750
4003
 
3751
4004
 
3752
4005
  // Computes HTML classNames for a single-day element
3753
- getDayClasses: function(date) {
4006
+ getDayClasses: function(date, noThemeHighlight) {
3754
4007
  var view = this.view;
3755
4008
  var today = view.calendar.getNow();
3756
4009
  var classes = [ 'fc-' + dayIDs[date.day()] ];
@@ -3763,10 +4016,11 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
3763
4016
  }
3764
4017
 
3765
4018
  if (date.isSame(today, 'day')) {
3766
- classes.push(
3767
- 'fc-today',
3768
- view.highlightStateClass
3769
- );
4019
+ classes.push('fc-today');
4020
+
4021
+ if (noThemeHighlight !== true) {
4022
+ classes.push(view.highlightStateClass);
4023
+ }
3770
4024
  }
3771
4025
  else if (date < today) {
3772
4026
  classes.push('fc-past');
@@ -3962,16 +4216,33 @@ Grid.mixin({
3962
4216
 
3963
4217
  // Compute business hour segs for the grid's current date range.
3964
4218
  // Caller must ask if whole-day business hours are needed.
3965
- buildBusinessHourSegs: function(wholeDay) {
3966
- 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);
3967
4241
 
3968
4242
  // HACK. Eventually refactor business hours "events" system.
3969
4243
  // If no events are given, but businessHours is activated, this means the entire visible range should be
3970
4244
  // marked as *not* business-hours, via inverse-background rendering.
3971
- if (
3972
- !events.length &&
3973
- this.view.calendar.options.businessHours // don't access view option. doesn't update with dynamic options
3974
- ) {
4245
+ if (!events.length && businessHours) {
3975
4246
  events = [
3976
4247
  $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, {
3977
4248
  start: this.view.end, // guaranteed out-of-range
@@ -3981,7 +4252,7 @@ Grid.mixin({
3981
4252
  ];
3982
4253
  }
3983
4254
 
3984
- return this.eventsToSegs(events);
4255
+ return events;
3985
4256
  },
3986
4257
 
3987
4258
 
@@ -4023,7 +4294,7 @@ Grid.mixin({
4023
4294
 
4024
4295
 
4025
4296
  handleSegClick: function(seg, ev) {
4026
- 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
4027
4298
  if (res === false) {
4028
4299
  ev.preventDefault();
4029
4300
  }
@@ -4040,7 +4311,7 @@ Grid.mixin({
4040
4311
  if (this.view.isEventResizable(seg.event)) {
4041
4312
  seg.el.addClass('fc-allow-mouse-resize');
4042
4313
  }
4043
- this.view.trigger('eventMouseover', seg.el[0], seg.event, ev);
4314
+ this.view.publiclyTrigger('eventMouseover', seg.el[0], seg.event, ev);
4044
4315
  }
4045
4316
  },
4046
4317
 
@@ -4056,7 +4327,7 @@ Grid.mixin({
4056
4327
  if (this.view.isEventResizable(seg.event)) {
4057
4328
  seg.el.removeClass('fc-allow-mouse-resize');
4058
4329
  }
4059
- this.view.trigger('eventMouseout', seg.el[0], seg.event, ev);
4330
+ this.view.publiclyTrigger('eventMouseout', seg.el[0], seg.event, ev);
4060
4331
  }
4061
4332
  },
4062
4333
 
@@ -4081,6 +4352,7 @@ Grid.mixin({
4081
4352
  var isResizable = view.isEventResizable(event);
4082
4353
  var isResizing = false;
4083
4354
  var dragListener;
4355
+ var eventLongPressDelay;
4084
4356
 
4085
4357
  if (isSelected && isResizable) {
4086
4358
  // only allow resizing of the event is selected
@@ -4089,12 +4361,17 @@ Grid.mixin({
4089
4361
 
4090
4362
  if (!isResizing && (isDraggable || isResizable)) { // allowed to be selected?
4091
4363
 
4364
+ eventLongPressDelay = view.opt('eventLongPressDelay');
4365
+ if (eventLongPressDelay == null) {
4366
+ eventLongPressDelay = view.opt('longPressDelay'); // fallback
4367
+ }
4368
+
4092
4369
  dragListener = isDraggable ?
4093
4370
  this.buildSegDragListener(seg) :
4094
4371
  this.buildSegSelectListener(seg); // seg isn't draggable, but still needs to be selected
4095
4372
 
4096
4373
  dragListener.startInteraction(ev, { // won't start if already started
4097
- delay: isSelected ? 0 : this.view.opt('longPressDelay') // do delay if not already selected
4374
+ delay: isSelected ? 0 : eventLongPressDelay // do delay if not already selected
4098
4375
  });
4099
4376
  }
4100
4377
 
@@ -4227,11 +4504,15 @@ Grid.mixin({
4227
4504
  mouseFollower.stop(!dropLocation, function() {
4228
4505
  if (isDragging) {
4229
4506
  view.unrenderDrag();
4230
- view.showEvent(event);
4231
4507
  _this.segDragStop(seg, ev);
4232
4508
  }
4509
+
4233
4510
  if (dropLocation) {
4234
- 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);
4235
4516
  }
4236
4517
  });
4237
4518
  _this.segDragListener = null;
@@ -4273,14 +4554,14 @@ Grid.mixin({
4273
4554
  // Called before event segment dragging starts
4274
4555
  segDragStart: function(seg, ev) {
4275
4556
  this.isDraggingSeg = true;
4276
- 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
4277
4558
  },
4278
4559
 
4279
4560
 
4280
4561
  // Called after event segment dragging stops
4281
4562
  segDragStop: function(seg, ev) {
4282
4563
  this.isDraggingSeg = false;
4283
- 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
4284
4565
  },
4285
4566
 
4286
4567
 
@@ -4518,18 +4799,23 @@ Grid.mixin({
4518
4799
  },
4519
4800
  hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
4520
4801
  resizeLocation = null;
4802
+ view.showEvent(event); // for when out-of-bounds. show original
4521
4803
  },
4522
4804
  hitDone: function() { // resets the rendering to show the original event
4523
4805
  _this.unrenderEventResize();
4524
- view.showEvent(event);
4525
4806
  enableCursor();
4526
4807
  },
4527
4808
  interactionEnd: function(ev) {
4528
4809
  if (isDragging) {
4529
4810
  _this.segResizeStop(seg, ev);
4530
4811
  }
4812
+
4531
4813
  if (resizeLocation) { // valid date to resize to?
4532
- 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);
4533
4819
  }
4534
4820
  _this.segResizeListener = null;
4535
4821
  }
@@ -4542,14 +4828,14 @@ Grid.mixin({
4542
4828
  // Called before event segment resizing starts
4543
4829
  segResizeStart: function(seg, ev) {
4544
4830
  this.isResizingSeg = true;
4545
- 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
4546
4832
  },
4547
4833
 
4548
4834
 
4549
4835
  // Called after event segment resizing stops
4550
4836
  segResizeStop: function(seg, ev) {
4551
4837
  this.isResizingSeg = false;
4552
- 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
4553
4839
  },
4554
4840
 
4555
4841
 
@@ -5100,7 +5386,7 @@ var DayTableMixin = FC.DayTableMixin = {
5100
5386
  this.dayIndices = dayIndices;
5101
5387
  this.daysPerRow = daysPerRow;
5102
5388
  this.rowCnt = rowCnt;
5103
-
5389
+
5104
5390
  this.updateDayTableCols();
5105
5391
  },
5106
5392
 
@@ -5338,9 +5624,25 @@ var DayTableMixin = FC.DayTableMixin = {
5338
5624
  // (colspan should be no different)
5339
5625
  renderHeadDateCellHtml: function(date, colspan, otherAttrs) {
5340
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
+ }
5341
5643
 
5342
5644
  return '' +
5343
- '<th class="fc-day-header ' + view.widgetHeaderClass + ' fc-' + dayIDs[date.day()] + '"' +
5645
+ '<th class="' + classNames.join(' ') + '"' +
5344
5646
  (this.rowCnt === 1 ?
5345
5647
  ' data-date="' + date.format('YYYY-MM-DD') + '"' :
5346
5648
  '') +
@@ -5491,7 +5793,7 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
5491
5793
  // trigger dayRender with each cell's element
5492
5794
  for (row = 0; row < rowCnt; row++) {
5493
5795
  for (col = 0; col < colCnt; col++) {
5494
- view.trigger(
5796
+ view.publiclyTrigger(
5495
5797
  'dayRender',
5496
5798
  null,
5497
5799
  this.getCellDate(row, col),
@@ -6426,7 +6728,7 @@ DayGrid.mixin({
6426
6728
 
6427
6729
  if (typeof clickOption === 'function') {
6428
6730
  // the returned value can be an atomic option
6429
- clickOption = view.trigger('eventLimitClick', null, {
6731
+ clickOption = view.publiclyTrigger('eventLimitClick', null, {
6430
6732
  date: date,
6431
6733
  dayEl: dayEl,
6432
6734
  moreEl: moreEl,
@@ -6469,6 +6771,14 @@ DayGrid.mixin({
6469
6771
  viewportConstrain: view.opt('popoverViewportConstrain'),
6470
6772
  hide: function() {
6471
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
+ }
6472
6782
  _this.segPopover.removeElement();
6473
6783
  _this.segPopover = null;
6474
6784
  _this.popoverSegs = null;
@@ -7746,9 +8056,14 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
7746
8056
  options: null, // hash containing all options. already merged with view-specific-options
7747
8057
  el: null, // the view's containing element. set by Calendar
7748
8058
 
7749
- displaying: null, // a promise representing the state of rendering. null if no render requested
7750
- isSkeletonRendered: false,
8059
+ isDateSet: false,
8060
+ isDateRendered: false,
8061
+ dateRenderQueue: null,
8062
+
8063
+ isEventsBound: false,
8064
+ isEventsSet: false,
7751
8065
  isEventsRendered: false,
8066
+ eventRenderQueue: null,
7752
8067
 
7753
8068
  // range the view is actually displaying (moments)
7754
8069
  start: null,
@@ -7798,6 +8113,9 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
7798
8113
 
7799
8114
  this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder'));
7800
8115
 
8116
+ this.dateRenderQueue = new TaskQueue();
8117
+ this.eventRenderQueue = new TaskQueue(this.opt('eventRenderWait'));
8118
+
7801
8119
  this.initialize();
7802
8120
  },
7803
8121
 
@@ -7815,10 +8133,10 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
7815
8133
 
7816
8134
 
7817
8135
  // Triggers handlers that are view-related. Modifies args before passing to calendar.
7818
- trigger: function(name, thisObj) { // arguments beyond thisObj are passed along
8136
+ publiclyTrigger: function(name, thisObj) { // arguments beyond thisObj are passed along
7819
8137
  var calendar = this.calendar;
7820
8138
 
7821
- return calendar.trigger.apply(
8139
+ return calendar.publiclyTrigger.apply(
7822
8140
  calendar,
7823
8141
  [name, thisObj || this].concat(
7824
8142
  Array.prototype.slice.call(arguments, 2), // arguments beyond thisObj
@@ -7828,16 +8146,33 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
7828
8146
  },
7829
8147
 
7830
8148
 
7831
- /* Dates
7832
- ------------------------------------------------------------------------------------------------------------------*/
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;
7833
8153
 
8154
+ return new Promise(function(resolve, reject) {
8155
+ _this.one(eventName, reject);
7834
8156
 
7835
- // Updates all internal dates to center around the given current unzoned date.
7836
- setDate: function(date) {
7837
- 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
+ });
7838
8169
  },
7839
8170
 
7840
8171
 
8172
+ /* Date Computation
8173
+ ------------------------------------------------------------------------------------------------------------------*/
8174
+
8175
+
7841
8176
  // Updates all internal dates for displaying the given unzoned range.
7842
8177
  setRange: function(range) {
7843
8178
  $.extend(this, range); // assigns every property to this object's member variables
@@ -7920,6 +8255,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
7920
8255
  // Sets the view's title property to the most updated computed value
7921
8256
  updateTitle: function() {
7922
8257
  this.title = this.computeTitle();
8258
+ this.calendar.setToolbarsTitle(this.title);
7923
8259
  },
7924
8260
 
7925
8261
 
@@ -8025,183 +8361,238 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8025
8361
  },
8026
8362
 
8027
8363
 
8028
- /* Rendering
8029
- ------------------------------------------------------------------------------------------------------------------*/
8364
+ // Rendering Non-date-related Content
8365
+ // -----------------------------------------------------------------------------------------------------------------
8030
8366
 
8031
8367
 
8032
- // Sets the container element that the view should render inside of.
8033
- // 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.
8034
8370
  setElement: function(el) {
8035
8371
  this.el = el;
8036
8372
  this.bindGlobalHandlers();
8373
+ this.renderSkeleton();
8037
8374
  },
8038
8375
 
8039
8376
 
8040
8377
  // Removes the view's container element from the DOM, clearing any content beforehand.
8041
8378
  // Undoes any other DOM-related attachments.
8042
8379
  removeElement: function() {
8043
- this.clear(); // clears all content
8044
-
8045
- // clean up the skeleton
8046
- if (this.isSkeletonRendered) {
8047
- this.unrenderSkeleton();
8048
- this.isSkeletonRendered = false;
8049
- }
8380
+ this.unsetDate();
8381
+ this.unrenderSkeleton();
8050
8382
 
8051
8383
  this.unbindGlobalHandlers();
8052
8384
 
8053
8385
  this.el.remove();
8054
-
8055
8386
  // NOTE: don't null-out this.el in case the View was destroyed within an API callback.
8056
8387
  // We don't null-out the View's other jQuery element references upon destroy,
8057
8388
  // so we shouldn't kill this.el either.
8058
8389
  },
8059
8390
 
8060
8391
 
8061
- // Does everything necessary to display the view centered around the given unzoned date.
8062
- // Does every type of rendering EXCEPT rendering events.
8063
- // Is asychronous and returns a promise.
8064
- display: function(date, explicitScrollState) {
8065
- var _this = this;
8066
- var prevScrollState = null;
8067
-
8068
- if (explicitScrollState != null && this.displaying) { // don't need prevScrollState if explicitScrollState
8069
- prevScrollState = this.queryScroll();
8070
- }
8071
-
8072
- this.calendar.freezeContentHeight();
8073
-
8074
- return syncThen(this.clear(), function() { // clear the content first
8075
- return (
8076
- _this.displaying =
8077
- syncThen(_this.displayView(date), function() { // displayView might return a promise
8392
+ // Renders the basic structure of the view before any content is rendered
8393
+ renderSkeleton: function() {
8394
+ // subclasses should implement
8395
+ },
8078
8396
 
8079
- // caller of display() wants a specific scroll state?
8080
- if (explicitScrollState != null) {
8081
- // we make an assumption that this is NOT the initial render,
8082
- // and thus don't need forceScroll (is inconveniently asynchronous)
8083
- _this.setScroll(explicitScrollState);
8084
- }
8085
- else {
8086
- _this.forceScroll(_this.computeInitialScroll(prevScrollState));
8087
- }
8088
8397
 
8089
- _this.calendar.unfreezeContentHeight();
8090
- _this.triggerRender();
8091
- })
8092
- );
8093
- });
8398
+ // Unrenders the basic structure of the view
8399
+ unrenderSkeleton: function() {
8400
+ // subclasses should implement
8094
8401
  },
8095
8402
 
8096
8403
 
8097
- // Does everything necessary to clear the content of the view.
8098
- // Clears dates and events. Does not clear the skeleton.
8099
- // Is asychronous and returns a promise.
8100
- clear: function() {
8101
- var _this = this;
8102
- var displaying = this.displaying;
8404
+ // Date Setting/Unsetting
8405
+ // -----------------------------------------------------------------------------------------------------------------
8103
8406
 
8104
- if (displaying) { // previously displayed, or in the process of being displayed?
8105
- return syncThen(displaying, function() { // wait for the display to finish
8106
- _this.displaying = null;
8107
- _this.clearEvents();
8108
- return _this.clearView(); // might return a promise. chain it
8109
- });
8110
- }
8111
- else {
8112
- return $.when(); // an immediately-resolved promise
8113
- }
8114
- },
8115
8407
 
8408
+ setDate: function(date) {
8409
+ var isReset = this.isDateSet;
8116
8410
 
8117
- // Displays the view's non-event content, such as date-related content or anything required by events.
8118
- // Renders the view's non-content skeleton if necessary.
8119
- // Can be asynchronous and return a promise.
8120
- displayView: function(date) {
8121
- if (!this.isSkeletonRendered) {
8122
- this.renderSkeleton();
8123
- this.isSkeletonRendered = true;
8124
- }
8125
- if (date) {
8126
- this.setDate(date);
8127
- }
8128
- if (this.render) {
8129
- this.render(); // TODO: deprecate
8130
- }
8131
- this.renderDates();
8132
- this.updateSize();
8133
- this.renderBusinessHours(); // might need coordinates, so should go after updateSize()
8134
- this.startNowIndicator();
8411
+ this.isDateSet = true;
8412
+ this.handleDate(date, isReset);
8413
+ this.trigger(isReset ? 'dateReset' : 'dateSet', date);
8135
8414
  },
8136
8415
 
8137
8416
 
8138
- // Unrenders the view content that was rendered in displayView.
8139
- // Can be asynchronous and return a promise.
8140
- clearView: function() {
8141
- this.unselect();
8142
- this.stopNowIndicator();
8143
- this.triggerUnrender();
8144
- this.unrenderBusinessHours();
8145
- this.unrenderDates();
8146
- if (this.destroy) {
8147
- this.destroy(); // TODO: deprecate
8417
+ unsetDate: function() {
8418
+ if (this.isDateSet) {
8419
+ this.isDateSet = false;
8420
+ this.handleDateUnset();
8421
+ this.trigger('dateUnset');
8148
8422
  }
8149
8423
  },
8150
8424
 
8151
8425
 
8152
- // Renders the basic structure of the view before any content is rendered
8153
- renderSkeleton: function() {
8154
- // subclasses should implement
8155
- },
8156
-
8426
+ // Date Handling
8427
+ // -----------------------------------------------------------------------------------------------------------------
8157
8428
 
8158
- // Unrenders the basic structure of the view
8159
- unrenderSkeleton: function() {
8160
- // subclasses should implement
8161
- },
8162
8429
 
8430
+ handleDate: function(date, isReset) {
8431
+ var _this = this;
8163
8432
 
8164
- // Renders the view's date-related content.
8165
- // Assumes setRange has already been called and the skeleton has already been rendered.
8166
- renderDates: function() {
8167
- // subclasses should implement
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
8437
+ });
8168
8438
  },
8169
8439
 
8170
8440
 
8171
- // Unrenders the view's date-related content
8172
- unrenderDates: function() {
8173
- // subclasses should override
8441
+ handleDateUnset: function() {
8442
+ this.unbindEvents();
8443
+ this.requestDateUnrender();
8174
8444
  },
8175
8445
 
8176
8446
 
8177
- // Signals that the view's content has been rendered
8178
- triggerRender: function() {
8179
- this.trigger('viewRender', this, this, this.el);
8180
- },
8447
+ // Date Render Queuing
8448
+ // -----------------------------------------------------------------------------------------------------------------
8181
8449
 
8182
8450
 
8183
- // Signals that the view's content is about to be unrendered
8184
- triggerUnrender: function() {
8185
- this.trigger('viewDestroy', this, this, this.el);
8451
+ // if date not specified, uses current
8452
+ requestDateRender: function(date) {
8453
+ var _this = this;
8454
+
8455
+ return this.dateRenderQueue.add(function() {
8456
+ return _this.executeDateRender(date);
8457
+ });
8186
8458
  },
8187
8459
 
8188
8460
 
8189
- // Binds DOM handlers to elements that reside outside the view container, such as the document
8190
- bindGlobalHandlers: function() {
8191
- this.listenTo($(document), 'mousedown', this.handleDocumentMousedown);
8192
- this.listenTo($(document), 'touchstart', this.processUnselect);
8461
+ requestDateUnrender: function() {
8462
+ var _this = this;
8463
+
8464
+ return this.dateRenderQueue.add(function() {
8465
+ return _this.executeDateUnrender();
8466
+ });
8193
8467
  },
8194
8468
 
8195
8469
 
8196
- // Unbinds DOM handlers from elements that reside outside the view container
8197
- unbindGlobalHandlers: function() {
8198
- this.stopListeningTo($(document));
8199
- },
8470
+ // Date High-level Rendering
8471
+ // -----------------------------------------------------------------------------------------------------------------
8200
8472
 
8201
8473
 
8202
- // Initializes internal variables related to theming
8203
- initThemingProps: function() {
8204
- var tm = this.opt('theme') ? 'ui' : 'fc';
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)
8479
+ if (date) {
8480
+ this.captureInitialScroll();
8481
+ }
8482
+ else {
8483
+ this.captureScroll(); // a rerender of the current date
8484
+ }
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
+ });
8510
+ },
8511
+
8512
+
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();
8535
+ }
8536
+ },
8537
+
8538
+
8539
+ // Date Rendering Triggers
8540
+ // -----------------------------------------------------------------------------------------------------------------
8541
+
8542
+
8543
+ onDateRender: function() {
8544
+ this.triggerRender();
8545
+ },
8546
+
8547
+
8548
+ // Date Low-level Rendering
8549
+ // -----------------------------------------------------------------------------------------------------------------
8550
+
8551
+
8552
+ // date-cell content only
8553
+ renderDates: function() {
8554
+ // subclasses should implement
8555
+ },
8556
+
8557
+
8558
+ // date-cell content only
8559
+ unrenderDates: function() {
8560
+ // subclasses should override
8561
+ },
8562
+
8563
+
8564
+ // Misc view rendering utils
8565
+ // -------------------------
8566
+
8567
+
8568
+ // Signals that the view's content has been rendered
8569
+ triggerRender: function() {
8570
+ this.publiclyTrigger('viewRender', this, this, this.el);
8571
+ },
8572
+
8573
+
8574
+ // Signals that the view's content is about to be unrendered
8575
+ triggerUnrender: function() {
8576
+ this.publiclyTrigger('viewDestroy', this, this, this.el);
8577
+ },
8578
+
8579
+
8580
+ // Binds DOM handlers to elements that reside outside the view container, such as the document
8581
+ bindGlobalHandlers: function() {
8582
+ this.listenTo($(document), 'mousedown', this.handleDocumentMousedown);
8583
+ this.listenTo($(document), 'touchstart', this.processUnselect);
8584
+ },
8585
+
8586
+
8587
+ // Unbinds DOM handlers from elements that reside outside the view container
8588
+ unbindGlobalHandlers: function() {
8589
+ this.stopListeningTo($(document));
8590
+ },
8591
+
8592
+
8593
+ // Initializes internal variables related to theming
8594
+ initThemingProps: function() {
8595
+ var tm = this.opt('theme') ? 'ui' : 'fc';
8205
8596
 
8206
8597
  this.widgetHeaderClass = tm + '-widget-header';
8207
8598
  this.widgetContentClass = tm + '-widget-content';
@@ -8319,10 +8710,9 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8319
8710
 
8320
8711
  // Refreshes anything dependant upon sizing of the container element of the grid
8321
8712
  updateSize: function(isResize) {
8322
- var scrollState;
8323
8713
 
8324
8714
  if (isResize) {
8325
- scrollState = this.queryScroll();
8715
+ this.captureScroll();
8326
8716
  }
8327
8717
 
8328
8718
  this.updateHeight(isResize);
@@ -8330,7 +8720,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8330
8720
  this.updateNowIndicator();
8331
8721
 
8332
8722
  if (isResize) {
8333
- this.setScroll(scrollState);
8723
+ this.releaseScroll();
8334
8724
  }
8335
8725
  },
8336
8726
 
@@ -8363,72 +8753,294 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8363
8753
  ------------------------------------------------------------------------------------------------------------------*/
8364
8754
 
8365
8755
 
8366
- // Computes the initial pre-configured scroll state prior to allowing the user to change it.
8367
- // Given the scroll state from the previous rendering. If first time rendering, given null.
8368
- computeInitialScroll: function(previousScrollState) {
8369
- 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;
8370
8766
  },
8371
8767
 
8372
8768
 
8373
- // Retrieves the view's current natural scroll state. Can return an arbitrary format.
8374
- queryScroll: function() {
8375
- // 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
+ }
8376
8780
  },
8377
8781
 
8378
8782
 
8379
- // Sets the view's scroll state. Will accept the same format computeInitialScroll and queryScroll produce.
8380
- setScroll: function(scrollState) {
8381
- // 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 {};
8823
+ },
8824
+
8825
+
8826
+ queryScroll: function() {
8827
+ return {};
8382
8828
  },
8383
8829
 
8384
8830
 
8385
- // Sets the scroll state, making sure to overcome any predefined scroll value the browser has in mind
8386
- forceScroll: function(scrollState) {
8831
+ hardSetScroll: function(scroll) {
8387
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
+ },
8388
8837
 
8389
- this.setScroll(scrollState);
8390
- setTimeout(function() {
8391
- _this.setScroll(scrollState);
8392
- }, 0);
8838
+
8839
+ setScroll: function(scroll) {
8393
8840
  },
8394
8841
 
8395
8842
 
8396
- /* Event Elements / Segments
8843
+ /* Height Freezing
8397
8844
  ------------------------------------------------------------------------------------------------------------------*/
8398
8845
 
8399
8846
 
8400
- // Does everything necessary to display the given events onto the current view
8401
- displayEvents: function(events) {
8402
- 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
+ // -----------------------------------------------------------------------------------------------------------------
8403
8922
 
8404
- this.clearEvents();
8405
- this.renderEvents(events);
8406
- this.isEventsRendered = true;
8407
- this.setScroll(scrollState);
8408
- this.triggerEventRender();
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
+ },
8946
+
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
+ }
8959
+ },
8960
+
8961
+
8962
+ requestCurrentEventsRender: function() {
8963
+ if (this.isEventsSet) {
8964
+ this.requestEventsRender(this.getCurrentEvents());
8965
+ }
8966
+ else {
8967
+ return Promise.reject();
8968
+ }
8409
8969
  },
8410
8970
 
8411
8971
 
8412
- // Does everything necessary to clear the view's currently-rendered events
8413
- clearEvents: function() {
8414
- var scrollState;
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
+
8415
8994
 
8995
+ executeEventsUnrender: function() {
8416
8996
  if (this.isEventsRendered) {
8997
+ this.onBeforeEventsUnrender();
8417
8998
 
8418
- // TODO: optimize: if we know this is part of a displayEvents call, don't queryScroll/setScroll
8419
- scrollState = this.queryScroll();
8999
+ this.captureScroll();
9000
+ this.freezeHeight();
8420
9001
 
8421
- this.triggerEventUnrender();
8422
9002
  if (this.destroyEvents) {
8423
9003
  this.destroyEvents(); // TODO: deprecate
8424
9004
  }
9005
+
8425
9006
  this.unrenderEvents();
8426
- this.setScroll(scrollState);
9007
+
9008
+ this.thawHeight();
9009
+ this.releaseScroll();
9010
+
8427
9011
  this.isEventsRendered = false;
9012
+ this.trigger('eventsUnrender');
8428
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
+ });
8429
9037
  },
8430
9038
 
8431
9039
 
9040
+ // Event Low-level Rendering
9041
+ // -----------------------------------------------------------------------------------------------------------------
9042
+
9043
+
8432
9044
  // Renders the events onto the view.
8433
9045
  renderEvents: function(events) {
8434
9046
  // subclasses should implement
@@ -8441,27 +9053,28 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8441
9053
  },
8442
9054
 
8443
9055
 
8444
- // Signals that all events have been rendered
8445
- triggerEventRender: function() {
8446
- this.renderedEventSegEach(function(seg) {
8447
- this.trigger('eventAfterRender', seg.event, seg.event, seg.el);
8448
- });
8449
- this.trigger('eventAfterAllRender');
9056
+ // Event Data Access
9057
+ // -----------------------------------------------------------------------------------------------------------------
9058
+
9059
+
9060
+ requestEvents: function() {
9061
+ return this.calendar.requestEvents(this.start, this.end);
8450
9062
  },
8451
9063
 
8452
9064
 
8453
- // Signals that all event elements are about to be removed
8454
- triggerEventUnrender: function() {
8455
- this.renderedEventSegEach(function(seg) {
8456
- this.trigger('eventDestroy', seg.event, seg.event, seg.el);
8457
- });
9065
+ getCurrentEvents: function() {
9066
+ return this.calendar.getPrunedEventCache();
8458
9067
  },
8459
9068
 
8460
9069
 
9070
+ // Event Rendering Utils
9071
+ // -----------------------------------------------------------------------------------------------------------------
9072
+
9073
+
8461
9074
  // Given an event and the default element used for rendering, returns the element that should actually be used.
8462
9075
  // Basically runs events and elements through the eventRender hook.
8463
9076
  resolveEventEl: function(event, el) {
8464
- var custom = this.trigger('eventRender', event, event, el);
9077
+ var custom = this.publiclyTrigger('eventRender', event, event, el);
8465
9078
 
8466
9079
  if (custom === false) { // means don't render at all
8467
9080
  el = null;
@@ -8560,7 +9173,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8560
9173
 
8561
9174
  // Triggers event-drop handlers that have subscribed via the API
8562
9175
  triggerEventDrop: function(event, dateDelta, undoFunc, el, ev) {
8563
- this.trigger('eventDrop', el[0], event, dateDelta, undoFunc, ev, {}); // {} = jqui dummy
9176
+ this.publiclyTrigger('eventDrop', el[0], event, dateDelta, undoFunc, ev, {}); // {} = jqui dummy
8564
9177
  },
8565
9178
 
8566
9179
 
@@ -8590,10 +9203,10 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8590
9203
  triggerExternalDrop: function(event, dropLocation, el, ev, ui) {
8591
9204
 
8592
9205
  // trigger 'drop' regardless of whether element represents an event
8593
- this.trigger('drop', el[0], dropLocation.start, ev, ui);
9206
+ this.publiclyTrigger('drop', el[0], dropLocation.start, ev, ui);
8594
9207
 
8595
9208
  if (event) {
8596
- this.trigger('eventReceive', null, event); // signal an external event landed
9209
+ this.publiclyTrigger('eventReceive', null, event); // signal an external event landed
8597
9210
  }
8598
9211
  },
8599
9212
 
@@ -8663,7 +9276,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8663
9276
 
8664
9277
  // Triggers event-resize handlers that have subscribed via the API
8665
9278
  triggerEventResize: function(event, durationDelta, undoFunc, el, ev) {
8666
- this.trigger('eventResize', el[0], event, durationDelta, undoFunc, ev, {}); // {} = jqui dummy
9279
+ this.publiclyTrigger('eventResize', el[0], event, durationDelta, undoFunc, ev, {}); // {} = jqui dummy
8667
9280
  },
8668
9281
 
8669
9282
 
@@ -8695,7 +9308,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8695
9308
 
8696
9309
  // Triggers handlers to 'select'
8697
9310
  triggerSelect: function(span, ev) {
8698
- this.trigger(
9311
+ this.publiclyTrigger(
8699
9312
  'select',
8700
9313
  null,
8701
9314
  this.calendar.applyTimezone(span.start), // convert to calendar's tz for external API
@@ -8714,7 +9327,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8714
9327
  this.destroySelection(); // TODO: deprecate
8715
9328
  }
8716
9329
  this.unrenderSelection();
8717
- this.trigger('unselect', null, ev);
9330
+ this.publiclyTrigger('unselect', null, ev);
8718
9331
  }
8719
9332
  },
8720
9333
 
@@ -8806,7 +9419,7 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8806
9419
  // Triggers handlers to 'dayClick'
8807
9420
  // Span has start/end of the clicked area. Only the start is useful.
8808
9421
  triggerDayClick: function(span, dayEl, ev) {
8809
- this.trigger(
9422
+ this.publiclyTrigger(
8810
9423
  'dayClick',
8811
9424
  dayEl,
8812
9425
  this.calendar.applyTimezone(span.start), // convert to calendar's timezone for external API
@@ -9033,31 +9646,321 @@ var Scroller = FC.Scroller = Class.extend({
9033
9646
  });
9034
9647
 
9035
9648
  ;;
9649
+ function Iterator(items) {
9650
+ this.items = items || [];
9651
+ }
9036
9652
 
9037
- var Calendar = FC.Calendar = Class.extend({
9038
9653
 
9039
- dirDefaults: null, // option defaults related to LTR or RTL
9040
- localeDefaults: null, // option defaults related to current locale
9041
- overrides: null, // option overrides given to the fullCalendar constructor
9042
- dynamicOverrides: null, // options set with dynamic setter method. higher precedence than view overrides.
9043
- options: null, // all defaults combined with overrides
9044
- viewSpecCache: null, // cache of view definitions
9045
- view: null, // current View object
9046
- header: null,
9047
- 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 = [];
9048
9658
 
9659
+ this.items.forEach(function(item) {
9660
+ results.push(item[methodName].apply(item, args));
9661
+ });
9049
9662
 
9050
- // a lot of this class' OOP logic is scoped within this constructor function,
9051
- // but in the future, write individual methods on the prototype.
9052
- constructor: Calendar_constructor,
9663
+ return results;
9664
+ };
9053
9665
 
9666
+ ;;
9054
9667
 
9055
- // Subclasses can override this for initialization logic after the constructor has been called
9056
- initialize: function() {
9057
- },
9668
+ /* Toolbar with buttons and title
9669
+ ----------------------------------------------------------------------------------------------------------------------*/
9058
9670
 
9671
+ function Toolbar(calendar, toolbarOptions) {
9672
+ var t = this;
9059
9673
 
9060
- // Computes the flattened options hash for the calendar and assigns to `this.options`.
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`
9685
+
9686
+ // locals
9687
+ var el;
9688
+ var viewsWithButtons = [];
9689
+ var tm;
9690
+
9691
+ // method to update toolbar-specific options, not calendar-wide options
9692
+ function setToolbarOptions(newToolbarOptions) {
9693
+ toolbarOptions = newToolbarOptions;
9694
+ }
9695
+
9696
+ // can be called repeatedly and will rerender
9697
+ function render() {
9698
+ var sections = toolbarOptions.layout;
9699
+
9700
+ tm = calendar.options.theme ? 'ui' : 'fc';
9701
+
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
+ }
9718
+
9719
+
9720
+ function removeElement() {
9721
+ if (el) {
9722
+ el.remove();
9723
+ el = t.el = null;
9724
+ }
9725
+ }
9726
+
9727
+
9728
+ function renderSection(position) {
9729
+ var sectionEl = $('<div class="fc-' + position + '"/>');
9730
+ var buttonStr = toolbarOptions.layout[position];
9731
+
9732
+ if (buttonStr) {
9733
+ $.each(buttonStr.split(' '), function(i) {
9734
+ var groupChildren = $();
9735
+ var isOnlyButtons = true;
9736
+ var groupEl;
9737
+
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;
9753
+ }
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
+ }
9779
+
9780
+ if (buttonClick) {
9781
+
9782
+ themeIcon =
9783
+ customButtonProps ?
9784
+ customButtonProps.themeIcon :
9785
+ calendar.options.themeButtonIcons[buttonName];
9786
+
9787
+ normalIcon =
9788
+ customButtonProps ?
9789
+ customButtonProps.icon :
9790
+ calendar.options.buttonIcons[buttonName];
9791
+
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
+ }
9804
+
9805
+ classes = [
9806
+ 'fc-' + buttonName + '-button',
9807
+ tm + '-button',
9808
+ tm + '-state-default'
9809
+ ];
9810
+
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')) {
9819
+
9820
+ buttonClick(ev);
9821
+
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
+ );
9860
+
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();
9870
+ }
9871
+
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
+ }
9885
+
9886
+ return sectionEl;
9887
+ }
9888
+
9889
+
9890
+ function updateTitle(text) {
9891
+ if (el) {
9892
+ el.find('h2').text(text);
9893
+ }
9894
+ }
9895
+
9896
+
9897
+ function activateButton(buttonName) {
9898
+ if (el) {
9899
+ el.find('.fc-' + buttonName + '-button')
9900
+ .addClass(tm + '-state-active');
9901
+ }
9902
+ }
9903
+
9904
+
9905
+ function deactivateButton(buttonName) {
9906
+ if (el) {
9907
+ el.find('.fc-' + buttonName + '-button')
9908
+ .removeClass(tm + '-state-active');
9909
+ }
9910
+ }
9911
+
9912
+
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`.
9061
9964
  // Assumes this.overrides and this.dynamicOverrides have already been initialized.
9062
9965
  populateOptionsHash: function() {
9063
9966
  var locale, localeDefaults;
@@ -9112,7 +10015,7 @@ var Calendar = FC.Calendar = Class.extend({
9112
10015
  if ($.inArray(unit, intervalUnits) != -1) {
9113
10016
 
9114
10017
  // put views that have buttons first. there will be duplicates, but oh well
9115
- viewTypes = this.header.getViewsWithButtons();
10018
+ viewTypes = this.header.getViewsWithButtons(); // TODO: include footer as well?
9116
10019
  $.each(FC.views, function(viewType) { // all views
9117
10020
  viewTypes.push(viewType);
9118
10021
  });
@@ -9259,7 +10162,7 @@ var Calendar = FC.Calendar = Class.extend({
9259
10162
  // Should be called when any type of async data fetching begins
9260
10163
  pushLoading: function() {
9261
10164
  if (!(this.loadingLevel++)) {
9262
- this.trigger('loading', null, true, this.view);
10165
+ this.publiclyTrigger('loading', null, true, this.view);
9263
10166
  }
9264
10167
  },
9265
10168
 
@@ -9267,7 +10170,7 @@ var Calendar = FC.Calendar = Class.extend({
9267
10170
  // Should be called when any type of async data fetching completes
9268
10171
  popLoading: function() {
9269
10172
  if (!(--this.loadingLevel)) {
9270
- this.trigger('loading', null, false, this.view);
10173
+ this.publiclyTrigger('loading', null, false, this.view);
9271
10174
  }
9272
10175
  },
9273
10176
 
@@ -9305,11 +10208,7 @@ function Calendar_constructor(element, overrides) {
9305
10208
 
9306
10209
  t.render = render;
9307
10210
  t.destroy = destroy;
9308
- t.refetchEvents = refetchEvents;
9309
- t.refetchEventSources = refetchEventSources;
9310
- t.reportEvents = reportEvents;
9311
- t.reportEventChange = reportEventChange;
9312
- t.rerenderEvents = renderEvents; // `renderEvents` serves as a rerender. an API method
10211
+ t.rerenderEvents = rerenderEvents;
9313
10212
  t.changeView = renderView; // `renderView` will switch to another view
9314
10213
  t.select = select;
9315
10214
  t.unselect = unselect;
@@ -9325,7 +10224,7 @@ function Calendar_constructor(element, overrides) {
9325
10224
  t.getCalendar = getCalendar;
9326
10225
  t.getView = getView;
9327
10226
  t.option = option; // getter/setter method
9328
- t.trigger = trigger;
10227
+ t.publiclyTrigger = publiclyTrigger;
9329
10228
 
9330
10229
 
9331
10230
  // Options
@@ -9518,15 +10417,12 @@ function Calendar_constructor(element, overrides) {
9518
10417
  };
9519
10418
 
9520
10419
 
9521
-
10420
+
9522
10421
  // Imports
9523
10422
  // -----------------------------------------------------------------------------------
9524
10423
 
9525
10424
 
9526
10425
  EventManager.call(t);
9527
- var isFetchNeeded = t.isFetchNeeded;
9528
- var fetchEvents = t.fetchEvents;
9529
- var fetchEventSources = t.fetchEventSources;
9530
10426
 
9531
10427
 
9532
10428
 
@@ -9535,7 +10431,9 @@ function Calendar_constructor(element, overrides) {
9535
10431
 
9536
10432
 
9537
10433
  var _element = element[0];
10434
+ var toolbarsManager;
9538
10435
  var header;
10436
+ var footer;
9539
10437
  var content;
9540
10438
  var tm; // for making theme classes
9541
10439
  var currentView; // NOTE: keep this in sync with this.view
@@ -9543,11 +10441,10 @@ function Calendar_constructor(element, overrides) {
9543
10441
  var suggestedViewHeight;
9544
10442
  var windowResizeProxy; // wraps the windowResize function
9545
10443
  var ignoreWindowResize = 0;
9546
- var events = [];
9547
10444
  var date; // unzoned
9548
-
9549
-
9550
-
10445
+
10446
+
10447
+
9551
10448
  // Main Rendering
9552
10449
  // -----------------------------------------------------------------------------------
9553
10450
 
@@ -9559,8 +10456,8 @@ function Calendar_constructor(element, overrides) {
9559
10456
  else {
9560
10457
  date = t.getNow(); // getNow already returns unzoned
9561
10458
  }
9562
-
9563
-
10459
+
10460
+
9564
10461
  function render() {
9565
10462
  if (!content) {
9566
10463
  initialRender();
@@ -9571,8 +10468,8 @@ function Calendar_constructor(element, overrides) {
9571
10468
  renderView();
9572
10469
  }
9573
10470
  }
9574
-
9575
-
10471
+
10472
+
9576
10473
  function initialRender() {
9577
10474
  element.addClass('fc');
9578
10475
 
@@ -9613,9 +10510,14 @@ function Calendar_constructor(element, overrides) {
9613
10510
 
9614
10511
  content = $("<div class='fc-view-container'/>").prependTo(element);
9615
10512
 
9616
- header = t.header = new Header(t);
9617
- renderHeader();
10513
+ var toolbars = buildToolbars();
10514
+ toolbarsManager = new Iterator(toolbars);
10515
+
10516
+ header = t.header = toolbars[0];
10517
+ footer = t.footer = toolbars[1];
9618
10518
 
10519
+ renderHeader();
10520
+ renderFooter();
9619
10521
  renderView(t.options.defaultView);
9620
10522
 
9621
10523
  if (t.options.handleWindowResize) {
@@ -9625,15 +10527,6 @@ function Calendar_constructor(element, overrides) {
9625
10527
  }
9626
10528
 
9627
10529
 
9628
- // can be called repeatedly and Header will rerender
9629
- function renderHeader() {
9630
- header.render();
9631
- if (header.el) {
9632
- element.prepend(header.el);
9633
- }
9634
- }
9635
-
9636
-
9637
10530
  function destroy() {
9638
10531
 
9639
10532
  if (currentView) {
@@ -9643,7 +10536,7 @@ function Calendar_constructor(element, overrides) {
9643
10536
  // It is still the "current" view, just not rendered.
9644
10537
  }
9645
10538
 
9646
- header.removeElement();
10539
+ toolbarsManager.proxyCall('removeElement');
9647
10540
  content.remove();
9648
10541
  element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');
9649
10542
 
@@ -9653,13 +10546,13 @@ function Calendar_constructor(element, overrides) {
9653
10546
  $(window).unbind('resize', windowResizeProxy);
9654
10547
  }
9655
10548
  }
9656
-
9657
-
10549
+
10550
+
9658
10551
  function elementVisible() {
9659
10552
  return element.is(':visible');
9660
10553
  }
9661
-
9662
-
10554
+
10555
+
9663
10556
 
9664
10557
  // View Rendering
9665
10558
  // -----------------------------------------------------------------------------------
@@ -9668,11 +10561,13 @@ function Calendar_constructor(element, overrides) {
9668
10561
  // Renders a view because of a date change, view-type change, or for the first time.
9669
10562
  // If not given a viewType, keep the current view but render different dates.
9670
10563
  // Accepts an optional scroll state to restore to.
9671
- function renderView(viewType, explicitScrollState) {
10564
+ function renderView(viewType, forcedScroll) {
9672
10565
  ignoreWindowResize++;
9673
10566
 
10567
+ var needsClearView = currentView && viewType && currentView.type !== viewType;
10568
+
9674
10569
  // if viewType is changing, remove the old view's rendering
9675
- if (currentView && viewType && currentView.type !== viewType) {
10570
+ if (needsClearView) {
9676
10571
  freezeContentHeight(); // prevent a scroll jump when view element is removed
9677
10572
  clearView();
9678
10573
  }
@@ -9686,7 +10581,7 @@ function Calendar_constructor(element, overrides) {
9686
10581
  currentView.setElement(
9687
10582
  $("<div class='fc-view fc-" + viewType + "-view' />").appendTo(content)
9688
10583
  );
9689
- header.activateButton(viewType);
10584
+ toolbarsManager.proxyCall('activateButton', viewType);
9690
10585
  }
9691
10586
 
9692
10587
  if (currentView) {
@@ -9696,7 +10591,7 @@ function Calendar_constructor(element, overrides) {
9696
10591
 
9697
10592
  // render or rerender the view
9698
10593
  if (
9699
- !currentView.displaying ||
10594
+ !currentView.isDateSet ||
9700
10595
  !( // NOT within interval range signals an implicit date window change
9701
10596
  date >= currentView.intervalStart &&
9702
10597
  date < currentView.intervalEnd
@@ -9704,19 +10599,27 @@ function Calendar_constructor(element, overrides) {
9704
10599
  ) {
9705
10600
  if (elementVisible()) {
9706
10601
 
9707
- currentView.display(date, explicitScrollState); // will call freezeContentHeight
9708
- unfreezeContentHeight(); // immediately unfreeze regardless of whether display is async
10602
+ if (forcedScroll) {
10603
+ currentView.captureInitialScroll(forcedScroll);
10604
+ }
9709
10605
 
9710
- // need to do this after View::render, so dates are calculated
9711
- updateHeaderTitle();
9712
- updateTodayButton();
10606
+ currentView.setDate(date, forcedScroll);
10607
+
10608
+ if (forcedScroll) {
10609
+ currentView.releaseScroll();
10610
+ }
9713
10611
 
9714
- getAndRenderEvents();
10612
+ // need to do this after View::render, so dates are calculated
10613
+ // NOTE: view updates title text proactively
10614
+ updateToolbarsTodayButton();
9715
10615
  }
9716
10616
  }
9717
10617
  }
9718
10618
 
9719
- unfreezeContentHeight(); // undo any lone freezeContentHeight calls
10619
+ if (needsClearView) {
10620
+ thawContentHeight();
10621
+ }
10622
+
9720
10623
  ignoreWindowResize--;
9721
10624
  }
9722
10625
 
@@ -9724,7 +10627,7 @@ function Calendar_constructor(element, overrides) {
9724
10627
  // Unrenders the current view and reflects this change in the Header.
9725
10628
  // Unregsiters the `currentView`, but does not remove from viewByType hash.
9726
10629
  function clearView() {
9727
- header.deactivateButton(currentView.type);
10630
+ toolbarsManager.proxyCall('deactivateButton', currentView.type);
9728
10631
  currentView.removeElement();
9729
10632
  currentView = t.view = null;
9730
10633
  }
@@ -9740,13 +10643,14 @@ function Calendar_constructor(element, overrides) {
9740
10643
  var viewType = currentView.type;
9741
10644
  var scrollState = currentView.queryScroll();
9742
10645
  clearView();
10646
+ calcSize();
9743
10647
  renderView(viewType, scrollState);
9744
10648
 
9745
- unfreezeContentHeight();
10649
+ thawContentHeight();
9746
10650
  ignoreWindowResize--;
9747
10651
  }
9748
10652
 
9749
-
10653
+
9750
10654
 
9751
10655
  // Resizing
9752
10656
  // -----------------------------------------------------------------------------------
@@ -9763,8 +10667,8 @@ function Calendar_constructor(element, overrides) {
9763
10667
  t.isHeightAuto = function() {
9764
10668
  return t.options.contentHeight === 'auto' || t.options.height === 'auto';
9765
10669
  };
9766
-
9767
-
10670
+
10671
+
9768
10672
  function updateSize(shouldRecalc) {
9769
10673
  if (elementVisible()) {
9770
10674
 
@@ -9786,8 +10690,8 @@ function Calendar_constructor(element, overrides) {
9786
10690
  _calcSize();
9787
10691
  }
9788
10692
  }
9789
-
9790
-
10693
+
10694
+
9791
10695
  function _calcSize() { // assumes elementVisible
9792
10696
  var contentHeightInput = t.options.contentHeight;
9793
10697
  var heightInput = t.options.height;
@@ -9799,13 +10703,13 @@ function Calendar_constructor(element, overrides) {
9799
10703
  suggestedViewHeight = contentHeightInput();
9800
10704
  }
9801
10705
  else if (typeof heightInput === 'number') { // exists and not 'auto'
9802
- suggestedViewHeight = heightInput - queryHeaderHeight();
10706
+ suggestedViewHeight = heightInput - queryToolbarsHeight();
9803
10707
  }
9804
10708
  else if (typeof heightInput === 'function') { // exists and is a function
9805
- suggestedViewHeight = heightInput() - queryHeaderHeight();
10709
+ suggestedViewHeight = heightInput() - queryToolbarsHeight();
9806
10710
  }
9807
10711
  else if (heightInput === 'parent') { // set to height of parent element
9808
- suggestedViewHeight = element.parent().height() - queryHeaderHeight();
10712
+ suggestedViewHeight = element.parent().height() - queryToolbarsHeight();
9809
10713
  }
9810
10714
  else {
9811
10715
  suggestedViewHeight = Math.round(content.width() / Math.max(t.options.aspectRatio, .5));
@@ -9813,11 +10717,14 @@ function Calendar_constructor(element, overrides) {
9813
10717
  }
9814
10718
 
9815
10719
 
9816
- function queryHeaderHeight() {
9817
- 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);
9818
10725
  }
9819
-
9820
-
10726
+
10727
+
9821
10728
  function windowResize(ev) {
9822
10729
  if (
9823
10730
  !ignoreWindowResize &&
@@ -9825,94 +10732,93 @@ function Calendar_constructor(element, overrides) {
9825
10732
  currentView.start // view has already been rendered
9826
10733
  ) {
9827
10734
  if (updateSize(true)) {
9828
- currentView.trigger('windowResize', _element);
10735
+ currentView.publiclyTrigger('windowResize', _element);
9829
10736
  }
9830
10737
  }
9831
10738
  }
9832
-
9833
-
9834
-
9835
- /* Event Fetching/Rendering
9836
- -----------------------------------------------------------------------------*/
9837
- // TODO: going forward, most of this stuff should be directly handled by the view
9838
-
9839
10739
 
9840
- function refetchEvents() { // can be called as an API method
9841
- fetchAndRenderEvents();
9842
- }
9843
10740
 
9844
10741
 
9845
- // TODO: move this into EventManager?
9846
- function refetchEventSources(matchInputs) {
9847
- fetchEventSources(t.getEventSourcesByMatchArray(matchInputs));
9848
- }
10742
+ /* Event Rendering
10743
+ -----------------------------------------------------------------------------*/
9849
10744
 
9850
10745
 
9851
- function renderEvents() { // destroys old events if previously rendered
10746
+ function rerenderEvents() { // API method. destroys old events if previously rendered.
9852
10747
  if (elementVisible()) {
9853
- freezeContentHeight();
9854
- currentView.displayEvents(events);
9855
- unfreezeContentHeight();
10748
+ t.reportEventChange(); // will re-trasmit events to the view, causing a rerender
9856
10749
  }
9857
10750
  }
9858
-
9859
10751
 
9860
- function getAndRenderEvents() {
9861
- if (!t.options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) {
9862
- fetchAndRenderEvents();
9863
- }
9864
- else {
9865
- renderEvents();
9866
- }
9867
- }
9868
10752
 
9869
10753
 
9870
- function fetchAndRenderEvents() {
9871
- fetchEvents(currentView.start, currentView.end);
9872
- // ... will call reportEvents
9873
- // ... which will call renderEvents
9874
- }
10754
+ /* Toolbars
10755
+ -----------------------------------------------------------------------------*/
9875
10756
 
9876
-
9877
- // called when event data arrives
9878
- function reportEvents(_events) {
9879
- events = _events;
9880
- renderEvents();
10757
+
10758
+ function buildToolbars() {
10759
+ return [
10760
+ new Toolbar(t, computeHeaderOptions()),
10761
+ new Toolbar(t, computeFooterOptions())
10762
+ ];
9881
10763
  }
9882
10764
 
9883
10765
 
9884
- // called when a single event's data has been changed
9885
- function reportEventChange() {
9886
- renderEvents();
10766
+ function computeHeaderOptions() {
10767
+ return {
10768
+ extraClasses: 'fc-header-toolbar',
10769
+ layout: t.options.header
10770
+ };
9887
10771
  }
9888
10772
 
9889
10773
 
10774
+ function computeFooterOptions() {
10775
+ return {
10776
+ extraClasses: 'fc-footer-toolbar',
10777
+ layout: t.options.footer
10778
+ };
10779
+ }
9890
10780
 
9891
- /* Header Updating
9892
- -----------------------------------------------------------------------------*/
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
+ }
9893
10790
 
9894
10791
 
9895
- function updateHeaderTitle() {
9896
- 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
+ }
9897
10799
  }
9898
10800
 
9899
10801
 
9900
- function updateTodayButton() {
9901
- var now = t.getNow();
10802
+ t.setToolbarsTitle = function(title) {
10803
+ toolbarsManager.proxyCall('updateTitle', title);
10804
+ };
10805
+
9902
10806
 
10807
+ function updateToolbarsTodayButton() {
10808
+ var now = t.getNow();
9903
10809
  if (now >= currentView.intervalStart && now < currentView.intervalEnd) {
9904
- header.disableButton('today');
10810
+ toolbarsManager.proxyCall('disableButton', 'today');
9905
10811
  }
9906
10812
  else {
9907
- header.enableButton('today');
10813
+ toolbarsManager.proxyCall('enableButton', 'today');
9908
10814
  }
9909
10815
  }
9910
-
10816
+
9911
10817
 
9912
10818
 
9913
10819
  /* Selection
9914
10820
  -----------------------------------------------------------------------------*/
9915
-
10821
+
9916
10822
 
9917
10823
  // this public method receives start/end dates in any format, with any timezone
9918
10824
  function select(zonedStartInput, zonedEndInput) {
@@ -9920,56 +10826,56 @@ function Calendar_constructor(element, overrides) {
9920
10826
  t.buildSelectSpan.apply(t, arguments)
9921
10827
  );
9922
10828
  }
9923
-
10829
+
9924
10830
 
9925
10831
  function unselect() { // safe to be called before renderView
9926
10832
  if (currentView) {
9927
10833
  currentView.unselect();
9928
10834
  }
9929
10835
  }
9930
-
9931
-
9932
-
10836
+
10837
+
10838
+
9933
10839
  /* Date
9934
10840
  -----------------------------------------------------------------------------*/
9935
-
9936
-
10841
+
10842
+
9937
10843
  function prev() {
9938
10844
  date = currentView.computePrevDate(date);
9939
10845
  renderView();
9940
10846
  }
9941
-
9942
-
10847
+
10848
+
9943
10849
  function next() {
9944
10850
  date = currentView.computeNextDate(date);
9945
10851
  renderView();
9946
10852
  }
9947
-
9948
-
10853
+
10854
+
9949
10855
  function prevYear() {
9950
10856
  date.add(-1, 'years');
9951
10857
  renderView();
9952
- }
9953
-
9954
-
10858
+ }
10859
+
10860
+
9955
10861
  function nextYear() {
9956
10862
  date.add(1, 'years');
9957
10863
  renderView();
9958
10864
  }
9959
-
9960
-
10865
+
10866
+
9961
10867
  function today() {
9962
10868
  date = t.getNow();
9963
10869
  renderView();
9964
10870
  }
9965
-
9966
-
10871
+
10872
+
9967
10873
  function gotoDate(zonedDateInput) {
9968
10874
  date = t.moment(zonedDateInput).stripZone();
9969
10875
  renderView();
9970
10876
  }
9971
-
9972
-
10877
+
10878
+
9973
10879
  function incrementDate(delta) {
9974
10880
  date.add(moment.duration(delta));
9975
10881
  renderView();
@@ -9987,8 +10893,8 @@ function Calendar_constructor(element, overrides) {
9987
10893
  date = newDate.clone();
9988
10894
  renderView(spec ? spec.type : null);
9989
10895
  }
9990
-
9991
-
10896
+
10897
+
9992
10898
  // for external API
9993
10899
  function getDate() {
9994
10900
  return t.applyTimezone(date); // infuse the calendar's timezone
@@ -9998,45 +10904,51 @@ function Calendar_constructor(element, overrides) {
9998
10904
 
9999
10905
  /* Height "Freezing"
10000
10906
  -----------------------------------------------------------------------------*/
10001
- // TODO: move this into the view
10907
+
10002
10908
 
10003
10909
  t.freezeContentHeight = freezeContentHeight;
10004
- t.unfreezeContentHeight = unfreezeContentHeight;
10910
+ t.thawContentHeight = thawContentHeight;
10911
+
10912
+ var freezeContentHeightDepth = 0;
10005
10913
 
10006
10914
 
10007
10915
  function freezeContentHeight() {
10008
- content.css({
10009
- width: '100%',
10010
- height: content.height(),
10011
- overflow: 'hidden'
10012
- });
10916
+ if (!(freezeContentHeightDepth++)) {
10917
+ content.css({
10918
+ width: '100%',
10919
+ height: content.height(),
10920
+ overflow: 'hidden'
10921
+ });
10922
+ }
10013
10923
  }
10014
10924
 
10015
10925
 
10016
- function unfreezeContentHeight() {
10017
- content.css({
10018
- width: '',
10019
- height: '',
10020
- overflow: ''
10021
- });
10926
+ function thawContentHeight() {
10927
+ if (!(--freezeContentHeightDepth)) {
10928
+ content.css({
10929
+ width: '',
10930
+ height: '',
10931
+ overflow: ''
10932
+ });
10933
+ }
10022
10934
  }
10023
-
10024
-
10025
-
10935
+
10936
+
10937
+
10026
10938
  /* Misc
10027
10939
  -----------------------------------------------------------------------------*/
10028
-
10940
+
10029
10941
 
10030
10942
  function getCalendar() {
10031
10943
  return t;
10032
10944
  }
10033
10945
 
10034
-
10946
+
10035
10947
  function getView() {
10036
10948
  return currentView;
10037
10949
  }
10038
-
10039
-
10950
+
10951
+
10040
10952
  function option(name, value) {
10041
10953
  var newOptionHash;
10042
10954
 
@@ -10092,19 +11004,20 @@ function Calendar_constructor(element, overrides) {
10092
11004
  }
10093
11005
  else if (optionName === 'timezone') {
10094
11006
  t.rezoneArrayEventSources();
10095
- refetchEvents();
11007
+ t.refetchEvents();
10096
11008
  return;
10097
11009
  }
10098
11010
  }
10099
11011
 
10100
- // 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
10101
11013
  renderHeader();
11014
+ renderFooter();
10102
11015
  viewsByType = {}; // even non-current views will be affected by this option change. do before rerender
10103
11016
  reinitView();
10104
11017
  }
10105
-
10106
-
10107
- function trigger(name, thisObj) { // overrides the Emitter's trigger method :(
11018
+
11019
+
11020
+ function publiclyTrigger(name, thisObj) {
10108
11021
  var args = Array.prototype.slice.call(arguments, 2);
10109
11022
 
10110
11023
  thisObj = thisObj || _element;
@@ -10267,6 +11180,7 @@ Calendar.defaults = {
10267
11180
  dropAccept: '*',
10268
11181
 
10269
11182
  eventOrder: 'title',
11183
+ //eventRenderWait: null,
10270
11184
 
10271
11185
  eventLimit: false,
10272
11186
  eventLimitText: 'more',
@@ -10470,308 +11384,39 @@ var instanceComputableOptions = {
10470
11384
  },
10471
11385
 
10472
11386
  // Produces format strings for results like "Wk 5"
10473
- weekFormat: function(options) {
10474
- return options.isRTL ?
10475
- 'w[ ' + options.weekNumberTitle + ']' :
10476
- '[' + options.weekNumberTitle + ' ]w';
10477
- },
10478
-
10479
- // Produces format strings for results like "Wk5"
10480
- smallWeekFormat: function(options) {
10481
- return options.isRTL ?
10482
- 'w[' + options.weekNumberTitle + ']' :
10483
- '[' + options.weekNumberTitle + ']w';
10484
- }
10485
-
10486
- };
10487
-
10488
- function populateInstanceComputableOptions(options) {
10489
- $.each(instanceComputableOptions, function(name, func) {
10490
- if (options[name] == null) {
10491
- options[name] = func(options);
10492
- }
10493
- });
10494
- }
10495
-
10496
-
10497
- // Returns moment's internal locale data. If doesn't exist, returns English.
10498
- function getMomentLocaleData(localeCode) {
10499
- return moment.localeData(localeCode) || moment.localeData('en');
10500
- }
10501
-
10502
-
10503
- // Initialize English by forcing computation of moment-derived options.
10504
- // Also, sets it as the default.
10505
- FC.locale('en', Calendar.englishDefaults);
10506
-
10507
- ;;
10508
-
10509
- /* Top toolbar area with buttons and title
10510
- ----------------------------------------------------------------------------------------------------------------------*/
10511
- // TODO: rename all header-related things to "toolbar"
10512
-
10513
- function Header(calendar) {
10514
- var t = this;
10515
-
10516
- // exports
10517
- t.render = render;
10518
- t.removeElement = removeElement;
10519
- t.updateTitle = updateTitle;
10520
- t.activateButton = activateButton;
10521
- t.deactivateButton = deactivateButton;
10522
- t.disableButton = disableButton;
10523
- t.enableButton = enableButton;
10524
- t.getViewsWithButtons = getViewsWithButtons;
10525
- t.el = null; // mirrors local `el`
10526
-
10527
- // locals
10528
- var el;
10529
- var viewsWithButtons = [];
10530
- var tm;
10531
-
10532
-
10533
- // can be called repeatedly and will rerender
10534
- function render() {
10535
- var options = calendar.options;
10536
- var sections = options.header;
10537
-
10538
- tm = options.theme ? 'ui' : 'fc';
10539
-
10540
- if (sections) {
10541
- if (!el) {
10542
- el = this.el = $("<div class='fc-toolbar'/>");
10543
- }
10544
- else {
10545
- el.empty();
10546
- }
10547
- el.append(renderSection('left'))
10548
- .append(renderSection('right'))
10549
- .append(renderSection('center'))
10550
- .append('<div class="fc-clear"/>');
10551
- }
10552
- else {
10553
- removeElement();
10554
- }
10555
- }
10556
-
10557
-
10558
- function removeElement() {
10559
- if (el) {
10560
- el.remove();
10561
- el = t.el = null;
10562
- }
10563
- }
10564
-
10565
-
10566
- function renderSection(position) {
10567
- var sectionEl = $('<div class="fc-' + position + '"/>');
10568
- var options = calendar.options;
10569
- var buttonStr = options.header[position];
10570
-
10571
- if (buttonStr) {
10572
- $.each(buttonStr.split(' '), function(i) {
10573
- var groupChildren = $();
10574
- var isOnlyButtons = true;
10575
- var groupEl;
10576
-
10577
- $.each(this.split(','), function(j, buttonName) {
10578
- var customButtonProps;
10579
- var viewSpec;
10580
- var buttonClick;
10581
- var overrideText; // text explicitly set by calendar's constructor options. overcomes icons
10582
- var defaultText;
10583
- var themeIcon;
10584
- var normalIcon;
10585
- var innerHtml;
10586
- var classes;
10587
- var button; // the element
10588
-
10589
- if (buttonName == 'title') {
10590
- groupChildren = groupChildren.add($('<h2>&nbsp;</h2>')); // we always want it to take up height
10591
- isOnlyButtons = false;
10592
- }
10593
- else {
10594
- if ((customButtonProps = (options.customButtons || {})[buttonName])) {
10595
- buttonClick = function(ev) {
10596
- if (customButtonProps.click) {
10597
- customButtonProps.click.call(button[0], ev);
10598
- }
10599
- };
10600
- overrideText = ''; // icons will override text
10601
- defaultText = customButtonProps.text;
10602
- }
10603
- else if ((viewSpec = calendar.getViewSpec(buttonName))) {
10604
- buttonClick = function() {
10605
- calendar.changeView(buttonName);
10606
- };
10607
- viewsWithButtons.push(buttonName);
10608
- overrideText = viewSpec.buttonTextOverride;
10609
- defaultText = viewSpec.buttonTextDefault;
10610
- }
10611
- else if (calendar[buttonName]) { // a calendar method
10612
- buttonClick = function() {
10613
- calendar[buttonName]();
10614
- };
10615
- overrideText = (calendar.overrides.buttonText || {})[buttonName];
10616
- defaultText = options.buttonText[buttonName]; // everything else is considered default
10617
- }
10618
-
10619
- if (buttonClick) {
10620
-
10621
- themeIcon =
10622
- customButtonProps ?
10623
- customButtonProps.themeIcon :
10624
- options.themeButtonIcons[buttonName];
10625
-
10626
- normalIcon =
10627
- customButtonProps ?
10628
- customButtonProps.icon :
10629
- options.buttonIcons[buttonName];
10630
-
10631
- if (overrideText) {
10632
- innerHtml = htmlEscape(overrideText);
10633
- }
10634
- else if (themeIcon && options.theme) {
10635
- innerHtml = "<span class='ui-icon ui-icon-" + themeIcon + "'></span>";
10636
- }
10637
- else if (normalIcon && !options.theme) {
10638
- innerHtml = "<span class='fc-icon fc-icon-" + normalIcon + "'></span>";
10639
- }
10640
- else {
10641
- innerHtml = htmlEscape(defaultText);
10642
- }
10643
-
10644
- classes = [
10645
- 'fc-' + buttonName + '-button',
10646
- tm + '-button',
10647
- tm + '-state-default'
10648
- ];
10649
-
10650
- button = $( // type="button" so that it doesn't submit a form
10651
- '<button type="button" class="' + classes.join(' ') + '">' +
10652
- innerHtml +
10653
- '</button>'
10654
- )
10655
- .click(function(ev) {
10656
- // don't process clicks for disabled buttons
10657
- if (!button.hasClass(tm + '-state-disabled')) {
10658
-
10659
- buttonClick(ev);
10660
-
10661
- // after the click action, if the button becomes the "active" tab, or disabled,
10662
- // it should never have a hover class, so remove it now.
10663
- if (
10664
- button.hasClass(tm + '-state-active') ||
10665
- button.hasClass(tm + '-state-disabled')
10666
- ) {
10667
- button.removeClass(tm + '-state-hover');
10668
- }
10669
- }
10670
- })
10671
- .mousedown(function() {
10672
- // the *down* effect (mouse pressed in).
10673
- // only on buttons that are not the "active" tab, or disabled
10674
- button
10675
- .not('.' + tm + '-state-active')
10676
- .not('.' + tm + '-state-disabled')
10677
- .addClass(tm + '-state-down');
10678
- })
10679
- .mouseup(function() {
10680
- // undo the *down* effect
10681
- button.removeClass(tm + '-state-down');
10682
- })
10683
- .hover(
10684
- function() {
10685
- // the *hover* effect.
10686
- // only on buttons that are not the "active" tab, or disabled
10687
- button
10688
- .not('.' + tm + '-state-active')
10689
- .not('.' + tm + '-state-disabled')
10690
- .addClass(tm + '-state-hover');
10691
- },
10692
- function() {
10693
- // undo the *hover* effect
10694
- button
10695
- .removeClass(tm + '-state-hover')
10696
- .removeClass(tm + '-state-down'); // if mouseleave happens before mouseup
10697
- }
10698
- );
11387
+ weekFormat: function(options) {
11388
+ return options.isRTL ?
11389
+ 'w[ ' + options.weekNumberTitle + ']' :
11390
+ '[' + options.weekNumberTitle + ' ]w';
11391
+ },
10699
11392
 
10700
- groupChildren = groupChildren.add(button);
10701
- }
10702
- }
10703
- });
11393
+ // Produces format strings for results like "Wk5"
11394
+ smallWeekFormat: function(options) {
11395
+ return options.isRTL ?
11396
+ 'w[' + options.weekNumberTitle + ']' :
11397
+ '[' + options.weekNumberTitle + ']w';
11398
+ }
10704
11399
 
10705
- if (isOnlyButtons) {
10706
- groupChildren
10707
- .first().addClass(tm + '-corner-left').end()
10708
- .last().addClass(tm + '-corner-right').end();
10709
- }
11400
+ };
10710
11401
 
10711
- if (groupChildren.length > 1) {
10712
- groupEl = $('<div/>');
10713
- if (isOnlyButtons) {
10714
- groupEl.addClass('fc-button-group');
10715
- }
10716
- groupEl.append(groupChildren);
10717
- sectionEl.append(groupEl);
10718
- }
10719
- else {
10720
- sectionEl.append(groupChildren); // 1 or 0 children
10721
- }
10722
- });
11402
+ function populateInstanceComputableOptions(options) {
11403
+ $.each(instanceComputableOptions, function(name, func) {
11404
+ if (options[name] == null) {
11405
+ options[name] = func(options);
10723
11406
  }
11407
+ });
11408
+ }
10724
11409
 
10725
- return sectionEl;
10726
- }
10727
-
10728
-
10729
- function updateTitle(text) {
10730
- if (el) {
10731
- el.find('h2').text(text);
10732
- }
10733
- }
10734
-
10735
-
10736
- function activateButton(buttonName) {
10737
- if (el) {
10738
- el.find('.fc-' + buttonName + '-button')
10739
- .addClass(tm + '-state-active');
10740
- }
10741
- }
10742
-
10743
-
10744
- function deactivateButton(buttonName) {
10745
- if (el) {
10746
- el.find('.fc-' + buttonName + '-button')
10747
- .removeClass(tm + '-state-active');
10748
- }
10749
- }
10750
-
10751
-
10752
- function disableButton(buttonName) {
10753
- if (el) {
10754
- el.find('.fc-' + buttonName + '-button')
10755
- .prop('disabled', true)
10756
- .addClass(tm + '-state-disabled');
10757
- }
10758
- }
10759
-
10760
-
10761
- function enableButton(buttonName) {
10762
- if (el) {
10763
- el.find('.fc-' + buttonName + '-button')
10764
- .prop('disabled', false)
10765
- .removeClass(tm + '-state-disabled');
10766
- }
10767
- }
10768
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');
11414
+ }
10769
11415
 
10770
- function getViewsWithButtons() {
10771
- return viewsWithButtons;
10772
- }
10773
11416
 
10774
- }
11417
+ // Initialize English by forcing computation of moment-derived options.
11418
+ // Also, sets it as the default.
11419
+ FC.locale('en', Calendar.englishDefaults);
10775
11420
 
10776
11421
  ;;
10777
11422
 
@@ -10788,38 +11433,39 @@ var eventGUID = 1;
10788
11433
 
10789
11434
  function EventManager() { // assumed to be a calendar
10790
11435
  var t = this;
10791
-
10792
-
11436
+
11437
+
10793
11438
  // exports
11439
+ t.requestEvents = requestEvents;
11440
+ t.reportEventChange = reportEventChange;
10794
11441
  t.isFetchNeeded = isFetchNeeded;
10795
11442
  t.fetchEvents = fetchEvents;
10796
11443
  t.fetchEventSources = fetchEventSources;
11444
+ t.refetchEvents = refetchEvents;
11445
+ t.refetchEventSources = refetchEventSources;
10797
11446
  t.getEventSources = getEventSources;
10798
11447
  t.getEventSourceById = getEventSourceById;
10799
- t.getEventSourcesByMatchArray = getEventSourcesByMatchArray;
10800
- t.getEventSourcesByMatch = getEventSourcesByMatch;
10801
11448
  t.addEventSource = addEventSource;
10802
11449
  t.removeEventSource = removeEventSource;
10803
11450
  t.removeEventSources = removeEventSources;
10804
11451
  t.updateEvent = updateEvent;
11452
+ t.updateEvents = updateEvents;
10805
11453
  t.renderEvent = renderEvent;
11454
+ t.renderEvents = renderEvents;
10806
11455
  t.removeEvents = removeEvents;
10807
11456
  t.clientEvents = clientEvents;
10808
11457
  t.mutateEvent = mutateEvent;
10809
11458
  t.normalizeEventDates = normalizeEventDates;
10810
11459
  t.normalizeEventTimes = normalizeEventTimes;
10811
-
10812
-
10813
- // imports
10814
- var reportEvents = t.reportEvents;
10815
-
10816
-
11460
+
11461
+
10817
11462
  // locals
10818
11463
  var stickySource = { events: [] };
10819
11464
  var sources = [ stickySource ];
10820
11465
  var rangeStart, rangeEnd;
10821
11466
  var pendingSourceCnt = 0; // outstanding fetch requests, max one per source
10822
11467
  var cache = []; // holds events that have already been expanded
11468
+ var prunedCache; // like cache, but only events that intersect with rangeStart/rangeEnd
10823
11469
 
10824
11470
 
10825
11471
  $.each(
@@ -10831,9 +11477,55 @@ function EventManager() { // assumed to be a calendar
10831
11477
  }
10832
11478
  }
10833
11479
  );
10834
-
10835
-
10836
-
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
+
10837
11529
  /* Fetching
10838
11530
  -----------------------------------------------------------------------------*/
10839
11531
 
@@ -10843,12 +11535,24 @@ function EventManager() { // assumed to be a calendar
10843
11535
  return !rangeStart || // nothing has been fetched yet?
10844
11536
  start < rangeStart || end > rangeEnd; // is part of the new range outside of the old range?
10845
11537
  }
10846
-
10847
-
11538
+
11539
+
10848
11540
  function fetchEvents(start, end) {
10849
11541
  rangeStart = start;
10850
11542
  rangeEnd = end;
10851
- 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));
10852
11556
  }
10853
11557
 
10854
11558
 
@@ -10878,9 +11582,17 @@ function EventManager() { // assumed to be a calendar
10878
11582
 
10879
11583
  for (i = 0; i < specificSources.length; i++) {
10880
11584
  source = specificSources[i];
10881
-
10882
11585
  tryFetchEventSource(source, source._fetchId);
10883
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
+ }
10884
11596
  }
10885
11597
 
10886
11598
 
@@ -10913,7 +11625,7 @@ function EventManager() { // assumed to be a calendar
10913
11625
  }
10914
11626
 
10915
11627
  if (abstractEvent) { // not false (an invalid event)
10916
- cache.push.apply(
11628
+ cache.push.apply( // append
10917
11629
  cache,
10918
11630
  expandEvent(abstractEvent) // add individual expanded events to the cache
10919
11631
  );
@@ -10941,11 +11653,12 @@ function EventManager() { // assumed to be a calendar
10941
11653
  function decrementPendingSourceCnt() {
10942
11654
  pendingSourceCnt--;
10943
11655
  if (!pendingSourceCnt) {
10944
- reportEvents(cache);
11656
+ reportEventChange(cache); // updates prunedCache
11657
+ t.trigger('eventsReceived', prunedCache);
10945
11658
  }
10946
11659
  }
10947
-
10948
-
11660
+
11661
+
10949
11662
  function _fetchEventSource(source, callback) {
10950
11663
  var i;
10951
11664
  var fetchers = FC.sourceFetchers;
@@ -11054,9 +11767,9 @@ function EventManager() { // assumed to be a calendar
11054
11767
  }
11055
11768
  }
11056
11769
  }
11057
-
11058
-
11059
-
11770
+
11771
+
11772
+
11060
11773
  /* Sources
11061
11774
  -----------------------------------------------------------------------------*/
11062
11775
 
@@ -11065,7 +11778,7 @@ function EventManager() { // assumed to be a calendar
11065
11778
  var source = buildEventSource(sourceInput);
11066
11779
  if (source) {
11067
11780
  sources.push(source);
11068
- fetchEventSources([ source ], 'add'); // will eventually call reportEvents
11781
+ fetchEventSources([ source ], 'add'); // will eventually call reportEventChange
11069
11782
  }
11070
11783
  }
11071
11784
 
@@ -11161,7 +11874,7 @@ function EventManager() { // assumed to be a calendar
11161
11874
  cache = excludeEventsBySources(cache, targetSources);
11162
11875
  }
11163
11876
 
11164
- reportEvents(cache);
11877
+ reportEventChange();
11165
11878
  }
11166
11879
 
11167
11880
 
@@ -11255,27 +11968,39 @@ function EventManager() { // assumed to be a calendar
11255
11968
  return true; // keep
11256
11969
  });
11257
11970
  }
11258
-
11259
-
11260
-
11971
+
11972
+
11973
+
11261
11974
  /* Manipulation
11262
11975
  -----------------------------------------------------------------------------*/
11263
11976
 
11264
11977
 
11265
11978
  // Only ever called from the externally-facing API
11266
11979
  function updateEvent(event) {
11980
+ updateEvents([ event ]);
11981
+ }
11267
11982
 
11268
- // massage start/end values, even if date string values
11269
- event.start = t.moment(event.start);
11270
- if (event.end) {
11271
- event.end = t.moment(event.end);
11272
- }
11273
- else {
11274
- 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
11275
12001
  }
11276
12002
 
11277
- mutateEvent(event, getMiscEventProps(event)); // will handle start/end/allDay normalization
11278
- reportEvents(cache); // reports event modifications (so we can redraw)
12003
+ reportEventChange(); // reports event modifications (so we can redraw)
11279
12004
  }
11280
12005
 
11281
12006
 
@@ -11299,37 +12024,50 @@ function EventManager() { // assumed to be a calendar
11299
12024
  return !/^_|^(id|allDay|start|end)$/.test(name);
11300
12025
  }
11301
12026
 
11302
-
12027
+
11303
12028
  // returns the expanded events that were created
11304
12029
  function renderEvent(eventInput, stick) {
11305
- var abstractEvent = buildEventFromInput(eventInput);
11306
- var events;
11307
- var i, event;
12030
+ return renderEvents([ eventInput ], stick);
12031
+ }
11308
12032
 
11309
- if (abstractEvent) { // not false (a valid input)
11310
- events = expandEvent(abstractEvent);
11311
12033
 
11312
- for (i = 0; i < events.length; i++) {
11313
- 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);
11314
12046
 
11315
- if (!event.source) {
11316
- if (stick) {
11317
- stickySource.events.push(event);
11318
- event.source = stickySource;
12047
+ for (j = 0; j < renderableEvents.length; j++) {
12048
+ event = renderableEvents[j];
12049
+
12050
+ if (!event.source) {
12051
+ if (stick) {
12052
+ stickySource.events.push(event);
12053
+ event.source = stickySource;
12054
+ }
12055
+ cache.push(event);
11319
12056
  }
11320
- cache.push(event);
11321
12057
  }
11322
- }
11323
12058
 
11324
- reportEvents(cache);
12059
+ renderedEvents = renderedEvents.concat(renderableEvents);
12060
+ }
12061
+ }
11325
12062
 
11326
- return events;
12063
+ if (renderedEvents.length) { // any new events rendered?
12064
+ reportEventChange();
11327
12065
  }
11328
12066
 
11329
- return [];
12067
+ return renderedEvents;
11330
12068
  }
11331
-
11332
-
12069
+
12070
+
11333
12071
  function removeEvents(filter) {
11334
12072
  var eventID;
11335
12073
  var i;
@@ -11356,10 +12094,10 @@ function EventManager() { // assumed to be a calendar
11356
12094
  }
11357
12095
  }
11358
12096
 
11359
- reportEvents(cache);
12097
+ reportEventChange();
11360
12098
  }
11361
12099
 
11362
-
12100
+
11363
12101
  function clientEvents(filter) {
11364
12102
  if ($.isFunction(filter)) {
11365
12103
  return $.grep(cache, filter);
@@ -11399,8 +12137,8 @@ function EventManager() { // assumed to be a calendar
11399
12137
  }
11400
12138
  backupEventDates(event);
11401
12139
  }
11402
-
11403
-
12140
+
12141
+
11404
12142
  /* Event Normalization
11405
12143
  -----------------------------------------------------------------------------*/
11406
12144
 
@@ -11810,11 +12548,6 @@ function EventManager() { // assumed to be a calendar
11810
12548
  };
11811
12549
  }
11812
12550
 
11813
-
11814
- t.getEventCache = function() {
11815
- return cache;
11816
- };
11817
-
11818
12551
  }
11819
12552
 
11820
12553
 
@@ -12343,13 +13076,18 @@ var BasicView = FC.BasicView = View.extend({
12343
13076
  ------------------------------------------------------------------------------------------------------------------*/
12344
13077
 
12345
13078
 
13079
+ computeInitialScroll: function() {
13080
+ return { top: 0 };
13081
+ },
13082
+
13083
+
12346
13084
  queryScroll: function() {
12347
- return this.scroller.getScrollTop();
13085
+ return { top: this.scroller.getScrollTop() };
12348
13086
  },
12349
13087
 
12350
13088
 
12351
- setScroll: function(top) {
12352
- this.scroller.setScrollTop(top);
13089
+ setScroll: function(scroll) {
13090
+ this.scroller.setScrollTop(scroll.top);
12353
13091
  },
12354
13092
 
12355
13093
 
@@ -12866,17 +13604,17 @@ var AgendaView = FC.AgendaView = View.extend({
12866
13604
  top++; // to overcome top border that slots beyond the first have. looks better
12867
13605
  }
12868
13606
 
12869
- return top;
13607
+ return { top: top };
12870
13608
  },
12871
13609
 
12872
13610
 
12873
13611
  queryScroll: function() {
12874
- return this.scroller.getScrollTop();
13612
+ return { top: this.scroller.getScrollTop() };
12875
13613
  },
12876
13614
 
12877
13615
 
12878
- setScroll: function(top) {
12879
- this.scroller.setScrollTop(top);
13616
+ setScroll: function(scroll) {
13617
+ this.scroller.setScrollTop(scroll.top);
12880
13618
  },
12881
13619
 
12882
13620
 
@@ -13214,21 +13952,36 @@ var ListViewGrid = Grid.extend({
13214
13952
  // slices by day
13215
13953
  spanToSegs: function(span) {
13216
13954
  var view = this.view;
13217
- var dayStart = view.start.clone();
13218
- var dayEnd;
13955
+ var dayStart = view.start.clone().time(0); // timed, so segs get times!
13956
+ var dayIndex = 0;
13219
13957
  var seg;
13220
13958
  var segs = [];
13221
13959
 
13222
13960
  while (dayStart < view.end) {
13223
- dayEnd = dayStart.clone().add(1, 'day');
13961
+
13224
13962
  seg = intersectRanges(span, {
13225
13963
  start: dayStart,
13226
- end: dayEnd
13964
+ end: dayStart.clone().add(1, 'day')
13227
13965
  });
13966
+
13228
13967
  if (seg) {
13968
+ seg.dayIndex = dayIndex;
13229
13969
  segs.push(seg);
13230
13970
  }
13231
- dayStart = dayEnd;
13971
+
13972
+ dayStart.add(1, 'day');
13973
+ dayIndex++;
13974
+
13975
+ // detect when span won't go fully into the next day,
13976
+ // and mutate the latest seg to the be the end.
13977
+ if (
13978
+ seg && !seg.isEnd && span.end.hasTime() &&
13979
+ span.end < dayStart.clone().add(this.view.nextDayThreshold)
13980
+ ) {
13981
+ seg.end = span.end.clone();
13982
+ seg.isEnd = true;
13983
+ break;
13984
+ }
13232
13985
  }
13233
13986
 
13234
13987
  return segs;
@@ -13261,11 +14014,12 @@ var ListViewGrid = Grid.extend({
13261
14014
 
13262
14015
  if (!segs.length) {
13263
14016
  this.renderEmptyMessage();
13264
- return segs;
13265
14017
  }
13266
14018
  else {
13267
- return this.renderSegList(segs);
14019
+ this.renderSegList(segs);
13268
14020
  }
14021
+
14022
+ return segs;
13269
14023
  },
13270
14024
 
13271
14025
  renderEmptyMessage: function() {
@@ -13280,30 +14034,47 @@ var ListViewGrid = Grid.extend({
13280
14034
  );
13281
14035
  },
13282
14036
 
13283
- // render the event segments in the view. returns the mutated array.
13284
- renderSegList: function(segs) {
14037
+ // render the event segments in the view
14038
+ renderSegList: function(allSegs) {
14039
+ var segsByDay = this.groupSegsByDay(allSegs); // sparse array
14040
+ var dayIndex;
14041
+ var daySegs;
14042
+ var i;
13285
14043
  var tableEl = $('<table class="fc-list-table"><tbody/></table>');
13286
14044
  var tbodyEl = tableEl.find('tbody');
13287
- var i, seg;
13288
- var dayDate;
13289
14045
 
13290
- this.sortEventSegs(segs);
14046
+ for (dayIndex = 0; dayIndex < segsByDay.length; dayIndex++) {
14047
+ daySegs = segsByDay[dayIndex];
14048
+ if (daySegs) { // sparse array, so might be undefined
13291
14049
 
13292
- for (i = 0; i < segs.length; i++) {
13293
- seg = segs[i];
14050
+ // append a day header
14051
+ tbodyEl.append(this.dayHeaderHtml(
14052
+ this.view.start.clone().add(dayIndex, 'days')
14053
+ ));
13294
14054
 
13295
- // append a day header
13296
- if (!dayDate || !seg.start.isSame(dayDate, 'day')) {
13297
- dayDate = seg.start.clone().stripTime();
13298
- tbodyEl.append(this.dayHeaderHtml(dayDate));
13299
- }
14055
+ this.sortEventSegs(daySegs);
13300
14056
 
13301
- tbodyEl.append(seg.el); // append event row
14057
+ for (i = 0; i < daySegs.length; i++) {
14058
+ tbodyEl.append(daySegs[i].el); // append event row
14059
+ }
14060
+ }
13302
14061
  }
13303
14062
 
13304
14063
  this.el.empty().append(tableEl);
14064
+ },
14065
+
14066
+ // Returns a sparse array of arrays, segs grouped by their dayIndex
14067
+ groupSegsByDay: function(segs) {
14068
+ var segsByDay = []; // sparse array
14069
+ var i, seg;
14070
+
14071
+ for (i = 0; i < segs.length; i++) {
14072
+ seg = segs[i];
14073
+ (segsByDay[seg.dayIndex] || (segsByDay[seg.dayIndex] = []))
14074
+ .push(seg);
14075
+ }
13305
14076
 
13306
- return segs; // return the sorted list
14077
+ return segsByDay;
13307
14078
  },
13308
14079
 
13309
14080
  // generates the HTML for the day headers that live amongst the event rows
@@ -13341,13 +14112,20 @@ var ListViewGrid = Grid.extend({
13341
14112
  var url = event.url;
13342
14113
  var timeHtml;
13343
14114
 
13344
- if (!seg.start.hasTime()) {
13345
- if (this.displayEventTime) {
14115
+ if (event.allDay) {
14116
+ timeHtml = view.getAllDayHtml();
14117
+ }
14118
+ else if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day
14119
+ if (seg.isStart || seg.isEnd) { // outer segment that probably lasts part of the day
14120
+ timeHtml = htmlEscape(this.getEventTimeText(seg));
14121
+ }
14122
+ else { // inner segment that lasts the whole day
13346
14123
  timeHtml = view.getAllDayHtml();
13347
14124
  }
13348
14125
  }
13349
14126
  else {
13350
- timeHtml = htmlEscape(this.getEventTimeText(event)); // might return empty
14127
+ // Display the normal time text for the *event's* times
14128
+ timeHtml = htmlEscape(this.getEventTimeText(event));
13351
14129
  }
13352
14130
 
13353
14131
  if (url) {
@@ -13355,9 +14133,9 @@ var ListViewGrid = Grid.extend({
13355
14133
  }
13356
14134
 
13357
14135
  return '<tr class="' + classes.join(' ') + '">' +
13358
- (timeHtml ?
14136
+ (this.displayEventTime ?
13359
14137
  '<td class="fc-list-item-time ' + view.widgetContentClass + '">' +
13360
- timeHtml +
14138
+ (timeHtml || '') +
13361
14139
  '</td>' :
13362
14140
  '') +
13363
14141
  '<td class="fc-list-item-marker ' + view.widgetContentClass + '">' +
@@ -13369,7 +14147,7 @@ var ListViewGrid = Grid.extend({
13369
14147
  '</td>' +
13370
14148
  '<td class="fc-list-item-title ' + view.widgetContentClass + '">' +
13371
14149
  '<a' + (url ? ' href="' + htmlEscape(url) + '"' : '') + '>' +
13372
- htmlEscape(seg.event.title) +
14150
+ htmlEscape(seg.event.title || '') +
13373
14151
  '</a>' +
13374
14152
  '</td>' +
13375
14153
  '</tr>';
@@ -13384,7 +14162,6 @@ fcViews.list = {
13384
14162
  buttonTextKey: 'list', // what to lookup in locale files
13385
14163
  defaults: {
13386
14164
  buttonText: 'list', // text to display for English
13387
- listTime: true, // show the time column?
13388
14165
  listDayFormat: 'LL', // like "January 1, 2016"
13389
14166
  noEventsMessage: 'No events to display'
13390
14167
  }