fullcalendar-rails 3.0.0.0 → 3.1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
  }