fullcalendar-rails 2.1.1.0 → 2.2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -0
  3. data/lib/fullcalendar-rails/version.rb +1 -1
  4. data/vendor/assets/javascripts/fullcalendar.js +1340 -470
  5. data/vendor/assets/javascripts/fullcalendar/gcal.js +1 -1
  6. data/vendor/assets/javascripts/fullcalendar/lang-all.js +2 -2
  7. data/vendor/assets/javascripts/fullcalendar/lang/ar.js +1 -1
  8. data/vendor/assets/javascripts/fullcalendar/lang/cs.js +1 -1
  9. data/vendor/assets/javascripts/fullcalendar/lang/de-at.js +1 -1
  10. data/vendor/assets/javascripts/fullcalendar/lang/de.js +1 -1
  11. data/vendor/assets/javascripts/fullcalendar/lang/el.js +1 -1
  12. data/vendor/assets/javascripts/fullcalendar/lang/es.js +1 -1
  13. data/vendor/assets/javascripts/fullcalendar/lang/fa.js +1 -1
  14. data/vendor/assets/javascripts/fullcalendar/lang/fi.js +1 -1
  15. data/vendor/assets/javascripts/fullcalendar/lang/fr.js +1 -1
  16. data/vendor/assets/javascripts/fullcalendar/lang/hu.js +1 -1
  17. data/vendor/assets/javascripts/fullcalendar/lang/is.js +1 -1
  18. data/vendor/assets/javascripts/fullcalendar/lang/ko.js +1 -1
  19. data/vendor/assets/javascripts/fullcalendar/lang/lt.js +1 -1
  20. data/vendor/assets/javascripts/fullcalendar/lang/lv.js +1 -1
  21. data/vendor/assets/javascripts/fullcalendar/lang/nl.js +1 -1
  22. data/vendor/assets/javascripts/fullcalendar/lang/pl.js +1 -1
  23. data/vendor/assets/javascripts/fullcalendar/lang/ro.js +1 -1
  24. data/vendor/assets/javascripts/fullcalendar/lang/sk.js +1 -1
  25. data/vendor/assets/javascripts/fullcalendar/lang/sr-cyrl.js +1 -1
  26. data/vendor/assets/stylesheets/fullcalendar.css +58 -14
  27. data/vendor/assets/stylesheets/fullcalendar.print.css +2 -1
  28. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c1a3d9465867d20f837190078e851b7f2b75df14
4
- data.tar.gz: 25a76f957ea1fc17e5df76b5c798c3f030bb8375
3
+ metadata.gz: 39c7834b7ba126f3242c0091c7dac7977af0a556
4
+ data.tar.gz: eae6565cfcf9f0f662dac7e1c0c732995129e9ce
5
5
  SHA512:
6
- metadata.gz: c7d381caf66e5698d22c53d75d7fa232b31623115fb109fbb67af1c45d803710e73663463061b7ba12b7f8b7f08250633489b2acc59aa7c487efc23188ea6783
7
- data.tar.gz: ea41503ef6b05d3534b3280d5f54ffc6253543011f6ff4c35c881e89433feb43a499b35c748d59fc2f3f5146ada90cf114c4893d41f86a859df142ce527163a0
6
+ metadata.gz: bb227fa10b55e0c192e77e16351084eb38d5aadcc4530f73096488af5ec63617e34aade1599b199d713d415dce827015b39a525b6b14da807635ba96831852e7
7
+ data.tar.gz: 9a5f726f28ba0f22c9d9efd9ca21189d56f44b06b8e60a70b7c15e55adcdf5554dbfe47aea91e08399a65a986f9826219395947827d57b8491028d7282e5bc39
data/README.md CHANGED
@@ -23,6 +23,16 @@ If you need a specific version of FullCalendar (e.g X.Y.Z), you can explicitly r
23
23
 
24
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
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
+ gem 'momentjs-rails'
29
+
30
+ to your Gemfile and
31
+
32
+ //= require moment
33
+
34
+ to yout application.js manifest.
35
+
26
36
  Finally execute:
27
37
 
28
38
  $ bundle
@@ -1,5 +1,5 @@
1
1
  module Fullcalendar
2
2
  module Rails
3
- VERSION = "2.1.1.0"
3
+ VERSION = "2.2.0.0"
4
4
  end
5
5
  end
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * FullCalendar v2.1.1
2
+ * FullCalendar v2.2.0
3
3
  * Docs & License: http://arshaw.com/fullcalendar/
4
4
  * (c) 2013 Adam Shaw
5
5
  */
@@ -174,7 +174,7 @@ var rtlDefaults = {
174
174
 
175
175
  ;;
176
176
 
177
- var fc = $.fullCalendar = { version: "2.1.1" };
177
+ var fc = $.fullCalendar = { version: "2.2.0" };
178
178
  var fcViews = fc.views = {};
179
179
 
180
180
 
@@ -1321,7 +1321,7 @@ function EventManager(options) { // assumed to be a calendar
1321
1321
  var currentFetchID = 0;
1322
1322
  var pendingSourceCnt = 0;
1323
1323
  var loadingLevel = 0;
1324
- var cache = [];
1324
+ var cache = []; // holds events that have already been expanded
1325
1325
 
1326
1326
 
1327
1327
  $.each(
@@ -1362,24 +1362,29 @@ function EventManager(options) { // assumed to be a calendar
1362
1362
 
1363
1363
 
1364
1364
  function fetchEventSource(source, fetchID) {
1365
- _fetchEventSource(source, function(events) {
1365
+ _fetchEventSource(source, function(eventInputs) {
1366
1366
  var isArraySource = $.isArray(source.events);
1367
- var i;
1368
- var event;
1367
+ var i, eventInput;
1368
+ var abstractEvent;
1369
1369
 
1370
1370
  if (fetchID == currentFetchID) {
1371
1371
 
1372
- if (events) {
1373
- for (i=0; i<events.length; i++) {
1374
- event = events[i];
1372
+ if (eventInputs) {
1373
+ for (i = 0; i < eventInputs.length; i++) {
1374
+ eventInput = eventInputs[i];
1375
1375
 
1376
- // event array sources have already been convert to Event Objects
1377
- if (!isArraySource) {
1378
- event = buildEvent(event, source);
1376
+ if (isArraySource) { // array sources have already been convert to Event Objects
1377
+ abstractEvent = eventInput;
1378
+ }
1379
+ else {
1380
+ abstractEvent = buildEventFromInput(eventInput, source);
1379
1381
  }
1380
1382
 
1381
- if (event) {
1382
- cache.push(event);
1383
+ if (abstractEvent) { // not false (an invalid event)
1384
+ cache.push.apply(
1385
+ cache,
1386
+ expandEvent(abstractEvent) // add individual expanded events to the cache
1387
+ );
1383
1388
  }
1384
1389
  }
1385
1390
  }
@@ -1550,7 +1555,7 @@ function EventManager(options) { // assumed to be a calendar
1550
1555
  if ($.isArray(source.events)) {
1551
1556
  source.origArray = source.events; // for removeEventSource
1552
1557
  source.events = $.map(source.events, function(eventInput) {
1553
- return buildEvent(eventInput, source);
1558
+ return buildEventFromInput(eventInput, source);
1554
1559
  });
1555
1560
  }
1556
1561
 
@@ -1640,19 +1645,33 @@ function EventManager(options) { // assumed to be a calendar
1640
1645
  }
1641
1646
 
1642
1647
 
1643
-
1644
- function renderEvent(eventData, stick) {
1645
- var event = buildEvent(eventData);
1646
- if (event) {
1647
- if (!event.source) {
1648
- if (stick) {
1649
- stickySource.events.push(event);
1650
- event.source = stickySource;
1648
+ // returns the expanded events that were created
1649
+ function renderEvent(eventInput, stick) {
1650
+ var abstractEvent = buildEventFromInput(eventInput);
1651
+ var events;
1652
+ var i, event;
1653
+
1654
+ if (abstractEvent) { // not false (a valid input)
1655
+ events = expandEvent(abstractEvent);
1656
+
1657
+ for (i = 0; i < events.length; i++) {
1658
+ event = events[i];
1659
+
1660
+ if (!event.source) {
1661
+ if (stick) {
1662
+ stickySource.events.push(event);
1663
+ event.source = stickySource;
1664
+ }
1665
+ cache.push(event);
1651
1666
  }
1652
- cache.push(event);
1653
1667
  }
1668
+
1654
1669
  reportEvents(cache);
1670
+
1671
+ return events;
1655
1672
  }
1673
+
1674
+ return [];
1656
1675
  }
1657
1676
 
1658
1677
 
@@ -1723,49 +1742,108 @@ function EventManager(options) { // assumed to be a calendar
1723
1742
  /* Event Normalization
1724
1743
  -----------------------------------------------------------------------------*/
1725
1744
 
1726
- function buildEvent(data, source) { // source may be undefined!
1745
+
1746
+ // Given a raw object with key/value properties, returns an "abstract" Event object.
1747
+ // An "abstract" event is an event that, if recurring, will not have been expanded yet.
1748
+ // Will return `false` when input is invalid.
1749
+ // `source` is optional
1750
+ function buildEventFromInput(input, source) {
1727
1751
  var out = {};
1728
- var start;
1729
- var end;
1752
+ var start, end;
1730
1753
  var allDay;
1731
1754
  var allDayDefault;
1732
1755
 
1733
1756
  if (options.eventDataTransform) {
1734
- data = options.eventDataTransform(data);
1757
+ input = options.eventDataTransform(input);
1735
1758
  }
1736
1759
  if (source && source.eventDataTransform) {
1737
- data = source.eventDataTransform(data);
1760
+ input = source.eventDataTransform(input);
1738
1761
  }
1739
1762
 
1740
- start = t.moment(data.start || data.date); // "date" is an alias for "start"
1741
- if (!start.isValid()) {
1742
- return;
1763
+ // Copy all properties over to the resulting object.
1764
+ // The special-case properties will be copied over afterwards.
1765
+ $.extend(out, input);
1766
+
1767
+ if (source) {
1768
+ out.source = source;
1743
1769
  }
1744
1770
 
1745
- end = null;
1746
- if (data.end) {
1747
- end = t.moment(data.end);
1748
- if (!end.isValid()) {
1749
- return;
1771
+ out._id = input._id || (input.id === undefined ? '_fc' + eventGUID++ : input.id + '');
1772
+
1773
+ if (input.className) {
1774
+ if (typeof input.className == 'string') {
1775
+ out.className = input.className.split(/\s+/);
1776
+ }
1777
+ else { // assumed to be an array
1778
+ out.className = input.className;
1750
1779
  }
1751
1780
  }
1781
+ else {
1782
+ out.className = [];
1783
+ }
1752
1784
 
1753
- allDay = data.allDay;
1754
- if (allDay === undefined) {
1755
- allDayDefault = firstDefined(
1756
- source ? source.allDayDefault : undefined,
1757
- options.allDayDefault
1758
- );
1759
- if (allDayDefault !== undefined) {
1760
- // use the default
1761
- allDay = allDayDefault;
1785
+ start = input.start || input.date; // "date" is an alias for "start"
1786
+ end = input.end;
1787
+
1788
+ // parse as a time (Duration) if applicable
1789
+ if (isTimeString(start)) {
1790
+ start = moment.duration(start);
1791
+ }
1792
+ if (isTimeString(end)) {
1793
+ end = moment.duration(end);
1794
+ }
1795
+
1796
+ if (input.dow || moment.isDuration(start) || moment.isDuration(end)) {
1797
+
1798
+ // the event is "abstract" (recurring) so don't calculate exact start/end dates just yet
1799
+ out.start = start ? moment.duration(start) : null; // will be a Duration or null
1800
+ out.end = end ? moment.duration(end) : null; // will be a Duration or null
1801
+ out._recurring = true; // our internal marker
1802
+ }
1803
+ else {
1804
+
1805
+ if (start) {
1806
+ start = t.moment(start);
1807
+ if (!start.isValid()) {
1808
+ return false;
1809
+ }
1762
1810
  }
1763
- else {
1764
- // all dates need to have ambig time for the event to be considered allDay
1765
- allDay = !start.hasTime() && (!end || !end.hasTime());
1811
+
1812
+ if (end) {
1813
+ end = t.moment(end);
1814
+ if (!end.isValid()) {
1815
+ return false;
1816
+ }
1817
+ }
1818
+
1819
+ allDay = input.allDay;
1820
+ if (allDay === undefined) {
1821
+ allDayDefault = firstDefined(
1822
+ source ? source.allDayDefault : undefined,
1823
+ options.allDayDefault
1824
+ );
1825
+ if (allDayDefault !== undefined) {
1826
+ // use the default
1827
+ allDay = allDayDefault;
1828
+ }
1829
+ else {
1830
+ // if a single date has a time, the event should not be all-day
1831
+ allDay = !start.hasTime() && (!end || !end.hasTime());
1832
+ }
1766
1833
  }
1834
+
1835
+ assignDatesToEvent(start, end, allDay, out);
1767
1836
  }
1768
1837
 
1838
+ return out;
1839
+ }
1840
+
1841
+
1842
+ // Normalizes and assigns the given dates to the given partially-formed event object.
1843
+ // Requires an explicit `allDay` boolean parameter.
1844
+ // NOTE: mutates the given start/end moments. does not make an internal copy
1845
+ function assignDatesToEvent(start, end, allDay, event) {
1846
+
1769
1847
  // normalize the date based on allDay
1770
1848
  if (allDay) {
1771
1849
  // neither date should have a time
@@ -1786,39 +1864,88 @@ function EventManager(options) { // assumed to be a calendar
1786
1864
  }
1787
1865
  }
1788
1866
 
1789
- // Copy all properties over to the resulting object.
1790
- // The special-case properties will be copied over afterwards.
1791
- $.extend(out, data);
1867
+ event.allDay = allDay;
1868
+ event.start = start;
1869
+ event.end = end || null; // ensure null if falsy
1792
1870
 
1793
- if (source) {
1794
- out.source = source;
1871
+ if (options.forceEventDuration && !event.end) {
1872
+ event.end = getEventEnd(event);
1795
1873
  }
1796
1874
 
1797
- out._id = data._id || (data.id === undefined ? '_fc' + eventGUID++ : data.id + '');
1875
+ backupEventDates(event);
1876
+ }
1798
1877
 
1799
- if (data.className) {
1800
- if (typeof data.className == 'string') {
1801
- out.className = data.className.split(/\s+/);
1802
- }
1803
- else { // assumed to be an array
1804
- out.className = data.className;
1805
- }
1806
- }
1807
- else {
1808
- out.className = [];
1809
- }
1810
1878
 
1811
- out.allDay = allDay;
1812
- out.start = start;
1813
- out.end = end;
1879
+ // If the given event is a recurring event, break it down into an array of individual instances.
1880
+ // If not a recurring event, return an array with the single original event.
1881
+ // If given a falsy input (probably because of a failed buildEventFromInput call), returns an empty array.
1882
+ function expandEvent(abstractEvent) {
1883
+ var events = [];
1884
+ var view;
1885
+ var _rangeStart = rangeStart;
1886
+ var _rangeEnd = rangeEnd;
1887
+ var dowHash;
1888
+ var dow;
1889
+ var i;
1890
+ var date;
1891
+ var startTime, endTime;
1892
+ var start, end;
1893
+ var event;
1814
1894
 
1815
- if (options.forceEventDuration && !out.end) {
1816
- out.end = getEventEnd(out);
1895
+ // hack for when fetchEvents hasn't been called yet (calculating businessHours for example)
1896
+ if (!_rangeStart || !_rangeEnd) {
1897
+ view = t.getView();
1898
+ _rangeStart = view.start;
1899
+ _rangeEnd = view.end;
1817
1900
  }
1818
1901
 
1819
- backupEventDates(out);
1902
+ if (abstractEvent) {
1903
+ if (abstractEvent._recurring) {
1820
1904
 
1821
- return out;
1905
+ // make a boolean hash as to whether the event occurs on each day-of-week
1906
+ if ((dow = abstractEvent.dow)) {
1907
+ dowHash = {};
1908
+ for (i = 0; i < dow.length; i++) {
1909
+ dowHash[dow[i]] = true;
1910
+ }
1911
+ }
1912
+
1913
+ // iterate through every day in the current range
1914
+ date = _rangeStart.clone().stripTime(); // holds the date of the current day
1915
+ while (date.isBefore(_rangeEnd)) {
1916
+
1917
+ if (!dowHash || dowHash[date.day()]) { // if everyday, or this particular day-of-week
1918
+
1919
+ startTime = abstractEvent.start; // the stored start and end properties are times (Durations)
1920
+ endTime = abstractEvent.end; // "
1921
+ start = date.clone();
1922
+ end = null;
1923
+
1924
+ if (startTime) {
1925
+ start = start.time(startTime);
1926
+ }
1927
+ if (endTime) {
1928
+ end = date.clone().time(endTime);
1929
+ }
1930
+
1931
+ event = $.extend({}, abstractEvent); // make a copy of the original
1932
+ assignDatesToEvent(
1933
+ start, end,
1934
+ !startTime && !endTime, // allDay?
1935
+ event
1936
+ );
1937
+ events.push(event);
1938
+ }
1939
+
1940
+ date.add(1, 'days');
1941
+ }
1942
+ }
1943
+ else {
1944
+ events.push(abstractEvent); // return the original event. will be a one-item array
1945
+ }
1946
+ }
1947
+
1948
+ return events;
1822
1949
  }
1823
1950
 
1824
1951
 
@@ -1995,6 +2122,202 @@ function EventManager(options) { // assumed to be a calendar
1995
2122
  };
1996
2123
  }
1997
2124
 
2125
+
2126
+ /* Business Hours
2127
+ -----------------------------------------------------------------------------------------*/
2128
+
2129
+ t.getBusinessHoursEvents = getBusinessHoursEvents;
2130
+
2131
+
2132
+ // Returns an array of events as to when the business hours occur in the current view.
2133
+ // Abuse of our event system :(
2134
+ function getBusinessHoursEvents() {
2135
+ var optionVal = options.businessHours;
2136
+ var defaultVal = {
2137
+ className: 'fc-nonbusiness',
2138
+ start: '09:00',
2139
+ end: '17:00',
2140
+ dow: [ 1, 2, 3, 4, 5 ], // monday - friday
2141
+ rendering: 'inverse-background'
2142
+ };
2143
+ var eventInput;
2144
+
2145
+ if (optionVal) {
2146
+ if (typeof optionVal === 'object') {
2147
+ // option value is an object that can override the default business hours
2148
+ eventInput = $.extend({}, defaultVal, optionVal);
2149
+ }
2150
+ else {
2151
+ // option value is `true`. use default business hours
2152
+ eventInput = defaultVal;
2153
+ }
2154
+ }
2155
+
2156
+ if (eventInput) {
2157
+ return expandEvent(buildEventFromInput(eventInput));
2158
+ }
2159
+
2160
+ return [];
2161
+ }
2162
+
2163
+
2164
+ /* Overlapping / Constraining
2165
+ -----------------------------------------------------------------------------------------*/
2166
+
2167
+ t.isEventAllowedInRange = isEventAllowedInRange;
2168
+ t.isSelectionAllowedInRange = isSelectionAllowedInRange;
2169
+ t.isExternalDragAllowedInRange = isExternalDragAllowedInRange;
2170
+
2171
+
2172
+ function isEventAllowedInRange(event, start, end) {
2173
+ var source = event.source || {};
2174
+ var constraint = firstDefined(
2175
+ event.constraint,
2176
+ source.constraint,
2177
+ options.eventConstraint
2178
+ );
2179
+ var overlap = firstDefined(
2180
+ event.overlap,
2181
+ source.overlap,
2182
+ options.eventOverlap
2183
+ );
2184
+
2185
+ return isRangeAllowed(start, end, constraint, overlap, event);
2186
+ }
2187
+
2188
+
2189
+ function isSelectionAllowedInRange(start, end) {
2190
+ return isRangeAllowed(
2191
+ start,
2192
+ end,
2193
+ options.selectConstraint,
2194
+ options.selectOverlap
2195
+ );
2196
+ }
2197
+
2198
+
2199
+ function isExternalDragAllowedInRange(start, end, eventInput) { // eventInput is optional associated event data
2200
+ var event;
2201
+
2202
+ if (eventInput) {
2203
+ event = expandEvent(buildEventFromInput(eventInput))[0];
2204
+ if (event) {
2205
+ return isEventAllowedInRange(event, start, end);
2206
+ }
2207
+ }
2208
+
2209
+ return isSelectionAllowedInRange(start, end); // treat it as a selection
2210
+ }
2211
+
2212
+
2213
+ // Returns true if the given range (caused by an event drop/resize or a selection) is allowed to exist
2214
+ // according to the constraint/overlap settings.
2215
+ // `event` is not required if checking a selection.
2216
+ function isRangeAllowed(start, end, constraint, overlap, event) {
2217
+ var constraintEvents;
2218
+ var anyContainment;
2219
+ var i, otherEvent;
2220
+ var otherOverlap;
2221
+
2222
+ // normalize. fyi, we're normalizing in too many places :(
2223
+ start = start.clone().stripZone();
2224
+ end = end.clone().stripZone();
2225
+
2226
+ // the range must be fully contained by at least one of produced constraint events
2227
+ if (constraint != null) {
2228
+ constraintEvents = constraintToEvents(constraint);
2229
+ anyContainment = false;
2230
+
2231
+ for (i = 0; i < constraintEvents.length; i++) {
2232
+ if (eventContainsRange(constraintEvents[i], start, end)) {
2233
+ anyContainment = true;
2234
+ break;
2235
+ }
2236
+ }
2237
+
2238
+ if (!anyContainment) {
2239
+ return false;
2240
+ }
2241
+ }
2242
+
2243
+ for (i = 0; i < cache.length; i++) { // loop all events and detect overlap
2244
+ otherEvent = cache[i];
2245
+
2246
+ // don't compare the event to itself or other related [repeating] events
2247
+ if (event && event._id === otherEvent._id) {
2248
+ continue;
2249
+ }
2250
+
2251
+ // there needs to be an actual intersection before disallowing anything
2252
+ if (eventIntersectsRange(otherEvent, start, end)) {
2253
+
2254
+ // evaluate overlap for the given range and short-circuit if necessary
2255
+ if (overlap === false) {
2256
+ return false;
2257
+ }
2258
+ else if (typeof overlap === 'function' && !overlap(otherEvent, event)) {
2259
+ return false;
2260
+ }
2261
+
2262
+ // if we are computing if the given range is allowable for an event, consider the other event's
2263
+ // EventObject-specific or Source-specific `overlap` property
2264
+ if (event) {
2265
+ otherOverlap = firstDefined(
2266
+ otherEvent.overlap,
2267
+ (otherEvent.source || {}).overlap
2268
+ // we already considered the global `eventOverlap`
2269
+ );
2270
+ if (otherOverlap === false) {
2271
+ return false;
2272
+ }
2273
+ if (typeof otherOverlap === 'function' && !otherOverlap(event, otherEvent)) {
2274
+ return false;
2275
+ }
2276
+ }
2277
+ }
2278
+ }
2279
+
2280
+ return true;
2281
+ }
2282
+
2283
+
2284
+ // Given an event input from the API, produces an array of event objects. Possible event inputs:
2285
+ // 'businessHours'
2286
+ // An event ID (number or string)
2287
+ // An object with specific start/end dates or a recurring event (like what businessHours accepts)
2288
+ function constraintToEvents(constraintInput) {
2289
+
2290
+ if (constraintInput === 'businessHours') {
2291
+ return getBusinessHoursEvents();
2292
+ }
2293
+
2294
+ if (typeof constraintInput === 'object') {
2295
+ return expandEvent(buildEventFromInput(constraintInput));
2296
+ }
2297
+
2298
+ return clientEvents(constraintInput); // probably an ID
2299
+ }
2300
+
2301
+
2302
+ // Is the event's date ranged fully contained by the given range?
2303
+ // start/end already assumed to have stripped zones :(
2304
+ function eventContainsRange(event, start, end) {
2305
+ var eventStart = event.start.clone().stripZone();
2306
+ var eventEnd = t.getEventEnd(event).stripZone();
2307
+
2308
+ return start >= eventStart && end <= eventEnd;
2309
+ }
2310
+
2311
+
2312
+ // Does the event's date range intersect with the given range?
2313
+ // start/end already assumed to have stripped zones :(
2314
+ function eventIntersectsRange(event, start, end) {
2315
+ var eventStart = event.start.clone().stripZone();
2316
+ var eventEnd = t.getEventEnd(event).stripZone();
2317
+
2318
+ return start < eventEnd && end > eventStart;
2319
+ }
2320
+
1998
2321
  }
1999
2322
 
2000
2323
 
@@ -2040,6 +2363,18 @@ function uncompensateScroll(rowEls) {
2040
2363
  }
2041
2364
 
2042
2365
 
2366
+ // Make the mouse cursor express that an event is not allowed in the current area
2367
+ function disableCursor() {
2368
+ $('body').addClass('fc-not-allowed');
2369
+ }
2370
+
2371
+
2372
+ // Returns the mouse cursor to its original look
2373
+ function enableCursor() {
2374
+ $('body').removeClass('fc-not-allowed');
2375
+ }
2376
+
2377
+
2043
2378
  // Given a total available height to fill, have `els` (essentially child rows) expand to accomodate.
2044
2379
  // By default, all elements that are shorter than the recommended height are expanded uniformly, not considering
2045
2380
  // any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and
@@ -2269,6 +2604,12 @@ function dateCompare(a, b) { // works with Moments and native Dates
2269
2604
  }
2270
2605
 
2271
2606
 
2607
+ // Returns a boolean about whether the given input is a time string, like "06:40:00" or "06:00"
2608
+ function isTimeString(str) {
2609
+ return /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str);
2610
+ }
2611
+
2612
+
2272
2613
  /* General Utilities
2273
2614
  ----------------------------------------------------------------------------------------------------------------------*/
2274
2615
 
@@ -2283,17 +2624,6 @@ function createObject(proto) {
2283
2624
  }
2284
2625
 
2285
2626
 
2286
- // Copies specifically-owned (non-protoype) properties of `b` onto `a`.
2287
- // FYI, $.extend would copy *all* properties of `b` onto `a`.
2288
- function extend(a, b) {
2289
- for (var i in b) {
2290
- if (b.hasOwnProperty(i)) {
2291
- a[i] = b[i];
2292
- }
2293
- }
2294
- }
2295
-
2296
-
2297
2627
  function applyAll(functions, thisObj, args) {
2298
2628
  if ($.isFunction(functions)) {
2299
2629
  functions = [ functions ];
@@ -2376,13 +2706,18 @@ function debounce(func, wait) {
2376
2706
  var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/;
2377
2707
  var ambigTimeOrZoneRegex =
2378
2708
  /^\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+)?)?)?)?)?$/;
2709
+ var newMomentProto = moment.fn; // where we will attach our new methods
2710
+ var oldMomentProto = $.extend({}, newMomentProto); // copy of original moment methods
2711
+ var allowValueOptimization;
2712
+ var setUTCValues; // function defined below
2713
+ var setLocalValues; // function defined below
2379
2714
 
2380
2715
 
2381
2716
  // Creating
2382
2717
  // -------------------------------------------------------------------------------------------------
2383
2718
 
2384
2719
  // Creates a new moment, similar to the vanilla moment(...) constructor, but with
2385
- // extra features (ambiguous time, enhanced formatting). When gived an existing moment,
2720
+ // extra features (ambiguous time, enhanced formatting). When given an existing moment,
2386
2721
  // it will function as a clone (and retain the zone of the moment). Anything else will
2387
2722
  // result in a moment in the local zone.
2388
2723
  fc.moment = function() {
@@ -2393,7 +2728,8 @@ fc.moment = function() {
2393
2728
  fc.moment.utc = function() {
2394
2729
  var mom = makeMoment(arguments, true);
2395
2730
 
2396
- // Force it into UTC because makeMoment doesn't guarantee it.
2731
+ // Force it into UTC because makeMoment doesn't guarantee it
2732
+ // (if given a pre-existing moment for example)
2397
2733
  if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone
2398
2734
  mom.utc();
2399
2735
  }
@@ -2407,8 +2743,8 @@ fc.moment.parseZone = function() {
2407
2743
  return makeMoment(arguments, true, true);
2408
2744
  };
2409
2745
 
2410
- // Builds an FCMoment from args. When given an existing moment, it clones. When given a native
2411
- // Date, or called with no arguments (the current time), the resulting moment will be local.
2746
+ // Builds an enhanced moment from args. When given an existing moment, it clones. When given a
2747
+ // native Date, or called with no arguments (the current time), the resulting moment will be local.
2412
2748
  // Anything else needs to be "parsed" (a string or an array), and will be affected by:
2413
2749
  // parseAsUTC - if there is no zone information, should we parse the input in UTC?
2414
2750
  // parseZone - if there is zone information, should we force the zone of the moment?
@@ -2418,21 +2754,14 @@ function makeMoment(args, parseAsUTC, parseZone) {
2418
2754
  var isAmbigTime;
2419
2755
  var isAmbigZone;
2420
2756
  var ambigMatch;
2421
- var output; // an object with fields for the new FCMoment object
2757
+ var mom;
2422
2758
 
2423
2759
  if (moment.isMoment(input)) {
2424
- output = moment.apply(null, args); // clone it
2425
-
2426
- // the ambig properties have not been preserved in the clone, so reassign them
2427
- if (input._ambigTime) {
2428
- output._ambigTime = true;
2429
- }
2430
- if (input._ambigZone) {
2431
- output._ambigZone = true;
2432
- }
2760
+ mom = moment.apply(null, args); // clone it
2761
+ transferAmbigs(input, mom); // the ambig flags weren't transfered with the clone
2433
2762
  }
2434
2763
  else if (isNativeDate(input) || input === undefined) {
2435
- output = moment.apply(null, args); // will be local
2764
+ mom = moment.apply(null, args); // will be local
2436
2765
  }
2437
2766
  else { // "parsing" is required
2438
2767
  isAmbigTime = false;
@@ -2458,44 +2787,44 @@ function makeMoment(args, parseAsUTC, parseZone) {
2458
2787
  // otherwise, probably a string with a format
2459
2788
 
2460
2789
  if (parseAsUTC) {
2461
- output = moment.utc.apply(moment, args);
2790
+ mom = moment.utc.apply(moment, args);
2462
2791
  }
2463
2792
  else {
2464
- output = moment.apply(null, args);
2793
+ mom = moment.apply(null, args);
2465
2794
  }
2466
2795
 
2467
2796
  if (isAmbigTime) {
2468
- output._ambigTime = true;
2469
- output._ambigZone = true; // ambiguous time always means ambiguous zone
2797
+ mom._ambigTime = true;
2798
+ mom._ambigZone = true; // ambiguous time always means ambiguous zone
2470
2799
  }
2471
2800
  else if (parseZone) { // let's record the inputted zone somehow
2472
2801
  if (isAmbigZone) {
2473
- output._ambigZone = true;
2802
+ mom._ambigZone = true;
2474
2803
  }
2475
2804
  else if (isSingleString) {
2476
- output.zone(input); // if not a valid zone, will assign UTC
2805
+ mom.zone(input); // if not a valid zone, will assign UTC
2477
2806
  }
2478
2807
  }
2479
2808
  }
2480
2809
 
2481
- return new FCMoment(output);
2482
- }
2810
+ mom._fullCalendar = true; // flag for extended functionality
2483
2811
 
2484
- // Our subclass of Moment.
2485
- // Accepts an object with the internal Moment properties that should be copied over to
2486
- // `this` object (most likely another Moment object). The values in this data must not
2487
- // be referenced by anything else (two moments sharing a Date object for example).
2488
- function FCMoment(internalData) {
2489
- extend(this, internalData);
2812
+ return mom;
2490
2813
  }
2491
2814
 
2492
- // Chain the prototype to Moment's
2493
- FCMoment.prototype = createObject(moment.fn);
2494
2815
 
2495
- // We need this because Moment's implementation won't create an FCMoment,
2496
- // nor will it copy over the ambig flags.
2497
- FCMoment.prototype.clone = function() {
2498
- return makeMoment([ this ]);
2816
+ // A clone method that works with the flags related to our enhanced functionality.
2817
+ // In the future, use moment.momentProperties
2818
+ newMomentProto.clone = function() {
2819
+ var mom = oldMomentProto.clone.apply(this, arguments);
2820
+
2821
+ // these flags weren't transfered with the clone
2822
+ transferAmbigs(this, mom);
2823
+ if (this._fullCalendar) {
2824
+ mom._fullCalendar = true;
2825
+ }
2826
+
2827
+ return mom;
2499
2828
  };
2500
2829
 
2501
2830
 
@@ -2509,7 +2838,14 @@ FCMoment.prototype.clone = function() {
2509
2838
  // SETTER
2510
2839
  // You can supply a Duration, a Moment, or a Duration-like argument.
2511
2840
  // When setting the time, and the moment has an ambiguous time, it then becomes unambiguous.
2512
- FCMoment.prototype.time = function(time) {
2841
+ newMomentProto.time = function(time) {
2842
+
2843
+ // Fallback to the original method (if there is one) if this moment wasn't created via FullCalendar.
2844
+ // `time` is a generic enough method name where this precaution is necessary to avoid collisions w/ other plugins.
2845
+ if (!this._fullCalendar) {
2846
+ return oldMomentProto.time.apply(this, arguments);
2847
+ }
2848
+
2513
2849
  if (time == null) { // getter
2514
2850
  return moment.duration({
2515
2851
  hours: this.hours(),
@@ -2520,7 +2856,7 @@ FCMoment.prototype.time = function(time) {
2520
2856
  }
2521
2857
  else { // setter
2522
2858
 
2523
- delete this._ambigTime; // mark that the moment now has a time
2859
+ this._ambigTime = false; // mark that the moment now has a time
2524
2860
 
2525
2861
  if (!moment.isDuration(time) && !moment.isMoment(time)) {
2526
2862
  time = moment.duration(time);
@@ -2545,22 +2881,14 @@ FCMoment.prototype.time = function(time) {
2545
2881
  // Converts the moment to UTC, stripping out its time-of-day and timezone offset,
2546
2882
  // but preserving its YMD. A moment with a stripped time will display no time
2547
2883
  // nor timezone offset when .format() is called.
2548
- FCMoment.prototype.stripTime = function() {
2884
+ newMomentProto.stripTime = function() {
2549
2885
  var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array
2550
2886
 
2551
- // set the internal UTC flag
2552
- moment.fn.utc.call(this); // call the original method, because we don't want to affect _ambigZone
2887
+ this.utc(); // set the internal UTC flag (will clear the ambig flags)
2888
+ setUTCValues(this, a.slice(0, 3)); // set the year/month/date. time will be zero
2553
2889
 
2554
- this.year(a[0]) // TODO: find a way to do this in one shot
2555
- .month(a[1])
2556
- .date(a[2])
2557
- .hours(0)
2558
- .minutes(0)
2559
- .seconds(0)
2560
- .milliseconds(0);
2561
-
2562
- // Mark the time as ambiguous. This needs to happen after the .utc() call, which calls .zone(), which
2563
- // clears all ambig flags. Same concept with the .year/month/date calls in the case of moment-timezone.
2890
+ // Mark the time as ambiguous. This needs to happen after the .utc() call, which calls .zone(),
2891
+ // which clears all ambig flags. Same with setUTCValues with moment-timezone.
2564
2892
  this._ambigTime = true;
2565
2893
  this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset
2566
2894
 
@@ -2568,7 +2896,7 @@ FCMoment.prototype.stripTime = function() {
2568
2896
  };
2569
2897
 
2570
2898
  // Returns if the moment has a non-ambiguous time (boolean)
2571
- FCMoment.prototype.hasTime = function() {
2899
+ newMomentProto.hasTime = function() {
2572
2900
  return !this._ambigTime;
2573
2901
  };
2574
2902
 
@@ -2579,111 +2907,84 @@ FCMoment.prototype.hasTime = function() {
2579
2907
  // Converts the moment to UTC, stripping out its timezone offset, but preserving its
2580
2908
  // YMD and time-of-day. A moment with a stripped timezone offset will display no
2581
2909
  // timezone offset when .format() is called.
2582
- FCMoment.prototype.stripZone = function() {
2910
+ newMomentProto.stripZone = function() {
2583
2911
  var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array
2584
2912
  var wasAmbigTime = this._ambigTime;
2585
2913
 
2586
- moment.fn.utc.call(this); // set the internal UTC flag
2587
-
2588
- this.year(a[0]) // TODO: find a way to do this in one shot
2589
- .month(a[1])
2590
- .date(a[2])
2591
- .hours(a[3])
2592
- .minutes(a[4])
2593
- .seconds(a[5])
2594
- .milliseconds(a[6]);
2914
+ this.utc(); // set the internal UTC flag (will clear the ambig flags)
2915
+ setUTCValues(this, a); // will set the year/month/date/hours/minutes/seconds/ms
2595
2916
 
2596
2917
  if (wasAmbigTime) {
2597
2918
  // the above call to .utc()/.zone() unfortunately clears the ambig flags, so reassign
2598
2919
  this._ambigTime = true;
2599
2920
  }
2600
2921
 
2601
- // Mark the zone as ambiguous. This needs to happen after the .utc() call, which calls .zone(), which
2602
- // clears all ambig flags. Same concept with the .year/month/date calls in the case of moment-timezone.
2922
+ // Mark the zone as ambiguous. This needs to happen after the .utc() call, which calls .zone(),
2923
+ // which clears all ambig flags. Same with setUTCValues with moment-timezone.
2603
2924
  this._ambigZone = true;
2604
2925
 
2605
2926
  return this; // for chaining
2606
2927
  };
2607
2928
 
2608
2929
  // Returns of the moment has a non-ambiguous timezone offset (boolean)
2609
- FCMoment.prototype.hasZone = function() {
2930
+ newMomentProto.hasZone = function() {
2610
2931
  return !this._ambigZone;
2611
2932
  };
2612
2933
 
2613
- // this method implicitly marks a zone
2614
- FCMoment.prototype.zone = function(tzo) {
2934
+ // this method implicitly marks a zone (will get called upon .utc() and .local())
2935
+ newMomentProto.zone = function(tzo) {
2615
2936
 
2616
- if (tzo != null) {
2617
- // FYI, the delete statements need to be before the .zone() call or else chaos ensues
2618
- // for reasons I don't understand.
2619
- delete this._ambigTime;
2620
- delete this._ambigZone;
2937
+ if (tzo != null) { // setter
2938
+ // these assignments needs to happen before the original zone method is called.
2939
+ // I forget why, something to do with a browser crash.
2940
+ this._ambigTime = false;
2941
+ this._ambigZone = false;
2621
2942
  }
2622
2943
 
2623
- return moment.fn.zone.apply(this, arguments);
2944
+ return oldMomentProto.zone.apply(this, arguments);
2624
2945
  };
2625
2946
 
2626
2947
  // this method implicitly marks a zone
2627
- FCMoment.prototype.local = function() {
2628
- var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array
2948
+ newMomentProto.local = function() {
2949
+ var a = this.toArray(); // year,month,date,hours,minutes,seconds,ms as an array
2629
2950
  var wasAmbigZone = this._ambigZone;
2630
2951
 
2631
- // will happen anyway via .local()/.zone(), but don't want to rely on internal implementation
2632
- delete this._ambigTime;
2633
- delete this._ambigZone;
2634
-
2635
- moment.fn.local.apply(this, arguments);
2952
+ oldMomentProto.local.apply(this, arguments); // will clear ambig flags
2636
2953
 
2637
2954
  if (wasAmbigZone) {
2638
2955
  // If the moment was ambiguously zoned, the date fields were stored as UTC.
2639
2956
  // We want to preserve these, but in local time.
2640
- this.year(a[0]) // TODO: find a way to do this in one shot
2641
- .month(a[1])
2642
- .date(a[2])
2643
- .hours(a[3])
2644
- .minutes(a[4])
2645
- .seconds(a[5])
2646
- .milliseconds(a[6]);
2957
+ setLocalValues(this, a);
2647
2958
  }
2648
2959
 
2649
2960
  return this; // for chaining
2650
2961
  };
2651
2962
 
2652
- // this method implicitly marks a zone
2653
- FCMoment.prototype.utc = function() {
2654
-
2655
- // will happen anyway via .local()/.zone(), but don't want to rely on internal implementation
2656
- delete this._ambigTime;
2657
- delete this._ambigZone;
2658
-
2659
- return moment.fn.utc.apply(this, arguments);
2660
- };
2661
-
2662
2963
 
2663
2964
  // Formatting
2664
2965
  // -------------------------------------------------------------------------------------------------
2665
2966
 
2666
- FCMoment.prototype.format = function() {
2667
- if (arguments[0]) {
2967
+ newMomentProto.format = function() {
2968
+ if (this._fullCalendar && arguments[0]) { // an enhanced moment? and a format string provided?
2668
2969
  return formatDate(this, arguments[0]); // our extended formatting
2669
2970
  }
2670
2971
  if (this._ambigTime) {
2671
- return momentFormat(this, 'YYYY-MM-DD');
2972
+ return oldMomentFormat(this, 'YYYY-MM-DD');
2672
2973
  }
2673
2974
  if (this._ambigZone) {
2674
- return momentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
2975
+ return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
2675
2976
  }
2676
- return momentFormat(this); // default moment original formatting
2977
+ return oldMomentProto.format.apply(this, arguments);
2677
2978
  };
2678
2979
 
2679
- FCMoment.prototype.toISOString = function() {
2980
+ newMomentProto.toISOString = function() {
2680
2981
  if (this._ambigTime) {
2681
- return momentFormat(this, 'YYYY-MM-DD');
2982
+ return oldMomentFormat(this, 'YYYY-MM-DD');
2682
2983
  }
2683
2984
  if (this._ambigZone) {
2684
- return momentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
2985
+ return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
2685
2986
  }
2686
- return moment.fn.toISOString.apply(this, arguments);
2987
+ return oldMomentProto.toISOString.apply(this, arguments);
2687
2988
  };
2688
2989
 
2689
2990
 
@@ -2691,23 +2992,29 @@ FCMoment.prototype.toISOString = function() {
2691
2992
  // -------------------------------------------------------------------------------------------------
2692
2993
 
2693
2994
  // Is the moment within the specified range? `end` is exclusive.
2694
- FCMoment.prototype.isWithin = function(start, end) {
2995
+ // FYI, this method is not a standard Moment method, so always do our enhanced logic.
2996
+ newMomentProto.isWithin = function(start, end) {
2695
2997
  var a = commonlyAmbiguate([ this, start, end ]);
2696
2998
  return a[0] >= a[1] && a[0] < a[2];
2697
2999
  };
2698
3000
 
2699
3001
  // When isSame is called with units, timezone ambiguity is normalized before the comparison happens.
2700
- // If no units are specified, the two moments must be identically the same, with matching ambig flags.
2701
- FCMoment.prototype.isSame = function(input, units) {
3002
+ // If no units specified, the two moments must be identically the same, with matching ambig flags.
3003
+ newMomentProto.isSame = function(input, units) {
2702
3004
  var a;
2703
3005
 
3006
+ // only do custom logic if this is an enhanced moment
3007
+ if (!this._fullCalendar) {
3008
+ return oldMomentProto.isSame.apply(this, arguments);
3009
+ }
3010
+
2704
3011
  if (units) {
2705
3012
  a = commonlyAmbiguate([ this, input ], true); // normalize timezones but don't erase times
2706
- return moment.fn.isSame.call(a[0], a[1], units);
3013
+ return oldMomentProto.isSame.call(a[0], a[1], units);
2707
3014
  }
2708
3015
  else {
2709
3016
  input = fc.moment.parseZone(input); // normalize input
2710
- return moment.fn.isSame.call(this, input) &&
3017
+ return oldMomentProto.isSame.call(this, input) &&
2711
3018
  Boolean(this._ambigTime) === Boolean(input._ambigTime) &&
2712
3019
  Boolean(this._ambigZone) === Boolean(input._ambigZone);
2713
3020
  }
@@ -2718,9 +3025,16 @@ $.each([
2718
3025
  'isBefore',
2719
3026
  'isAfter'
2720
3027
  ], function(i, methodName) {
2721
- FCMoment.prototype[methodName] = function(input, units) {
2722
- var a = commonlyAmbiguate([ this, input ]);
2723
- return moment.fn[methodName].call(a[0], a[1], units);
3028
+ newMomentProto[methodName] = function(input, units) {
3029
+ var a;
3030
+
3031
+ // only do custom logic if this is an enhanced moment
3032
+ if (!this._fullCalendar) {
3033
+ return oldMomentProto[methodName].apply(this, arguments);
3034
+ }
3035
+
3036
+ a = commonlyAmbiguate([ this, input ]);
3037
+ return oldMomentProto[methodName].call(a[0], a[1], units);
2724
3038
  };
2725
3039
  });
2726
3040
 
@@ -2755,6 +3069,63 @@ function commonlyAmbiguate(inputs, preserveTime) {
2755
3069
  return outputs;
2756
3070
  }
2757
3071
 
3072
+ // Transfers all the flags related to ambiguous time/zone from the `src` moment to the `dest` moment
3073
+ function transferAmbigs(src, dest) {
3074
+ if (src._ambigTime) {
3075
+ dest._ambigTime = true;
3076
+ }
3077
+ else if (dest._ambigTime) {
3078
+ dest._ambigTime = false;
3079
+ }
3080
+
3081
+ if (src._ambigZone) {
3082
+ dest._ambigZone = true;
3083
+ }
3084
+ else if (dest._ambigZone) {
3085
+ dest._ambigZone = false;
3086
+ }
3087
+ }
3088
+
3089
+
3090
+ // Sets the year/month/date/etc values of the moment from the given array.
3091
+ // Inefficient because it calls each individual setter.
3092
+ function setMomentValues(mom, a) {
3093
+ mom.year(a[0] || 0)
3094
+ .month(a[1] || 0)
3095
+ .date(a[2] || 0)
3096
+ .hours(a[3] || 0)
3097
+ .minutes(a[4] || 0)
3098
+ .seconds(a[5] || 0)
3099
+ .milliseconds(a[6] || 0);
3100
+ }
3101
+
3102
+ // Can we set the moment's internal date directly?
3103
+ allowValueOptimization = '_d' in moment() && 'updateOffset' in moment;
3104
+
3105
+ // Utility function. Accepts a moment and an array of the UTC year/month/date/etc values to set.
3106
+ // Assumes the given moment is already in UTC mode.
3107
+ setUTCValues = allowValueOptimization ? function(mom, a) {
3108
+ // simlate what moment's accessors do
3109
+ mom._d.setTime(Date.UTC.apply(Date, a));
3110
+ moment.updateOffset(mom, false); // keepTime=false
3111
+ } : setMomentValues;
3112
+
3113
+ // Utility function. Accepts a moment and an array of the local year/month/date/etc values to set.
3114
+ // Assumes the given moment is already in local mode.
3115
+ setLocalValues = allowValueOptimization ? function(mom, a) {
3116
+ // simlate what moment's accessors do
3117
+ mom._d.setTime(+new Date( // FYI, there is now way to apply an array of args to a constructor
3118
+ a[0] || 0,
3119
+ a[1] || 0,
3120
+ a[2] || 0,
3121
+ a[3] || 0,
3122
+ a[4] || 0,
3123
+ a[5] || 0,
3124
+ a[6] || 0
3125
+ ));
3126
+ moment.updateOffset(mom, false); // keepTime=false
3127
+ } : setMomentValues;
3128
+
2758
3129
  ;;
2759
3130
 
2760
3131
  // Single Date Formatting
@@ -2762,8 +3133,8 @@ function commonlyAmbiguate(inputs, preserveTime) {
2762
3133
 
2763
3134
 
2764
3135
  // call this if you want Moment's original format method to be used
2765
- function momentFormat(mom, formatStr) {
2766
- return moment.fn.format.call(mom, formatStr);
3136
+ function oldMomentFormat(mom, formatStr) {
3137
+ return oldMomentProto.format.call(mom, formatStr); // oldMomentProto defined in moment-ext.js
2767
3138
  }
2768
3139
 
2769
3140
 
@@ -2789,10 +3160,10 @@ function formatDateWithChunks(date, chunks) {
2789
3160
  // addition formatting tokens we want recognized
2790
3161
  var tokenOverrides = {
2791
3162
  t: function(date) { // "a" or "p"
2792
- return momentFormat(date, 'a').charAt(0);
3163
+ return oldMomentFormat(date, 'a').charAt(0);
2793
3164
  },
2794
3165
  T: function(date) { // "A" or "P"
2795
- return momentFormat(date, 'A').charAt(0);
3166
+ return oldMomentFormat(date, 'A').charAt(0);
2796
3167
  }
2797
3168
  };
2798
3169
 
@@ -2808,7 +3179,7 @@ function formatDateWithChunk(date, chunk) {
2808
3179
  if (tokenOverrides[token]) {
2809
3180
  return tokenOverrides[token](date); // use our custom token
2810
3181
  }
2811
- return momentFormat(date, token);
3182
+ return oldMomentFormat(date, token);
2812
3183
  }
2813
3184
  else if (chunk.maybe) { // a grouping of other chunks that must be non-zero
2814
3185
  maybeStr = formatDateWithChunks(date, chunk.maybe);
@@ -2936,7 +3307,7 @@ function formatSimilarChunk(date1, date2, chunk) {
2936
3307
  unit = similarUnitMap[token.charAt(0)];
2937
3308
  // are the dates the same for this unit of measurement?
2938
3309
  if (unit && date1.isSame(date2, unit)) {
2939
- return momentFormat(date1, token); // would be the same if we used `date2`
3310
+ return oldMomentFormat(date1, token); // would be the same if we used `date2`
2940
3311
  // BTW, don't support custom tokens
2941
3312
  }
2942
3313
  }
@@ -3302,7 +3673,7 @@ ComboCoordMap.prototype = {
3302
3673
 
3303
3674
  /* Tracks mouse movements over a CoordMap and raises events about which cell the mouse is over.
3304
3675
  ----------------------------------------------------------------------------------------------------------------------*/
3305
- // TODO: implement scrolling
3676
+ // TODO: very useful to have a handler that gets called upon cellOut OR when dragging stops (for cleanup)
3306
3677
 
3307
3678
  function DragListener(coordMap, options) {
3308
3679
  this.coordMap = coordMap;
@@ -4007,7 +4378,7 @@ RowRenderer.prototype = {
4007
4378
  }
4008
4379
 
4009
4380
  if (typeof renderer === 'function') {
4010
- return function(row) {
4381
+ return function() {
4011
4382
  return renderer.apply(provider, arguments) || ''; // use correct `this` and always return a string
4012
4383
  };
4013
4384
  }
@@ -4028,6 +4399,7 @@ RowRenderer.prototype = {
4028
4399
  function Grid(view) {
4029
4400
  RowRenderer.call(this, view); // call the super-constructor
4030
4401
  this.coordMap = new GridCoordMap(this);
4402
+ this.elsByFill = {};
4031
4403
  }
4032
4404
 
4033
4405
 
@@ -4037,6 +4409,7 @@ $.extend(Grid.prototype, {
4037
4409
  el: null, // the containing element
4038
4410
  coordMap: null, // a GridCoordMap that converts pixel values to datetimes
4039
4411
  cellDuration: null, // a cell's duration. subclasses must assign this ASAP
4412
+ elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.
4040
4413
 
4041
4414
 
4042
4415
  // Renders the grid into the `el` element.
@@ -4107,6 +4480,7 @@ $.extend(Grid.prototype, {
4107
4480
  dayMousedown: function(ev) {
4108
4481
  var _this = this;
4109
4482
  var view = this.view;
4483
+ var calendar = view.calendar;
4110
4484
  var isSelectable = view.opt('selectable');
4111
4485
  var dates = null; // the inclusive dates of the selection. will be null if no selection
4112
4486
  var start; // the inclusive start of the selection
@@ -4132,13 +4506,20 @@ $.extend(Grid.prototype, {
4132
4506
  end = dates[1].clone().add(_this.cellDuration);
4133
4507
 
4134
4508
  if (isSelectable) {
4135
- _this.renderSelection(start, end);
4509
+ if (calendar.isSelectionAllowedInRange(start, end)) { // allowed to select within this range?
4510
+ _this.renderSelection(start, end);
4511
+ }
4512
+ else {
4513
+ dates = null; // flag for an invalid selection
4514
+ disableCursor();
4515
+ }
4136
4516
  }
4137
4517
  }
4138
4518
  },
4139
4519
  cellOut: function(cell, date) {
4140
4520
  dates = null;
4141
4521
  _this.destroySelection();
4522
+ enableCursor();
4142
4523
  },
4143
4524
  listenStop: function(ev) {
4144
4525
  if (dates) { // started and ended on a cell?
@@ -4150,6 +4531,7 @@ $.extend(Grid.prototype, {
4150
4531
  view.reportSelection(start, end, ev);
4151
4532
  }
4152
4533
  }
4534
+ enableCursor();
4153
4535
  }
4154
4536
  });
4155
4537
 
@@ -4257,18 +4639,108 @@ $.extend(Grid.prototype, {
4257
4639
  ------------------------------------------------------------------------------------------------------------------*/
4258
4640
 
4259
4641
 
4260
- // Puts visual emphasis on a certain date range
4642
+ // Renders an emphasis on the given date range. `start` is inclusive. `end` is exclusive.
4261
4643
  renderHighlight: function(start, end) {
4262
- // subclasses should implement
4644
+ this.renderFill('highlight', this.rangeToSegs(start, end));
4263
4645
  },
4264
4646
 
4265
4647
 
4266
- // Removes visual emphasis on a date range
4648
+ // Unrenders the emphasis on a date range
4267
4649
  destroyHighlight: function() {
4268
- // subclasses should implement
4650
+ this.destroyFill('highlight');
4651
+ },
4652
+
4653
+
4654
+ // Generates an array of classNames for rendering the highlight. Used by the fill system.
4655
+ highlightSegClasses: function() {
4656
+ return [ 'fc-highlight' ];
4657
+ },
4658
+
4659
+
4660
+ /* Fill System (highlight, background events, business hours)
4661
+ ------------------------------------------------------------------------------------------------------------------*/
4662
+
4663
+
4664
+ // Renders a set of rectangles over the given segments of time.
4665
+ // Returns a subset of segs, the segs that were actually rendered.
4666
+ // Responsible for populating this.elsByFill
4667
+ renderFill: function(type, segs) {
4668
+ // subclasses must implement
4669
+ },
4670
+
4671
+
4672
+ // Unrenders a specific type of fill that is currently rendered on the grid
4673
+ destroyFill: function(type) {
4674
+ var el = this.elsByFill[type];
4675
+
4676
+ if (el) {
4677
+ el.remove();
4678
+ delete this.elsByFill[type];
4679
+ }
4680
+ },
4681
+
4682
+
4683
+ // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types.
4684
+ // Only returns segments that successfully rendered.
4685
+ // To be harnessed by renderFill (implemented by subclasses).
4686
+ // Analagous to renderFgSegEls.
4687
+ renderFillSegEls: function(type, segs) {
4688
+ var _this = this;
4689
+ var segElMethod = this[type + 'SegEl'];
4690
+ var html = '';
4691
+ var renderedSegs = [];
4692
+ var i;
4693
+
4694
+ if (segs.length) {
4695
+
4696
+ // build a large concatenation of segment HTML
4697
+ for (i = 0; i < segs.length; i++) {
4698
+ html += this.fillSegHtml(type, segs[i]);
4699
+ }
4700
+
4701
+ // Grab individual elements from the combined HTML string. Use each as the default rendering.
4702
+ // Then, compute the 'el' for each segment.
4703
+ $(html).each(function(i, node) {
4704
+ var seg = segs[i];
4705
+ var el = $(node);
4706
+
4707
+ // allow custom filter methods per-type
4708
+ if (segElMethod) {
4709
+ el = segElMethod.call(_this, seg, el);
4710
+ }
4711
+
4712
+ if (el) { // custom filters did not cancel the render
4713
+ el = $(el); // allow custom filter to return raw DOM node
4714
+
4715
+ // correct element type? (would be bad if a non-TD were inserted into a table for example)
4716
+ if (el.is(_this.fillSegTag)) {
4717
+ seg.el = el;
4718
+ renderedSegs.push(seg);
4719
+ }
4720
+ }
4721
+ });
4722
+ }
4723
+
4724
+ return renderedSegs;
4269
4725
  },
4270
4726
 
4271
4727
 
4728
+ fillSegTag: 'div', // subclasses can override
4729
+
4730
+
4731
+ // Builds the HTML needed for one fill segment. Generic enought o work with different types.
4732
+ fillSegHtml: function(type, seg) {
4733
+ var classesMethod = this[type + 'SegClasses']; // custom hooks per-type
4734
+ var stylesMethod = this[type + 'SegStyles']; //
4735
+ var classes = classesMethod ? classesMethod.call(this, seg) : [];
4736
+ var styles = stylesMethod ? stylesMethod.call(this, seg) : ''; // a semi-colon separated CSS property string
4737
+
4738
+ return '<' + this.fillSegTag +
4739
+ (classes.length ? ' class="' + classes.join(' ') + '"' : '') +
4740
+ (styles ? ' style="' + styles + '"' : '') +
4741
+ ' />';
4742
+ },
4743
+
4272
4744
 
4273
4745
  /* Generic rendering utilities for subclasses
4274
4746
  ------------------------------------------------------------------------------------------------------------------*/
@@ -4352,96 +4824,174 @@ $.extend(Grid.prototype, {
4352
4824
  mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing
4353
4825
  isDraggingSeg: false, // is a segment being dragged? boolean
4354
4826
  isResizingSeg: false, // is a segment being resized? boolean
4827
+ segs: null, // the event segments currently rendered in the grid
4355
4828
 
4356
4829
 
4357
4830
  // Renders the given events onto the grid
4358
4831
  renderEvents: function(events) {
4359
- // subclasses must implement
4832
+ var segs = this.eventsToSegs(events);
4833
+ var bgSegs = [];
4834
+ var fgSegs = [];
4835
+ var i, seg;
4836
+
4837
+ for (i = 0; i < segs.length; i++) {
4838
+ seg = segs[i];
4839
+
4840
+ if (isBgEvent(seg.event)) {
4841
+ bgSegs.push(seg);
4842
+ }
4843
+ else {
4844
+ fgSegs.push(seg);
4845
+ }
4846
+ }
4847
+
4848
+ // Render each different type of segment.
4849
+ // Each function may return a subset of the segs, segs that were actually rendered.
4850
+ bgSegs = this.renderBgSegs(bgSegs) || bgSegs;
4851
+ fgSegs = this.renderFgSegs(fgSegs) || fgSegs;
4852
+
4853
+ this.segs = bgSegs.concat(fgSegs);
4360
4854
  },
4361
4855
 
4362
4856
 
4363
- // Retrieves all rendered segment objects in this grid
4857
+ // Unrenders all events currently rendered on the grid
4858
+ destroyEvents: function() {
4859
+ this.triggerSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
4860
+
4861
+ this.destroyFgSegs();
4862
+ this.destroyBgSegs();
4863
+
4864
+ this.segs = null;
4865
+ },
4866
+
4867
+
4868
+ // Retrieves all rendered segment objects currently rendered on the grid
4364
4869
  getSegs: function() {
4870
+ return this.segs || [];
4871
+ },
4872
+
4873
+
4874
+ /* Foreground Segment Rendering
4875
+ ------------------------------------------------------------------------------------------------------------------*/
4876
+
4877
+
4878
+ // Renders foreground event segments onto the grid. May return a subset of segs that were rendered.
4879
+ renderFgSegs: function(segs) {
4365
4880
  // subclasses must implement
4366
4881
  },
4367
4882
 
4368
4883
 
4369
- // Unrenders all events. Subclasses should implement, calling this super-method first.
4370
- destroyEvents: function() {
4371
- this.triggerSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
4884
+ // Unrenders all currently rendered foreground segments
4885
+ destroyFgSegs: function() {
4886
+ // subclasses must implement
4372
4887
  },
4373
4888
 
4374
4889
 
4375
- // Renders a `el` property for each seg, and only returns segments that successfully rendered
4376
- renderSegs: function(segs, disableResizing) {
4890
+ // Renders and assigns an `el` property for each foreground event segment.
4891
+ // Only returns segments that successfully rendered.
4892
+ // A utility that subclasses may use.
4893
+ renderFgSegEls: function(segs, disableResizing) {
4377
4894
  var view = this.view;
4378
4895
  var html = '';
4379
4896
  var renderedSegs = [];
4380
4897
  var i;
4381
4898
 
4382
- // build a large concatenation of event segment HTML
4383
- for (i = 0; i < segs.length; i++) {
4384
- html += this.renderSegHtml(segs[i], disableResizing);
4385
- }
4899
+ if (segs.length) { // don't build an empty html string
4386
4900
 
4387
- // Grab individual elements from the combined HTML string. Use each as the default rendering.
4388
- // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
4389
- $(html).each(function(i, node) {
4390
- var seg = segs[i];
4391
- var el = view.resolveEventEl(seg.event, $(node));
4392
- if (el) {
4393
- el.data('fc-seg', seg); // used by handlers
4394
- seg.el = el;
4395
- renderedSegs.push(seg);
4901
+ // build a large concatenation of event segment HTML
4902
+ for (i = 0; i < segs.length; i++) {
4903
+ html += this.fgSegHtml(segs[i], disableResizing);
4396
4904
  }
4397
- });
4905
+
4906
+ // Grab individual elements from the combined HTML string. Use each as the default rendering.
4907
+ // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
4908
+ $(html).each(function(i, node) {
4909
+ var seg = segs[i];
4910
+ var el = view.resolveEventEl(seg.event, $(node));
4911
+
4912
+ if (el) {
4913
+ el.data('fc-seg', seg); // used by handlers
4914
+ seg.el = el;
4915
+ renderedSegs.push(seg);
4916
+ }
4917
+ });
4918
+ }
4398
4919
 
4399
4920
  return renderedSegs;
4400
4921
  },
4401
4922
 
4402
4923
 
4403
- // Generates the HTML for the default rendering of a segment
4404
- renderSegHtml: function(seg, disableResizing) {
4405
- // subclasses must implement
4924
+ // Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls()
4925
+ fgSegHtml: function(seg, disableResizing) {
4926
+ // subclasses should implement
4927
+ },
4928
+
4929
+
4930
+ /* Background Segment Rendering
4931
+ ------------------------------------------------------------------------------------------------------------------*/
4932
+
4933
+
4934
+ // Renders the given background event segments onto the grid.
4935
+ // Returns a subset of the segs that were actually rendered.
4936
+ renderBgSegs: function(segs) {
4937
+ return this.renderFill('bgEvent', segs);
4938
+ },
4939
+
4940
+
4941
+ // Unrenders all the currently rendered background event segments
4942
+ destroyBgSegs: function() {
4943
+ this.destroyFill('bgEvent');
4944
+ },
4945
+
4946
+
4947
+ // Renders a background event element, given the default rendering. Called by the fill system.
4948
+ bgEventSegEl: function(seg, el) {
4949
+ return this.view.resolveEventEl(seg.event, el); // will filter through eventRender
4406
4950
  },
4407
4951
 
4408
4952
 
4409
- // Converts an array of event objects into an array of segment objects
4410
- eventsToSegs: function(events, intervalStart, intervalEnd) {
4411
- var _this = this;
4953
+ // Generates an array of classNames to be used for the default rendering of a background event.
4954
+ // Called by the fill system.
4955
+ bgEventSegClasses: function(seg) {
4956
+ var event = seg.event;
4957
+ var source = event.source || {};
4958
+
4959
+ return [ 'fc-bgevent' ].concat(
4960
+ event.className,
4961
+ source.className || []
4962
+ );
4963
+ },
4964
+
4965
+
4966
+ // Generates a semicolon-separated CSS string to be used for the default rendering of a background event.
4967
+ // Called by the fill system.
4968
+ // TODO: consolidate with getEventSkinCss?
4969
+ bgEventSegStyles: function(seg) {
4970
+ var view = this.view;
4971
+ var event = seg.event;
4972
+ var source = event.source || {};
4973
+ var eventColor = event.color;
4974
+ var sourceColor = source.color;
4975
+ var optionColor = view.opt('eventColor');
4976
+ var backgroundColor =
4977
+ event.backgroundColor ||
4978
+ eventColor ||
4979
+ source.backgroundColor ||
4980
+ sourceColor ||
4981
+ view.opt('eventBackgroundColor') ||
4982
+ optionColor;
4983
+
4984
+ if (backgroundColor) {
4985
+ return 'background-color:' + backgroundColor;
4986
+ }
4412
4987
 
4413
- return $.map(events, function(event) {
4414
- return _this.eventToSegs(event, intervalStart, intervalEnd); // $.map flattens all returned arrays together
4415
- });
4988
+ return '';
4416
4989
  },
4417
4990
 
4418
4991
 
4419
- // Slices a single event into an array of event segments.
4420
- // When `intervalStart` and `intervalEnd` are specified, intersect the events with that interval.
4421
- // Otherwise, let the subclass decide how it wants to slice the segments over the grid.
4422
- eventToSegs: function(event, intervalStart, intervalEnd) {
4423
- var eventStart = event.start.clone().stripZone(); // normalize
4424
- var eventEnd = this.view.calendar.getEventEnd(event).stripZone(); // compute (if necessary) and normalize
4425
- var segs;
4426
- var i, seg;
4427
-
4428
- if (intervalStart && intervalEnd) {
4429
- seg = intersectionToSeg(eventStart, eventEnd, intervalStart, intervalEnd);
4430
- segs = seg ? [ seg ] : [];
4431
- }
4432
- else {
4433
- segs = this.rangeToSegs(eventStart, eventEnd); // defined by the subclass
4434
- }
4435
-
4436
- // assign extra event-related properties to the segment objects
4437
- for (i = 0; i < segs.length; i++) {
4438
- seg = segs[i];
4439
- seg.event = event;
4440
- seg.eventStartMS = +eventStart;
4441
- seg.eventDurationMS = eventEnd - eventStart;
4442
- }
4443
-
4444
- return segs;
4992
+ // Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system.
4993
+ businessHoursSegClasses: function(seg) {
4994
+ return [ 'fc-nonbusiness', 'fc-bgevent' ];
4445
4995
  },
4446
4996
 
4447
4997
 
@@ -4520,6 +5070,7 @@ $.extend(Grid.prototype, {
4520
5070
  segDragMousedown: function(seg, ev) {
4521
5071
  var _this = this;
4522
5072
  var view = this.view;
5073
+ var calendar = view.calendar;
4523
5074
  var el = seg.el;
4524
5075
  var event = seg.event;
4525
5076
  var newStart, newEnd;
@@ -4553,17 +5104,26 @@ $.extend(Grid.prototype, {
4553
5104
  newStart = res.start;
4554
5105
  newEnd = res.end;
4555
5106
 
4556
- if (view.renderDrag(newStart, newEnd, seg)) { // have the view render a visual indication
4557
- mouseFollower.hide(); // if the view is already using a mock event "helper", hide our own
5107
+ if (calendar.isEventAllowedInRange(event, newStart, res.visibleEnd)) { // allowed to drop here?
5108
+ if (view.renderDrag(newStart, newEnd, seg)) { // have the view render a visual indication
5109
+ mouseFollower.hide(); // if the view is already using a mock event "helper", hide our own
5110
+ }
5111
+ else {
5112
+ mouseFollower.show();
5113
+ }
4558
5114
  }
4559
5115
  else {
5116
+ // have the helper follow the mouse (no snapping) with a warning-style cursor
5117
+ newStart = null; // mark an invalid drop date
4560
5118
  mouseFollower.show();
5119
+ disableCursor();
4561
5120
  }
4562
5121
  },
4563
5122
  cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells
4564
5123
  newStart = null;
4565
5124
  view.destroyDrag(); // unrender whatever was done in view.renderDrag
4566
5125
  mouseFollower.show(); // show in case we are moving out of all cells
5126
+ enableCursor();
4567
5127
  },
4568
5128
  dragStop: function(ev) {
4569
5129
  var hasChanged = newStart && !newStart.isSame(event.start);
@@ -4579,6 +5139,8 @@ $.extend(Grid.prototype, {
4579
5139
  view.eventDrop(el[0], event, newStart, ev); // will rerender all events...
4580
5140
  }
4581
5141
  });
5142
+
5143
+ enableCursor();
4582
5144
  },
4583
5145
  listenStop: function() {
4584
5146
  mouseFollower.stop(); // put in listenStop in case there was a mousedown but the drag never started
@@ -4589,7 +5151,8 @@ $.extend(Grid.prototype, {
4589
5151
  },
4590
5152
 
4591
5153
 
4592
- // Given a segment, the dates where a drag began and ended, calculates the Event Object's new start and end dates
5154
+ // Given a segment, the dates where a drag began and ended, calculates the Event Object's new start and end dates.
5155
+ // Might return a `null` end (even when forceEventDuration is on).
4593
5156
  computeDraggedEventDates: function(seg, dragStartDate, dropDate) {
4594
5157
  var view = this.view;
4595
5158
  var event = seg.event;
@@ -4598,6 +5161,8 @@ $.extend(Grid.prototype, {
4598
5161
  var delta;
4599
5162
  var newStart;
4600
5163
  var newEnd;
5164
+ var newAllDay;
5165
+ var visibleEnd;
4601
5166
 
4602
5167
  if (dropDate.hasTime() === dragStartDate.hasTime()) {
4603
5168
  delta = dayishDiff(dropDate, dragStartDate);
@@ -4608,14 +5173,19 @@ $.extend(Grid.prototype, {
4608
5173
  else {
4609
5174
  newEnd = end.clone().add(delta);
4610
5175
  }
5176
+ newAllDay = event.allDay; // keep it the same
4611
5177
  }
4612
5178
  else {
4613
5179
  // if switching from day <-> timed, start should be reset to the dropped date, and the end cleared
4614
5180
  newStart = dropDate;
4615
5181
  newEnd = null; // end should be cleared
5182
+ newAllDay = !dropDate.hasTime();
4616
5183
  }
4617
5184
 
4618
- return { start: newStart, end: newEnd };
5185
+ // compute what the end date will appear to be
5186
+ visibleEnd = newEnd || view.calendar.getDefaultEventEnd(newAllDay, newStart);
5187
+
5188
+ return { start: newStart, end: newEnd, visibleEnd: visibleEnd };
4619
5189
  },
4620
5190
 
4621
5191
 
@@ -4628,6 +5198,7 @@ $.extend(Grid.prototype, {
4628
5198
  segResizeMousedown: function(seg, ev) {
4629
5199
  var _this = this;
4630
5200
  var view = this.view;
5201
+ var calendar = view.calendar;
4631
5202
  var el = seg.el;
4632
5203
  var event = seg.event;
4633
5204
  var start = event.start;
@@ -4635,7 +5206,7 @@ $.extend(Grid.prototype, {
4635
5206
  var newEnd = null;
4636
5207
  var dragListener;
4637
5208
 
4638
- function destroy() { // resets the rendering
5209
+ function destroy() { // resets the rendering to show the original event
4639
5210
  _this.destroyResize();
4640
5211
  view.showEvent(event);
4641
5212
  }
@@ -4656,22 +5227,31 @@ $.extend(Grid.prototype, {
4656
5227
  }
4657
5228
  newEnd = date.clone().add(_this.cellDuration); // make it an exclusive end
4658
5229
 
4659
- if (newEnd.isSame(end)) {
4660
- newEnd = null;
4661
- destroy();
5230
+ if (calendar.isEventAllowedInRange(event, start, newEnd)) { // allowed to be resized here?
5231
+ if (newEnd.isSame(end)) {
5232
+ newEnd = null; // mark an invalid resize
5233
+ destroy();
5234
+ }
5235
+ else {
5236
+ _this.renderResize(start, newEnd, seg);
5237
+ view.hideEvent(event);
5238
+ }
4662
5239
  }
4663
5240
  else {
4664
- _this.renderResize(start, newEnd, seg);
4665
- view.hideEvent(event);
5241
+ newEnd = null; // mark an invalid resize
5242
+ destroy();
5243
+ disableCursor();
4666
5244
  }
4667
5245
  },
4668
5246
  cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells
4669
5247
  newEnd = null;
4670
5248
  destroy();
5249
+ enableCursor();
4671
5250
  },
4672
5251
  dragStop: function(ev) {
4673
5252
  _this.isResizingSeg = false;
4674
5253
  destroy();
5254
+ enableCursor();
4675
5255
  view.trigger('eventResizeStop', el[0], event, ev, {}); // last argument is jqui dummy
4676
5256
 
4677
5257
  if (newEnd) {
@@ -4747,16 +5327,193 @@ $.extend(Grid.prototype, {
4747
5327
  statements.push('color:' + textColor);
4748
5328
  }
4749
5329
  return statements.join(';');
5330
+ },
5331
+
5332
+
5333
+ /* Converting events -> ranges -> segs
5334
+ ------------------------------------------------------------------------------------------------------------------*/
5335
+
5336
+
5337
+ // Converts an array of event objects into an array of event segment objects.
5338
+ // A custom `rangeToSegsFunc` may be given for arbitrarily slicing up events.
5339
+ eventsToSegs: function(events, rangeToSegsFunc) {
5340
+ var eventRanges = this.eventsToRanges(events);
5341
+ var segs = [];
5342
+ var i;
5343
+
5344
+ for (i = 0; i < eventRanges.length; i++) {
5345
+ segs.push.apply(
5346
+ segs,
5347
+ this.eventRangeToSegs(eventRanges[i], rangeToSegsFunc)
5348
+ );
5349
+ }
5350
+
5351
+ return segs;
5352
+ },
5353
+
5354
+
5355
+ // Converts an array of events into an array of "range" objects.
5356
+ // A "range" object is a plain object with start/end properties denoting the time it covers. Also an event property.
5357
+ // For "normal" events, this will be identical to the event's start/end, but for "inverse-background" events,
5358
+ // will create an array of ranges that span the time *not* covered by the given event.
5359
+ eventsToRanges: function(events) {
5360
+ var _this = this;
5361
+ var eventsById = groupEventsById(events);
5362
+ var ranges = [];
5363
+
5364
+ // group by ID so that related inverse-background events can be rendered together
5365
+ $.each(eventsById, function(id, eventGroup) {
5366
+ if (eventGroup.length) {
5367
+ ranges.push.apply(
5368
+ ranges,
5369
+ isInverseBgEvent(eventGroup[0]) ?
5370
+ _this.eventsToInverseRanges(eventGroup) :
5371
+ _this.eventsToNormalRanges(eventGroup)
5372
+ );
5373
+ }
5374
+ });
5375
+
5376
+ return ranges;
5377
+ },
5378
+
5379
+
5380
+ // Converts an array of "normal" events (not inverted rendering) into a parallel array of ranges
5381
+ eventsToNormalRanges: function(events) {
5382
+ var calendar = this.view.calendar;
5383
+ var ranges = [];
5384
+ var i, event;
5385
+ var eventStart, eventEnd;
5386
+
5387
+ for (i = 0; i < events.length; i++) {
5388
+ event = events[i];
5389
+
5390
+ // make copies and normalize by stripping timezone
5391
+ eventStart = event.start.clone().stripZone();
5392
+ eventEnd = calendar.getEventEnd(event).stripZone();
5393
+
5394
+ ranges.push({
5395
+ event: event,
5396
+ start: eventStart,
5397
+ end: eventEnd,
5398
+ eventStartMS: +eventStart,
5399
+ eventDurationMS: eventEnd - eventStart
5400
+ });
5401
+ }
5402
+
5403
+ return ranges;
5404
+ },
5405
+
5406
+
5407
+ // Converts an array of events, with inverse-background rendering, into an array of range objects.
5408
+ // The range objects will cover all the time NOT covered by the events.
5409
+ eventsToInverseRanges: function(events) {
5410
+ var view = this.view;
5411
+ var viewStart = view.start.clone().stripZone(); // normalize timezone
5412
+ var viewEnd = view.end.clone().stripZone(); // normalize timezone
5413
+ var normalRanges = this.eventsToNormalRanges(events); // will give us normalized dates we can use w/o copies
5414
+ var inverseRanges = [];
5415
+ var event0 = events[0]; // assign this to each range's `.event`
5416
+ var start = viewStart; // the end of the previous range. the start of the new range
5417
+ var i, normalRange;
5418
+
5419
+ // ranges need to be in order. required for our date-walking algorithm
5420
+ normalRanges.sort(compareNormalRanges);
5421
+
5422
+ for (i = 0; i < normalRanges.length; i++) {
5423
+ normalRange = normalRanges[i];
5424
+
5425
+ // add the span of time before the event (if there is any)
5426
+ if (normalRange.start > start) { // compare millisecond time (skip any ambig logic)
5427
+ inverseRanges.push({
5428
+ event: event0,
5429
+ start: start,
5430
+ end: normalRange.start
5431
+ });
5432
+ }
5433
+
5434
+ start = normalRange.end;
5435
+ }
5436
+
5437
+ // add the span of time after the last event (if there is any)
5438
+ if (start < viewEnd) { // compare millisecond time (skip any ambig logic)
5439
+ inverseRanges.push({
5440
+ event: event0,
5441
+ start: start,
5442
+ end: viewEnd
5443
+ });
5444
+ }
5445
+
5446
+ return inverseRanges;
5447
+ },
5448
+
5449
+
5450
+ // Slices the given event range into one or more segment objects.
5451
+ // A `rangeToSegsFunc` custom slicing function can be given.
5452
+ eventRangeToSegs: function(eventRange, rangeToSegsFunc) {
5453
+ var segs;
5454
+ var i, seg;
5455
+
5456
+ if (rangeToSegsFunc) {
5457
+ segs = rangeToSegsFunc(eventRange.start, eventRange.end);
5458
+ }
5459
+ else {
5460
+ segs = this.rangeToSegs(eventRange.start, eventRange.end); // defined by the subclass
5461
+ }
5462
+
5463
+ for (i = 0; i < segs.length; i++) {
5464
+ seg = segs[i];
5465
+ seg.event = eventRange.event;
5466
+ seg.eventStartMS = eventRange.eventStartMS;
5467
+ seg.eventDurationMS = eventRange.eventDurationMS;
5468
+ }
5469
+
5470
+ return segs;
4750
5471
  }
4751
5472
 
4752
5473
  });
4753
5474
 
4754
5475
 
4755
- /* Event Segment Utilities
5476
+ /* Utilities
4756
5477
  ----------------------------------------------------------------------------------------------------------------------*/
4757
5478
 
4758
5479
 
5480
+ function isBgEvent(event) { // returns true if background OR inverse-background
5481
+ var rendering = getEventRendering(event);
5482
+ return rendering === 'background' || rendering === 'inverse-background';
5483
+ }
5484
+
5485
+
5486
+ function isInverseBgEvent(event) {
5487
+ return getEventRendering(event) === 'inverse-background';
5488
+ }
5489
+
5490
+
5491
+ function getEventRendering(event) {
5492
+ return firstDefined((event.source || {}).rendering, event.rendering);
5493
+ }
5494
+
5495
+
5496
+ function groupEventsById(events) {
5497
+ var eventsById = {};
5498
+ var i, event;
5499
+
5500
+ for (i = 0; i < events.length; i++) {
5501
+ event = events[i];
5502
+ (eventsById[event._id] || (eventsById[event._id] = [])).push(event);
5503
+ }
5504
+
5505
+ return eventsById;
5506
+ }
5507
+
5508
+
5509
+ // A cmp function for determining which non-inverted "ranges" (see above) happen earlier
5510
+ function compareNormalRanges(range1, range2) {
5511
+ return range1.eventStartMS - range2.eventStartMS; // earlier ranges go first
5512
+ }
5513
+
5514
+
4759
5515
  // A cmp function for determining which segments should take visual priority
5516
+ // DOES NOT WORK ON INVERTED BACKGROUND EVENTS because they have no eventStartMS/eventDurationMS
4760
5517
  function compareSegs(seg1, seg2) {
4761
5518
  return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first
4762
5519
  seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first
@@ -4785,7 +5542,6 @@ $.extend(DayGrid.prototype, {
4785
5542
  rowEls: null, // set of fake row elements
4786
5543
  dayEls: null, // set of whole-day elements comprising the row's background
4787
5544
  helperEls: null, // set of cell skeleton elements for rendering the mock event "helper"
4788
- highlightEls: null, // set of cell skeleton elements for rendering the highlight
4789
5545
 
4790
5546
 
4791
5547
  // Renders the rows and columns into the component's `this.el`, which should already be assigned.
@@ -4971,7 +5727,11 @@ $.extend(DayGrid.prototype, {
4971
5727
  // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null.
4972
5728
  renderHelper: function(event, sourceSeg) {
4973
5729
  var helperNodes = [];
4974
- var rowStructs = this.renderEventRows([ event ]);
5730
+ var segs = this.eventsToSegs([ event ]);
5731
+ var rowStructs;
5732
+
5733
+ segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered
5734
+ rowStructs = this.renderSegRows(segs);
4975
5735
 
4976
5736
  // inject each new event skeleton into each associated row
4977
5737
  this.rowEls.each(function(row, rowNode) {
@@ -5008,65 +5768,65 @@ $.extend(DayGrid.prototype, {
5008
5768
  },
5009
5769
 
5010
5770
 
5011
- /* Highlighting
5771
+ /* Fill System (highlight, background events, business hours)
5012
5772
  ------------------------------------------------------------------------------------------------------------------*/
5013
5773
 
5014
5774
 
5015
- // Renders an emphasis on the given date range. `start` is an inclusive, `end` is exclusive.
5016
- renderHighlight: function(start, end) {
5017
- var segs = this.rangeToSegs(start, end);
5018
- var highlightNodes = [];
5775
+ fillSegTag: 'td', // override the default tag name
5776
+
5777
+
5778
+ // Renders a set of rectangles over the given segments of days.
5779
+ // Only returns segments that successfully rendered.
5780
+ renderFill: function(type, segs) {
5781
+ var nodes = [];
5019
5782
  var i, seg;
5020
- var el;
5783
+ var skeletonEl;
5784
+
5785
+ segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs
5021
5786
 
5022
- // build an event skeleton for each row that needs it
5023
5787
  for (i = 0; i < segs.length; i++) {
5024
5788
  seg = segs[i];
5025
- el = $(
5026
- this.highlightSkeletonHtml(seg.leftCol, seg.rightCol + 1) // make end exclusive
5027
- );
5028
- el.appendTo(this.rowEls[seg.row]);
5029
- highlightNodes.push(el[0]);
5789
+ skeletonEl = this.renderFillRow(type, seg);
5790
+ this.rowEls.eq(seg.row).append(skeletonEl);
5791
+ nodes.push(skeletonEl[0]);
5030
5792
  }
5031
5793
 
5032
- this.highlightEls = $(highlightNodes); // array -> jQuery set
5033
- },
5034
-
5794
+ this.elsByFill[type] = $(nodes);
5035
5795
 
5036
- // Unrenders any visual emphasis on a date range
5037
- destroyHighlight: function() {
5038
- if (this.highlightEls) {
5039
- this.highlightEls.remove();
5040
- this.highlightEls = null;
5041
- }
5796
+ return segs;
5042
5797
  },
5043
5798
 
5044
5799
 
5045
- // Generates the HTML used to build a single-row "highlight skeleton", a table that frames highlight cells
5046
- highlightSkeletonHtml: function(startCol, endCol) {
5800
+ // Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered.
5801
+ renderFillRow: function(type, seg) {
5047
5802
  var colCnt = this.view.colCnt;
5048
- var cellHtml = '';
5803
+ var startCol = seg.leftCol;
5804
+ var endCol = seg.rightCol + 1;
5805
+ var skeletonEl;
5806
+ var trEl;
5807
+
5808
+ skeletonEl = $(
5809
+ '<div class="fc-' + type.toLowerCase() + '-skeleton">' +
5810
+ '<table><tr/></table>' +
5811
+ '</div>'
5812
+ );
5813
+ trEl = skeletonEl.find('tr');
5049
5814
 
5050
5815
  if (startCol > 0) {
5051
- cellHtml += '<td colspan="' + startCol + '"/>';
5052
- }
5053
- if (endCol > startCol) {
5054
- cellHtml += '<td colspan="' + (endCol - startCol) + '" class="fc-highlight" />';
5816
+ trEl.append('<td colspan="' + startCol + '"/>');
5055
5817
  }
5056
- if (colCnt > endCol) {
5057
- cellHtml += '<td colspan="' + (colCnt - endCol) + '"/>';
5818
+
5819
+ trEl.append(
5820
+ seg.el.attr('colspan', endCol - startCol)
5821
+ );
5822
+
5823
+ if (endCol < colCnt) {
5824
+ trEl.append('<td colspan="' + (colCnt - endCol) + '"/>');
5058
5825
  }
5059
5826
 
5060
- cellHtml = this.bookendCells(cellHtml, 'highlight');
5827
+ this.bookendCells(trEl, type);
5061
5828
 
5062
- return '' +
5063
- '<div class="fc-highlight-skeleton">' +
5064
- '<table>' +
5065
- '<tr>' +
5066
- cellHtml +
5067
- '</tr>' +
5068
- '</table>' +
5069
- '</div>';
5829
+ return skeletonEl;
5070
5830
  }
5071
5831
 
5072
5832
  });
@@ -5078,67 +5838,83 @@ $.extend(DayGrid.prototype, {
5078
5838
 
5079
5839
  $.extend(DayGrid.prototype, {
5080
5840
 
5081
- segs: null,
5082
- rowStructs: null, // an array of objects, each holding information about a row's event-rendering
5841
+ rowStructs: null, // an array of objects, each holding information about a row's foreground event-rendering
5083
5842
 
5084
5843
 
5085
- // Render the given events onto the Grid and return the rendered segments
5086
- renderEvents: function(events) {
5087
- var rowStructs = this.rowStructs = this.renderEventRows(events);
5088
- var segs = [];
5844
+ // Unrenders all events currently rendered on the grid
5845
+ destroyEvents: function() {
5846
+ this.destroySegPopover(); // removes the "more.." events popover
5847
+ Grid.prototype.destroyEvents.apply(this, arguments); // calls the super-method
5848
+ },
5849
+
5850
+
5851
+ // Retrieves all rendered segment objects currently rendered on the grid
5852
+ getSegs: function() {
5853
+ return Grid.prototype.getSegs.call(this) // get the segments from the super-method
5854
+ .concat(this.popoverSegs || []); // append the segments from the "more..." popover
5855
+ },
5856
+
5857
+
5858
+ // Renders the given background event segments onto the grid
5859
+ renderBgSegs: function(segs) {
5860
+
5861
+ // don't render timed background events
5862
+ var allDaySegs = $.grep(segs, function(seg) {
5863
+ return seg.event.allDay;
5864
+ });
5865
+
5866
+ return Grid.prototype.renderBgSegs.call(this, allDaySegs); // call the super-method
5867
+ },
5868
+
5869
+
5870
+ // Renders the given foreground event segments onto the grid
5871
+ renderFgSegs: function(segs) {
5872
+ var rowStructs;
5873
+
5874
+ // render an `.el` on each seg
5875
+ // returns a subset of the segs. segs that were actually rendered
5876
+ segs = this.renderFgSegEls(segs);
5877
+
5878
+ rowStructs = this.rowStructs = this.renderSegRows(segs);
5089
5879
 
5090
5880
  // append to each row's content skeleton
5091
5881
  this.rowEls.each(function(i, rowNode) {
5092
5882
  $(rowNode).find('.fc-content-skeleton > table').append(
5093
5883
  rowStructs[i].tbodyEl
5094
5884
  );
5095
- segs.push.apply(segs, rowStructs[i].segs);
5096
5885
  });
5097
5886
 
5098
- this.segs = segs;
5099
- },
5100
-
5101
-
5102
- // Retrieves all segment objects that have been rendered
5103
- getSegs: function() {
5104
- return (this.segs || []).concat(
5105
- this.popoverSegs || [] // segs rendered in the "more" events popover
5106
- );
5887
+ return segs; // return only the segs that were actually rendered
5107
5888
  },
5108
5889
 
5109
5890
 
5110
- // Removes all rendered event elements
5111
- destroyEvents: function() {
5112
- var rowStructs;
5891
+ // Unrenders all currently rendered foreground event segments
5892
+ destroyFgSegs: function() {
5893
+ var rowStructs = this.rowStructs || [];
5113
5894
  var rowStruct;
5114
5895
 
5115
- Grid.prototype.destroyEvents.call(this); // call the super-method
5116
-
5117
- rowStructs = this.rowStructs || [];
5118
5896
  while ((rowStruct = rowStructs.pop())) {
5119
5897
  rowStruct.tbodyEl.remove();
5120
5898
  }
5121
5899
 
5122
- this.segs = null;
5123
- this.destroySegPopover(); // removes the "more.." events popover
5900
+ this.rowStructs = null;
5124
5901
  },
5125
5902
 
5126
5903
 
5127
5904
  // Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton.
5128
- // Returns an array of rowStruct objects (see the bottom of `renderEventRow`).
5129
- renderEventRows: function(events) {
5130
- var segs = this.eventsToSegs(events);
5905
+ // Returns an array of rowStruct objects (see the bottom of `renderSegRow`).
5906
+ // PRECONDITION: each segment shoud already have a rendered and assigned `.el`
5907
+ renderSegRows: function(segs) {
5131
5908
  var rowStructs = [];
5132
5909
  var segRows;
5133
5910
  var row;
5134
5911
 
5135
- segs = this.renderSegs(segs); // returns a new array with only visible segments
5136
5912
  segRows = this.groupSegRows(segs); // group into nested arrays
5137
5913
 
5138
5914
  // iterate each row of segment groupings
5139
5915
  for (row = 0; row < segRows.length; row++) {
5140
5916
  rowStructs.push(
5141
- this.renderEventRow(row, segRows[row])
5917
+ this.renderSegRow(row, segRows[row])
5142
5918
  );
5143
5919
  }
5144
5920
 
@@ -5147,7 +5923,7 @@ $.extend(DayGrid.prototype, {
5147
5923
 
5148
5924
 
5149
5925
  // Builds the HTML to be used for the default element for an individual segment
5150
- renderSegHtml: function(seg, disableResizing) {
5926
+ fgSegHtml: function(seg, disableResizing) {
5151
5927
  var view = this.view;
5152
5928
  var isRTL = view.opt('isRTL');
5153
5929
  var event = seg.event;
@@ -5196,7 +5972,7 @@ $.extend(DayGrid.prototype, {
5196
5972
 
5197
5973
  // Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains
5198
5974
  // the segments. Returns object with a bunch of internal data about how the render was calculated.
5199
- renderEventRow: function(row, rowSegs) {
5975
+ renderSegRow: function(row, rowSegs) {
5200
5976
  var view = this.view;
5201
5977
  var colCnt = view.colCnt;
5202
5978
  var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels
@@ -5367,6 +6143,7 @@ function compareDaySegCols(a, b) {
5367
6143
 
5368
6144
  /* Methods relate to limiting the number events for a given day on a DayGrid
5369
6145
  ----------------------------------------------------------------------------------------------------------------------*/
6146
+ // NOTE: all the segs being passed around in here are foreground segs
5370
6147
 
5371
6148
  $.extend(DayGrid.prototype, {
5372
6149
 
@@ -5646,7 +6423,7 @@ $.extend(DayGrid.prototype, {
5646
6423
  var i;
5647
6424
 
5648
6425
  // render each seg's `el` and only return the visible segs
5649
- segs = this.renderSegs(segs, true); // disableResizing=true
6426
+ segs = this.renderFgSegEls(segs, true); // disableResizing=true
5650
6427
  this.popoverSegs = segs;
5651
6428
 
5652
6429
  for (i = 0; i < segs.length; i++) {
@@ -5664,13 +6441,23 @@ $.extend(DayGrid.prototype, {
5664
6441
 
5665
6442
  // Given the events within an array of segment objects, reslice them to be in a single day
5666
6443
  resliceDaySegs: function(segs, dayDate) {
6444
+
6445
+ // build an array of the original events
5667
6446
  var events = $.map(segs, function(seg) {
5668
6447
  return seg.event;
5669
6448
  });
6449
+
5670
6450
  var dayStart = dayDate.clone().stripTime();
5671
6451
  var dayEnd = dayStart.clone().add(1, 'days');
5672
6452
 
5673
- return this.eventsToSegs(events, dayStart, dayEnd);
6453
+ // slice the events with a custom slicing function
6454
+ return this.eventsToSegs(
6455
+ events,
6456
+ function(rangeStart, rangeEnd) {
6457
+ var seg = intersectionToSeg(rangeStart, rangeEnd, dayStart, dayEnd); // if no intersection, undefined
6458
+ return seg ? [ seg ] : []; // must return an array of segments
6459
+ }
6460
+ );
5674
6461
  },
5675
6462
 
5676
6463
 
@@ -5733,9 +6520,10 @@ $.extend(TimeGrid.prototype, {
5733
6520
 
5734
6521
  slatTops: null, // an array of top positions, relative to the container. last item holds bottom of last slot
5735
6522
 
5736
- highlightEl: null, // cell skeleton element for rendering the highlight
5737
6523
  helperEl: null, // cell skeleton element for rendering the mock event "helper"
5738
6524
 
6525
+ businessHourSegs: null,
6526
+
5739
6527
 
5740
6528
  // Renders the time grid into `this.el`, which should already be assigned.
5741
6529
  // Relies on the view's colCnt. In the future, this component should probably be self-sufficient.
@@ -5749,10 +6537,18 @@ $.extend(TimeGrid.prototype, {
5749
6537
 
5750
6538
  this.computeSlatTops();
5751
6539
 
6540
+ this.renderBusinessHours();
6541
+
5752
6542
  Grid.prototype.render.call(this); // call the super-method
5753
6543
  },
5754
6544
 
5755
6545
 
6546
+ renderBusinessHours: function() {
6547
+ var events = this.view.calendar.getBusinessHoursEvents();
6548
+ this.businessHourSegs = this.renderFill('businessHours', this.eventsToSegs(events), 'bgevent');
6549
+ },
6550
+
6551
+
5756
6552
  // Renders the basic HTML skeleton for the grid
5757
6553
  renderHtml: function() {
5758
6554
  return '' +
@@ -6038,12 +6834,14 @@ $.extend(TimeGrid.prototype, {
6038
6834
 
6039
6835
  // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag)
6040
6836
  renderHelper: function(event, sourceSeg) {
6041
- var res = this.renderEventTable([ event ]);
6042
- var tableEl = res.tableEl;
6043
- var segs = res.segs;
6837
+ var segs = this.eventsToSegs([ event ]);
6838
+ var tableEl;
6044
6839
  var i, seg;
6045
6840
  var sourceEl;
6046
6841
 
6842
+ segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered
6843
+ tableEl = this.renderSegTable(segs);
6844
+
6047
6845
  // Try to make the segment that is in the same row as sourceSeg look the same
6048
6846
  for (i = 0; i < segs.length; i++) {
6049
6847
  seg = segs[i];
@@ -6095,79 +6893,63 @@ $.extend(TimeGrid.prototype, {
6095
6893
  },
6096
6894
 
6097
6895
 
6098
- /* Highlight
6896
+ /* Fill System (highlight, background events, business hours)
6099
6897
  ------------------------------------------------------------------------------------------------------------------*/
6100
6898
 
6101
6899
 
6102
- // Renders an emphasis on the given date range. `start` is inclusive. `end` is exclusive.
6103
- renderHighlight: function(start, end) {
6104
- this.highlightEl = $(
6105
- this.highlightSkeletonHtml(start, end)
6106
- ).appendTo(this.el);
6107
- },
6108
-
6109
-
6110
- // Unrenders the emphasis on a date range
6111
- destroyHighlight: function() {
6112
- if (this.highlightEl) {
6113
- this.highlightEl.remove();
6114
- this.highlightEl = null;
6115
- }
6116
- },
6117
-
6118
-
6119
- // Generates HTML for a table element with containers in each column, responsible for absolutely positioning the
6120
- // highlight elements to cover the highlighted slots.
6121
- highlightSkeletonHtml: function(start, end) {
6900
+ // Renders a set of rectangles over the given time segments.
6901
+ // Only returns segments that successfully rendered.
6902
+ renderFill: function(type, segs, className) {
6122
6903
  var view = this.view;
6123
- var segs = this.rangeToSegs(start, end);
6124
- var cellHtml = '';
6125
- var col = 0;
6126
- var i, seg;
6904
+ var segCols;
6905
+ var skeletonEl;
6906
+ var trEl;
6907
+ var col, colSegs;
6908
+ var tdEl;
6909
+ var containerEl;
6127
6910
  var dayDate;
6128
- var top, bottom;
6129
-
6130
- for (i = 0; i < segs.length; i++) { // loop through the segments. one per column
6131
- seg = segs[i];
6911
+ var i, seg;
6132
6912
 
6133
- // need empty cells beforehand?
6134
- if (col < seg.col) {
6135
- cellHtml += '<td colspan="' + (seg.col - col) + '"/>';
6136
- col = seg.col;
6137
- }
6913
+ if (segs.length) {
6138
6914
 
6139
- // compute vertical position
6140
- dayDate = view.cellToDate(0, col);
6141
- top = this.computeDateTop(seg.start, dayDate);
6142
- bottom = this.computeDateTop(seg.end, dayDate); // the y position of the bottom edge
6915
+ segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs
6916
+ segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg
6143
6917
 
6144
- // generate the cell HTML. bottom becomes negative because it needs to be a CSS value relative to the
6145
- // bottom edge of the zero-height container.
6146
- cellHtml +=
6147
- '<td>' +
6148
- '<div class="fc-highlight-container">' +
6149
- '<div class="fc-highlight" style="top:' + top + 'px;bottom:-' + bottom + 'px"/>' +
6150
- '</div>' +
6151
- '</td>';
6918
+ className = className || type.toLowerCase();
6919
+ skeletonEl = $(
6920
+ '<div class="fc-' + className + '-skeleton">' +
6921
+ '<table><tr/></table>' +
6922
+ '</div>'
6923
+ );
6924
+ trEl = skeletonEl.find('tr');
6925
+
6926
+ for (col = 0; col < segCols.length; col++) {
6927
+ colSegs = segCols[col];
6928
+ tdEl = $('<td/>').appendTo(trEl);
6929
+
6930
+ if (colSegs.length) {
6931
+ containerEl = $('<div class="fc-' + className + '-container"/>').appendTo(tdEl);
6932
+ dayDate = view.cellToDate(0, col);
6933
+
6934
+ for (i = 0; i < colSegs.length; i++) {
6935
+ seg = colSegs[i];
6936
+ containerEl.append(
6937
+ seg.el.css({
6938
+ top: this.computeDateTop(seg.start, dayDate),
6939
+ bottom: -this.computeDateTop(seg.end, dayDate) // the y position of the bottom edge
6940
+ })
6941
+ );
6942
+ }
6943
+ }
6944
+ }
6152
6945
 
6153
- col++;
6154
- }
6946
+ this.bookendCells(trEl, type);
6155
6947
 
6156
- // need empty cells after the last segment?
6157
- if (col < view.colCnt) {
6158
- cellHtml += '<td colspan="' + (view.colCnt - col) + '"/>';
6948
+ this.el.append(skeletonEl);
6949
+ this.elsByFill[type] = skeletonEl;
6159
6950
  }
6160
6951
 
6161
- cellHtml = this.bookendCells(cellHtml, 'highlight');
6162
-
6163
- return '' +
6164
- '<div class="fc-highlight-skeleton">' +
6165
- '<table>' +
6166
- '<tr>' +
6167
- cellHtml +
6168
- '</tr>' +
6169
- '</table>' +
6170
- '</div>';
6952
+ return segs;
6171
6953
  }
6172
6954
 
6173
6955
  });
@@ -6179,52 +6961,41 @@ $.extend(TimeGrid.prototype, {
6179
6961
 
6180
6962
  $.extend(TimeGrid.prototype, {
6181
6963
 
6182
- segs: null, // segment objects rendered in the component. null of events haven't been rendered yet
6183
6964
  eventSkeletonEl: null, // has cells with event-containers, which contain absolutely positioned event elements
6184
6965
 
6185
6966
 
6186
- // Renders the events onto the grid and returns an array of segments that have been rendered
6187
- renderEvents: function(events) {
6188
- var res = this.renderEventTable(events);
6189
-
6190
- this.eventSkeletonEl = $('<div class="fc-content-skeleton"/>').append(res.tableEl);
6191
- this.el.append(this.eventSkeletonEl);
6192
-
6193
- this.segs = res.segs;
6194
- },
6967
+ // Renders the given foreground event segments onto the grid
6968
+ renderFgSegs: function(segs) {
6969
+ segs = this.renderFgSegEls(segs); // returns a subset of the segs. segs that were actually rendered
6195
6970
 
6971
+ this.el.append(
6972
+ this.eventSkeletonEl = $('<div class="fc-content-skeleton"/>')
6973
+ .append(this.renderSegTable(segs))
6974
+ );
6196
6975
 
6197
- // Retrieves rendered segment objects
6198
- getSegs: function() {
6199
- return this.segs || [];
6976
+ return segs; // return only the segs that were actually rendered
6200
6977
  },
6201
6978
 
6202
6979
 
6203
- // Removes all event segment elements from the view
6204
- destroyEvents: function() {
6205
- Grid.prototype.destroyEvents.call(this); // call the super-method
6206
-
6980
+ // Unrenders all currently rendered foreground event segments
6981
+ destroyFgSegs: function(segs) {
6207
6982
  if (this.eventSkeletonEl) {
6208
6983
  this.eventSkeletonEl.remove();
6209
6984
  this.eventSkeletonEl = null;
6210
6985
  }
6211
-
6212
- this.segs = null;
6213
6986
  },
6214
6987
 
6215
6988
 
6216
6989
  // Renders and returns the <table> portion of the event-skeleton.
6217
6990
  // Returns an object with properties 'tbodyEl' and 'segs'.
6218
- renderEventTable: function(events) {
6991
+ renderSegTable: function(segs) {
6219
6992
  var tableEl = $('<table><tr/></table>');
6220
6993
  var trEl = tableEl.find('tr');
6221
- var segs = this.eventsToSegs(events);
6222
6994
  var segCols;
6223
6995
  var i, seg;
6224
6996
  var col, colSegs;
6225
6997
  var containerEl;
6226
6998
 
6227
- segs = this.renderSegs(segs); // returns only the visible segs
6228
6999
  segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg
6229
7000
 
6230
7001
  this.computeSegVerticals(segs); // compute and assign top/bottom
@@ -6253,26 +7024,22 @@ $.extend(TimeGrid.prototype, {
6253
7024
 
6254
7025
  this.bookendCells(trEl, 'eventSkeleton');
6255
7026
 
6256
- return {
6257
- tableEl: tableEl,
6258
- segs: segs
6259
- };
7027
+ return tableEl;
6260
7028
  },
6261
7029
 
6262
7030
 
6263
7031
  // Refreshes the CSS top/bottom coordinates for each segment element. Probably after a window resize/zoom.
7032
+ // Repositions business hours segs too, so not just for events. Maybe shouldn't be here.
6264
7033
  updateSegVerticals: function() {
6265
- var segs = this.segs;
7034
+ var allSegs = (this.segs || []).concat(this.businessHourSegs || []);
6266
7035
  var i;
6267
7036
 
6268
- if (segs) {
6269
- this.computeSegVerticals(segs);
7037
+ this.computeSegVerticals(allSegs);
6270
7038
 
6271
- for (i = 0; i < segs.length; i++) {
6272
- segs[i].el.css(
6273
- this.generateSegVerticalCss(segs[i])
6274
- );
6275
- }
7039
+ for (i = 0; i < allSegs.length; i++) {
7040
+ allSegs[i].el.css(
7041
+ this.generateSegVerticalCss(allSegs[i])
7042
+ );
6276
7043
  }
6277
7044
  },
6278
7045
 
@@ -6290,7 +7057,7 @@ $.extend(TimeGrid.prototype, {
6290
7057
 
6291
7058
 
6292
7059
  // Renders the HTML for a single event segment's default rendering
6293
- renderSegHtml: function(seg, disableResizing) {
7060
+ fgSegHtml: function(seg, disableResizing) {
6294
7061
  var view = this.view;
6295
7062
  var event = seg.event;
6296
7063
  var isDraggable = view.isEventDraggable(event);
@@ -6858,32 +7625,81 @@ View.prototype = {
6858
7625
  // Gets called when jqui's 'dragstart' is fired.
6859
7626
  documentDragStart: function(ev, ui) {
6860
7627
  var _this = this;
6861
- var dropDate = null;
7628
+ var calendar = this.calendar;
7629
+ var eventStart = null; // a null value signals an unsuccessful drag
7630
+ var eventEnd = null;
7631
+ var visibleEnd = null; // will be calculated event when no eventEnd
7632
+ var el;
7633
+ var accept;
7634
+ var meta;
7635
+ var eventProps; // if an object, signals an event should be created upon drop
6862
7636
  var dragListener;
6863
7637
 
6864
7638
  if (this.opt('droppable')) { // only listen if this setting is on
7639
+ el = $(ev.target);
7640
+
7641
+ // Test that the dragged element passes the dropAccept selector or filter function.
7642
+ // FYI, the default is "*" (matches all)
7643
+ accept = this.opt('dropAccept');
7644
+ if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) {
7645
+
7646
+ meta = getDraggedElMeta(el); // data for possibly creating an event
7647
+ eventProps = meta.eventProps;
7648
+
7649
+ // listener that tracks mouse movement over date-associated pixel regions
7650
+ dragListener = new DragListener(this.coordMap, {
7651
+ cellOver: function(cell, cellDate) {
7652
+ eventStart = cellDate;
7653
+ eventEnd = meta.duration ? eventStart.clone().add(meta.duration) : null;
7654
+ visibleEnd = eventEnd || calendar.getDefaultEventEnd(!eventStart.hasTime(), eventStart);
7655
+
7656
+ // keep the start/end up to date when dragging
7657
+ if (eventProps) {
7658
+ $.extend(eventProps, { start: eventStart, end: eventEnd });
7659
+ }
7660
+
7661
+ if (calendar.isExternalDragAllowedInRange(eventStart, visibleEnd, eventProps)) {
7662
+ _this.renderDrag(eventStart, visibleEnd);
7663
+ }
7664
+ else {
7665
+ eventStart = null; // signal unsuccessful
7666
+ disableCursor();
7667
+ }
7668
+ },
7669
+ cellOut: function() {
7670
+ eventStart = null;
7671
+ _this.destroyDrag();
7672
+ enableCursor();
7673
+ }
7674
+ });
7675
+
7676
+ // gets called, only once, when jqui drag is finished
7677
+ $(document).one('dragstop', function(ev, ui) {
7678
+ var renderedEvents;
6865
7679
 
6866
- // listener that tracks mouse movement over date-associated pixel regions
6867
- dragListener = new DragListener(this.coordMap, {
6868
- cellOver: function(cell, date) {
6869
- dropDate = date;
6870
- _this.renderDrag(date);
6871
- },
6872
- cellOut: function() {
6873
- dropDate = null;
6874
7680
  _this.destroyDrag();
6875
- }
6876
- });
7681
+ enableCursor();
6877
7682
 
6878
- // gets called, only once, when jqui drag is finished
6879
- $(document).one('dragstop', function(ev, ui) {
6880
- _this.destroyDrag();
6881
- if (dropDate) {
6882
- _this.trigger('drop', ev.target, dropDate, ev, ui);
6883
- }
6884
- });
7683
+ if (eventStart) { // element was dropped on a valid date/time cell
7684
+
7685
+ // if dropped on an all-day cell, and element's metadata specified a time, set it
7686
+ if (meta.startTime && !eventStart.hasTime()) {
7687
+ eventStart.time(meta.startTime);
7688
+ }
7689
+
7690
+ // trigger 'drop' regardless of whether element represents an event
7691
+ _this.trigger('drop', el[0], eventStart, ev, ui);
7692
+
7693
+ // create an event from the given properties and the latest dates
7694
+ if (eventProps) {
7695
+ renderedEvents = calendar.renderEvent(eventProps, meta.stick);
7696
+ _this.trigger('eventReceive', null, renderedEvents[0]); // signal an external event landed
7697
+ }
7698
+ }
7699
+ });
6885
7700
 
6886
- dragListener.startDrag(ev); // start listening immediately
7701
+ dragListener.startDrag(ev); // start listening immediately
7702
+ }
6887
7703
  }
6888
7704
  },
6889
7705
 
@@ -7414,6 +8230,60 @@ function View(calendar) {
7414
8230
 
7415
8231
  }
7416
8232
 
8233
+
8234
+ /* Utils
8235
+ ----------------------------------------------------------------------------------------------------------------------*/
8236
+
8237
+ // Require all HTML5 data-* attributes used by FullCalendar to have this prefix.
8238
+ // A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event.
8239
+ fc.dataAttrPrefix = '';
8240
+
8241
+ // Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure
8242
+ // to be used for Event Object creation.
8243
+ // A defined `.eventProps`, even when empty, indicates that an event should be created.
8244
+ function getDraggedElMeta(el) {
8245
+ var prefix = fc.dataAttrPrefix;
8246
+ var eventProps; // properties for creating the event, not related to date/time
8247
+ var startTime; // a Duration
8248
+ var duration;
8249
+ var stick;
8250
+
8251
+ if (prefix) { prefix += '-'; }
8252
+ eventProps = el.data(prefix + 'event') || null;
8253
+
8254
+ if (eventProps) {
8255
+ if (typeof eventProps === 'object') {
8256
+ eventProps = $.extend({}, eventProps); // make a copy
8257
+ }
8258
+ else { // something like 1 or true. still signal event creation
8259
+ eventProps = {};
8260
+ }
8261
+
8262
+ // pluck special-cased date/time properties
8263
+ startTime = eventProps.start;
8264
+ if (startTime == null) { startTime = eventProps.time; } // accept 'time' as well
8265
+ duration = eventProps.duration;
8266
+ stick = eventProps.stick;
8267
+ delete eventProps.start;
8268
+ delete eventProps.time;
8269
+ delete eventProps.duration;
8270
+ delete eventProps.stick;
8271
+ }
8272
+
8273
+ // fallback to standalone attribute values for each of the date/time properties
8274
+ if (startTime == null) { startTime = el.data(prefix + 'start'); }
8275
+ if (startTime == null) { startTime = el.data(prefix + 'time'); } // accept 'time' as well
8276
+ if (duration == null) { duration = el.data(prefix + 'duration'); }
8277
+ if (stick == null) { stick = el.data(prefix + 'stick'); }
8278
+
8279
+ // massage into correct data types
8280
+ startTime = startTime != null ? moment.duration(startTime) : null;
8281
+ duration = duration != null ? moment.duration(duration) : null;
8282
+ stick = Boolean(stick);
8283
+
8284
+ return { eventProps: eventProps, startTime: startTime, duration: duration, stick: stick };
8285
+ }
8286
+
7417
8287
  ;;
7418
8288
 
7419
8289
  /* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells.