fullcalendar-rails 2.8.0.0 → 3.0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +59 -57
  4. data/lib/fullcalendar-rails/version.rb +1 -1
  5. data/vendor/assets/javascripts/fullcalendar.js +1468 -896
  6. data/vendor/assets/javascripts/fullcalendar/gcal.js +1 -1
  7. data/vendor/assets/javascripts/fullcalendar/lang/ar-ma.js +1 -1
  8. data/vendor/assets/javascripts/fullcalendar/lang/ar-sa.js +1 -1
  9. data/vendor/assets/javascripts/fullcalendar/lang/ar-tn.js +1 -1
  10. data/vendor/assets/javascripts/fullcalendar/lang/ar.js +1 -1
  11. data/vendor/assets/javascripts/fullcalendar/lang/bg.js +1 -1
  12. data/vendor/assets/javascripts/fullcalendar/lang/ca.js +1 -1
  13. data/vendor/assets/javascripts/fullcalendar/lang/cs.js +1 -1
  14. data/vendor/assets/javascripts/fullcalendar/lang/da.js +1 -1
  15. data/vendor/assets/javascripts/fullcalendar/lang/de-at.js +1 -1
  16. data/vendor/assets/javascripts/fullcalendar/lang/de.js +1 -1
  17. data/vendor/assets/javascripts/fullcalendar/lang/el.js +1 -1
  18. data/vendor/assets/javascripts/fullcalendar/lang/en-au.js +1 -1
  19. data/vendor/assets/javascripts/fullcalendar/lang/en-ca.js +1 -1
  20. data/vendor/assets/javascripts/fullcalendar/lang/en-gb.js +1 -1
  21. data/vendor/assets/javascripts/fullcalendar/lang/en-ie.js +1 -1
  22. data/vendor/assets/javascripts/fullcalendar/lang/en-nz.js +1 -1
  23. data/vendor/assets/javascripts/fullcalendar/lang/es-do.js +1 -0
  24. data/vendor/assets/javascripts/fullcalendar/lang/es.js +1 -1
  25. data/vendor/assets/javascripts/fullcalendar/lang/eu.js +1 -1
  26. data/vendor/assets/javascripts/fullcalendar/lang/fa.js +1 -1
  27. data/vendor/assets/javascripts/fullcalendar/lang/fi.js +1 -1
  28. data/vendor/assets/javascripts/fullcalendar/lang/fr-ca.js +1 -1
  29. data/vendor/assets/javascripts/fullcalendar/lang/fr-ch.js +1 -1
  30. data/vendor/assets/javascripts/fullcalendar/lang/fr.js +1 -1
  31. data/vendor/assets/javascripts/fullcalendar/lang/gl.js +1 -1
  32. data/vendor/assets/javascripts/fullcalendar/lang/he.js +1 -1
  33. data/vendor/assets/javascripts/fullcalendar/lang/hi.js +1 -1
  34. data/vendor/assets/javascripts/fullcalendar/lang/hr.js +1 -1
  35. data/vendor/assets/javascripts/fullcalendar/lang/hu.js +1 -1
  36. data/vendor/assets/javascripts/fullcalendar/lang/id.js +1 -1
  37. data/vendor/assets/javascripts/fullcalendar/lang/is.js +1 -1
  38. data/vendor/assets/javascripts/fullcalendar/lang/it.js +1 -1
  39. data/vendor/assets/javascripts/fullcalendar/lang/ja.js +1 -1
  40. data/vendor/assets/javascripts/fullcalendar/lang/ko.js +1 -1
  41. data/vendor/assets/javascripts/fullcalendar/lang/lb.js +1 -1
  42. data/vendor/assets/javascripts/fullcalendar/lang/lt.js +1 -1
  43. data/vendor/assets/javascripts/fullcalendar/lang/lv.js +1 -1
  44. data/vendor/assets/javascripts/fullcalendar/lang/mk.js +1 -0
  45. data/vendor/assets/javascripts/fullcalendar/lang/ms-my.js +1 -0
  46. data/vendor/assets/javascripts/fullcalendar/lang/ms.js +1 -0
  47. data/vendor/assets/javascripts/fullcalendar/lang/nb.js +1 -1
  48. data/vendor/assets/javascripts/fullcalendar/lang/nl.js +1 -1
  49. data/vendor/assets/javascripts/fullcalendar/lang/nn.js +1 -0
  50. data/vendor/assets/javascripts/fullcalendar/lang/pl.js +1 -1
  51. data/vendor/assets/javascripts/fullcalendar/lang/pt-br.js +1 -1
  52. data/vendor/assets/javascripts/fullcalendar/lang/pt.js +1 -1
  53. data/vendor/assets/javascripts/fullcalendar/lang/ro.js +1 -1
  54. data/vendor/assets/javascripts/fullcalendar/lang/ru.js +1 -1
  55. data/vendor/assets/javascripts/fullcalendar/lang/sk.js +1 -1
  56. data/vendor/assets/javascripts/fullcalendar/lang/sl.js +1 -1
  57. data/vendor/assets/javascripts/fullcalendar/lang/sr-cyrl.js +1 -1
  58. data/vendor/assets/javascripts/fullcalendar/lang/sr.js +1 -1
  59. data/vendor/assets/javascripts/fullcalendar/lang/sv.js +1 -1
  60. data/vendor/assets/javascripts/fullcalendar/lang/th.js +1 -1
  61. data/vendor/assets/javascripts/fullcalendar/lang/tr.js +1 -1
  62. data/vendor/assets/javascripts/fullcalendar/lang/uk.js +1 -1
  63. data/vendor/assets/javascripts/fullcalendar/lang/vi.js +1 -1
  64. data/vendor/assets/javascripts/fullcalendar/lang/zh-cn.js +1 -1
  65. data/vendor/assets/javascripts/fullcalendar/lang/zh-tw.js +1 -1
  66. data/vendor/assets/javascripts/fullcalendar/locale-all.js +5 -0
  67. data/vendor/assets/stylesheets/fullcalendar.css +170 -34
  68. data/vendor/assets/stylesheets/fullcalendar.print.css +1 -1
  69. metadata +8 -3
  70. data/vendor/assets/javascripts/fullcalendar/lang-all.js +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bf346a46d7b099097273d5778484477c361d80d0
4
- data.tar.gz: 1ef451e5d2a42a11c33174170e84394169ebf449
3
+ metadata.gz: 8773252a35a176af5a30a696c6f5d850ad818da1
4
+ data.tar.gz: 846cdb8578a403af18fd01d5a85dda750bbd6c74
5
5
  SHA512:
6
- metadata.gz: 073212fe38be4290162ce05f714f9154979eafc35227132a311a8be3d260a2eecbb5e48987ef9cf3649e878f2b7c8e0c980963945f309b5e8602f78911930182
7
- data.tar.gz: fc867f9f0a959c44749a5d2d0401725e235b379206537fc2cd2927b78c6d0a020a81b8cf6960b8229cd2c832f9bc16b60e309e1c501b498decc965e48602895a
6
+ metadata.gz: 90d7ab81c55d9fc97353af6c7f26f7b9c96b097fe8c9099c971f2f146617156ed5a7da127f87dd8afc20d4228ee607748fda8d5ba0a4e5160ef5634c874be802
7
+ data.tar.gz: ba68426e2a511b8b7226036ab4524458820e841e1cff661b39fc4e7e4b27060500a44519f8be752016173af34a901e4b5cddaadb90939d7d25b8df4dbd0b316b
data/LICENSE CHANGED
@@ -19,4 +19,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
19
  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
20
  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
21
  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,82 +1,84 @@
1
1
  # Fullcalendar::Rails
2
2
 
3
- This gem for Ruby on Rails is a simple rebundling of the contents of the jQuery FullCalendar plugin from Adam Shaw:
4
-
5
- http://arshaw.com/fullcalendar/
6
-
7
- I had created an earlier version of a demonstration of using FullCalendar in a Rails app. That code is available at:
8
-
9
- https://github.com/bokmann/fullcalendar_assets
10
-
11
- But I have always disliked finding random files and copying them into a public directory. So I made an asset gem for use with the asset pipeline.
12
-
3
+ This gem for Ruby on Rails adds the content of the jQuery FullCalendar plugin from Adam Shaw (found here http://arshaw.com/fullcalendar/) within your RoR application such that you do not have to download and install all the FullCalendar assets yourself.
13
4
 
14
5
  ## Installation
6
+ In order to install the fullcalendar-rails gem and get FullCalendar working within your application, do the following steps:
15
7
 
16
- Add this line to your application's Gemfile:
17
-
8
+ 1. Add to `gemfile`
9
+ ```ruby
18
10
  gem 'fullcalendar-rails'
19
-
20
- If you need a specific version of FullCalendar (e.g X.Y.Z), you can explicitly require it like this:
21
-
22
- gem 'fullcalendar-rails', '~> X.Y.Z.0'
23
-
24
- (Note that the last number ("0" in the line above) indicates the release of this gem, so it may change for the same version of FullCalender, see Versioning section below)
25
-
26
- Since version 2.1.1.0 of this gem, the gems `jquery-rails` and `momentjs-rails` are included as dependencies. If you copied moment.js manually into your project, you have to delete the file so it doesn't clash with the required version from the gem. You have to also add
27
-
28
11
  gem 'momentjs-rails'
12
+ ```
29
13
 
30
- to your Gemfile and
31
-
32
- //= require moment
33
-
34
- to your application.js manifest (before requiring fullcalendar).
35
-
36
- Finally execute:
14
+ 1. Bundle install and restart rails server.
37
15
 
38
- $ bundle
39
-
40
- Or install it yourself as:
41
-
42
- $ gem install fullcalendar-rails
43
-
44
- At this point, the files from the FullCalendar project are in your asset pipeline's path... all you have to do is require them in the appropriate places. For instance, in your application.css.scss:
16
+ 1. Add to `application.js`
17
+ ```jquery
18
+ //= require moment
19
+ //= require fullcalendar
20
+
21
+ $('#calendar').fullCalendar({});
22
+ ```
45
23
 
24
+ 1. Add to `application.css`
25
+ ```css
46
26
  *= require fullcalendar
27
+ ```
28
+
29
+ 1. In view, include the following html:
30
+ ```html
31
+ <div id="calendar"></div>
32
+ ```
33
+ Now if you go to that view you should see the FullCalendar.
47
34
 
48
- And in your application.js:
35
+ 1. Reference the Using FullCalendar section for details on populating FullCalendar.
49
36
 
50
- //= require fullcalendar
37
+ ### Installing Google Calander support
38
+ FullCalendar comes with Google calendar support, which can be implemented within your application with the following step:
51
39
 
52
- You can also include the fullcalendar.print file in your css manifest like this:
40
+ * Using `gem fullcalendar-rails >= 2.1.1`, add `//= require fullcalendar/gcal` to `application.js`
41
+ * Using `gem fullcalendar-rails < 2.1.1`, add `//= require gcal` to `application.js`
53
42
 
54
- *= require fullcalendar.print
43
+ ### Installing a specific version:
44
+ If you want a specific version of FullCalendar, use the following line in your Gemfile:
55
45
 
56
- but as Ray Zane pointed out in issue #11, this will cause a problem if you try to change the colors of events.
46
+ gem 'fullcalendar-rails', '~> X.Y.Z.0'
47
+
48
+ where **X.Y.Z** is the specific version of FullCalendar you wish to install (**Note: the last number "0" in the line above indicates the version of the fullcalendar-rails gem and may be something other than "0", but will still provide the FullCalendar version specified by X.Y.Z**).
57
49
 
58
- An ideal solution, if you need a print stylesheet, is to create an application-print.css.scss file and include this (and other print-related css) in it. You'll then need to add it to to the precompile array in config/application.rb:
50
+ ### Install for fullcalendar-print
51
+ After following the above instalations steps, you may choose to use the `fullcalendar-print` file within your application to better customize the appearance of FullCalandar. To do so, follow these steps:
59
52
 
53
+ + Option 1: Add to `application.css`
54
+ ```css
55
+ *= require fullcalendar.print
56
+ ```
57
+ *Note: This method causes issues with changing the color of events within FullCalendar, pointed out in issue #11.*
58
+
59
+ + Option 2:
60
+ 1. Create `application-print.css.scss`.
61
+ 2. Add to `application-print.css.scss`
62
+ ```
63
+ @import 'fullcalendar.print';
64
+ ```
65
+
66
+ 3. Add to `config/application.rb`
67
+ ```
60
68
  config.assets.precompile += ['application-print.css']
61
-
62
- and then in your layouts where you need it, add:
63
-
69
+ ```
70
+
71
+ 4. Add to `layouts`
72
+ ```ruby
64
73
  <%= stylesheet_link_tag "application-print", :media => "print" %>
74
+ ```
65
75
 
76
+ ## Using FullCalendar
77
+ A step by step tutorial for creating events for FullCalendar in rails may be followed here:
78
+ http://blog.crowdint.com/2014/02/18/fancy-calendars-for-your-web-application-with-fullcalendar.html
66
79
 
67
- While its not needed for this library, the original javascript library's author also includes an adapter for integrating google calendars as an event source. For pre-2.1.1-versions of this gem, use:
68
-
69
- //= require gcal
70
-
71
- Since version 2.1.1 of this gem, gcal and all other related javascript files (lang-all.js and individual language files) have been relocated to a subdirectory as requested in issue #24:
72
-
73
- //= require fullcalendar/gcal
74
-
75
- ## Usage
76
-
77
- See the website of the original project for the usage, or my original Rails 3 example I point to above. This gem just saves you from having to locate and copy the javascript and stylesheet into place.
78
-
79
- http://arshaw.com/fullcalendar/
80
+ And general documentation for FullCalendar may be found here:
81
+ http://fullcalendar.io/docs/
80
82
 
81
83
  ## Motivations
82
84
 
@@ -1,5 +1,5 @@
1
1
  module Fullcalendar
2
2
  module Rails
3
- VERSION = "2.8.0.0"
3
+ VERSION = "3.0.0.0"
4
4
  end
5
5
  end
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * FullCalendar v2.8.0
2
+ * FullCalendar v3.0.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: "2.8.0",
23
- internalApiVersion: 4
22
+ version: "3.0.0",
23
+ internalApiVersion: 6
24
24
  };
25
25
  var fcViews = FC.views = {};
26
26
 
@@ -71,56 +71,6 @@ function mergeOptions(optionObjs) {
71
71
  return mergeProps(optionObjs, complexOptions);
72
72
  }
73
73
 
74
-
75
- // Given options specified for the calendar's constructor, massages any legacy options into a non-legacy form.
76
- // Converts View-Option-Hashes into the View-Specific-Options format.
77
- function massageOverrides(input) {
78
- var overrides = { views: input.views || {} }; // the output. ensure a `views` hash
79
- var subObj;
80
-
81
- // iterate through all option override properties (except `views`)
82
- $.each(input, function(name, val) {
83
- if (name != 'views') {
84
-
85
- // could the value be a legacy View-Option-Hash?
86
- if (
87
- $.isPlainObject(val) &&
88
- !/(time|duration|interval)$/i.test(name) && // exclude duration options. might be given as objects
89
- $.inArray(name, complexOptions) == -1 // complex options aren't allowed to be View-Option-Hashes
90
- ) {
91
- subObj = null;
92
-
93
- // iterate through the properties of this possible View-Option-Hash value
94
- $.each(val, function(subName, subVal) {
95
-
96
- // is the property targeting a view?
97
- if (/^(month|week|day|default|basic(Week|Day)?|agenda(Week|Day)?)$/.test(subName)) {
98
- if (!overrides.views[subName]) { // ensure the view-target entry exists
99
- overrides.views[subName] = {};
100
- }
101
- overrides.views[subName][name] = subVal; // record the value in the `views` object
102
- }
103
- else { // a non-View-Option-Hash property
104
- if (!subObj) {
105
- subObj = {};
106
- }
107
- subObj[subName] = subVal; // accumulate these unrelated values for later
108
- }
109
- });
110
-
111
- if (subObj) { // non-View-Option-Hash properties? transfer them as-is
112
- overrides[name] = subObj;
113
- }
114
- }
115
- else {
116
- overrides[name] = val; // transfer normal options as-is
117
- }
118
- }
119
- });
120
-
121
- return overrides;
122
- }
123
-
124
74
  ;;
125
75
 
126
76
  // exports
@@ -247,7 +197,7 @@ function undistributeHeight(els) {
247
197
  function matchCellWidths(els) {
248
198
  var maxInnerWidth = 0;
249
199
 
250
- els.find('> span').each(function(i, innerEl) {
200
+ els.find('> *').each(function(i, innerEl) {
251
201
  var innerWidth = $(innerEl).outerWidth();
252
202
  if (innerWidth > maxInnerWidth) {
253
203
  maxInnerWidth = innerWidth;
@@ -628,7 +578,8 @@ function flexibleCompare(a, b) {
628
578
  ----------------------------------------------------------------------------------------------------------------------*/
629
579
 
630
580
 
631
- // Computes the intersection of the two ranges. Returns undefined if no intersection.
581
+ // Computes the intersection of the two ranges. Will return fresh date clones in a range.
582
+ // Returns undefined if no intersection.
632
583
  // Expects all dates to be normalized to the same timezone beforehand.
633
584
  // TODO: move to date section?
634
585
  function intersectRanges(subjectRange, constraintRange) {
@@ -908,22 +859,6 @@ function copyOwnProps(src, dest) {
908
859
  }
909
860
 
910
861
 
911
- // Copies over certain methods with the same names as Object.prototype methods. Overcomes an IE<=8 bug:
912
- // https://developer.mozilla.org/en-US/docs/ECMAScript_DontEnum_attribute#JScript_DontEnum_Bug
913
- function copyNativeMethods(src, dest) {
914
- var names = [ 'constructor', 'toString', 'valueOf' ];
915
- var i, name;
916
-
917
- for (i = 0; i < names.length; i++) {
918
- name = names[i];
919
-
920
- if (src[name] !== Object.prototype[name]) {
921
- dest[name] = src[name];
922
- }
923
- }
924
- }
925
-
926
-
927
862
  function hasOwnProp(obj, name) {
928
863
  return hasOwnPropMethod.call(obj, name);
929
864
  }
@@ -989,6 +924,21 @@ function cssToStr(cssProps) {
989
924
  }
990
925
 
991
926
 
927
+ // Given an object hash of HTML attribute names to values,
928
+ // generates a string that can be injected between < > in HTML
929
+ function attrsToStr(attrs) {
930
+ var parts = [];
931
+
932
+ $.each(attrs, function(name, val) {
933
+ if (val != null) {
934
+ parts.push(name + '="' + htmlEscape(val) + '"');
935
+ }
936
+ });
937
+
938
+ return parts.join(' ');
939
+ }
940
+
941
+
992
942
  function capitaliseFirstLetter(str) {
993
943
  return str.charAt(0).toUpperCase() + str.slice(1);
994
944
  }
@@ -1070,14 +1020,24 @@ function syncThen(promise, thenFunc) {
1070
1020
 
1071
1021
  ;;
1072
1022
 
1023
+ /*
1024
+ GENERAL NOTE on moments throughout the *entire rest* of the codebase:
1025
+ All moments are assumed to be ambiguously-zoned unless otherwise noted,
1026
+ with the NOTABLE EXCEOPTION of start/end dates that live on *Event Objects*.
1027
+ Ambiguously-TIMED moments are assumed to be ambiguously-zoned by nature.
1028
+ */
1029
+
1073
1030
  var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/;
1074
1031
  var ambigTimeOrZoneRegex =
1075
1032
  /^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/;
1076
1033
  var newMomentProto = moment.fn; // where we will attach our new methods
1077
1034
  var oldMomentProto = $.extend({}, newMomentProto); // copy of original moment methods
1078
- var allowValueOptimization;
1079
- var setUTCValues; // function defined below
1080
- var setLocalValues; // function defined below
1035
+
1036
+ // tell momentjs to transfer these properties upon clone
1037
+ var momentProperties = moment.momentProperties;
1038
+ momentProperties.push('_fullCalendar');
1039
+ momentProperties.push('_ambigTime');
1040
+ momentProperties.push('_ambigZone');
1081
1041
 
1082
1042
 
1083
1043
  // Creating
@@ -1123,12 +1083,8 @@ function makeMoment(args, parseAsUTC, parseZone) {
1123
1083
  var ambigMatch;
1124
1084
  var mom;
1125
1085
 
1126
- if (moment.isMoment(input)) {
1127
- mom = moment.apply(null, args); // clone it
1128
- transferAmbigs(input, mom); // the ambig flags weren't transfered with the clone
1129
- }
1130
- else if (isNativeDate(input) || input === undefined) {
1131
- mom = moment.apply(null, args); // will be local
1086
+ if (moment.isMoment(input) || isNativeDate(input) || input === undefined) {
1087
+ mom = moment.apply(null, args);
1132
1088
  }
1133
1089
  else { // "parsing" is required
1134
1090
  isAmbigTime = false;
@@ -1169,12 +1125,7 @@ function makeMoment(args, parseAsUTC, parseZone) {
1169
1125
  mom._ambigZone = true;
1170
1126
  }
1171
1127
  else if (isSingleString) {
1172
- if (mom.utcOffset) {
1173
- mom.utcOffset(input); // if not a valid zone, will assign UTC
1174
- }
1175
- else {
1176
- mom.zone(input); // for moment-pre-2.9
1177
- }
1128
+ mom.utcOffset(input); // if not a valid zone, will assign UTC
1178
1129
  }
1179
1130
  }
1180
1131
  }
@@ -1185,21 +1136,6 @@ function makeMoment(args, parseAsUTC, parseZone) {
1185
1136
  }
1186
1137
 
1187
1138
 
1188
- // A clone method that works with the flags related to our enhanced functionality.
1189
- // In the future, use moment.momentProperties
1190
- newMomentProto.clone = function() {
1191
- var mom = oldMomentProto.clone.apply(this, arguments);
1192
-
1193
- // these flags weren't transfered with the clone
1194
- transferAmbigs(this, mom);
1195
- if (this._fullCalendar) {
1196
- mom._fullCalendar = true;
1197
- }
1198
-
1199
- return mom;
1200
- };
1201
-
1202
-
1203
1139
  // Week Number
1204
1140
  // -------------------------------------------------------------------------------------------------
1205
1141
 
@@ -1207,8 +1143,7 @@ newMomentProto.clone = function() {
1207
1143
  // Returns the week number, considering the locale's custom week number calcuation
1208
1144
  // `weeks` is an alias for `week`
1209
1145
  newMomentProto.week = newMomentProto.weeks = function(input) {
1210
- var weekCalc = (this._locale || this._lang) // works pre-moment-2.8
1211
- ._fullCalendar_weekCalc;
1146
+ var weekCalc = this._locale._fullCalendar_weekCalc;
1212
1147
 
1213
1148
  if (input == null && typeof weekCalc === 'function') { // custom function only works for getter
1214
1149
  return weekCalc(this);
@@ -1275,19 +1210,21 @@ newMomentProto.time = function(time) {
1275
1210
  // but preserving its YMD. A moment with a stripped time will display no time
1276
1211
  // nor timezone offset when .format() is called.
1277
1212
  newMomentProto.stripTime = function() {
1278
- var a;
1279
1213
 
1280
1214
  if (!this._ambigTime) {
1281
1215
 
1282
- // get the values before any conversion happens
1283
- a = this.toArray(); // array of y/m/d/h/m/s/ms
1216
+ this.utc(true); // keepLocalTime=true (for keeping *date* value)
1284
1217
 
1285
- // TODO: use keepLocalTime in the future
1286
- this.utc(); // set the internal UTC flag (will clear the ambig flags)
1287
- setUTCValues(this, a.slice(0, 3)); // set the year/month/date. time will be zero
1218
+ // set time to zero
1219
+ this.set({
1220
+ hours: 0,
1221
+ minutes: 0,
1222
+ seconds: 0,
1223
+ ms: 0
1224
+ });
1288
1225
 
1289
1226
  // Mark the time as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(),
1290
- // which clears all ambig flags. Same with setUTCValues with moment-timezone.
1227
+ // which clears all ambig flags.
1291
1228
  this._ambigTime = true;
1292
1229
  this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset
1293
1230
  }
@@ -1307,24 +1244,20 @@ newMomentProto.hasTime = function() {
1307
1244
  // Converts the moment to UTC, stripping out its timezone offset, but preserving its
1308
1245
  // YMD and time-of-day. A moment with a stripped timezone offset will display no
1309
1246
  // timezone offset when .format() is called.
1310
- // TODO: look into Moment's keepLocalTime functionality
1311
1247
  newMomentProto.stripZone = function() {
1312
- var a, wasAmbigTime;
1248
+ var wasAmbigTime;
1313
1249
 
1314
1250
  if (!this._ambigZone) {
1315
1251
 
1316
- // get the values before any conversion happens
1317
- a = this.toArray(); // array of y/m/d/h/m/s/ms
1318
1252
  wasAmbigTime = this._ambigTime;
1319
1253
 
1320
- this.utc(); // set the internal UTC flag (might clear the ambig flags, depending on Moment internals)
1321
- setUTCValues(this, a); // will set the year/month/date/hours/minutes/seconds/ms
1254
+ this.utc(true); // keepLocalTime=true (for keeping date and time values)
1322
1255
 
1323
1256
  // the above call to .utc()/.utcOffset() unfortunately might clear the ambig flags, so restore
1324
1257
  this._ambigTime = wasAmbigTime || false;
1325
1258
 
1326
1259
  // Mark the zone as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(),
1327
- // which clears the ambig flags. Same with setUTCValues with moment-timezone.
1260
+ // which clears the ambig flags.
1328
1261
  this._ambigZone = true;
1329
1262
  }
1330
1263
 
@@ -1337,32 +1270,26 @@ newMomentProto.hasZone = function() {
1337
1270
  };
1338
1271
 
1339
1272
 
1340
- // this method implicitly marks a zone
1341
- newMomentProto.local = function() {
1342
- var a = this.toArray(); // year,month,date,hours,minutes,seconds,ms as an array
1343
- var wasAmbigZone = this._ambigZone;
1273
+ // implicitly marks a zone
1274
+ newMomentProto.local = function(keepLocalTime) {
1344
1275
 
1345
- oldMomentProto.local.apply(this, arguments);
1276
+ // for when converting from ambiguously-zoned to local,
1277
+ // keep the time values when converting from UTC -> local
1278
+ oldMomentProto.local.call(this, this._ambigZone || keepLocalTime);
1346
1279
 
1347
1280
  // ensure non-ambiguous
1348
1281
  // this probably already happened via local() -> utcOffset(), but don't rely on Moment's internals
1349
1282
  this._ambigTime = false;
1350
1283
  this._ambigZone = false;
1351
1284
 
1352
- if (wasAmbigZone) {
1353
- // If the moment was ambiguously zoned, the date fields were stored as UTC.
1354
- // We want to preserve these, but in local time.
1355
- // TODO: look into Moment's keepLocalTime functionality
1356
- setLocalValues(this, a);
1357
- }
1358
-
1359
1285
  return this; // for chaining
1360
1286
  };
1361
1287
 
1362
1288
 
1363
1289
  // implicitly marks a zone
1364
- newMomentProto.utc = function() {
1365
- oldMomentProto.utc.apply(this, arguments);
1290
+ newMomentProto.utc = function(keepLocalTime) {
1291
+
1292
+ oldMomentProto.utc.call(this, keepLocalTime);
1366
1293
 
1367
1294
  // ensure non-ambiguous
1368
1295
  // this probably already happened via utc() -> utcOffset(), but don't rely on Moment's internals
@@ -1373,28 +1300,18 @@ newMomentProto.utc = function() {
1373
1300
  };
1374
1301
 
1375
1302
 
1376
- // methods for arbitrarily manipulating timezone offset.
1377
- // should clear time/zone ambiguity when called.
1378
- $.each([
1379
- 'zone', // only in moment-pre-2.9. deprecated afterwards
1380
- 'utcOffset'
1381
- ], function(i, name) {
1382
- if (oldMomentProto[name]) { // original method exists?
1383
-
1384
- // this method implicitly marks a zone (will probably get called upon .utc() and .local())
1385
- newMomentProto[name] = function(tzo) {
1303
+ // implicitly marks a zone (will probably get called upon .utc() and .local())
1304
+ newMomentProto.utcOffset = function(tzo) {
1386
1305
 
1387
- if (tzo != null) { // setter
1388
- // these assignments needs to happen before the original zone method is called.
1389
- // I forget why, something to do with a browser crash.
1390
- this._ambigTime = false;
1391
- this._ambigZone = false;
1392
- }
1393
-
1394
- return oldMomentProto[name].apply(this, arguments);
1395
- };
1306
+ if (tzo != null) { // setter
1307
+ // these assignments needs to happen before the original zone method is called.
1308
+ // I forget why, something to do with a browser crash.
1309
+ this._ambigTime = false;
1310
+ this._ambigZone = false;
1396
1311
  }
1397
- });
1312
+
1313
+ return oldMomentProto.utcOffset.apply(this, arguments);
1314
+ };
1398
1315
 
1399
1316
 
1400
1317
  // Formatting
@@ -1423,156 +1340,6 @@ newMomentProto.toISOString = function() {
1423
1340
  return oldMomentProto.toISOString.apply(this, arguments);
1424
1341
  };
1425
1342
 
1426
-
1427
- // Querying
1428
- // -------------------------------------------------------------------------------------------------
1429
-
1430
- // Is the moment within the specified range? `end` is exclusive.
1431
- // FYI, this method is not a standard Moment method, so always do our enhanced logic.
1432
- newMomentProto.isWithin = function(start, end) {
1433
- var a = commonlyAmbiguate([ this, start, end ]);
1434
- return a[0] >= a[1] && a[0] < a[2];
1435
- };
1436
-
1437
- // When isSame is called with units, timezone ambiguity is normalized before the comparison happens.
1438
- // If no units specified, the two moments must be identically the same, with matching ambig flags.
1439
- newMomentProto.isSame = function(input, units) {
1440
- var a;
1441
-
1442
- // only do custom logic if this is an enhanced moment
1443
- if (!this._fullCalendar) {
1444
- return oldMomentProto.isSame.apply(this, arguments);
1445
- }
1446
-
1447
- if (units) {
1448
- a = commonlyAmbiguate([ this, input ], true); // normalize timezones but don't erase times
1449
- return oldMomentProto.isSame.call(a[0], a[1], units);
1450
- }
1451
- else {
1452
- input = FC.moment.parseZone(input); // normalize input
1453
- return oldMomentProto.isSame.call(this, input) &&
1454
- Boolean(this._ambigTime) === Boolean(input._ambigTime) &&
1455
- Boolean(this._ambigZone) === Boolean(input._ambigZone);
1456
- }
1457
- };
1458
-
1459
- // Make these query methods work with ambiguous moments
1460
- $.each([
1461
- 'isBefore',
1462
- 'isAfter'
1463
- ], function(i, methodName) {
1464
- newMomentProto[methodName] = function(input, units) {
1465
- var a;
1466
-
1467
- // only do custom logic if this is an enhanced moment
1468
- if (!this._fullCalendar) {
1469
- return oldMomentProto[methodName].apply(this, arguments);
1470
- }
1471
-
1472
- a = commonlyAmbiguate([ this, input ]);
1473
- return oldMomentProto[methodName].call(a[0], a[1], units);
1474
- };
1475
- });
1476
-
1477
-
1478
- // Misc Internals
1479
- // -------------------------------------------------------------------------------------------------
1480
-
1481
- // given an array of moment-like inputs, return a parallel array w/ moments similarly ambiguated.
1482
- // for example, of one moment has ambig time, but not others, all moments will have their time stripped.
1483
- // set `preserveTime` to `true` to keep times, but only normalize zone ambiguity.
1484
- // returns the original moments if no modifications are necessary.
1485
- function commonlyAmbiguate(inputs, preserveTime) {
1486
- var anyAmbigTime = false;
1487
- var anyAmbigZone = false;
1488
- var len = inputs.length;
1489
- var moms = [];
1490
- var i, mom;
1491
-
1492
- // parse inputs into real moments and query their ambig flags
1493
- for (i = 0; i < len; i++) {
1494
- mom = inputs[i];
1495
- if (!moment.isMoment(mom)) {
1496
- mom = FC.moment.parseZone(mom);
1497
- }
1498
- anyAmbigTime = anyAmbigTime || mom._ambigTime;
1499
- anyAmbigZone = anyAmbigZone || mom._ambigZone;
1500
- moms.push(mom);
1501
- }
1502
-
1503
- // strip each moment down to lowest common ambiguity
1504
- // use clones to avoid modifying the original moments
1505
- for (i = 0; i < len; i++) {
1506
- mom = moms[i];
1507
- if (!preserveTime && anyAmbigTime && !mom._ambigTime) {
1508
- moms[i] = mom.clone().stripTime();
1509
- }
1510
- else if (anyAmbigZone && !mom._ambigZone) {
1511
- moms[i] = mom.clone().stripZone();
1512
- }
1513
- }
1514
-
1515
- return moms;
1516
- }
1517
-
1518
- // Transfers all the flags related to ambiguous time/zone from the `src` moment to the `dest` moment
1519
- // TODO: look into moment.momentProperties for this.
1520
- function transferAmbigs(src, dest) {
1521
- if (src._ambigTime) {
1522
- dest._ambigTime = true;
1523
- }
1524
- else if (dest._ambigTime) {
1525
- dest._ambigTime = false;
1526
- }
1527
-
1528
- if (src._ambigZone) {
1529
- dest._ambigZone = true;
1530
- }
1531
- else if (dest._ambigZone) {
1532
- dest._ambigZone = false;
1533
- }
1534
- }
1535
-
1536
-
1537
- // Sets the year/month/date/etc values of the moment from the given array.
1538
- // Inefficient because it calls each individual setter.
1539
- function setMomentValues(mom, a) {
1540
- mom.year(a[0] || 0)
1541
- .month(a[1] || 0)
1542
- .date(a[2] || 0)
1543
- .hours(a[3] || 0)
1544
- .minutes(a[4] || 0)
1545
- .seconds(a[5] || 0)
1546
- .milliseconds(a[6] || 0);
1547
- }
1548
-
1549
- // Can we set the moment's internal date directly?
1550
- allowValueOptimization = '_d' in moment() && 'updateOffset' in moment;
1551
-
1552
- // Utility function. Accepts a moment and an array of the UTC year/month/date/etc values to set.
1553
- // Assumes the given moment is already in UTC mode.
1554
- setUTCValues = allowValueOptimization ? function(mom, a) {
1555
- // simlate what moment's accessors do
1556
- mom._d.setTime(Date.UTC.apply(Date, a));
1557
- moment.updateOffset(mom, false); // keepTime=false
1558
- } : setMomentValues;
1559
-
1560
- // Utility function. Accepts a moment and an array of the local year/month/date/etc values to set.
1561
- // Assumes the given moment is already in local mode.
1562
- setLocalValues = allowValueOptimization ? function(mom, a) {
1563
- // simlate what moment's accessors do
1564
- mom._d.setTime(+new Date( // FYI, there is now way to apply an array of args to a constructor
1565
- a[0] || 0,
1566
- a[1] || 0,
1567
- a[2] || 0,
1568
- a[3] || 0,
1569
- a[4] || 0,
1570
- a[5] || 0,
1571
- a[6] || 0
1572
- ));
1573
- moment.updateOffset(mom, false); // keepTime=false
1574
- } : setMomentValues;
1575
-
1576
1343
  ;;
1577
1344
 
1578
1345
  // Single Date Formatting
@@ -1653,7 +1420,7 @@ function formatRange(date1, date2, formatStr, separator, isRTL) {
1653
1420
  date1 = FC.moment.parseZone(date1);
1654
1421
  date2 = FC.moment.parseZone(date2);
1655
1422
 
1656
- localeData = (date1.localeData || date1.lang).call(date1); // works with moment-pre-2.8
1423
+ localeData = date1.localeData();
1657
1424
 
1658
1425
  // Expand localized format strings, like "LL" -> "MMMM D YYYY"
1659
1426
  formatStr = localeData.longDateFormat(formatStr) || formatStr;
@@ -1858,7 +1625,6 @@ function extendClass(superClass, members) {
1858
1625
 
1859
1626
  // copy each member variable/method onto the the subclass's prototype
1860
1627
  copyOwnProps(members, subClass.prototype);
1861
- copyNativeMethods(members, subClass.prototype); // hack for IE8
1862
1628
 
1863
1629
  // copy over all class variables/methods to the subclass, such as `extend` and `mixin`
1864
1630
  copyOwnProps(superClass, subClass);
@@ -1868,7 +1634,7 @@ function extendClass(superClass, members) {
1868
1634
 
1869
1635
 
1870
1636
  function mixIntoClass(theClass, members) {
1871
- copyOwnProps(members, theClass.prototype); // TODO: copyNativeMethods?
1637
+ copyOwnProps(members, theClass.prototype);
1872
1638
  }
1873
1639
  ;;
1874
1640
 
@@ -2263,17 +2029,6 @@ var CoordCache = FC.CoordCache = Class.extend({
2263
2029
  },
2264
2030
 
2265
2031
 
2266
- // Compute and return what the elements' bounding rectangle is, from the user's perspective.
2267
- // Right now, only returns a rectangle if constrained by an overflow:scroll element.
2268
- queryBoundingRect: function() {
2269
- var scrollParentEl = getScrollParent(this.els.eq(0));
2270
-
2271
- if (!scrollParentEl.is(document)) {
2272
- return getClientRect(scrollParentEl);
2273
- }
2274
- },
2275
-
2276
-
2277
2032
  // Populates the left/right internal coordinate arrays
2278
2033
  buildElHorizontals: function() {
2279
2034
  var lefts = [];
@@ -2313,42 +2068,36 @@ var CoordCache = FC.CoordCache = Class.extend({
2313
2068
 
2314
2069
 
2315
2070
  // Given a left offset (from document left), returns the index of the el that it horizontally intersects.
2316
- // If no intersection is made, or outside of the boundingRect, returns undefined.
2071
+ // If no intersection is made, returns undefined.
2317
2072
  getHorizontalIndex: function(leftOffset) {
2318
2073
  this.ensureBuilt();
2319
2074
 
2320
- var boundingRect = this.boundingRect;
2321
2075
  var lefts = this.lefts;
2322
2076
  var rights = this.rights;
2323
2077
  var len = lefts.length;
2324
2078
  var i;
2325
2079
 
2326
- if (!boundingRect || (leftOffset >= boundingRect.left && leftOffset < boundingRect.right)) {
2327
- for (i = 0; i < len; i++) {
2328
- if (leftOffset >= lefts[i] && leftOffset < rights[i]) {
2329
- return i;
2330
- }
2080
+ for (i = 0; i < len; i++) {
2081
+ if (leftOffset >= lefts[i] && leftOffset < rights[i]) {
2082
+ return i;
2331
2083
  }
2332
2084
  }
2333
2085
  },
2334
2086
 
2335
2087
 
2336
2088
  // Given a top offset (from document top), returns the index of the el that it vertically intersects.
2337
- // If no intersection is made, or outside of the boundingRect, returns undefined.
2089
+ // If no intersection is made, returns undefined.
2338
2090
  getVerticalIndex: function(topOffset) {
2339
2091
  this.ensureBuilt();
2340
2092
 
2341
- var boundingRect = this.boundingRect;
2342
2093
  var tops = this.tops;
2343
2094
  var bottoms = this.bottoms;
2344
2095
  var len = tops.length;
2345
2096
  var i;
2346
2097
 
2347
- if (!boundingRect || (topOffset >= boundingRect.top && topOffset < boundingRect.bottom)) {
2348
- for (i = 0; i < len; i++) {
2349
- if (topOffset >= tops[i] && topOffset < bottoms[i]) {
2350
- return i;
2351
- }
2098
+ for (i = 0; i < len; i++) {
2099
+ if (topOffset >= tops[i] && topOffset < bottoms[i]) {
2100
+ return i;
2352
2101
  }
2353
2102
  }
2354
2103
  },
@@ -2424,6 +2173,32 @@ var CoordCache = FC.CoordCache = Class.extend({
2424
2173
  getHeight: function(topIndex) {
2425
2174
  this.ensureBuilt();
2426
2175
  return this.bottoms[topIndex] - this.tops[topIndex];
2176
+ },
2177
+
2178
+
2179
+ // Bounding Rect
2180
+ // TODO: decouple this from CoordCache
2181
+
2182
+ // Compute and return what the elements' bounding rectangle is, from the user's perspective.
2183
+ // Right now, only returns a rectangle if constrained by an overflow:scroll element.
2184
+ queryBoundingRect: function() {
2185
+ var scrollParentEl = getScrollParent(this.els.eq(0));
2186
+
2187
+ if (!scrollParentEl.is(document)) {
2188
+ return getClientRect(scrollParentEl);
2189
+ }
2190
+ },
2191
+
2192
+ isPointInBounds: function(leftOffset, topOffset) {
2193
+ return this.isLeftInBounds(leftOffset) && this.isTopInBounds(topOffset);
2194
+ },
2195
+
2196
+ isLeftInBounds: function(leftOffset) {
2197
+ return !this.boundingRect || (leftOffset >= this.boundingRect.left && leftOffset < this.boundingRect.right);
2198
+ },
2199
+
2200
+ isTopInBounds: function(topOffset) {
2201
+ return !this.boundingRect || (topOffset >= this.boundingRect.top && topOffset < this.boundingRect.bottom);
2427
2202
  }
2428
2203
 
2429
2204
  });
@@ -2437,10 +2212,7 @@ var CoordCache = FC.CoordCache = Class.extend({
2437
2212
  var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMixin, {
2438
2213
 
2439
2214
  options: null,
2440
-
2441
- // for IE8 bug-fighting behavior
2442
2215
  subjectEl: null,
2443
- subjectHref: null,
2444
2216
 
2445
2217
  // coordinates of the initial mousedown
2446
2218
  originX: null,
@@ -2631,7 +2403,6 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMix
2631
2403
 
2632
2404
  handleDragStart: function(ev) {
2633
2405
  this.trigger('dragStart', ev);
2634
- this.initHrefHack();
2635
2406
  },
2636
2407
 
2637
2408
 
@@ -2671,7 +2442,6 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMix
2671
2442
 
2672
2443
  handleDragEnd: function(ev) {
2673
2444
  this.trigger('dragEnd', ev);
2674
- this.destroyHrefHack();
2675
2445
  },
2676
2446
 
2677
2447
 
@@ -2747,33 +2517,6 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMix
2747
2517
  },
2748
2518
 
2749
2519
 
2750
- // <A> HREF Hack
2751
- // -----------------------------------------------------------------------------------------------------------------
2752
-
2753
-
2754
- initHrefHack: function() {
2755
- var subjectEl = this.subjectEl;
2756
-
2757
- // remove a mousedown'd <a>'s href so it is not visited (IE8 bug)
2758
- if ((this.subjectHref = subjectEl ? subjectEl.attr('href') : null)) {
2759
- subjectEl.removeAttr('href');
2760
- }
2761
- },
2762
-
2763
-
2764
- destroyHrefHack: function() {
2765
- var subjectEl = this.subjectEl;
2766
- var subjectHref = this.subjectHref;
2767
-
2768
- // restore a mousedown'd <a>'s href (for IE8 bug)
2769
- setTimeout(function() { // must be outside of the click's execution
2770
- if (subjectHref) {
2771
- subjectEl.attr('href', subjectHref);
2772
- }
2773
- }, 0);
2774
- },
2775
-
2776
-
2777
2520
  // Utils
2778
2521
  // -----------------------------------------------------------------------------------------------------------------
2779
2522
 
@@ -3259,11 +3002,11 @@ var MouseFollower = Class.extend(ListenerMixin, {
3259
3002
  var _this = this;
3260
3003
  var revertDuration = this.options.revertDuration;
3261
3004
 
3262
- function complete() {
3263
- this.isAnimating = false;
3005
+ function complete() { // might be called by .animate(), which might change `this` context
3006
+ _this.isAnimating = false;
3264
3007
  _this.removeElement();
3265
3008
 
3266
- this.top0 = this.left0 = null; // reset state for future updatePosition calls
3009
+ _this.top0 = _this.left0 = null; // reset state for future updatePosition calls
3267
3010
 
3268
3011
  if (callback) {
3269
3012
  callback();
@@ -3297,7 +3040,6 @@ var MouseFollower = Class.extend(ListenerMixin, {
3297
3040
  var el = this.el;
3298
3041
 
3299
3042
  if (!el) {
3300
- this.sourceEl.width(); // hack to force IE8 to compute correct bounding box
3301
3043
  el = this.el = this.sourceEl.clone()
3302
3044
  .addClass(this.options.additionalClass || '')
3303
3045
  .css({
@@ -3342,7 +3084,6 @@ var MouseFollower = Class.extend(ListenerMixin, {
3342
3084
 
3343
3085
  // make sure origin info was computed
3344
3086
  if (this.top0 === null) {
3345
- this.sourceEl.width(); // hack to force IE8 to compute correct bounding box
3346
3087
  sourceOffset = this.sourceEl.offset();
3347
3088
  origin = this.el.offsetParent().offset();
3348
3089
  this.top0 = sourceOffset.top - origin.top;
@@ -3396,6 +3137,9 @@ var MouseFollower = Class.extend(ListenerMixin, {
3396
3137
 
3397
3138
  var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
3398
3139
 
3140
+ // self-config, overridable by subclasses
3141
+ hasDayInteractions: true, // can user click/select ranges of time?
3142
+
3399
3143
  view: null, // a View object
3400
3144
  isRTL: null, // shortcut to the view's isRTL option
3401
3145
 
@@ -3563,10 +3307,13 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
3563
3307
  // Does other DOM-related initializations.
3564
3308
  setElement: function(el) {
3565
3309
  this.el = el;
3566
- preventSelection(el);
3567
3310
 
3568
- this.bindDayHandler('touchstart', this.dayTouchStart);
3569
- this.bindDayHandler('mousedown', this.dayMousedown);
3311
+ if (this.hasDayInteractions) {
3312
+ preventSelection(el);
3313
+
3314
+ this.bindDayHandler('touchstart', this.dayTouchStart);
3315
+ this.bindDayHandler('mousedown', this.dayMousedown);
3316
+ }
3570
3317
 
3571
3318
  // attach event-element-related handlers. in Grid.events
3572
3319
  // same garbage collection note as above.
@@ -3583,8 +3330,12 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
3583
3330
  // jQuery will take care of unregistering them when removeElement gets called.
3584
3331
  this.el.on(name, function(ev) {
3585
3332
  if (
3586
- !$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link
3587
- !$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one)
3333
+ !$(ev.target).is(
3334
+ _this.segSelector + ',' + // directly on an event element
3335
+ _this.segSelector + ' *,' + // within an event element
3336
+ '.fc-more,' + // a "more.." link
3337
+ 'a[data-goto]' // a clickable nav link
3338
+ )
3588
3339
  ) {
3589
3340
  return handler.call(_this, ev);
3590
3341
  }
@@ -3683,6 +3434,7 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
3683
3434
  scroll: view.opt('dragScroll'),
3684
3435
  interactionStart: function() {
3685
3436
  dayClickHit = dragListener.origHit; // for dayClick, where no dragging happens
3437
+ selectionSpan = null;
3686
3438
  },
3687
3439
  dragStart: function() {
3688
3440
  view.unselect(); // since we could be rendering a new selection, we want to clear any old one
@@ -3709,10 +3461,12 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
3709
3461
  }
3710
3462
  }
3711
3463
  },
3712
- hitOut: function() {
3464
+ hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
3713
3465
  dayClickHit = null;
3714
3466
  selectionSpan = null;
3715
3467
  _this.unrenderSelection();
3468
+ },
3469
+ hitDone: function() { // called after a hitOut OR before a dragEnd
3716
3470
  enableCursor();
3717
3471
  },
3718
3472
  interactionEnd: function(ev, isCancelled) {
@@ -3731,7 +3485,6 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
3731
3485
  // the selection will already have been rendered. just report it
3732
3486
  view.reportSelection(selectionSpan, ev);
3733
3487
  }
3734
- enableCursor();
3735
3488
  }
3736
3489
  }
3737
3490
  });
@@ -4034,6 +3787,9 @@ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
4034
3787
 
4035
3788
  Grid.mixin({
4036
3789
 
3790
+ // self-config, overridable by subclasses
3791
+ segSelector: '.fc-event-container > *', // what constitutes an event element?
3792
+
4037
3793
  mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing
4038
3794
  isDraggingSeg: false, // is a segment being dragged? boolean
4039
3795
  isResizingSeg: false, // is a segment being resized? boolean
@@ -4172,7 +3928,7 @@ Grid.mixin({
4172
3928
 
4173
3929
 
4174
3930
  // Generates an array of classNames to be used for the default rendering of a background event.
4175
- // Called by the fill system.
3931
+ // Called by fillSegHtml.
4176
3932
  bgEventSegClasses: function(seg) {
4177
3933
  var event = seg.event;
4178
3934
  var source = event.source || {};
@@ -4185,7 +3941,7 @@ Grid.mixin({
4185
3941
 
4186
3942
 
4187
3943
  // Generates a semicolon-separated CSS string to be used for the default rendering of a background event.
4188
- // Called by the fill system.
3944
+ // Called by fillSegHtml.
4189
3945
  bgEventSegCss: function(seg) {
4190
3946
  return {
4191
3947
  'background-color': this.getSegSkinCss(seg)['background-color']
@@ -4194,32 +3950,68 @@ Grid.mixin({
4194
3950
 
4195
3951
 
4196
3952
  // Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system.
3953
+ // Called by fillSegHtml.
4197
3954
  businessHoursSegClasses: function(seg) {
4198
3955
  return [ 'fc-nonbusiness', 'fc-bgevent' ];
4199
3956
  },
4200
3957
 
4201
3958
 
3959
+ /* Business Hours
3960
+ ------------------------------------------------------------------------------------------------------------------*/
3961
+
3962
+
3963
+ // Compute business hour segs for the grid's current date range.
3964
+ // Caller must ask if whole-day business hours are needed.
3965
+ buildBusinessHourSegs: function(wholeDay) {
3966
+ var events = this.view.calendar.getCurrentBusinessHourEvents(wholeDay);
3967
+
3968
+ // HACK. Eventually refactor business hours "events" system.
3969
+ // If no events are given, but businessHours is activated, this means the entire visible range should be
3970
+ // marked as *not* business-hours, via inverse-background rendering.
3971
+ if (
3972
+ !events.length &&
3973
+ this.view.calendar.options.businessHours // don't access view option. doesn't update with dynamic options
3974
+ ) {
3975
+ events = [
3976
+ $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, {
3977
+ start: this.view.end, // guaranteed out-of-range
3978
+ end: this.view.end, // "
3979
+ dow: null
3980
+ })
3981
+ ];
3982
+ }
3983
+
3984
+ return this.eventsToSegs(events);
3985
+ },
3986
+
3987
+
4202
3988
  /* Handlers
4203
3989
  ------------------------------------------------------------------------------------------------------------------*/
4204
3990
 
4205
3991
 
4206
- // Attaches event-element-related handlers to the container element and leverage bubbling
3992
+ // Attaches event-element-related handlers for *all* rendered event segments of the view.
4207
3993
  bindSegHandlers: function() {
4208
- this.bindSegHandler('touchstart', this.handleSegTouchStart);
4209
- this.bindSegHandler('touchend', this.handleSegTouchEnd);
4210
- this.bindSegHandler('mouseenter', this.handleSegMouseover);
4211
- this.bindSegHandler('mouseleave', this.handleSegMouseout);
4212
- this.bindSegHandler('mousedown', this.handleSegMousedown);
4213
- this.bindSegHandler('click', this.handleSegClick);
3994
+ this.bindSegHandlersToEl(this.el);
3995
+ },
3996
+
3997
+
3998
+ // Attaches event-element-related handlers to an arbitrary container element. leverages bubbling.
3999
+ bindSegHandlersToEl: function(el) {
4000
+ this.bindSegHandlerToEl(el, 'touchstart', this.handleSegTouchStart);
4001
+ this.bindSegHandlerToEl(el, 'touchend', this.handleSegTouchEnd);
4002
+ this.bindSegHandlerToEl(el, 'mouseenter', this.handleSegMouseover);
4003
+ this.bindSegHandlerToEl(el, 'mouseleave', this.handleSegMouseout);
4004
+ this.bindSegHandlerToEl(el, 'mousedown', this.handleSegMousedown);
4005
+ this.bindSegHandlerToEl(el, 'click', this.handleSegClick);
4214
4006
  },
4215
4007
 
4216
4008
 
4217
4009
  // Executes a handler for any a user-interaction on a segment.
4218
4010
  // Handler gets called with (seg, ev), and with the `this` context of the Grid
4219
- bindSegHandler: function(name, handler) {
4011
+ bindSegHandlerToEl: function(el, name, handler) {
4220
4012
  var _this = this;
4221
4013
 
4222
- this.el.on(name, '.fc-event-container > *', function(ev) {
4014
+ el.on(name, this.segSelector, function(ev) {
4223
4015
  var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents
4224
4016
 
4225
4017
  // only call the handlers if there is not a drag/resize in progress
@@ -4231,7 +4023,10 @@ Grid.mixin({
4231
4023
 
4232
4024
 
4233
4025
  handleSegClick: function(seg, ev) {
4234
- return this.view.trigger('eventClick', seg.el[0], seg.event, ev); // can return `false` to cancel
4026
+ var res = this.view.trigger('eventClick', seg.el[0], seg.event, ev); // can return `false` to cancel
4027
+ if (res === false) {
4028
+ ev.preventDefault();
4029
+ }
4235
4030
  },
4236
4031
 
4237
4032
 
@@ -4242,7 +4037,9 @@ Grid.mixin({
4242
4037
  !this.mousedOverSeg
4243
4038
  ) {
4244
4039
  this.mousedOverSeg = seg;
4245
- seg.el.addClass('fc-allow-mouse-resize');
4040
+ if (this.view.isEventResizable(seg.event)) {
4041
+ seg.el.addClass('fc-allow-mouse-resize');
4042
+ }
4246
4043
  this.view.trigger('eventMouseover', seg.el[0], seg.event, ev);
4247
4044
  }
4248
4045
  },
@@ -4256,7 +4053,9 @@ Grid.mixin({
4256
4053
  if (this.mousedOverSeg) {
4257
4054
  seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment
4258
4055
  this.mousedOverSeg = null;
4259
- seg.el.removeClass('fc-allow-mouse-resize');
4056
+ if (this.view.isEventResizable(seg.event)) {
4057
+ seg.el.removeClass('fc-allow-mouse-resize');
4058
+ }
4260
4059
  this.view.trigger('eventMouseout', seg.el[0], seg.event, ev);
4261
4060
  }
4262
4061
  },
@@ -4353,6 +4152,7 @@ Grid.mixin({
4353
4152
  subjectEl: el,
4354
4153
  subjectCenter: true,
4355
4154
  interactionStart: function(ev) {
4155
+ seg.component = _this; // for renderDrag
4356
4156
  isDragging = false;
4357
4157
  mouseFollower = new MouseFollower(seg.el, {
4358
4158
  additionalClass: 'fc-dragging',
@@ -4421,6 +4221,8 @@ Grid.mixin({
4421
4221
  enableCursor();
4422
4222
  },
4423
4223
  interactionEnd: function(ev) {
4224
+ delete seg.component; // prevent side effects
4225
+
4424
4226
  // do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
4425
4227
  mouseFollower.stop(!dropLocation, function() {
4426
4228
  if (isDragging) {
@@ -4508,11 +4310,7 @@ Grid.mixin({
4508
4310
  }
4509
4311
  // othewise, work off existing values
4510
4312
  else {
4511
- dropLocation = {
4512
- start: event.start.clone(),
4513
- end: event.end ? event.end.clone() : null,
4514
- allDay: event.allDay // keep it the same
4515
- };
4313
+ dropLocation = pluckEventDateProps(event);
4516
4314
  }
4517
4315
 
4518
4316
  dropLocation.start.add(delta);
@@ -4538,11 +4336,7 @@ Grid.mixin({
4538
4336
  var opacity = this.view.opt('dragOpacity');
4539
4337
 
4540
4338
  if (opacity != null) {
4541
- els.each(function(i, node) {
4542
- // Don't use jQuery (will set an IE filter), do it the old fashioned way.
4543
- // In IE8, a helper element will disappears if there's a filter.
4544
- node.style.opacity = opacity;
4545
- });
4339
+ els.css('opacity', opacity);
4546
4340
  }
4547
4341
  },
4548
4342
 
@@ -4708,8 +4502,11 @@ Grid.mixin({
4708
4502
  disableCursor();
4709
4503
  resizeLocation = null;
4710
4504
  }
4711
- // no change? (TODO: how does this work with timezones?)
4712
- else if (resizeLocation.start.isSame(event.start) && resizeLocation.end.isSame(eventEnd)) {
4505
+ // no change? (FYI, event dates might have zones)
4506
+ else if (
4507
+ resizeLocation.start.isSame(event.start.clone().stripZone()) &&
4508
+ resizeLocation.end.isSame(eventEnd.clone().stripZone())
4509
+ ) {
4713
4510
  resizeLocation = null;
4714
4511
  }
4715
4512
  }
@@ -4862,15 +4659,11 @@ Grid.mixin({
4862
4659
  // Generic utility for generating the HTML classNames for an event segment's element
4863
4660
  getSegClasses: function(seg, isDraggable, isResizable) {
4864
4661
  var view = this.view;
4865
- var event = seg.event;
4866
4662
  var classes = [
4867
4663
  'fc-event',
4868
4664
  seg.isStart ? 'fc-start' : 'fc-not-start',
4869
4665
  seg.isEnd ? 'fc-end' : 'fc-not-end'
4870
- ].concat(
4871
- event.className,
4872
- event.source ? event.source.className : []
4873
- );
4666
+ ].concat(this.getSegCustomClasses(seg));
4874
4667
 
4875
4668
  if (isDraggable) {
4876
4669
  classes.push('fc-draggable');
@@ -4880,7 +4673,7 @@ Grid.mixin({
4880
4673
  }
4881
4674
 
4882
4675
  // event is currently selected? attach a className.
4883
- if (view.isEventSelected(event)) {
4676
+ if (view.isEventSelected(seg.event)) {
4884
4677
  classes.push('fc-selected');
4885
4678
  }
4886
4679
 
@@ -4888,38 +4681,78 @@ Grid.mixin({
4888
4681
  },
4889
4682
 
4890
4683
 
4891
- // Utility for generating event skin-related CSS properties
4892
- getSegSkinCss: function(seg) {
4684
+ // List of classes that were defined by the caller of the API in some way
4685
+ getSegCustomClasses: function(seg) {
4893
4686
  var event = seg.event;
4894
- var view = this.view;
4895
- var source = event.source || {};
4896
- var eventColor = event.color;
4897
- var sourceColor = source.color;
4898
- var optionColor = view.opt('eventColor');
4899
4687
 
4688
+ return [].concat(
4689
+ event.className, // guaranteed to be an array
4690
+ event.source ? event.source.className : []
4691
+ );
4692
+ },
4693
+
4694
+
4695
+ // Utility for generating event skin-related CSS properties
4696
+ getSegSkinCss: function(seg) {
4900
4697
  return {
4901
- 'background-color':
4902
- event.backgroundColor ||
4903
- eventColor ||
4904
- source.backgroundColor ||
4905
- sourceColor ||
4906
- view.opt('eventBackgroundColor') ||
4907
- optionColor,
4908
- 'border-color':
4909
- event.borderColor ||
4910
- eventColor ||
4911
- source.borderColor ||
4912
- sourceColor ||
4913
- view.opt('eventBorderColor') ||
4914
- optionColor,
4915
- color:
4916
- event.textColor ||
4917
- source.textColor ||
4918
- view.opt('eventTextColor')
4698
+ 'background-color': this.getSegBackgroundColor(seg),
4699
+ 'border-color': this.getSegBorderColor(seg),
4700
+ color: this.getSegTextColor(seg)
4919
4701
  };
4920
4702
  },
4921
4703
 
4922
4704
 
4705
+ // Queries for caller-specified color, then falls back to default
4706
+ getSegBackgroundColor: function(seg) {
4707
+ return seg.event.backgroundColor ||
4708
+ seg.event.color ||
4709
+ this.getSegDefaultBackgroundColor(seg);
4710
+ },
4711
+
4712
+
4713
+ getSegDefaultBackgroundColor: function(seg) {
4714
+ var source = seg.event.source || {};
4715
+
4716
+ return source.backgroundColor ||
4717
+ source.color ||
4718
+ this.view.opt('eventBackgroundColor') ||
4719
+ this.view.opt('eventColor');
4720
+ },
4721
+
4722
+
4723
+ // Queries for caller-specified color, then falls back to default
4724
+ getSegBorderColor: function(seg) {
4725
+ return seg.event.borderColor ||
4726
+ seg.event.color ||
4727
+ this.getSegDefaultBorderColor(seg);
4728
+ },
4729
+
4730
+
4731
+ getSegDefaultBorderColor: function(seg) {
4732
+ var source = seg.event.source || {};
4733
+
4734
+ return source.borderColor ||
4735
+ source.color ||
4736
+ this.view.opt('eventBorderColor') ||
4737
+ this.view.opt('eventColor');
4738
+ },
4739
+
4740
+
4741
+ // Queries for caller-specified color, then falls back to default
4742
+ getSegTextColor: function(seg) {
4743
+ return seg.event.textColor ||
4744
+ this.getSegDefaultTextColor(seg);
4745
+ },
4746
+
4747
+
4748
+ getSegDefaultTextColor: function(seg) {
4749
+ var source = seg.event.source || {};
4750
+
4751
+ return source.textColor ||
4752
+ this.view.opt('eventTextColor');
4753
+ },
4754
+
4755
+
4923
4756
  /* Converting events -> eventRange -> eventSpan -> eventSegs
4924
4757
  ------------------------------------------------------------------------------------------------------------------*/
4925
4758
 
@@ -4987,20 +4820,25 @@ Grid.mixin({
4987
4820
  // Generates the unzoned start/end dates an event appears to occupy
4988
4821
  // Can accept an event "location" as well (which only has start/end and no allDay)
4989
4822
  eventToRange: function(event) {
4990
- return {
4991
- start: event.start.clone().stripZone(),
4992
- end: (
4823
+ var calendar = this.view.calendar;
4824
+ var start = event.start.clone().stripZone();
4825
+ var end = (
4993
4826
  event.end ?
4994
4827
  event.end.clone() :
4995
4828
  // derive the end from the start and allDay. compute allDay if necessary
4996
- this.view.calendar.getDefaultEventEnd(
4829
+ calendar.getDefaultEventEnd(
4997
4830
  event.allDay != null ?
4998
4831
  event.allDay :
4999
4832
  !event.start.hasTime(),
5000
4833
  event.start
5001
4834
  )
5002
- ).stripZone()
5003
- };
4835
+ ).stripZone();
4836
+
4837
+ // hack: dynamic locale change forgets to upate stored event localed
4838
+ calendar.localizeMoment(start);
4839
+ calendar.localizeMoment(end);
4840
+
4841
+ return { start: start, end: end };
5004
4842
  },
5005
4843
 
5006
4844
 
@@ -5103,6 +4941,16 @@ Grid.mixin({
5103
4941
  ----------------------------------------------------------------------------------------------------------------------*/
5104
4942
 
5105
4943
 
4944
+ function pluckEventDateProps(event) {
4945
+ return {
4946
+ start: event.start.clone(),
4947
+ end: event.end ? event.end.clone() : null,
4948
+ allDay: event.allDay // keep it the same
4949
+ };
4950
+ }
4951
+ FC.pluckEventDateProps = pluckEventDateProps;
4952
+
4953
+
5106
4954
  function isBgEvent(event) { // returns true if background OR inverse-background
5107
4955
  var rendering = getEventRendering(event);
5108
4956
  return rendering === 'background' || rendering === 'inverse-background';
@@ -5493,7 +5341,7 @@ var DayTableMixin = FC.DayTableMixin = {
5493
5341
 
5494
5342
  return '' +
5495
5343
  '<th class="fc-day-header ' + view.widgetHeaderClass + ' fc-' + dayIDs[date.day()] + '"' +
5496
- (this.rowCnt == 1 ?
5344
+ (this.rowCnt === 1 ?
5497
5345
  ' data-date="' + date.format('YYYY-MM-DD') + '"' :
5498
5346
  '') +
5499
5347
  (colspan > 1 ?
@@ -5502,8 +5350,12 @@ var DayTableMixin = FC.DayTableMixin = {
5502
5350
  (otherAttrs ?
5503
5351
  ' ' + otherAttrs :
5504
5352
  '') +
5505
- '>' +
5506
- htmlEscape(date.format(this.colHeadFormat)) +
5353
+ '>' +
5354
+ // don't make a link if the heading could represent multiple days, or if there's only one day (forceOff)
5355
+ view.buildGotoAnchorHtml(
5356
+ { date: date, forceOff: this.rowCnt > 1 || this.colCnt === 1 },
5357
+ htmlEscape(date.format(this.colHeadFormat)) // inner HTML
5358
+ ) +
5507
5359
  '</th>';
5508
5360
  },
5509
5361
 
@@ -5656,13 +5508,16 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
5656
5508
 
5657
5509
 
5658
5510
  renderBusinessHours: function() {
5659
- var events = this.view.calendar.getBusinessHoursEvents(true); // wholeDay=true
5660
- var segs = this.eventsToSegs(events);
5661
-
5511
+ var segs = this.buildBusinessHourSegs(true); // wholeDay=true
5662
5512
  this.renderFill('businessHours', segs, 'bgevent');
5663
5513
  },
5664
5514
 
5665
5515
 
5516
+ unrenderBusinessHours: function() {
5517
+ this.unrenderFill('businessHours');
5518
+ },
5519
+
5520
+
5666
5521
  // Generates the HTML for a single row, which is a div that wraps a table.
5667
5522
  // `row` is the row number.
5668
5523
  renderDayRowHtml: function(row, isRigid) {
@@ -5729,23 +5584,57 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
5729
5584
  // Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton.
5730
5585
  // The number row will only exist if either day numbers or week numbers are turned on.
5731
5586
  renderNumberCellHtml: function(date) {
5587
+ var html = '';
5732
5588
  var classes;
5589
+ var weekCalcFirstDoW;
5733
5590
 
5734
- if (!this.view.dayNumbersVisible) { // if there are week numbers but not day numbers
5591
+ if (!this.view.dayNumbersVisible && !this.view.cellWeekNumbersVisible) {
5592
+ // no numbers in day cell (week number must be along the side)
5735
5593
  return '<td/>'; // will create an empty space above events :(
5736
5594
  }
5737
5595
 
5738
5596
  classes = this.getDayClasses(date);
5739
- classes.unshift('fc-day-number');
5740
-
5741
- return '' +
5742
- '<td class="' + classes.join(' ') + '" data-date="' + date.format() + '">' +
5743
- date.date() +
5744
- '</td>';
5745
- },
5597
+ classes.unshift('fc-day-top');
5746
5598
 
5747
-
5748
- /* Options
5599
+ if (this.view.cellWeekNumbersVisible) {
5600
+ // To determine the day of week number change under ISO, we cannot
5601
+ // rely on moment.js methods such as firstDayOfWeek() or weekday(),
5602
+ // because they rely on the locale's dow (possibly overridden by
5603
+ // our firstDay option), which may not be Monday. We cannot change
5604
+ // dow, because that would affect the calendar start day as well.
5605
+ if (date._locale._fullCalendar_weekCalc === 'ISO') {
5606
+ weekCalcFirstDoW = 1; // Monday by ISO 8601 definition
5607
+ }
5608
+ else {
5609
+ weekCalcFirstDoW = date._locale.firstDayOfWeek();
5610
+ }
5611
+ }
5612
+
5613
+ html += '<td class="' + classes.join(' ') + '" data-date="' + date.format() + '">';
5614
+
5615
+ if (this.view.cellWeekNumbersVisible && (date.day() == weekCalcFirstDoW)) {
5616
+ html += this.view.buildGotoAnchorHtml(
5617
+ { date: date, type: 'week' },
5618
+ { 'class': 'fc-week-number' },
5619
+ date.format('w') // inner HTML
5620
+ );
5621
+ }
5622
+
5623
+ if (this.view.dayNumbersVisible) {
5624
+ html += this.view.buildGotoAnchorHtml(
5625
+ date,
5626
+ { 'class': 'fc-day-number' },
5627
+ date.date() // inner HTML
5628
+ );
5629
+ }
5630
+
5631
+ html += '</td>';
5632
+
5633
+ return html;
5634
+ },
5635
+
5636
+
5637
+ /* Options
5749
5638
  ------------------------------------------------------------------------------------------------------------------*/
5750
5639
 
5751
5640
 
@@ -5809,11 +5698,13 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
5809
5698
 
5810
5699
 
5811
5700
  queryHit: function(leftOffset, topOffset) {
5812
- var col = this.colCoordCache.getHorizontalIndex(leftOffset);
5813
- var row = this.rowCoordCache.getVerticalIndex(topOffset);
5701
+ if (this.colCoordCache.isLeftInBounds(leftOffset) && this.rowCoordCache.isTopInBounds(topOffset)) {
5702
+ var col = this.colCoordCache.getHorizontalIndex(leftOffset);
5703
+ var row = this.rowCoordCache.getVerticalIndex(topOffset);
5814
5704
 
5815
- if (row != null && col != null) {
5816
- return this.getCellHit(row, col);
5705
+ if (row != null && col != null) {
5706
+ return this.getCellHit(row, col);
5707
+ }
5817
5708
  }
5818
5709
  },
5819
5710
 
@@ -5864,8 +5755,7 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
5864
5755
  this.renderHighlight(this.eventToSpan(eventLocation));
5865
5756
 
5866
5757
  // if a segment from the same calendar but another component is being dragged, render a helper event
5867
- if (seg && !seg.el.closest(this.el).length) {
5868
-
5758
+ if (seg && seg.component !== this) {
5869
5759
  return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements
5870
5760
  }
5871
5761
  },
@@ -6573,7 +6463,7 @@ DayGrid.mixin({
6573
6463
  options = {
6574
6464
  className: 'fc-more-popover',
6575
6465
  content: this.renderSegPopoverContent(row, col, segs),
6576
- parentEl: this.el,
6466
+ parentEl: this.view.el, // attach to root of view. guarantees outside of scrollbars.
6577
6467
  top: topEl.offset().top,
6578
6468
  autoHide: true, // when the user clicks elsewhere, hide the popover
6579
6469
  viewportConstrain: view.opt('popoverViewportConstrain'),
@@ -6596,6 +6486,10 @@ DayGrid.mixin({
6596
6486
 
6597
6487
  this.segPopover = new Popover(options);
6598
6488
  this.segPopover.show();
6489
+
6490
+ // the popover doesn't live within the grid's container element, and thus won't get the event
6491
+ // delegated-handlers for free. attach event-related handlers to the popover.
6492
+ this.bindSegHandlersToEl(this.segPopover.el);
6599
6493
  },
6600
6494
 
6601
6495
 
@@ -6844,7 +6738,6 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
6844
6738
 
6845
6739
  this.labelFormat =
6846
6740
  input ||
6847
- view.opt('axisFormat') || // deprecated
6848
6741
  view.opt('smallTimeFormat'); // the computed default
6849
6742
 
6850
6743
  input = view.opt('slotLabelInterval');
@@ -6905,27 +6798,30 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
6905
6798
  var snapsPerSlot = this.snapsPerSlot;
6906
6799
  var colCoordCache = this.colCoordCache;
6907
6800
  var slatCoordCache = this.slatCoordCache;
6908
- var colIndex = colCoordCache.getHorizontalIndex(leftOffset);
6909
- var slatIndex = slatCoordCache.getVerticalIndex(topOffset);
6910
-
6911
- if (colIndex != null && slatIndex != null) {
6912
- var slatTop = slatCoordCache.getTopOffset(slatIndex);
6913
- var slatHeight = slatCoordCache.getHeight(slatIndex);
6914
- var partial = (topOffset - slatTop) / slatHeight; // floating point number between 0 and 1
6915
- var localSnapIndex = Math.floor(partial * snapsPerSlot); // the snap # relative to start of slat
6916
- var snapIndex = slatIndex * snapsPerSlot + localSnapIndex;
6917
- var snapTop = slatTop + (localSnapIndex / snapsPerSlot) * slatHeight;
6918
- var snapBottom = slatTop + ((localSnapIndex + 1) / snapsPerSlot) * slatHeight;
6919
-
6920
- return {
6921
- col: colIndex,
6922
- snap: snapIndex,
6923
- component: this, // needed unfortunately :(
6924
- left: colCoordCache.getLeftOffset(colIndex),
6925
- right: colCoordCache.getRightOffset(colIndex),
6926
- top: snapTop,
6927
- bottom: snapBottom
6928
- };
6801
+
6802
+ if (colCoordCache.isLeftInBounds(leftOffset) && slatCoordCache.isTopInBounds(topOffset)) {
6803
+ var colIndex = colCoordCache.getHorizontalIndex(leftOffset);
6804
+ var slatIndex = slatCoordCache.getVerticalIndex(topOffset);
6805
+
6806
+ if (colIndex != null && slatIndex != null) {
6807
+ var slatTop = slatCoordCache.getTopOffset(slatIndex);
6808
+ var slatHeight = slatCoordCache.getHeight(slatIndex);
6809
+ var partial = (topOffset - slatTop) / slatHeight; // floating point number between 0 and 1
6810
+ var localSnapIndex = Math.floor(partial * snapsPerSlot); // the snap # relative to start of slat
6811
+ var snapIndex = slatIndex * snapsPerSlot + localSnapIndex;
6812
+ var snapTop = slatTop + (localSnapIndex / snapsPerSlot) * slatHeight;
6813
+ var snapBottom = slatTop + ((localSnapIndex + 1) / snapsPerSlot) * slatHeight;
6814
+
6815
+ return {
6816
+ col: colIndex,
6817
+ snap: snapIndex,
6818
+ component: this, // needed unfortunately :(
6819
+ left: colCoordCache.getLeftOffset(colIndex),
6820
+ right: colCoordCache.getRightOffset(colIndex),
6821
+ top: snapTop,
6822
+ bottom: snapBottom
6823
+ };
6824
+ }
6929
6825
  }
6930
6826
  },
6931
6827
 
@@ -7128,10 +7024,9 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
7128
7024
 
7129
7025
 
7130
7026
  renderBusinessHours: function() {
7131
- var events = this.view.calendar.getBusinessHoursEvents();
7132
- var segs = this.eventsToSegs(events);
7133
-
7134
- this.renderBusinessSegs(segs);
7027
+ this.renderBusinessSegs(
7028
+ this.buildBusinessHourSegs()
7029
+ );
7135
7030
  },
7136
7031
 
7137
7032
 
@@ -8074,6 +7969,62 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8074
7969
  },
8075
7970
 
8076
7971
 
7972
+ getAllDayHtml: function() {
7973
+ return this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'));
7974
+ },
7975
+
7976
+
7977
+ /* Navigation
7978
+ ------------------------------------------------------------------------------------------------------------------*/
7979
+
7980
+
7981
+ // Generates HTML for an anchor to another view into the calendar.
7982
+ // Will either generate an <a> tag or a non-clickable <span> tag, depending on enabled settings.
7983
+ // `gotoOptions` can either be a moment input, or an object with the form:
7984
+ // { date, type, forceOff }
7985
+ // `type` is a view-type like "day" or "week". default value is "day".
7986
+ // `attrs` and `innerHtml` are use to generate the rest of the HTML tag.
7987
+ buildGotoAnchorHtml: function(gotoOptions, attrs, innerHtml) {
7988
+ var date, type, forceOff;
7989
+ var finalOptions;
7990
+
7991
+ if ($.isPlainObject(gotoOptions)) {
7992
+ date = gotoOptions.date;
7993
+ type = gotoOptions.type;
7994
+ forceOff = gotoOptions.forceOff;
7995
+ }
7996
+ else {
7997
+ date = gotoOptions; // a single moment input
7998
+ }
7999
+ date = FC.moment(date); // if a string, parse it
8000
+
8001
+ finalOptions = { // for serialization into the link
8002
+ date: date.format('YYYY-MM-DD'),
8003
+ type: type || 'day'
8004
+ };
8005
+
8006
+ if (typeof attrs === 'string') {
8007
+ innerHtml = attrs;
8008
+ attrs = null;
8009
+ }
8010
+
8011
+ attrs = attrs ? ' ' + attrsToStr(attrs) : ''; // will have a leading space
8012
+ innerHtml = innerHtml || '';
8013
+
8014
+ if (!forceOff && this.opt('navLinks')) {
8015
+ return '<a' + attrs +
8016
+ ' data-goto="' + htmlEscape(JSON.stringify(finalOptions)) + '">' +
8017
+ innerHtml +
8018
+ '</a>';
8019
+ }
8020
+ else {
8021
+ return '<span' + attrs + '>' +
8022
+ innerHtml +
8023
+ '</span>';
8024
+ }
8025
+ },
8026
+
8027
+
8077
8028
  /* Rendering
8078
8029
  ------------------------------------------------------------------------------------------------------------------*/
8079
8030
 
@@ -8110,12 +8061,12 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8110
8061
  // Does everything necessary to display the view centered around the given unzoned date.
8111
8062
  // Does every type of rendering EXCEPT rendering events.
8112
8063
  // Is asychronous and returns a promise.
8113
- display: function(date) {
8064
+ display: function(date, explicitScrollState) {
8114
8065
  var _this = this;
8115
- var scrollState = null;
8066
+ var prevScrollState = null;
8116
8067
 
8117
- if (this.displaying) {
8118
- scrollState = this.queryScroll();
8068
+ if (explicitScrollState != null && this.displaying) { // don't need prevScrollState if explicitScrollState
8069
+ prevScrollState = this.queryScroll();
8119
8070
  }
8120
8071
 
8121
8072
  this.calendar.freezeContentHeight();
@@ -8124,7 +8075,17 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8124
8075
  return (
8125
8076
  _this.displaying =
8126
8077
  syncThen(_this.displayView(date), function() { // displayView might return a promise
8127
- _this.forceScroll(_this.computeInitialScroll(scrollState));
8078
+
8079
+ // caller of display() wants a specific scroll state?
8080
+ if (explicitScrollState != null) {
8081
+ // we make an assumption that this is NOT the initial render,
8082
+ // and thus don't need forceScroll (is inconveniently asynchronous)
8083
+ _this.setScroll(explicitScrollState);
8084
+ }
8085
+ else {
8086
+ _this.forceScroll(_this.computeInitialScroll(prevScrollState));
8087
+ }
8088
+
8128
8089
  _this.calendar.unfreezeContentHeight();
8129
8090
  _this.triggerRender();
8130
8091
  })
@@ -8559,14 +8520,24 @@ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
8559
8520
 
8560
8521
  // Computes if the given event is allowed to be dragged by the user
8561
8522
  isEventDraggable: function(event) {
8562
- var source = event.source || {};
8523
+ return this.isEventStartEditable(event);
8524
+ },
8563
8525
 
8526
+
8527
+ isEventStartEditable: function(event) {
8564
8528
  return firstDefined(
8565
8529
  event.startEditable,
8566
- source.startEditable,
8530
+ (event.source || {}).startEditable,
8567
8531
  this.opt('eventStartEditable'),
8532
+ this.isEventGenerallyEditable(event)
8533
+ );
8534
+ },
8535
+
8536
+
8537
+ isEventGenerallyEditable: function(event) {
8538
+ return firstDefined(
8568
8539
  event.editable,
8569
- source.editable,
8540
+ (event.source || {}).editable,
8570
8541
  this.opt('editable')
8571
8542
  );
8572
8543
  },
@@ -9066,8 +9037,9 @@ var Scroller = FC.Scroller = Class.extend({
9066
9037
  var Calendar = FC.Calendar = Class.extend({
9067
9038
 
9068
9039
  dirDefaults: null, // option defaults related to LTR or RTL
9069
- langDefaults: null, // option defaults related to current locale
9040
+ localeDefaults: null, // option defaults related to current locale
9070
9041
  overrides: null, // option overrides given to the fullCalendar constructor
9042
+ dynamicOverrides: null, // options set with dynamic setter method. higher precedence than view overrides.
9071
9043
  options: null, // all defaults combined with overrides
9072
9044
  viewSpecCache: null, // cache of view definitions
9073
9045
  view: null, // current View object
@@ -9085,41 +9057,40 @@ var Calendar = FC.Calendar = Class.extend({
9085
9057
  },
9086
9058
 
9087
9059
 
9088
- // Initializes `this.options` and other important options-related objects
9089
- initOptions: function(overrides) {
9090
- var lang, langDefaults;
9060
+ // Computes the flattened options hash for the calendar and assigns to `this.options`.
9061
+ // Assumes this.overrides and this.dynamicOverrides have already been initialized.
9062
+ populateOptionsHash: function() {
9063
+ var locale, localeDefaults;
9091
9064
  var isRTL, dirDefaults;
9092
9065
 
9093
- // converts legacy options into non-legacy ones.
9094
- // in the future, when this is removed, don't use `overrides` reference. make a copy.
9095
- overrides = massageOverrides(overrides);
9096
-
9097
- lang = overrides.lang;
9098
- langDefaults = langOptionHash[lang];
9099
- if (!langDefaults) {
9100
- lang = Calendar.defaults.lang;
9101
- langDefaults = langOptionHash[lang] || {};
9066
+ locale = firstDefined( // explicit locale option given?
9067
+ this.dynamicOverrides.locale,
9068
+ this.overrides.locale
9069
+ );
9070
+ localeDefaults = localeOptionHash[locale];
9071
+ if (!localeDefaults) { // explicit locale option not given or invalid?
9072
+ locale = Calendar.defaults.locale;
9073
+ localeDefaults = localeOptionHash[locale] || {};
9102
9074
  }
9103
9075
 
9104
- isRTL = firstDefined(
9105
- overrides.isRTL,
9106
- langDefaults.isRTL,
9076
+ isRTL = firstDefined( // based on options computed so far, is direction RTL?
9077
+ this.dynamicOverrides.isRTL,
9078
+ this.overrides.isRTL,
9079
+ localeDefaults.isRTL,
9107
9080
  Calendar.defaults.isRTL
9108
9081
  );
9109
9082
  dirDefaults = isRTL ? Calendar.rtlDefaults : {};
9110
9083
 
9111
9084
  this.dirDefaults = dirDefaults;
9112
- this.langDefaults = langDefaults;
9113
- this.overrides = overrides;
9085
+ this.localeDefaults = localeDefaults;
9114
9086
  this.options = mergeOptions([ // merge defaults and overrides. lowest to highest precedence
9115
9087
  Calendar.defaults, // global defaults
9116
9088
  dirDefaults,
9117
- langDefaults,
9118
- overrides
9089
+ localeDefaults,
9090
+ this.overrides,
9091
+ this.dynamicOverrides
9119
9092
  ]);
9120
- populateInstanceComputableOptions(this.options);
9121
-
9122
- this.viewSpecCache = {}; // somewhat unrelated
9093
+ populateInstanceComputableOptions(this.options); // fill in gaps with computed options
9123
9094
  },
9124
9095
 
9125
9096
 
@@ -9231,9 +9202,10 @@ var Calendar = FC.Calendar = Class.extend({
9231
9202
  Calendar.defaults, // global defaults
9232
9203
  spec.defaults, // view's defaults (from ViewSubclass.defaults)
9233
9204
  this.dirDefaults,
9234
- this.langDefaults, // locale and dir take precedence over view's defaults!
9205
+ this.localeDefaults, // locale and dir take precedence over view's defaults!
9235
9206
  this.overrides, // calendar's overrides (options given to constructor)
9236
- spec.overrides // view's overrides (view-specific options)
9207
+ spec.overrides, // view's overrides (view-specific options)
9208
+ this.dynamicOverrides // dynamically set via setter. highest precedence
9237
9209
  ]);
9238
9210
  populateInstanceComputableOptions(spec.options);
9239
9211
  },
@@ -9247,17 +9219,21 @@ var Calendar = FC.Calendar = Class.extend({
9247
9219
  function queryButtonText(options) {
9248
9220
  var buttonText = options.buttonText || {};
9249
9221
  return buttonText[requestedViewType] ||
9222
+ // view can decide to look up a certain key
9223
+ (spec.buttonTextKey ? buttonText[spec.buttonTextKey] : null) ||
9224
+ // a key like "month"
9250
9225
  (spec.singleUnit ? buttonText[spec.singleUnit] : null);
9251
9226
  }
9252
9227
 
9253
9228
  // highest to lowest priority
9254
9229
  spec.buttonTextOverride =
9230
+ queryButtonText(this.dynamicOverrides) ||
9255
9231
  queryButtonText(this.overrides) || // constructor-specified buttonText lookup hash takes precedence
9256
9232
  spec.overrides.buttonText; // `buttonText` for view-specific options is a string
9257
9233
 
9258
9234
  // highest to lowest priority. mirrors buildViewSpecOptions
9259
9235
  spec.buttonTextDefault =
9260
- queryButtonText(this.langDefaults) ||
9236
+ queryButtonText(this.localeDefaults) ||
9261
9237
  queryButtonText(this.dirDefaults) ||
9262
9238
  spec.defaults.buttonText || // a single string. from ViewSubclass.defaults
9263
9239
  queryButtonText(Calendar.defaults) ||
@@ -9324,10 +9300,6 @@ function Calendar_constructor(element, overrides) {
9324
9300
  var t = this;
9325
9301
 
9326
9302
 
9327
- t.initOptions(overrides || {});
9328
- var options = this.options;
9329
-
9330
-
9331
9303
  // Exports
9332
9304
  // -----------------------------------------------------------------------------------
9333
9305
 
@@ -9352,67 +9324,95 @@ function Calendar_constructor(element, overrides) {
9352
9324
  t.getDate = getDate;
9353
9325
  t.getCalendar = getCalendar;
9354
9326
  t.getView = getView;
9355
- t.option = option;
9327
+ t.option = option; // getter/setter method
9356
9328
  t.trigger = trigger;
9357
9329
 
9358
9330
 
9331
+ // Options
9332
+ // -----------------------------------------------------------------------------------
9333
+
9334
+ t.dynamicOverrides = {};
9335
+ t.viewSpecCache = {};
9336
+ t.optionHandlers = {}; // for Calendar.options.js
9337
+ t.overrides = $.extend({}, overrides); // make a copy
9338
+
9339
+ t.populateOptionsHash(); // sets this.options
9340
+
9359
9341
 
9360
- // Language-data Internals
9342
+
9343
+ // Locale-data Internals
9361
9344
  // -----------------------------------------------------------------------------------
9362
- // Apply overrides to the current language's data
9345
+ // Apply overrides to the current locale's data
9363
9346
 
9347
+ var localeData;
9364
9348
 
9365
- var localeData = createObject( // make a cheap copy
9366
- getMomentLocaleData(options.lang) // will fall back to en
9367
- );
9349
+ // Called immediately, and when any of the options change.
9350
+ // Happens before any internal objects rebuild or rerender, because this is very core.
9351
+ t.bindOptions([
9352
+ 'locale', 'monthNames', 'monthNamesShort', 'dayNames', 'dayNamesShort', 'firstDay', 'weekNumberCalculation'
9353
+ ], function(locale, monthNames, monthNamesShort, dayNames, dayNamesShort, firstDay, weekNumberCalculation) {
9368
9354
 
9369
- if (options.monthNames) {
9370
- localeData._months = options.monthNames;
9371
- }
9372
- if (options.monthNamesShort) {
9373
- localeData._monthsShort = options.monthNamesShort;
9374
- }
9375
- if (options.dayNames) {
9376
- localeData._weekdays = options.dayNames;
9377
- }
9378
- if (options.dayNamesShort) {
9379
- localeData._weekdaysShort = options.dayNamesShort;
9380
- }
9381
- if (options.firstDay != null) {
9382
- var _week = createObject(localeData._week); // _week: { dow: # }
9383
- _week.dow = options.firstDay;
9384
- localeData._week = _week;
9385
- }
9355
+ // normalize
9356
+ if (weekNumberCalculation === 'iso') {
9357
+ weekNumberCalculation = 'ISO'; // normalize
9358
+ }
9359
+
9360
+ localeData = createObject( // make a cheap copy
9361
+ getMomentLocaleData(locale) // will fall back to en
9362
+ );
9363
+
9364
+ if (monthNames) {
9365
+ localeData._months = monthNames;
9366
+ }
9367
+ if (monthNamesShort) {
9368
+ localeData._monthsShort = monthNamesShort;
9369
+ }
9370
+ if (dayNames) {
9371
+ localeData._weekdays = dayNames;
9372
+ }
9373
+ if (dayNamesShort) {
9374
+ localeData._weekdaysShort = dayNamesShort;
9375
+ }
9386
9376
 
9387
- // assign a normalized value, to be used by our .week() moment extension
9388
- localeData._fullCalendar_weekCalc = (function(weekCalc) {
9389
- if (typeof weekCalc === 'function') {
9390
- return weekCalc;
9377
+ if (firstDay == null && weekNumberCalculation === 'ISO') {
9378
+ firstDay = 1;
9391
9379
  }
9392
- else if (weekCalc === 'local') {
9393
- return weekCalc;
9380
+ if (firstDay != null) {
9381
+ var _week = createObject(localeData._week); // _week: { dow: # }
9382
+ _week.dow = firstDay;
9383
+ localeData._week = _week;
9394
9384
  }
9395
- else if (weekCalc === 'iso' || weekCalc === 'ISO') {
9396
- return 'ISO';
9385
+
9386
+ if ( // whitelist certain kinds of input
9387
+ weekNumberCalculation === 'ISO' ||
9388
+ weekNumberCalculation === 'local' ||
9389
+ typeof weekNumberCalculation === 'function'
9390
+ ) {
9391
+ localeData._fullCalendar_weekCalc = weekNumberCalculation; // moment-ext will know what to do with it
9397
9392
  }
9398
- })(options.weekNumberCalculation);
9399
9393
 
9394
+ // If the internal current date object already exists, move to new locale.
9395
+ // We do NOT need to do this technique for event dates, because this happens when converting to "segments".
9396
+ if (date) {
9397
+ localizeMoment(date); // sets to localeData
9398
+ }
9399
+ });
9400
9400
 
9401
9401
 
9402
9402
  // Calendar-specific Date Utilities
9403
9403
  // -----------------------------------------------------------------------------------
9404
9404
 
9405
9405
 
9406
- t.defaultAllDayEventDuration = moment.duration(options.defaultAllDayEventDuration);
9407
- t.defaultTimedEventDuration = moment.duration(options.defaultTimedEventDuration);
9406
+ t.defaultAllDayEventDuration = moment.duration(t.options.defaultAllDayEventDuration);
9407
+ t.defaultTimedEventDuration = moment.duration(t.options.defaultTimedEventDuration);
9408
9408
 
9409
9409
 
9410
- // Builds a moment using the settings of the current calendar: timezone and language.
9410
+ // Builds a moment using the settings of the current calendar: timezone and locale.
9411
9411
  // Accepts anything the vanilla moment() constructor accepts.
9412
9412
  t.moment = function() {
9413
9413
  var mom;
9414
9414
 
9415
- if (options.timezone === 'local') {
9415
+ if (t.options.timezone === 'local') {
9416
9416
  mom = FC.moment.apply(null, arguments);
9417
9417
 
9418
9418
  // Force the moment to be local, because FC.moment doesn't guarantee it.
@@ -9420,28 +9420,30 @@ function Calendar_constructor(element, overrides) {
9420
9420
  mom.local();
9421
9421
  }
9422
9422
  }
9423
- else if (options.timezone === 'UTC') {
9423
+ else if (t.options.timezone === 'UTC') {
9424
9424
  mom = FC.moment.utc.apply(null, arguments); // process as UTC
9425
9425
  }
9426
9426
  else {
9427
9427
  mom = FC.moment.parseZone.apply(null, arguments); // let the input decide the zone
9428
9428
  }
9429
9429
 
9430
- if ('_locale' in mom) { // moment 2.8 and above
9431
- mom._locale = localeData;
9432
- }
9433
- else { // pre-moment-2.8
9434
- mom._lang = localeData;
9435
- }
9430
+ localizeMoment(mom);
9436
9431
 
9437
9432
  return mom;
9438
9433
  };
9439
9434
 
9440
9435
 
9436
+ // Updates the given moment's locale settings to the current calendar locale settings.
9437
+ function localizeMoment(mom) {
9438
+ mom._locale = localeData;
9439
+ }
9440
+ t.localizeMoment = localizeMoment;
9441
+
9442
+
9441
9443
  // Returns a boolean about whether or not the calendar knows how to calculate
9442
9444
  // the timezone offset of arbitrary dates in the current timezone.
9443
9445
  t.getIsAmbigTimezone = function() {
9444
- return options.timezone !== 'local' && options.timezone !== 'UTC';
9446
+ return t.options.timezone !== 'local' && t.options.timezone !== 'UTC';
9445
9447
  };
9446
9448
 
9447
9449
 
@@ -9470,7 +9472,7 @@ function Calendar_constructor(element, overrides) {
9470
9472
  // Returns a moment for the current date, as defined by the client's computer or from the `now` option.
9471
9473
  // Will return an moment with an ambiguous timezone.
9472
9474
  t.getNow = function() {
9473
- var now = options.now;
9475
+ var now = t.options.now;
9474
9476
  if (typeof now === 'function') {
9475
9477
  now = now();
9476
9478
  }
@@ -9512,8 +9514,7 @@ function Calendar_constructor(element, overrides) {
9512
9514
  // Produces a human-readable string for the given duration.
9513
9515
  // Side-effect: changes the locale of the given duration.
9514
9516
  t.humanizeDuration = function(duration) {
9515
- return (duration.locale || duration.lang).call(duration, options.lang) // works moment-pre-2.8
9516
- .humanize();
9517
+ return duration.locale(t.options.locale).humanize();
9517
9518
  };
9518
9519
 
9519
9520
 
@@ -9522,7 +9523,7 @@ function Calendar_constructor(element, overrides) {
9522
9523
  // -----------------------------------------------------------------------------------
9523
9524
 
9524
9525
 
9525
- EventManager.call(t, options);
9526
+ EventManager.call(t);
9526
9527
  var isFetchNeeded = t.isFetchNeeded;
9527
9528
  var fetchEvents = t.fetchEvents;
9528
9529
  var fetchEventSources = t.fetchEventSources;
@@ -9535,7 +9536,6 @@ function Calendar_constructor(element, overrides) {
9535
9536
 
9536
9537
  var _element = element[0];
9537
9538
  var header;
9538
- var headerElement;
9539
9539
  var content;
9540
9540
  var tm; // for making theme classes
9541
9541
  var currentView; // NOTE: keep this in sync with this.view
@@ -9553,8 +9553,8 @@ function Calendar_constructor(element, overrides) {
9553
9553
 
9554
9554
 
9555
9555
  // compute the initial ambig-timezone date
9556
- if (options.defaultDate != null) {
9557
- date = t.moment(options.defaultDate).stripZone();
9556
+ if (t.options.defaultDate != null) {
9557
+ date = t.moment(t.options.defaultDate).stripZone();
9558
9558
  }
9559
9559
  else {
9560
9560
  date = t.getNow(); // getNow already returns unzoned
@@ -9574,38 +9574,64 @@ function Calendar_constructor(element, overrides) {
9574
9574
 
9575
9575
 
9576
9576
  function initialRender() {
9577
- tm = options.theme ? 'ui' : 'fc';
9578
9577
  element.addClass('fc');
9579
9578
 
9580
- if (options.isRTL) {
9581
- element.addClass('fc-rtl');
9582
- }
9583
- else {
9584
- element.addClass('fc-ltr');
9585
- }
9579
+ // event delegation for nav links
9580
+ element.on('click.fc', 'a[data-goto]', function(ev) {
9581
+ var anchorEl = $(this);
9582
+ var gotoOptions = anchorEl.data('goto'); // will automatically parse JSON
9583
+ var date = t.moment(gotoOptions.date);
9584
+ var viewType = gotoOptions.type;
9586
9585
 
9587
- if (options.theme) {
9588
- element.addClass('ui-widget');
9589
- }
9590
- else {
9591
- element.addClass('fc-unthemed');
9592
- }
9586
+ // property like "navLinkDayClick". might be a string or a function
9587
+ var customAction = currentView.opt('navLink' + capitaliseFirstLetter(viewType) + 'Click');
9588
+
9589
+ if (typeof customAction === 'function') {
9590
+ customAction(date, ev);
9591
+ }
9592
+ else {
9593
+ if (typeof customAction === 'string') {
9594
+ viewType = customAction;
9595
+ }
9596
+ zoomTo(date, viewType);
9597
+ }
9598
+ });
9599
+
9600
+ // called immediately, and upon option change
9601
+ t.bindOption('theme', function(theme) {
9602
+ tm = theme ? 'ui' : 'fc'; // affects a larger scope
9603
+ element.toggleClass('ui-widget', theme);
9604
+ element.toggleClass('fc-unthemed', !theme);
9605
+ });
9606
+
9607
+ // called immediately, and upon option change.
9608
+ // HACK: locale often affects isRTL, so we explicitly listen to that too.
9609
+ t.bindOptions([ 'isRTL', 'locale' ], function(isRTL) {
9610
+ element.toggleClass('fc-ltr', !isRTL);
9611
+ element.toggleClass('fc-rtl', isRTL);
9612
+ });
9593
9613
 
9594
9614
  content = $("<div class='fc-view-container'/>").prependTo(element);
9595
9615
 
9596
- header = t.header = new Header(t, options);
9597
- headerElement = header.render();
9598
- if (headerElement) {
9599
- element.prepend(headerElement);
9600
- }
9616
+ header = t.header = new Header(t);
9617
+ renderHeader();
9601
9618
 
9602
- renderView(options.defaultView);
9619
+ renderView(t.options.defaultView);
9603
9620
 
9604
- if (options.handleWindowResize) {
9605
- windowResizeProxy = debounce(windowResize, options.windowResizeDelay); // prevents rapid calls
9621
+ if (t.options.handleWindowResize) {
9622
+ windowResizeProxy = debounce(windowResize, t.options.windowResizeDelay); // prevents rapid calls
9606
9623
  $(window).resize(windowResizeProxy);
9607
9624
  }
9608
9625
  }
9626
+
9627
+
9628
+ // can be called repeatedly and Header will rerender
9629
+ function renderHeader() {
9630
+ header.render();
9631
+ if (header.el) {
9632
+ element.prepend(header.el);
9633
+ }
9634
+ }
9609
9635
 
9610
9636
 
9611
9637
  function destroy() {
@@ -9621,6 +9647,8 @@ function Calendar_constructor(element, overrides) {
9621
9647
  content.remove();
9622
9648
  element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');
9623
9649
 
9650
+ element.off('.fc'); // unbind nav link handlers
9651
+
9624
9652
  if (windowResizeProxy) {
9625
9653
  $(window).unbind('resize', windowResizeProxy);
9626
9654
  }
@@ -9639,15 +9667,14 @@ function Calendar_constructor(element, overrides) {
9639
9667
 
9640
9668
  // Renders a view because of a date change, view-type change, or for the first time.
9641
9669
  // If not given a viewType, keep the current view but render different dates.
9642
- function renderView(viewType) {
9670
+ // Accepts an optional scroll state to restore to.
9671
+ function renderView(viewType, explicitScrollState) {
9643
9672
  ignoreWindowResize++;
9644
9673
 
9645
9674
  // if viewType is changing, remove the old view's rendering
9646
9675
  if (currentView && viewType && currentView.type !== viewType) {
9647
- header.deactivateButton(currentView.type);
9648
9676
  freezeContentHeight(); // prevent a scroll jump when view element is removed
9649
- currentView.removeElement();
9650
- currentView = t.view = null;
9677
+ clearView();
9651
9678
  }
9652
9679
 
9653
9680
  // if viewType changed, or the view was never created, create a fresh view
@@ -9670,11 +9697,14 @@ function Calendar_constructor(element, overrides) {
9670
9697
  // render or rerender the view
9671
9698
  if (
9672
9699
  !currentView.displaying ||
9673
- !date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change
9700
+ !( // NOT within interval range signals an implicit date window change
9701
+ date >= currentView.intervalStart &&
9702
+ date < currentView.intervalEnd
9703
+ )
9674
9704
  ) {
9675
9705
  if (elementVisible()) {
9676
9706
 
9677
- currentView.display(date); // will call freezeContentHeight
9707
+ currentView.display(date, explicitScrollState); // will call freezeContentHeight
9678
9708
  unfreezeContentHeight(); // immediately unfreeze regardless of whether display is async
9679
9709
 
9680
9710
  // need to do this after View::render, so dates are calculated
@@ -9690,6 +9720,32 @@ function Calendar_constructor(element, overrides) {
9690
9720
  ignoreWindowResize--;
9691
9721
  }
9692
9722
 
9723
+
9724
+ // Unrenders the current view and reflects this change in the Header.
9725
+ // Unregsiters the `currentView`, but does not remove from viewByType hash.
9726
+ function clearView() {
9727
+ header.deactivateButton(currentView.type);
9728
+ currentView.removeElement();
9729
+ currentView = t.view = null;
9730
+ }
9731
+
9732
+
9733
+ // Destroys the view, including the view object. Then, re-instantiates it and renders it.
9734
+ // Maintains the same scroll state.
9735
+ // TODO: maintain any other user-manipulated state.
9736
+ function reinitView() {
9737
+ ignoreWindowResize++;
9738
+ freezeContentHeight();
9739
+
9740
+ var viewType = currentView.type;
9741
+ var scrollState = currentView.queryScroll();
9742
+ clearView();
9743
+ renderView(viewType, scrollState);
9744
+
9745
+ unfreezeContentHeight();
9746
+ ignoreWindowResize--;
9747
+ }
9748
+
9693
9749
 
9694
9750
 
9695
9751
  // Resizing
@@ -9705,7 +9761,7 @@ function Calendar_constructor(element, overrides) {
9705
9761
 
9706
9762
 
9707
9763
  t.isHeightAuto = function() {
9708
- return options.contentHeight === 'auto' || options.height === 'auto';
9764
+ return t.options.contentHeight === 'auto' || t.options.height === 'auto';
9709
9765
  };
9710
9766
 
9711
9767
 
@@ -9733,16 +9789,33 @@ function Calendar_constructor(element, overrides) {
9733
9789
 
9734
9790
 
9735
9791
  function _calcSize() { // assumes elementVisible
9736
- if (typeof options.contentHeight === 'number') { // exists and not 'auto'
9737
- suggestedViewHeight = options.contentHeight;
9792
+ var contentHeightInput = t.options.contentHeight;
9793
+ var heightInput = t.options.height;
9794
+
9795
+ if (typeof contentHeightInput === 'number') { // exists and not 'auto'
9796
+ suggestedViewHeight = contentHeightInput;
9797
+ }
9798
+ else if (typeof contentHeightInput === 'function') { // exists and is a function
9799
+ suggestedViewHeight = contentHeightInput();
9738
9800
  }
9739
- else if (typeof options.height === 'number') { // exists and not 'auto'
9740
- suggestedViewHeight = options.height - (headerElement ? headerElement.outerHeight(true) : 0);
9801
+ else if (typeof heightInput === 'number') { // exists and not 'auto'
9802
+ suggestedViewHeight = heightInput - queryHeaderHeight();
9803
+ }
9804
+ else if (typeof heightInput === 'function') { // exists and is a function
9805
+ suggestedViewHeight = heightInput() - queryHeaderHeight();
9806
+ }
9807
+ else if (heightInput === 'parent') { // set to height of parent element
9808
+ suggestedViewHeight = element.parent().height() - queryHeaderHeight();
9741
9809
  }
9742
9810
  else {
9743
- suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5));
9811
+ suggestedViewHeight = Math.round(content.width() / Math.max(t.options.aspectRatio, .5));
9744
9812
  }
9745
9813
  }
9814
+
9815
+
9816
+ function queryHeaderHeight() {
9817
+ return header.el ? header.el.outerHeight(true) : 0; // includes margin
9818
+ }
9746
9819
 
9747
9820
 
9748
9821
  function windowResize(ev) {
@@ -9785,7 +9858,7 @@ function Calendar_constructor(element, overrides) {
9785
9858
 
9786
9859
 
9787
9860
  function getAndRenderEvents() {
9788
- if (!options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) {
9861
+ if (!t.options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) {
9789
9862
  fetchAndRenderEvents();
9790
9863
  }
9791
9864
  else {
@@ -9826,7 +9899,8 @@ function Calendar_constructor(element, overrides) {
9826
9899
 
9827
9900
  function updateTodayButton() {
9828
9901
  var now = t.getNow();
9829
- if (now.isWithin(currentView.intervalStart, currentView.intervalEnd)) {
9902
+
9903
+ if (now >= currentView.intervalStart && now < currentView.intervalEnd) {
9830
9904
  header.disableButton('today');
9831
9905
  }
9832
9906
  else {
@@ -9964,13 +10038,69 @@ function Calendar_constructor(element, overrides) {
9964
10038
 
9965
10039
 
9966
10040
  function option(name, value) {
9967
- if (value === undefined) {
9968
- return options[name];
10041
+ var newOptionHash;
10042
+
10043
+ if (typeof name === 'string') {
10044
+ if (value === undefined) { // getter
10045
+ return t.options[name];
10046
+ }
10047
+ else { // setter for individual option
10048
+ newOptionHash = {};
10049
+ newOptionHash[name] = value;
10050
+ setOptions(newOptionHash);
10051
+ }
10052
+ }
10053
+ else if (typeof name === 'object') { // compound setter with object input
10054
+ setOptions(name);
10055
+ }
10056
+ }
10057
+
10058
+
10059
+ function setOptions(newOptionHash) {
10060
+ var optionCnt = 0;
10061
+ var optionName;
10062
+
10063
+ for (optionName in newOptionHash) {
10064
+ t.dynamicOverrides[optionName] = newOptionHash[optionName];
10065
+ }
10066
+
10067
+ t.viewSpecCache = {}; // the dynamic override invalidates the options in this cache, so just clear it
10068
+ t.populateOptionsHash(); // this.options needs to be recomputed after the dynamic override
10069
+
10070
+ // trigger handlers after this.options has been updated
10071
+ for (optionName in newOptionHash) {
10072
+ t.triggerOptionHandlers(optionName); // recall bindOption/bindOptions
10073
+ optionCnt++;
9969
10074
  }
9970
- if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') {
9971
- options[name] = value;
9972
- updateSize(true); // true = allow recalculation of height
10075
+
10076
+ // special-case handling of single option change.
10077
+ // if only one option change, `optionName` will be its name.
10078
+ if (optionCnt === 1) {
10079
+ if (optionName === 'height' || optionName === 'contentHeight' || optionName === 'aspectRatio') {
10080
+ updateSize(true); // true = allow recalculation of height
10081
+ return;
10082
+ }
10083
+ else if (optionName === 'defaultDate') {
10084
+ return; // can't change date this way. use gotoDate instead
10085
+ }
10086
+ else if (optionName === 'businessHours') {
10087
+ if (currentView) {
10088
+ currentView.unrenderBusinessHours();
10089
+ currentView.renderBusinessHours();
10090
+ }
10091
+ return;
10092
+ }
10093
+ else if (optionName === 'timezone') {
10094
+ t.rezoneArrayEventSources();
10095
+ refetchEvents();
10096
+ return;
10097
+ }
9973
10098
  }
10099
+
10100
+ // catch-all. rerender the header and rebuild/rerender the current view
10101
+ renderHeader();
10102
+ viewsByType = {}; // even non-current views will be affected by this option change. do before rerender
10103
+ reinitView();
9974
10104
  }
9975
10105
 
9976
10106
 
@@ -9980,20 +10110,84 @@ function Calendar_constructor(element, overrides) {
9980
10110
  thisObj = thisObj || _element;
9981
10111
  this.triggerWith(name, thisObj, args); // Emitter's method
9982
10112
 
9983
- if (options[name]) {
9984
- return options[name].apply(thisObj, args);
10113
+ if (t.options[name]) {
10114
+ return t.options[name].apply(thisObj, args);
9985
10115
  }
9986
10116
  }
9987
10117
 
9988
10118
  t.initialize();
9989
10119
  }
9990
10120
 
10121
+ ;;
10122
+ /*
10123
+ Options binding/triggering system.
10124
+ */
10125
+ Calendar.mixin({
10126
+
10127
+ // A map of option names to arrays of handler objects. Initialized to {} in Calendar.
10128
+ // Format for a handler object:
10129
+ // {
10130
+ // func // callback function to be called upon change
10131
+ // names // option names whose values should be given to func
10132
+ // }
10133
+ optionHandlers: null,
10134
+
10135
+ // Calls handlerFunc immediately, and when the given option has changed.
10136
+ // handlerFunc will be given the option value.
10137
+ bindOption: function(optionName, handlerFunc) {
10138
+ this.bindOptions([ optionName ], handlerFunc);
10139
+ },
10140
+
10141
+ // Calls handlerFunc immediately, and when any of the given options change.
10142
+ // handlerFunc will be given each option value as ordered function arguments.
10143
+ bindOptions: function(optionNames, handlerFunc) {
10144
+ var handlerObj = { func: handlerFunc, names: optionNames };
10145
+ var i;
10146
+
10147
+ for (i = 0; i < optionNames.length; i++) {
10148
+ this.registerOptionHandlerObj(optionNames[i], handlerObj);
10149
+ }
10150
+
10151
+ this.triggerOptionHandlerObj(handlerObj);
10152
+ },
10153
+
10154
+ // Puts the given handler object into the internal hash
10155
+ registerOptionHandlerObj: function(optionName, handlerObj) {
10156
+ (this.optionHandlers[optionName] || (this.optionHandlers[optionName] = []))
10157
+ .push(handlerObj);
10158
+ },
10159
+
10160
+ // Reports that the given option has changed, and calls all appropriate handlers.
10161
+ triggerOptionHandlers: function(optionName) {
10162
+ var handlerObjs = this.optionHandlers[optionName] || [];
10163
+ var i;
10164
+
10165
+ for (i = 0; i < handlerObjs.length; i++) {
10166
+ this.triggerOptionHandlerObj(handlerObjs[i]);
10167
+ }
10168
+ },
10169
+
10170
+ // Calls the callback for a specific handler object, passing in the appropriate arguments.
10171
+ triggerOptionHandlerObj: function(handlerObj) {
10172
+ var optionNames = handlerObj.names;
10173
+ var optionValues = [];
10174
+ var i;
10175
+
10176
+ for (i = 0; i < optionNames.length; i++) {
10177
+ optionValues.push(this.options[optionNames[i]]);
10178
+ }
10179
+
10180
+ handlerObj.func.apply(this, optionValues); // maintain the Calendar's `this` context
10181
+ }
10182
+
10183
+ });
10184
+
9991
10185
  ;;
9992
10186
 
9993
10187
  Calendar.defaults = {
9994
10188
 
9995
10189
  titleRangeSeparator: ' \u2013 ', // en dash
9996
- monthYearFormat: 'MMMM YYYY', // required for en. other languages rely on datepicker computable option
10190
+ monthYearFormat: 'MMMM YYYY', // required for en. other locales rely on datepicker computable option
9997
10191
 
9998
10192
  defaultTimedEventDuration: '02:00:00',
9999
10193
  defaultAllDayEventDuration: { days: 1 },
@@ -10050,6 +10244,8 @@ Calendar.defaults = {
10050
10244
  prevYear: 'left-double-arrow',
10051
10245
  nextYear: 'right-double-arrow'
10052
10246
  },
10247
+
10248
+ allDayText: 'all-day',
10053
10249
 
10054
10250
  // jquery-ui theming
10055
10251
  theme: false,
@@ -10078,14 +10274,14 @@ Calendar.defaults = {
10078
10274
  dayPopoverFormat: 'LL',
10079
10275
 
10080
10276
  handleWindowResize: true,
10081
- windowResizeDelay: 200, // milliseconds before an updateSize happens
10277
+ windowResizeDelay: 100, // milliseconds before an updateSize happens
10082
10278
 
10083
10279
  longPressDelay: 1000
10084
10280
 
10085
10281
  };
10086
10282
 
10087
10283
 
10088
- Calendar.englishDefaults = { // used by lang.js
10284
+ Calendar.englishDefaults = { // used by locale.js
10089
10285
  dayPopoverFormat: 'dddd, MMMM D'
10090
10286
  };
10091
10287
 
@@ -10112,19 +10308,18 @@ Calendar.rtlDefaults = { // right-to-left defaults
10112
10308
 
10113
10309
  ;;
10114
10310
 
10115
- var langOptionHash = FC.langs = {}; // initialize and expose
10311
+ var localeOptionHash = FC.locales = {}; // initialize and expose
10116
10312
 
10117
10313
 
10118
- // TODO: document the structure and ordering of a FullCalendar lang file
10119
- // TODO: rename everything "lang" to "locale", like what the moment project did
10314
+ // TODO: document the structure and ordering of a FullCalendar locale file
10120
10315
 
10121
10316
 
10122
10317
  // Initialize jQuery UI datepicker translations while using some of the translations
10123
- // Will set this as the default language for datepicker.
10124
- FC.datepickerLang = function(langCode, dpLangCode, dpOptions) {
10318
+ // Will set this as the default locales for datepicker.
10319
+ FC.datepickerLocale = function(localeCode, dpLocaleCode, dpOptions) {
10125
10320
 
10126
- // get the FullCalendar internal option hash for this language. create if necessary
10127
- var fcOptions = langOptionHash[langCode] || (langOptionHash[langCode] = {});
10321
+ // get the FullCalendar internal option hash for this locale. create if necessary
10322
+ var fcOptions = localeOptionHash[localeCode] || (localeOptionHash[localeCode] = {});
10128
10323
 
10129
10324
  // transfer some simple options from datepicker to fc
10130
10325
  fcOptions.isRTL = dpOptions.isRTL;
@@ -10138,15 +10333,15 @@ FC.datepickerLang = function(langCode, dpLangCode, dpOptions) {
10138
10333
  // is jQuery UI Datepicker is on the page?
10139
10334
  if ($.datepicker) {
10140
10335
 
10141
- // Register the language data.
10142
- // FullCalendar and MomentJS use language codes like "pt-br" but Datepicker
10143
- // does it like "pt-BR" or if it doesn't have the language, maybe just "pt".
10144
- // Make an alias so the language can be referenced either way.
10145
- $.datepicker.regional[dpLangCode] =
10146
- $.datepicker.regional[langCode] = // alias
10336
+ // Register the locale data.
10337
+ // FullCalendar and MomentJS use locale codes like "pt-br" but Datepicker
10338
+ // does it like "pt-BR" or if it doesn't have the locale, maybe just "pt".
10339
+ // Make an alias so the locale can be referenced either way.
10340
+ $.datepicker.regional[dpLocaleCode] =
10341
+ $.datepicker.regional[localeCode] = // alias
10147
10342
  dpOptions;
10148
10343
 
10149
- // Alias 'en' to the default language data. Do this every time.
10344
+ // Alias 'en' to the default locale data. Do this every time.
10150
10345
  $.datepicker.regional.en = $.datepicker.regional[''];
10151
10346
 
10152
10347
  // Set as Datepicker's global defaults.
@@ -10155,35 +10350,35 @@ FC.datepickerLang = function(langCode, dpLangCode, dpOptions) {
10155
10350
  };
10156
10351
 
10157
10352
 
10158
- // Sets FullCalendar-specific translations. Will set the language as the global default.
10159
- FC.lang = function(langCode, newFcOptions) {
10353
+ // Sets FullCalendar-specific translations. Will set the locales as the global default.
10354
+ FC.locale = function(localeCode, newFcOptions) {
10160
10355
  var fcOptions;
10161
10356
  var momOptions;
10162
10357
 
10163
- // get the FullCalendar internal option hash for this language. create if necessary
10164
- fcOptions = langOptionHash[langCode] || (langOptionHash[langCode] = {});
10358
+ // get the FullCalendar internal option hash for this locale. create if necessary
10359
+ fcOptions = localeOptionHash[localeCode] || (localeOptionHash[localeCode] = {});
10165
10360
 
10166
- // provided new options for this language? merge them in
10361
+ // provided new options for this locales? merge them in
10167
10362
  if (newFcOptions) {
10168
- fcOptions = langOptionHash[langCode] = mergeOptions([ fcOptions, newFcOptions ]);
10363
+ fcOptions = localeOptionHash[localeCode] = mergeOptions([ fcOptions, newFcOptions ]);
10169
10364
  }
10170
10365
 
10171
- // compute language options that weren't defined.
10366
+ // compute locale options that weren't defined.
10172
10367
  // always do this. newFcOptions can be undefined when initializing from i18n file,
10173
10368
  // so no way to tell if this is an initialization or a default-setting.
10174
- momOptions = getMomentLocaleData(langCode); // will fall back to en
10369
+ momOptions = getMomentLocaleData(localeCode); // will fall back to en
10175
10370
  $.each(momComputableOptions, function(name, func) {
10176
10371
  if (fcOptions[name] == null) {
10177
10372
  fcOptions[name] = func(momOptions, fcOptions);
10178
10373
  }
10179
10374
  });
10180
10375
 
10181
- // set it as the default language for FullCalendar
10182
- Calendar.defaults.lang = langCode;
10376
+ // set it as the default locale for FullCalendar
10377
+ Calendar.defaults.locale = localeCode;
10183
10378
  };
10184
10379
 
10185
10380
 
10186
- // NOTE: can't guarantee any of these computations will run because not every language has datepicker
10381
+ // NOTE: can't guarantee any of these computations will run because not every locale has datepicker
10187
10382
  // configs, so make sure there are English fallbacks for these in the defaults file.
10188
10383
  var dpComputableOptions = {
10189
10384
 
@@ -10233,7 +10428,7 @@ var momComputableOptions = {
10233
10428
  smallTimeFormat: function(momOptions) {
10234
10429
  return momOptions.longDateFormat('LT')
10235
10430
  .replace(':mm', '(:mm)')
10236
- .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs
10431
+ .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales
10237
10432
  .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
10238
10433
  },
10239
10434
 
@@ -10241,7 +10436,7 @@ var momComputableOptions = {
10241
10436
  extraSmallTimeFormat: function(momOptions) {
10242
10437
  return momOptions.longDateFormat('LT')
10243
10438
  .replace(':mm', '(:mm)')
10244
- .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs
10439
+ .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales
10245
10440
  .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand
10246
10441
  },
10247
10442
 
@@ -10249,7 +10444,7 @@ var momComputableOptions = {
10249
10444
  hourFormat: function(momOptions) {
10250
10445
  return momOptions.longDateFormat('LT')
10251
10446
  .replace(':mm', '')
10252
- .replace(/(\Wmm)$/, '') // like above, but for foreign langs
10447
+ .replace(/(\Wmm)$/, '') // like above, but for foreign locales
10253
10448
  .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
10254
10449
  },
10255
10450
 
@@ -10263,7 +10458,7 @@ var momComputableOptions = {
10263
10458
 
10264
10459
 
10265
10460
  // options that should be computed off live calendar options (considers override options)
10266
- // TODO: best place for this? related to lang?
10461
+ // TODO: best place for this? related to locale?
10267
10462
  // TODO: flipping text based on isRTL is a bad idea because the CSS `direction` might want to handle it
10268
10463
  var instanceComputableOptions = {
10269
10464
 
@@ -10300,17 +10495,14 @@ function populateInstanceComputableOptions(options) {
10300
10495
 
10301
10496
 
10302
10497
  // Returns moment's internal locale data. If doesn't exist, returns English.
10303
- // Works with moment-pre-2.8
10304
- function getMomentLocaleData(langCode) {
10305
- var func = moment.localeData || moment.langData;
10306
- return func.call(moment, langCode) ||
10307
- func.call(moment, 'en'); // the newer localData could return null, so fall back to en
10498
+ function getMomentLocaleData(localeCode) {
10499
+ return moment.localeData(localeCode) || moment.localeData('en');
10308
10500
  }
10309
10501
 
10310
10502
 
10311
10503
  // Initialize English by forcing computation of moment-derived options.
10312
10504
  // Also, sets it as the default.
10313
- FC.lang('en', Calendar.englishDefaults);
10505
+ FC.locale('en', Calendar.englishDefaults);
10314
10506
 
10315
10507
  ;;
10316
10508
 
@@ -10318,7 +10510,7 @@ FC.lang('en', Calendar.englishDefaults);
10318
10510
  ----------------------------------------------------------------------------------------------------------------------*/
10319
10511
  // TODO: rename all header-related things to "toolbar"
10320
10512
 
10321
- function Header(calendar, options) {
10513
+ function Header(calendar) {
10322
10514
  var t = this;
10323
10515
 
10324
10516
  // exports
@@ -10330,38 +10522,50 @@ function Header(calendar, options) {
10330
10522
  t.disableButton = disableButton;
10331
10523
  t.enableButton = enableButton;
10332
10524
  t.getViewsWithButtons = getViewsWithButtons;
10525
+ t.el = null; // mirrors local `el`
10333
10526
 
10334
10527
  // locals
10335
- var el = $();
10528
+ var el;
10336
10529
  var viewsWithButtons = [];
10337
10530
  var tm;
10338
10531
 
10339
10532
 
10533
+ // can be called repeatedly and will rerender
10340
10534
  function render() {
10535
+ var options = calendar.options;
10341
10536
  var sections = options.header;
10342
10537
 
10343
10538
  tm = options.theme ? 'ui' : 'fc';
10344
10539
 
10345
10540
  if (sections) {
10346
- el = $("<div class='fc-toolbar'/>")
10347
- .append(renderSection('left'))
10541
+ if (!el) {
10542
+ el = this.el = $("<div class='fc-toolbar'/>");
10543
+ }
10544
+ else {
10545
+ el.empty();
10546
+ }
10547
+ el.append(renderSection('left'))
10348
10548
  .append(renderSection('right'))
10349
10549
  .append(renderSection('center'))
10350
10550
  .append('<div class="fc-clear"/>');
10351
-
10352
- return el;
10551
+ }
10552
+ else {
10553
+ removeElement();
10353
10554
  }
10354
10555
  }
10355
10556
 
10356
10557
 
10357
10558
  function removeElement() {
10358
- el.remove();
10359
- el = $();
10559
+ if (el) {
10560
+ el.remove();
10561
+ el = t.el = null;
10562
+ }
10360
10563
  }
10361
10564
 
10362
10565
 
10363
10566
  function renderSection(position) {
10364
10567
  var sectionEl = $('<div class="fc-' + position + '"/>');
10568
+ var options = calendar.options;
10365
10569
  var buttonStr = options.header[position];
10366
10570
 
10367
10571
  if (buttonStr) {
@@ -10387,7 +10591,7 @@ function Header(calendar, options) {
10387
10591
  isOnlyButtons = false;
10388
10592
  }
10389
10593
  else {
10390
- if ((customButtonProps = (calendar.options.customButtons || {})[buttonName])) {
10594
+ if ((customButtonProps = (options.customButtons || {})[buttonName])) {
10391
10595
  buttonClick = function(ev) {
10392
10596
  if (customButtonProps.click) {
10393
10597
  customButtonProps.click.call(button[0], ev);
@@ -10523,33 +10727,43 @@ function Header(calendar, options) {
10523
10727
 
10524
10728
 
10525
10729
  function updateTitle(text) {
10526
- el.find('h2').text(text);
10730
+ if (el) {
10731
+ el.find('h2').text(text);
10732
+ }
10527
10733
  }
10528
10734
 
10529
10735
 
10530
10736
  function activateButton(buttonName) {
10531
- el.find('.fc-' + buttonName + '-button')
10532
- .addClass(tm + '-state-active');
10737
+ if (el) {
10738
+ el.find('.fc-' + buttonName + '-button')
10739
+ .addClass(tm + '-state-active');
10740
+ }
10533
10741
  }
10534
10742
 
10535
10743
 
10536
10744
  function deactivateButton(buttonName) {
10537
- el.find('.fc-' + buttonName + '-button')
10538
- .removeClass(tm + '-state-active');
10745
+ if (el) {
10746
+ el.find('.fc-' + buttonName + '-button')
10747
+ .removeClass(tm + '-state-active');
10748
+ }
10539
10749
  }
10540
10750
 
10541
10751
 
10542
10752
  function disableButton(buttonName) {
10543
- el.find('.fc-' + buttonName + '-button')
10544
- .prop('disabled', true)
10545
- .addClass(tm + '-state-disabled');
10753
+ if (el) {
10754
+ el.find('.fc-' + buttonName + '-button')
10755
+ .prop('disabled', true)
10756
+ .addClass(tm + '-state-disabled');
10757
+ }
10546
10758
  }
10547
10759
 
10548
10760
 
10549
10761
  function enableButton(buttonName) {
10550
- el.find('.fc-' + buttonName + '-button')
10551
- .prop('disabled', false)
10552
- .removeClass(tm + '-state-disabled');
10762
+ if (el) {
10763
+ el.find('.fc-' + buttonName + '-button')
10764
+ .prop('disabled', false)
10765
+ .removeClass(tm + '-state-disabled');
10766
+ }
10553
10767
  }
10554
10768
 
10555
10769
 
@@ -10572,7 +10786,7 @@ var ajaxDefaults = {
10572
10786
  var eventGUID = 1;
10573
10787
 
10574
10788
 
10575
- function EventManager(options) { // assumed to be a calendar
10789
+ function EventManager() { // assumed to be a calendar
10576
10790
  var t = this;
10577
10791
 
10578
10792
 
@@ -10609,7 +10823,7 @@ function EventManager(options) { // assumed to be a calendar
10609
10823
 
10610
10824
 
10611
10825
  $.each(
10612
- (options.events ? [ options.events ] : []).concat(options.eventSources || []),
10826
+ (t.options.events ? [ t.options.events ] : []).concat(t.options.eventSources || []),
10613
10827
  function(i, sourceInput) {
10614
10828
  var source = buildEventSource(sourceInput);
10615
10829
  if (source) {
@@ -10743,7 +10957,7 @@ function EventManager(options) { // assumed to be a calendar
10743
10957
  source,
10744
10958
  rangeStart.clone(),
10745
10959
  rangeEnd.clone(),
10746
- options.timezone,
10960
+ t.options.timezone,
10747
10961
  callback
10748
10962
  );
10749
10963
 
@@ -10766,7 +10980,7 @@ function EventManager(options) { // assumed to be a calendar
10766
10980
  t, // this, the Calendar object
10767
10981
  rangeStart.clone(),
10768
10982
  rangeEnd.clone(),
10769
- options.timezone,
10983
+ t.options.timezone,
10770
10984
  function(events) {
10771
10985
  callback(events);
10772
10986
  t.popLoading();
@@ -10801,9 +11015,9 @@ function EventManager(options) { // assumed to be a calendar
10801
11015
  // and not affect the passed-in object.
10802
11016
  var data = $.extend({}, customData || {});
10803
11017
 
10804
- var startParam = firstDefined(source.startParam, options.startParam);
10805
- var endParam = firstDefined(source.endParam, options.endParam);
10806
- var timezoneParam = firstDefined(source.timezoneParam, options.timezoneParam);
11018
+ var startParam = firstDefined(source.startParam, t.options.startParam);
11019
+ var endParam = firstDefined(source.endParam, t.options.endParam);
11020
+ var timezoneParam = firstDefined(source.timezoneParam, t.options.timezoneParam);
10807
11021
 
10808
11022
  if (startParam) {
10809
11023
  data[startParam] = rangeStart.format();
@@ -10811,8 +11025,8 @@ function EventManager(options) { // assumed to be a calendar
10811
11025
  if (endParam) {
10812
11026
  data[endParam] = rangeEnd.format();
10813
11027
  }
10814
- if (options.timezone && options.timezone != 'local') {
10815
- data[timezoneParam] = options.timezone;
11028
+ if (t.options.timezone && t.options.timezone != 'local') {
11029
+ data[timezoneParam] = t.options.timezone;
10816
11030
  }
10817
11031
 
10818
11032
  t.pushLoading();
@@ -11144,7 +11358,7 @@ function EventManager(options) { // assumed to be a calendar
11144
11358
 
11145
11359
  reportEvents(cache);
11146
11360
  }
11147
-
11361
+
11148
11362
 
11149
11363
  function clientEvents(filter) {
11150
11364
  if ($.isFunction(filter)) {
@@ -11158,7 +11372,33 @@ function EventManager(options) { // assumed to be a calendar
11158
11372
  }
11159
11373
  return cache; // else, return all
11160
11374
  }
11161
-
11375
+
11376
+
11377
+ // Makes sure all array event sources have their internal event objects
11378
+ // converted over to the Calendar's current timezone.
11379
+ t.rezoneArrayEventSources = function() {
11380
+ var i;
11381
+ var events;
11382
+ var j;
11383
+
11384
+ for (i = 0; i < sources.length; i++) {
11385
+ events = sources[i].events;
11386
+ if ($.isArray(events)) {
11387
+
11388
+ for (j = 0; j < events.length; j++) {
11389
+ rezoneEventDates(events[j]);
11390
+ }
11391
+ }
11392
+ }
11393
+ };
11394
+
11395
+ function rezoneEventDates(event) {
11396
+ event.start = t.moment(event.start);
11397
+ if (event.end) {
11398
+ event.end = t.moment(event.end);
11399
+ }
11400
+ backupEventDates(event);
11401
+ }
11162
11402
 
11163
11403
 
11164
11404
  /* Event Normalization
@@ -11174,8 +11414,8 @@ function EventManager(options) { // assumed to be a calendar
11174
11414
  var start, end;
11175
11415
  var allDay;
11176
11416
 
11177
- if (options.eventDataTransform) {
11178
- input = options.eventDataTransform(input);
11417
+ if (t.options.eventDataTransform) {
11418
+ input = t.options.eventDataTransform(input);
11179
11419
  }
11180
11420
  if (source && source.eventDataTransform) {
11181
11421
  input = source.eventDataTransform(input);
@@ -11241,7 +11481,7 @@ function EventManager(options) { // assumed to be a calendar
11241
11481
  if (allDay === undefined) { // still undefined? fallback to default
11242
11482
  allDay = firstDefined(
11243
11483
  source ? source.allDayDefault : undefined,
11244
- options.allDayDefault
11484
+ t.options.allDayDefault
11245
11485
  );
11246
11486
  // still undefined? normalizeEventDates will calculate it
11247
11487
  }
@@ -11253,6 +11493,7 @@ function EventManager(options) { // assumed to be a calendar
11253
11493
 
11254
11494
  return out;
11255
11495
  }
11496
+ t.buildEventFromInput = buildEventFromInput;
11256
11497
 
11257
11498
 
11258
11499
  // Normalizes and assigns the given dates to the given partially-formed event object.
@@ -11277,7 +11518,7 @@ function EventManager(options) { // assumed to be a calendar
11277
11518
  }
11278
11519
 
11279
11520
  if (!eventProps.end) {
11280
- if (options.forceEventDuration) {
11521
+ if (t.options.forceEventDuration) {
11281
11522
  eventProps.end = t.getDefaultEventEnd(eventProps.allDay, eventProps.start);
11282
11523
  }
11283
11524
  else {
@@ -11376,6 +11617,7 @@ function EventManager(options) { // assumed to be a calendar
11376
11617
 
11377
11618
  return events;
11378
11619
  }
11620
+ t.expandEvent = expandEvent;
11379
11621
 
11380
11622
 
11381
11623
 
@@ -11569,125 +11811,134 @@ function EventManager(options) { // assumed to be a calendar
11569
11811
  }
11570
11812
 
11571
11813
 
11572
- /* Business Hours
11573
- -----------------------------------------------------------------------------------------*/
11574
-
11575
- t.getBusinessHoursEvents = getBusinessHoursEvents;
11576
-
11577
-
11578
- // Returns an array of events as to when the business hours occur in the given view.
11579
- // Abuse of our event system :(
11580
- function getBusinessHoursEvents(wholeDay) {
11581
- var optionVal = options.businessHours;
11582
- var defaultVal = {
11583
- className: 'fc-nonbusiness',
11584
- start: '09:00',
11585
- end: '17:00',
11586
- dow: [ 1, 2, 3, 4, 5 ], // monday - friday
11587
- rendering: 'inverse-background'
11588
- };
11589
- var view = t.getView();
11590
- var eventInput;
11814
+ t.getEventCache = function() {
11815
+ return cache;
11816
+ };
11591
11817
 
11592
- if (optionVal) { // `true` (which means "use the defaults") or an override object
11593
- eventInput = $.extend(
11594
- {}, // copy to a new object in either case
11595
- defaultVal,
11596
- typeof optionVal === 'object' ? optionVal : {} // override the defaults
11597
- );
11598
- }
11818
+ }
11599
11819
 
11600
- if (eventInput) {
11601
11820
 
11602
- // if a whole-day series is requested, clear the start/end times
11603
- if (wholeDay) {
11604
- eventInput.start = null;
11605
- eventInput.end = null;
11606
- }
11821
+ // hook for external libs to manipulate event properties upon creation.
11822
+ // should manipulate the event in-place.
11823
+ Calendar.prototype.normalizeEvent = function(event) {
11824
+ };
11607
11825
 
11608
- return expandEvent(
11609
- buildEventFromInput(eventInput),
11610
- view.start,
11611
- view.end
11612
- );
11613
- }
11614
11826
 
11615
- return [];
11616
- }
11827
+ // Does the given span (start, end, and other location information)
11828
+ // fully contain the other?
11829
+ Calendar.prototype.spanContainsSpan = function(outerSpan, innerSpan) {
11830
+ var eventStart = outerSpan.start.clone().stripZone();
11831
+ var eventEnd = this.getEventEnd(outerSpan).stripZone();
11617
11832
 
11833
+ return innerSpan.start >= eventStart && innerSpan.end <= eventEnd;
11834
+ };
11618
11835
 
11619
- /* Overlapping / Constraining
11620
- -----------------------------------------------------------------------------------------*/
11621
-
11622
- t.isEventSpanAllowed = isEventSpanAllowed;
11623
- t.isExternalSpanAllowed = isExternalSpanAllowed;
11624
- t.isSelectionSpanAllowed = isSelectionSpanAllowed;
11625
11836
 
11837
+ // Returns a list of events that the given event should be compared against when being considered for a move to
11838
+ // the specified span. Attached to the Calendar's prototype because EventManager is a mixin for a Calendar.
11839
+ Calendar.prototype.getPeerEvents = function(span, event) {
11840
+ var cache = this.getEventCache();
11841
+ var peerEvents = [];
11842
+ var i, otherEvent;
11626
11843
 
11627
- // Determines if the given event can be relocated to the given span (unzoned start/end with other misc data)
11628
- function isEventSpanAllowed(span, event) {
11629
- var source = event.source || {};
11630
- var constraint = firstDefined(
11631
- event.constraint,
11632
- source.constraint,
11633
- options.eventConstraint
11634
- );
11635
- var overlap = firstDefined(
11636
- event.overlap,
11637
- source.overlap,
11638
- options.eventOverlap
11639
- );
11640
- return isSpanAllowed(span, constraint, overlap, event);
11844
+ for (i = 0; i < cache.length; i++) {
11845
+ otherEvent = cache[i];
11846
+ if (
11847
+ !event ||
11848
+ event._id !== otherEvent._id // don't compare the event to itself or other related [repeating] events
11849
+ ) {
11850
+ peerEvents.push(otherEvent);
11851
+ }
11641
11852
  }
11642
11853
 
11854
+ return peerEvents;
11855
+ };
11643
11856
 
11644
- // Determines if an external event can be relocated to the given span (unzoned start/end with other misc data)
11645
- function isExternalSpanAllowed(eventSpan, eventLocation, eventProps) {
11646
- var eventInput;
11647
- var event;
11648
11857
 
11649
- // note: very similar logic is in View's reportExternalDrop
11650
- if (eventProps) {
11651
- eventInput = $.extend({}, eventProps, eventLocation);
11652
- event = expandEvent(buildEventFromInput(eventInput))[0];
11653
- }
11858
+ // updates the "backup" properties, which are preserved in order to compute diffs later on.
11859
+ function backupEventDates(event) {
11860
+ event._allDay = event.allDay;
11861
+ event._start = event.start.clone();
11862
+ event._end = event.end ? event.end.clone() : null;
11863
+ }
11654
11864
 
11655
- if (event) {
11656
- return isEventSpanAllowed(eventSpan, event);
11657
- }
11658
- else { // treat it as a selection
11659
11865
 
11660
- return isSelectionSpanAllowed(eventSpan);
11661
- }
11866
+ /* Overlapping / Constraining
11867
+ -----------------------------------------------------------------------------------------*/
11868
+
11869
+
11870
+ // Determines if the given event can be relocated to the given span (unzoned start/end with other misc data)
11871
+ Calendar.prototype.isEventSpanAllowed = function(span, event) {
11872
+ var source = event.source || {};
11873
+
11874
+ var constraint = firstDefined(
11875
+ event.constraint,
11876
+ source.constraint,
11877
+ this.options.eventConstraint
11878
+ );
11879
+
11880
+ var overlap = firstDefined(
11881
+ event.overlap,
11882
+ source.overlap,
11883
+ this.options.eventOverlap
11884
+ );
11885
+
11886
+ return this.isSpanAllowed(span, constraint, overlap, event) &&
11887
+ (!this.options.eventAllow || this.options.eventAllow(span, event) !== false);
11888
+ };
11889
+
11890
+
11891
+ // Determines if an external event can be relocated to the given span (unzoned start/end with other misc data)
11892
+ Calendar.prototype.isExternalSpanAllowed = function(eventSpan, eventLocation, eventProps) {
11893
+ var eventInput;
11894
+ var event;
11895
+
11896
+ // note: very similar logic is in View's reportExternalDrop
11897
+ if (eventProps) {
11898
+ eventInput = $.extend({}, eventProps, eventLocation);
11899
+ event = this.expandEvent(
11900
+ this.buildEventFromInput(eventInput)
11901
+ )[0];
11662
11902
  }
11663
11903
 
11904
+ if (event) {
11905
+ return this.isEventSpanAllowed(eventSpan, event);
11906
+ }
11907
+ else { // treat it as a selection
11664
11908
 
11665
- // Determines the given span (unzoned start/end with other misc data) can be selected.
11666
- function isSelectionSpanAllowed(span) {
11667
- return isSpanAllowed(span, options.selectConstraint, options.selectOverlap);
11909
+ return this.isSelectionSpanAllowed(eventSpan);
11668
11910
  }
11911
+ };
11912
+
11669
11913
 
11914
+ // Determines the given span (unzoned start/end with other misc data) can be selected.
11915
+ Calendar.prototype.isSelectionSpanAllowed = function(span) {
11916
+ return this.isSpanAllowed(span, this.options.selectConstraint, this.options.selectOverlap) &&
11917
+ (!this.options.selectAllow || this.options.selectAllow(span) !== false);
11918
+ };
11670
11919
 
11671
- // Returns true if the given span (caused by an event drop/resize or a selection) is allowed to exist
11672
- // according to the constraint/overlap settings.
11673
- // `event` is not required if checking a selection.
11674
- function isSpanAllowed(span, constraint, overlap, event) {
11675
- var constraintEvents;
11676
- var anyContainment;
11677
- var peerEvents;
11678
- var i, peerEvent;
11679
- var peerOverlap;
11680
11920
 
11681
- // the range must be fully contained by at least one of produced constraint events
11682
- if (constraint != null) {
11921
+ // Returns true if the given span (caused by an event drop/resize or a selection) is allowed to exist
11922
+ // according to the constraint/overlap settings.
11923
+ // `event` is not required if checking a selection.
11924
+ Calendar.prototype.isSpanAllowed = function(span, constraint, overlap, event) {
11925
+ var constraintEvents;
11926
+ var anyContainment;
11927
+ var peerEvents;
11928
+ var i, peerEvent;
11929
+ var peerOverlap;
11683
11930
 
11684
- // not treated as an event! intermediate data structure
11685
- // TODO: use ranges in the future
11686
- constraintEvents = constraintToEvents(constraint);
11931
+ // the range must be fully contained by at least one of produced constraint events
11932
+ if (constraint != null) {
11933
+
11934
+ // not treated as an event! intermediate data structure
11935
+ // TODO: use ranges in the future
11936
+ constraintEvents = this.constraintToEvents(constraint);
11937
+ if (constraintEvents) { // not invalid
11687
11938
 
11688
11939
  anyContainment = false;
11689
11940
  for (i = 0; i < constraintEvents.length; i++) {
11690
- if (eventContainsRange(constraintEvents[i], span)) {
11941
+ if (this.spanContainsSpan(constraintEvents[i], span)) {
11691
11942
  anyContainment = true;
11692
11943
  break;
11693
11944
  }
@@ -11697,126 +11948,150 @@ function EventManager(options) { // assumed to be a calendar
11697
11948
  return false;
11698
11949
  }
11699
11950
  }
11951
+ }
11700
11952
 
11701
- peerEvents = t.getPeerEvents(span, event);
11953
+ peerEvents = this.getPeerEvents(span, event);
11702
11954
 
11703
- for (i = 0; i < peerEvents.length; i++) {
11704
- peerEvent = peerEvents[i];
11955
+ for (i = 0; i < peerEvents.length; i++) {
11956
+ peerEvent = peerEvents[i];
11705
11957
 
11706
- // there needs to be an actual intersection before disallowing anything
11707
- if (eventIntersectsRange(peerEvent, span)) {
11958
+ // there needs to be an actual intersection before disallowing anything
11959
+ if (this.eventIntersectsRange(peerEvent, span)) {
11960
+
11961
+ // evaluate overlap for the given range and short-circuit if necessary
11962
+ if (overlap === false) {
11963
+ return false;
11964
+ }
11965
+ // if the event's overlap is a test function, pass the peer event in question as the first param
11966
+ else if (typeof overlap === 'function' && !overlap(peerEvent, event)) {
11967
+ return false;
11968
+ }
11708
11969
 
11709
- // evaluate overlap for the given range and short-circuit if necessary
11710
- if (overlap === false) {
11970
+ // if we are computing if the given range is allowable for an event, consider the other event's
11971
+ // EventObject-specific or Source-specific `overlap` property
11972
+ if (event) {
11973
+ peerOverlap = firstDefined(
11974
+ peerEvent.overlap,
11975
+ (peerEvent.source || {}).overlap
11976
+ // we already considered the global `eventOverlap`
11977
+ );
11978
+ if (peerOverlap === false) {
11711
11979
  return false;
11712
11980
  }
11713
- // if the event's overlap is a test function, pass the peer event in question as the first param
11714
- else if (typeof overlap === 'function' && !overlap(peerEvent, event)) {
11981
+ // if the peer event's overlap is a test function, pass the subject event as the first param
11982
+ if (typeof peerOverlap === 'function' && !peerOverlap(event, peerEvent)) {
11715
11983
  return false;
11716
11984
  }
11717
-
11718
- // if we are computing if the given range is allowable for an event, consider the other event's
11719
- // EventObject-specific or Source-specific `overlap` property
11720
- if (event) {
11721
- peerOverlap = firstDefined(
11722
- peerEvent.overlap,
11723
- (peerEvent.source || {}).overlap
11724
- // we already considered the global `eventOverlap`
11725
- );
11726
- if (peerOverlap === false) {
11727
- return false;
11728
- }
11729
- // if the peer event's overlap is a test function, pass the subject event as the first param
11730
- if (typeof peerOverlap === 'function' && !peerOverlap(event, peerEvent)) {
11731
- return false;
11732
- }
11733
- }
11734
11985
  }
11735
11986
  }
11736
-
11737
- return true;
11738
11987
  }
11739
11988
 
11989
+ return true;
11990
+ };
11740
11991
 
11741
- // Given an event input from the API, produces an array of event objects. Possible event inputs:
11742
- // 'businessHours'
11743
- // An event ID (number or string)
11744
- // An object with specific start/end dates or a recurring event (like what businessHours accepts)
11745
- function constraintToEvents(constraintInput) {
11746
11992
 
11747
- if (constraintInput === 'businessHours') {
11748
- return getBusinessHoursEvents();
11749
- }
11993
+ // Given an event input from the API, produces an array of event objects. Possible event inputs:
11994
+ // 'businessHours'
11995
+ // An event ID (number or string)
11996
+ // An object with specific start/end dates or a recurring event (like what businessHours accepts)
11997
+ Calendar.prototype.constraintToEvents = function(constraintInput) {
11750
11998
 
11751
- if (typeof constraintInput === 'object') {
11752
- return expandEvent(buildEventFromInput(constraintInput));
11753
- }
11999
+ if (constraintInput === 'businessHours') {
12000
+ return this.getCurrentBusinessHourEvents();
12001
+ }
11754
12002
 
11755
- return clientEvents(constraintInput); // probably an ID
12003
+ if (typeof constraintInput === 'object') {
12004
+ if (constraintInput.start != null) { // needs to be event-like input
12005
+ return this.expandEvent(this.buildEventFromInput(constraintInput));
12006
+ }
12007
+ else {
12008
+ return null; // invalid
12009
+ }
11756
12010
  }
11757
12011
 
12012
+ return this.clientEvents(constraintInput); // probably an ID
12013
+ };
11758
12014
 
11759
- // Does the event's date range fully contain the given range?
11760
- // start/end already assumed to have stripped zones :(
11761
- function eventContainsRange(event, range) {
11762
- var eventStart = event.start.clone().stripZone();
11763
- var eventEnd = t.getEventEnd(event).stripZone();
11764
12015
 
11765
- return range.start >= eventStart && range.end <= eventEnd;
11766
- }
12016
+ // Does the event's date range intersect with the given range?
12017
+ // start/end already assumed to have stripped zones :(
12018
+ Calendar.prototype.eventIntersectsRange = function(event, range) {
12019
+ var eventStart = event.start.clone().stripZone();
12020
+ var eventEnd = this.getEventEnd(event).stripZone();
11767
12021
 
12022
+ return range.start < eventEnd && range.end > eventStart;
12023
+ };
11768
12024
 
11769
- // Does the event's date range intersect with the given range?
11770
- // start/end already assumed to have stripped zones :(
11771
- function eventIntersectsRange(event, range) {
11772
- var eventStart = event.start.clone().stripZone();
11773
- var eventEnd = t.getEventEnd(event).stripZone();
11774
12025
 
11775
- return range.start < eventEnd && range.end > eventStart;
11776
- }
12026
+ /* Business Hours
12027
+ -----------------------------------------------------------------------------------------*/
11777
12028
 
12029
+ var BUSINESS_HOUR_EVENT_DEFAULTS = {
12030
+ id: '_fcBusinessHours', // will relate events from different calls to expandEvent
12031
+ start: '09:00',
12032
+ end: '17:00',
12033
+ dow: [ 1, 2, 3, 4, 5 ], // monday - friday
12034
+ rendering: 'inverse-background'
12035
+ // classNames are defined in businessHoursSegClasses
12036
+ };
11778
12037
 
11779
- t.getEventCache = function() {
11780
- return cache;
11781
- };
12038
+ // Return events objects for business hours within the current view.
12039
+ // Abuse of our event system :(
12040
+ Calendar.prototype.getCurrentBusinessHourEvents = function(wholeDay) {
12041
+ return this.computeBusinessHourEvents(wholeDay, this.options.businessHours);
12042
+ };
11782
12043
 
11783
- }
12044
+ // Given a raw input value from options, return events objects for business hours within the current view.
12045
+ Calendar.prototype.computeBusinessHourEvents = function(wholeDay, input) {
12046
+ if (input === true) {
12047
+ return this.expandBusinessHourEvents(wholeDay, [ {} ]);
12048
+ }
12049
+ else if ($.isPlainObject(input)) {
12050
+ return this.expandBusinessHourEvents(wholeDay, [ input ]);
12051
+ }
12052
+ else if ($.isArray(input)) {
12053
+ return this.expandBusinessHourEvents(wholeDay, input, true);
12054
+ }
12055
+ else {
12056
+ return [];
12057
+ }
12058
+ };
11784
12059
 
12060
+ // inputs expected to be an array of objects.
12061
+ // if ignoreNoDow is true, will ignore entries that don't specify a day-of-week (dow) key.
12062
+ Calendar.prototype.expandBusinessHourEvents = function(wholeDay, inputs, ignoreNoDow) {
12063
+ var view = this.getView();
12064
+ var events = [];
12065
+ var i, input;
11785
12066
 
11786
- // hook for external libs to manipulate event properties upon creation.
11787
- // should manipulate the event in-place.
11788
- Calendar.prototype.normalizeEvent = function(event) {
11789
- };
12067
+ for (i = 0; i < inputs.length; i++) {
12068
+ input = inputs[i];
11790
12069
 
12070
+ if (ignoreNoDow && !input.dow) {
12071
+ continue;
12072
+ }
11791
12073
 
11792
- // Returns a list of events that the given event should be compared against when being considered for a move to
11793
- // the specified span. Attached to the Calendar's prototype because EventManager is a mixin for a Calendar.
11794
- Calendar.prototype.getPeerEvents = function(span, event) {
11795
- var cache = this.getEventCache();
11796
- var peerEvents = [];
11797
- var i, otherEvent;
12074
+ // give defaults. will make a copy
12075
+ input = $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, input);
11798
12076
 
11799
- for (i = 0; i < cache.length; i++) {
11800
- otherEvent = cache[i];
11801
- if (
11802
- !event ||
11803
- event._id !== otherEvent._id // don't compare the event to itself or other related [repeating] events
11804
- ) {
11805
- peerEvents.push(otherEvent);
12077
+ // if a whole-day series is requested, clear the start/end times
12078
+ if (wholeDay) {
12079
+ input.start = null;
12080
+ input.end = null;
11806
12081
  }
12082
+
12083
+ events.push.apply(events, // append
12084
+ this.expandEvent(
12085
+ this.buildEventFromInput(input),
12086
+ view.start,
12087
+ view.end
12088
+ )
12089
+ );
11807
12090
  }
11808
12091
 
11809
- return peerEvents;
12092
+ return events;
11810
12093
  };
11811
12094
 
11812
-
11813
- // updates the "backup" properties, which are preserved in order to compute diffs later on.
11814
- function backupEventDates(event) {
11815
- event._allDay = event.allDay;
11816
- event._start = event.start.clone();
11817
- event._end = event.end ? event.end.clone() : null;
11818
- }
11819
-
11820
12095
  ;;
11821
12096
 
11822
12097
  /* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells.
@@ -11832,7 +12107,8 @@ var BasicView = FC.BasicView = View.extend({
11832
12107
  dayGrid: null, // the main subcomponent that does most of the heavy lifting
11833
12108
 
11834
12109
  dayNumbersVisible: false, // display day numbers on each day cell?
11835
- weekNumbersVisible: false, // display week numbers along the side?
12110
+ colWeekNumbersVisible: false, // display week numbers along the side?
12111
+ cellWeekNumbersVisible: false, // display week numbers in day cell?
11836
12112
 
11837
12113
  weekNumberWidth: null, // width of all the week-number cells running down the side
11838
12114
 
@@ -11893,8 +12169,18 @@ var BasicView = FC.BasicView = View.extend({
11893
12169
  renderDates: function() {
11894
12170
 
11895
12171
  this.dayNumbersVisible = this.dayGrid.rowCnt > 1; // TODO: make grid responsible
11896
- this.weekNumbersVisible = this.opt('weekNumbers');
11897
- this.dayGrid.numbersVisible = this.dayNumbersVisible || this.weekNumbersVisible;
12172
+ if (this.opt('weekNumbers')) {
12173
+ if (this.opt('weekNumbersWithinDays')) {
12174
+ this.cellWeekNumbersVisible = true;
12175
+ this.colWeekNumbersVisible = false;
12176
+ }
12177
+ else {
12178
+ this.cellWeekNumbersVisible = false;
12179
+ this.colWeekNumbersVisible = true;
12180
+ };
12181
+ }
12182
+ this.dayGrid.numbersVisible = this.dayNumbersVisible ||
12183
+ this.cellWeekNumbersVisible || this.colWeekNumbersVisible;
11898
12184
 
11899
12185
  this.el.addClass('fc-basic-view').html(this.renderSkeletonHtml());
11900
12186
  this.renderHead();
@@ -11932,6 +12218,11 @@ var BasicView = FC.BasicView = View.extend({
11932
12218
  },
11933
12219
 
11934
12220
 
12221
+ unrenderBusinessHours: function() {
12222
+ this.dayGrid.unrenderBusinessHours();
12223
+ },
12224
+
12225
+
11935
12226
  // Builds the HTML skeleton for the view.
11936
12227
  // The day-grid component will render inside of a container defined by this HTML.
11937
12228
  renderSkeletonHtml: function() {
@@ -11973,7 +12264,7 @@ var BasicView = FC.BasicView = View.extend({
11973
12264
 
11974
12265
  // Refreshes the horizontal dimensions of the view
11975
12266
  updateWidth: function() {
11976
- if (this.weekNumbersVisible) {
12267
+ if (this.colWeekNumbersVisible) {
11977
12268
  // Make sure all week number cells running down the side have the same width.
11978
12269
  // Record the width for cells created later.
11979
12270
  this.weekNumberWidth = matchCellWidths(
@@ -12114,9 +12405,8 @@ var BasicView = FC.BasicView = View.extend({
12114
12405
  unrenderEvents: function() {
12115
12406
  this.dayGrid.unrenderEvents();
12116
12407
 
12117
- // we DON'T need to call updateHeight() because:
12118
- // A) a renderEvents() call always happens after this, which will eventually call updateHeight()
12119
- // B) in IE8, this causes a flash whenever events are rerendered
12408
+ // we DON'T need to call updateHeight() because
12409
+ // a renderEvents() call always happens after this, which will eventually call updateHeight()
12120
12410
  },
12121
12411
 
12122
12412
 
@@ -12161,7 +12451,7 @@ var basicDayGridMethods = {
12161
12451
  renderHeadIntroHtml: function() {
12162
12452
  var view = this.view;
12163
12453
 
12164
- if (view.weekNumbersVisible) {
12454
+ if (view.colWeekNumbersVisible) {
12165
12455
  return '' +
12166
12456
  '<th class="fc-week-number ' + view.widgetHeaderClass + '" ' + view.weekNumberStyleAttr() + '>' +
12167
12457
  '<span>' + // needed for matchCellWidths
@@ -12177,13 +12467,15 @@ var basicDayGridMethods = {
12177
12467
  // Generates the HTML that will go before content-skeleton cells that display the day/week numbers
12178
12468
  renderNumberIntroHtml: function(row) {
12179
12469
  var view = this.view;
12470
+ var weekStart = this.getCellDate(row, 0);
12180
12471
 
12181
- if (view.weekNumbersVisible) {
12472
+ if (view.colWeekNumbersVisible) {
12182
12473
  return '' +
12183
12474
  '<td class="fc-week-number" ' + view.weekNumberStyleAttr() + '>' +
12184
- '<span>' + // needed for matchCellWidths
12185
- this.getCellDate(row, 0).format('w') +
12186
- '</span>' +
12475
+ view.buildGotoAnchorHtml( // aside from link, important for matchCellWidths
12476
+ { date: weekStart, type: 'week', forceOff: this.colCnt === 1 },
12477
+ weekStart.format('w') // inner HTML
12478
+ ) +
12187
12479
  '</td>';
12188
12480
  }
12189
12481
 
@@ -12195,7 +12487,7 @@ var basicDayGridMethods = {
12195
12487
  renderBgIntroHtml: function() {
12196
12488
  var view = this.view;
12197
12489
 
12198
- if (view.weekNumbersVisible) {
12490
+ if (view.colWeekNumbersVisible) {
12199
12491
  return '<td class="fc-week-number ' + view.widgetContentClass + '" ' +
12200
12492
  view.weekNumberStyleAttr() + '></td>';
12201
12493
  }
@@ -12209,7 +12501,7 @@ var basicDayGridMethods = {
12209
12501
  renderIntroHtml: function() {
12210
12502
  var view = this.view;
12211
12503
 
12212
- if (view.weekNumbersVisible) {
12504
+ if (view.colWeekNumbersVisible) {
12213
12505
  return '<td class="fc-week-number" ' + view.weekNumberStyleAttr() + '></td>';
12214
12506
  }
12215
12507
 
@@ -12243,8 +12535,6 @@ var MonthView = FC.MonthView = BasicView.extend({
12243
12535
  // Overrides the default BasicView behavior to have special multi-week auto-height logic
12244
12536
  setGridHeight: function(height, isAuto) {
12245
12537
 
12246
- isAuto = isAuto || this.opt('weekMode') === 'variable'; // LEGACY: weekMode is deprecated
12247
-
12248
12538
  // if auto, make the height of each row the height that it would be if there were 6 weeks
12249
12539
  if (isAuto) {
12250
12540
  height *= this.rowCnt / 6;
@@ -12255,11 +12545,6 @@ var MonthView = FC.MonthView = BasicView.extend({
12255
12545
 
12256
12546
 
12257
12547
  isFixedWeeks: function() {
12258
- var weekMode = this.opt('weekMode'); // LEGACY: weekMode is deprecated
12259
- if (weekMode) {
12260
- return weekMode === 'fixed'; // if any other type of weekMode, assume NOT fixed
12261
- }
12262
-
12263
12548
  return this.opt('fixedWeekCount');
12264
12549
  }
12265
12550
 
@@ -12689,9 +12974,8 @@ var AgendaView = FC.AgendaView = View.extend({
12689
12974
  this.dayGrid.unrenderEvents();
12690
12975
  }
12691
12976
 
12692
- // we DON'T need to call updateHeight() because:
12693
- // A) a renderEvents() call always happens after this, which will eventually call updateHeight()
12694
- // B) in IE8, this causes a flash whenever events are rerendered
12977
+ // we DON'T need to call updateHeight() because
12978
+ // a renderEvents() call always happens after this, which will eventually call updateHeight()
12695
12979
  },
12696
12980
 
12697
12981
 
@@ -12759,9 +13043,10 @@ var agendaTimeGridMethods = {
12759
13043
 
12760
13044
  return '' +
12761
13045
  '<th class="fc-axis fc-week-number ' + view.widgetHeaderClass + '" ' + view.axisStyleAttr() + '>' +
12762
- '<span>' + // needed for matchCellWidths
12763
- htmlEscape(weekText) +
12764
- '</span>' +
13046
+ view.buildGotoAnchorHtml( // aside from link, important for matchCellWidths
13047
+ { date: this.start, type: 'week', forceOff: this.colCnt > 1 },
13048
+ htmlEscape(weekText) // inner HTML
13049
+ ) +
12765
13050
  '</th>';
12766
13051
  }
12767
13052
  else {
@@ -12800,7 +13085,7 @@ var agendaDayGridMethods = {
12800
13085
  return '' +
12801
13086
  '<td class="fc-axis ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' +
12802
13087
  '<span>' + // needed for matchCellWidths
12803
- (view.opt('allDayHtml') || htmlEscape(view.opt('allDayText'))) +
13088
+ view.getAllDayHtml() +
12804
13089
  '</span>' +
12805
13090
  '</td>';
12806
13091
  },
@@ -12834,7 +13119,6 @@ fcViews.agenda = {
12834
13119
  'class': AgendaView,
12835
13120
  defaults: {
12836
13121
  allDaySlot: true,
12837
- allDayText: 'all-day',
12838
13122
  slotDuration: '00:30:00',
12839
13123
  minTime: '00:00:00',
12840
13124
  maxTime: '24:00:00',
@@ -12853,5 +13137,293 @@ fcViews.agendaWeek = {
12853
13137
  };
12854
13138
  ;;
12855
13139
 
12856
- return FC; // export for Node/CommonJS
13140
+ /*
13141
+ Responsible for the scroller, and forwarding event-related actions into the "grid"
13142
+ */
13143
+ var ListView = View.extend({
13144
+
13145
+ grid: null,
13146
+ scroller: null,
13147
+
13148
+ initialize: function() {
13149
+ this.grid = new ListViewGrid(this);
13150
+ this.scroller = new Scroller({
13151
+ overflowX: 'hidden',
13152
+ overflowY: 'auto'
13153
+ });
13154
+ },
13155
+
13156
+ setRange: function(range) {
13157
+ View.prototype.setRange.call(this, range); // super
13158
+
13159
+ this.grid.setRange(range); // needs to process range-related options
13160
+ },
13161
+
13162
+ renderSkeleton: function() {
13163
+ this.el.addClass(
13164
+ 'fc-list-view ' +
13165
+ this.widgetContentClass
13166
+ );
13167
+
13168
+ this.scroller.render();
13169
+ this.scroller.el.appendTo(this.el);
13170
+
13171
+ this.grid.setElement(this.scroller.scrollEl);
13172
+ },
13173
+
13174
+ unrenderSkeleton: function() {
13175
+ this.scroller.destroy(); // will remove the Grid too
13176
+ },
13177
+
13178
+ setHeight: function(totalHeight, isAuto) {
13179
+ this.scroller.setHeight(this.computeScrollerHeight(totalHeight));
13180
+ },
13181
+
13182
+ computeScrollerHeight: function(totalHeight) {
13183
+ return totalHeight -
13184
+ subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
13185
+ },
13186
+
13187
+ renderEvents: function(events) {
13188
+ this.grid.renderEvents(events);
13189
+ },
13190
+
13191
+ unrenderEvents: function() {
13192
+ this.grid.unrenderEvents();
13193
+ },
13194
+
13195
+ isEventResizable: function(event) {
13196
+ return false;
13197
+ },
13198
+
13199
+ isEventDraggable: function(event) {
13200
+ return false;
13201
+ }
13202
+
13203
+ });
13204
+
13205
+ /*
13206
+ Responsible for event rendering and user-interaction.
13207
+ Its "el" is the inner-content of the above view's scroller.
13208
+ */
13209
+ var ListViewGrid = Grid.extend({
13210
+
13211
+ segSelector: '.fc-list-item', // which elements accept event actions
13212
+ hasDayInteractions: false, // no day selection or day clicking
13213
+
13214
+ // slices by day
13215
+ spanToSegs: function(span) {
13216
+ var view = this.view;
13217
+ var dayStart = view.start.clone();
13218
+ var dayEnd;
13219
+ var seg;
13220
+ var segs = [];
13221
+
13222
+ while (dayStart < view.end) {
13223
+ dayEnd = dayStart.clone().add(1, 'day');
13224
+ seg = intersectRanges(span, {
13225
+ start: dayStart,
13226
+ end: dayEnd
13227
+ });
13228
+ if (seg) {
13229
+ segs.push(seg);
13230
+ }
13231
+ dayStart = dayEnd;
13232
+ }
13233
+
13234
+ return segs;
13235
+ },
13236
+
13237
+ // like "4:00am"
13238
+ computeEventTimeFormat: function() {
13239
+ return this.view.opt('mediumTimeFormat');
13240
+ },
13241
+
13242
+ // for events with a url, the whole <tr> should be clickable,
13243
+ // but it's impossible to wrap with an <a> tag. simulate this.
13244
+ handleSegClick: function(seg, ev) {
13245
+ var url;
13246
+
13247
+ Grid.prototype.handleSegClick.apply(this, arguments); // super. might prevent the default action
13248
+
13249
+ // not clicking on or within an <a> with an href
13250
+ if (!$(ev.target).closest('a[href]').length) {
13251
+ url = seg.event.url;
13252
+ if (url && !ev.isDefaultPrevented()) { // jsEvent not cancelled in handler
13253
+ window.location.href = url; // simulate link click
13254
+ }
13255
+ }
13256
+ },
13257
+
13258
+ // returns list of foreground segs that were actually rendered
13259
+ renderFgSegs: function(segs) {
13260
+ segs = this.renderFgSegEls(segs); // might filter away hidden events
13261
+
13262
+ if (!segs.length) {
13263
+ this.renderEmptyMessage();
13264
+ return segs;
13265
+ }
13266
+ else {
13267
+ return this.renderSegList(segs);
13268
+ }
13269
+ },
13270
+
13271
+ renderEmptyMessage: function() {
13272
+ this.el.html(
13273
+ '<div class="fc-list-empty-wrap2">' + // TODO: try less wraps
13274
+ '<div class="fc-list-empty-wrap1">' +
13275
+ '<div class="fc-list-empty">' +
13276
+ htmlEscape(this.view.opt('noEventsMessage')) +
13277
+ '</div>' +
13278
+ '</div>' +
13279
+ '</div>'
13280
+ );
13281
+ },
13282
+
13283
+ // render the event segments in the view. returns the mutated array.
13284
+ renderSegList: function(segs) {
13285
+ var tableEl = $('<table class="fc-list-table"><tbody/></table>');
13286
+ var tbodyEl = tableEl.find('tbody');
13287
+ var i, seg;
13288
+ var dayDate;
13289
+
13290
+ this.sortEventSegs(segs);
13291
+
13292
+ for (i = 0; i < segs.length; i++) {
13293
+ seg = segs[i];
13294
+
13295
+ // append a day header
13296
+ if (!dayDate || !seg.start.isSame(dayDate, 'day')) {
13297
+ dayDate = seg.start.clone().stripTime();
13298
+ tbodyEl.append(this.dayHeaderHtml(dayDate));
13299
+ }
13300
+
13301
+ tbodyEl.append(seg.el); // append event row
13302
+ }
13303
+
13304
+ this.el.empty().append(tableEl);
13305
+
13306
+ return segs; // return the sorted list
13307
+ },
13308
+
13309
+ // generates the HTML for the day headers that live amongst the event rows
13310
+ dayHeaderHtml: function(dayDate) {
13311
+ var view = this.view;
13312
+ var mainFormat = view.opt('listDayFormat');
13313
+ var altFormat = view.opt('listDayAltFormat');
13314
+
13315
+ return '<tr class="fc-list-heading" data-date="' + dayDate.format('YYYY-MM-DD') + '">' +
13316
+ '<td class="' + view.widgetHeaderClass + '" colspan="3">' +
13317
+ (mainFormat ?
13318
+ view.buildGotoAnchorHtml(
13319
+ dayDate,
13320
+ { 'class': 'fc-list-heading-main' },
13321
+ htmlEscape(dayDate.format(mainFormat)) // inner HTML
13322
+ ) :
13323
+ '') +
13324
+ (altFormat ?
13325
+ view.buildGotoAnchorHtml(
13326
+ dayDate,
13327
+ { 'class': 'fc-list-heading-alt' },
13328
+ htmlEscape(dayDate.format(altFormat)) // inner HTML
13329
+ ) :
13330
+ '') +
13331
+ '</td>' +
13332
+ '</tr>';
13333
+ },
13334
+
13335
+ // generates the HTML for a single event row
13336
+ fgSegHtml: function(seg) {
13337
+ var view = this.view;
13338
+ var classes = [ 'fc-list-item' ].concat(this.getSegCustomClasses(seg));
13339
+ var bgColor = this.getSegBackgroundColor(seg);
13340
+ var event = seg.event;
13341
+ var url = event.url;
13342
+ var timeHtml;
13343
+
13344
+ if (!seg.start.hasTime()) {
13345
+ if (this.displayEventTime) {
13346
+ timeHtml = view.getAllDayHtml();
13347
+ }
13348
+ }
13349
+ else {
13350
+ timeHtml = htmlEscape(this.getEventTimeText(event)); // might return empty
13351
+ }
13352
+
13353
+ if (url) {
13354
+ classes.push('fc-has-url');
13355
+ }
13356
+
13357
+ return '<tr class="' + classes.join(' ') + '">' +
13358
+ (timeHtml ?
13359
+ '<td class="fc-list-item-time ' + view.widgetContentClass + '">' +
13360
+ timeHtml +
13361
+ '</td>' :
13362
+ '') +
13363
+ '<td class="fc-list-item-marker ' + view.widgetContentClass + '">' +
13364
+ '<span class="fc-event-dot"' +
13365
+ (bgColor ?
13366
+ ' style="background-color:' + bgColor + '"' :
13367
+ '') +
13368
+ '></span>' +
13369
+ '</td>' +
13370
+ '<td class="fc-list-item-title ' + view.widgetContentClass + '">' +
13371
+ '<a' + (url ? ' href="' + htmlEscape(url) + '"' : '') + '>' +
13372
+ htmlEscape(seg.event.title) +
13373
+ '</a>' +
13374
+ '</td>' +
13375
+ '</tr>';
13376
+ }
13377
+
13378
+ });
13379
+
13380
+ ;;
13381
+
13382
+ fcViews.list = {
13383
+ 'class': ListView,
13384
+ buttonTextKey: 'list', // what to lookup in locale files
13385
+ defaults: {
13386
+ buttonText: 'list', // text to display for English
13387
+ listTime: true, // show the time column?
13388
+ listDayFormat: 'LL', // like "January 1, 2016"
13389
+ noEventsMessage: 'No events to display'
13390
+ }
13391
+ };
13392
+
13393
+ fcViews.listDay = {
13394
+ type: 'list',
13395
+ duration: { days: 1 },
13396
+ defaults: {
13397
+ listDayFormat: 'dddd' // day-of-week is all we need. full date is probably in header
13398
+ }
13399
+ };
13400
+
13401
+ fcViews.listWeek = {
13402
+ type: 'list',
13403
+ duration: { weeks: 1 },
13404
+ defaults: {
13405
+ listDayFormat: 'dddd', // day-of-week is more important
13406
+ listDayAltFormat: 'LL'
13407
+ }
13408
+ };
13409
+
13410
+ fcViews.listMonth = {
13411
+ type: 'list',
13412
+ duration: { month: 1 },
13413
+ defaults: {
13414
+ listDayAltFormat: 'dddd' // day-of-week is nice-to-have
13415
+ }
13416
+ };
13417
+
13418
+ fcViews.listYear = {
13419
+ type: 'list',
13420
+ duration: { year: 1 },
13421
+ defaults: {
13422
+ listDayAltFormat: 'dddd' // day-of-week is nice-to-have
13423
+ }
13424
+ };
13425
+
13426
+ ;;
13427
+
13428
+ return FC; // export for Node/CommonJS
12857
13429
  });