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