fullcalendar.io-rails 2.6.1 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -0
  3. data/fullcalendar.io-rails.gemspec +1 -1
  4. data/lib/fullcalendar.io/rails/version.rb +1 -1
  5. data/vendor/assets/javascripts/fullcalendar.js +1116 -455
  6. data/vendor/assets/javascripts/fullcalendar/fullcalendar.js +12559 -0
  7. data/vendor/assets/javascripts/fullcalendar/gcal.js +1 -1
  8. data/vendor/assets/javascripts/fullcalendar/lang-all.js +0 -0
  9. data/vendor/assets/javascripts/fullcalendar/lang/ar-ma.js +0 -0
  10. data/vendor/assets/javascripts/fullcalendar/lang/ar-sa.js +0 -0
  11. data/vendor/assets/javascripts/fullcalendar/lang/ar-tn.js +0 -0
  12. data/vendor/assets/javascripts/fullcalendar/lang/ar.js +0 -0
  13. data/vendor/assets/javascripts/fullcalendar/lang/bg.js +0 -0
  14. data/vendor/assets/javascripts/fullcalendar/lang/ca.js +0 -0
  15. data/vendor/assets/javascripts/fullcalendar/lang/cs.js +0 -0
  16. data/vendor/assets/javascripts/fullcalendar/lang/da.js +0 -0
  17. data/vendor/assets/javascripts/fullcalendar/lang/de-at.js +0 -0
  18. data/vendor/assets/javascripts/fullcalendar/lang/de.js +0 -0
  19. data/vendor/assets/javascripts/fullcalendar/lang/el.js +0 -0
  20. data/vendor/assets/javascripts/fullcalendar/lang/en-au.js +0 -0
  21. data/vendor/assets/javascripts/fullcalendar/lang/en-ca.js +0 -0
  22. data/vendor/assets/javascripts/fullcalendar/lang/en-gb.js +0 -0
  23. data/vendor/assets/javascripts/fullcalendar/lang/en-ie.js +0 -0
  24. data/vendor/assets/javascripts/fullcalendar/lang/en-nz.js +0 -0
  25. data/vendor/assets/javascripts/fullcalendar/lang/es.js +0 -0
  26. data/vendor/assets/javascripts/fullcalendar/lang/fa.js +0 -0
  27. data/vendor/assets/javascripts/fullcalendar/lang/fi.js +0 -0
  28. data/vendor/assets/javascripts/fullcalendar/lang/fr-ca.js +0 -0
  29. data/vendor/assets/javascripts/fullcalendar/lang/fr-ch.js +0 -0
  30. data/vendor/assets/javascripts/fullcalendar/lang/fr.js +0 -0
  31. data/vendor/assets/javascripts/fullcalendar/lang/he.js +0 -0
  32. data/vendor/assets/javascripts/fullcalendar/lang/hi.js +0 -0
  33. data/vendor/assets/javascripts/fullcalendar/lang/hr.js +0 -0
  34. data/vendor/assets/javascripts/fullcalendar/lang/hu.js +0 -0
  35. data/vendor/assets/javascripts/fullcalendar/lang/id.js +0 -0
  36. data/vendor/assets/javascripts/fullcalendar/lang/is.js +0 -0
  37. data/vendor/assets/javascripts/fullcalendar/lang/it.js +0 -0
  38. data/vendor/assets/javascripts/fullcalendar/lang/ja.js +0 -0
  39. data/vendor/assets/javascripts/fullcalendar/lang/ko.js +0 -0
  40. data/vendor/assets/javascripts/fullcalendar/lang/lt.js +0 -0
  41. data/vendor/assets/javascripts/fullcalendar/lang/lv.js +0 -0
  42. data/vendor/assets/javascripts/fullcalendar/lang/nb.js +0 -0
  43. data/vendor/assets/javascripts/fullcalendar/lang/nl.js +0 -0
  44. data/vendor/assets/javascripts/fullcalendar/lang/pl.js +0 -0
  45. data/vendor/assets/javascripts/fullcalendar/lang/pt-br.js +0 -0
  46. data/vendor/assets/javascripts/fullcalendar/lang/pt.js +0 -0
  47. data/vendor/assets/javascripts/fullcalendar/lang/ro.js +0 -0
  48. data/vendor/assets/javascripts/fullcalendar/lang/ru.js +0 -0
  49. data/vendor/assets/javascripts/fullcalendar/lang/sk.js +0 -0
  50. data/vendor/assets/javascripts/fullcalendar/lang/sl.js +0 -0
  51. data/vendor/assets/javascripts/fullcalendar/lang/sr-cyrl.js +0 -0
  52. data/vendor/assets/javascripts/fullcalendar/lang/sr.js +0 -0
  53. data/vendor/assets/javascripts/fullcalendar/lang/sv.js +0 -0
  54. data/vendor/assets/javascripts/fullcalendar/lang/th.js +0 -0
  55. data/vendor/assets/javascripts/fullcalendar/lang/tr.js +0 -0
  56. data/vendor/assets/javascripts/fullcalendar/lang/uk.js +0 -0
  57. data/vendor/assets/javascripts/fullcalendar/lang/vi.js +0 -0
  58. data/vendor/assets/javascripts/fullcalendar/lang/zh-cn.js +0 -0
  59. data/vendor/assets/javascripts/fullcalendar/lang/zh-tw.js +0 -0
  60. data/vendor/assets/stylesheets/fullcalendar.css +182 -35
  61. data/vendor/assets/stylesheets/fullcalendar.print.css +1 -1
  62. metadata +54 -125
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2a218da8c33586580b96af5a4cbdc068fab02dae
4
- data.tar.gz: db2c25200412ef449844a5061496b2599dc7bc50
3
+ metadata.gz: b543ff2553f46e50d402c15b667d16db0ccaefa4
4
+ data.tar.gz: 8d4383e4ac10f442751447595ac1b8095afe3494
5
5
  SHA512:
6
- metadata.gz: 238e2d3646615ef31b710c372cfd603f6c0705e587e1c22ee288f86174a8892016184e7617b6e8cf19ed9ffa383c5763bfcfcc0eccffef778b9b71876222e938
7
- data.tar.gz: a5cb2e21d8fa96060b6fa98f6afdaaa30f7766870ef593308f94e1891fc38c9201b4fa2480444663d592acf3af91650821bbe7534e94411095c519718582f867
6
+ metadata.gz: ec4a8cf82f3ca63f5c8c86570f158c89c70c427a390c6bdc368e249d44cfa341cacbc441b88c4af39bfe35ace12618fe828b415ab88275974d20e923c025c73f
7
+ data.tar.gz: a056689056efb450c9107c8931c45f685c0b6fd5336c5e6d4018176da0f4d4fcf22469969b3b802a966650e74411ae5b7ae6cc025f417570b2e9b190e1344fed
data/README.md CHANGED
@@ -44,3 +44,7 @@ http://fullcalendar.io/docs/
44
44
  3. Commit your changes (`git commit -am 'Add some feature'`)
45
45
  4. Push to the branch (`git push origin my-new-feature`)
46
46
  5. Create a new Pull Request
47
+
48
+ ## License
49
+
50
+ Released under the MIT License.
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
20
20
  spec.test_files = Dir["spec/**/*"]
21
21
 
22
22
  spec.add_runtime_dependency 'jquery-rails', '>= 3.1.0', '< 5.0'
23
- spec.add_runtime_dependency 'momentjs-rails', '~> 2.8', '>= 2.8.4'
23
+ spec.add_runtime_dependency 'momentjs-rails', '~> 2.11', '>= 2.11.0'
24
24
  spec.add_development_dependency "bundler", "~> 1.7"
25
25
  spec.add_development_dependency "rake", "~> 10.0"
26
26
  spec.add_development_dependency 'rails', '4.2.5'
@@ -1,5 +1,5 @@
1
1
  module Fullcalendario
2
2
  module Rails
3
- VERSION = "2.6.1"
3
+ VERSION = "2.7.0"
4
4
  end
5
5
  end
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * FullCalendar v2.6.1
2
+ * FullCalendar v2.7.0
3
3
  * Docs & License: http://fullcalendar.io/
4
4
  * (c) 2015 Adam Shaw
5
5
  */
@@ -19,12 +19,15 @@
19
19
  ;;
20
20
 
21
21
  var FC = $.fullCalendar = {
22
- version: "2.6.1",
22
+ version: "2.7.0",
23
23
  internalApiVersion: 3
24
24
  };
25
25
  var fcViews = FC.views = {};
26
26
 
27
27
 
28
+ FC.isTouch = 'ontouchstart' in document;
29
+
30
+
28
31
  $.fn.fullCalendar = function(options) {
29
32
  var args = Array.prototype.slice.call(arguments, 1); // for a possible method call
30
33
  var res = this; // what this function will return (this jQuery object by default)
@@ -262,29 +265,25 @@ function matchCellWidths(els) {
262
265
  }
263
266
 
264
267
 
265
- // Turns a container element into a scroller if its contents is taller than the allotted height.
266
- // Returns true if the element is now a scroller, false otherwise.
267
- // NOTE: this method is best because it takes weird zooming dimensions into account
268
- function setPotentialScroller(containerEl, height) {
269
- containerEl.height(height).addClass('fc-scroller');
270
-
271
- // are scrollbars needed?
272
- if (containerEl[0].scrollHeight - 1 > containerEl[0].clientHeight) { // !!! -1 because IE is often off-by-one :(
273
- return true;
274
- }
275
-
276
- unsetScroller(containerEl); // undo
277
- return false;
278
- }
268
+ // Given one element that resides inside another,
269
+ // Subtracts the height of the inner element from the outer element.
270
+ function subtractInnerElHeight(outerEl, innerEl) {
271
+ var both = outerEl.add(innerEl);
272
+ var diff;
279
273
 
274
+ // fuckin IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked
275
+ both.css({
276
+ position: 'relative', // cause a reflow, which will force fresh dimension recalculation
277
+ left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll
278
+ });
279
+ diff = outerEl.outerHeight() - innerEl.outerHeight(); // grab the dimensions
280
+ both.css({ position: '', left: '' }); // undo hack
280
281
 
281
- // Takes an element that might have been a scroller, and turns it back into a normal element.
282
- function unsetScroller(containerEl) {
283
- containerEl.height('').removeClass('fc-scroller');
282
+ return diff;
284
283
  }
285
284
 
286
285
 
287
- /* General DOM Utilities
286
+ /* Element Geom Utilities
288
287
  ----------------------------------------------------------------------------------------------------------------------*/
289
288
 
290
289
  FC.getOuterRect = getOuterRect;
@@ -309,26 +308,30 @@ function getScrollParent(el) {
309
308
 
310
309
  // Queries the outer bounding area of a jQuery element.
311
310
  // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
312
- function getOuterRect(el) {
311
+ // Origin is optional.
312
+ function getOuterRect(el, origin) {
313
313
  var offset = el.offset();
314
+ var left = offset.left - (origin ? origin.left : 0);
315
+ var top = offset.top - (origin ? origin.top : 0);
314
316
 
315
317
  return {
316
- left: offset.left,
317
- right: offset.left + el.outerWidth(),
318
- top: offset.top,
319
- bottom: offset.top + el.outerHeight()
318
+ left: left,
319
+ right: left + el.outerWidth(),
320
+ top: top,
321
+ bottom: top + el.outerHeight()
320
322
  };
321
323
  }
322
324
 
323
325
 
324
326
  // Queries the area within the margin/border/scrollbars of a jQuery element. Does not go within the padding.
325
327
  // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
328
+ // Origin is optional.
326
329
  // NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
327
- function getClientRect(el) {
330
+ function getClientRect(el, origin) {
328
331
  var offset = el.offset();
329
332
  var scrollbarWidths = getScrollbarWidths(el);
330
- var left = offset.left + getCssFloat(el, 'border-left-width') + scrollbarWidths.left;
331
- var top = offset.top + getCssFloat(el, 'border-top-width') + scrollbarWidths.top;
333
+ var left = offset.left + getCssFloat(el, 'border-left-width') + scrollbarWidths.left - (origin ? origin.left : 0);
334
+ var top = offset.top + getCssFloat(el, 'border-top-width') + scrollbarWidths.top - (origin ? origin.top : 0);
332
335
 
333
336
  return {
334
337
  left: left,
@@ -341,10 +344,13 @@ function getClientRect(el) {
341
344
 
342
345
  // Queries the area within the margin/border/padding of a jQuery element. Assumed not to have scrollbars.
343
346
  // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
344
- function getContentRect(el) {
347
+ // Origin is optional.
348
+ function getContentRect(el, origin) {
345
349
  var offset = el.offset(); // just outside of border, margin not included
346
- var left = offset.left + getCssFloat(el, 'border-left-width') + getCssFloat(el, 'padding-left');
347
- var top = offset.top + getCssFloat(el, 'border-top-width') + getCssFloat(el, 'padding-top');
350
+ var left = offset.left + getCssFloat(el, 'border-left-width') + getCssFloat(el, 'padding-left') -
351
+ (origin ? origin.left : 0);
352
+ var top = offset.top + getCssFloat(el, 'border-top-width') + getCssFloat(el, 'padding-top') -
353
+ (origin ? origin.top : 0);
348
354
 
349
355
  return {
350
356
  left: left,
@@ -414,13 +420,58 @@ function getCssFloat(el, prop) {
414
420
  }
415
421
 
416
422
 
423
+ /* Mouse / Touch Utilities
424
+ ----------------------------------------------------------------------------------------------------------------------*/
425
+
426
+ FC.preventDefault = preventDefault;
427
+
428
+
417
429
  // Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
418
430
  function isPrimaryMouseButton(ev) {
419
431
  return ev.which == 1 && !ev.ctrlKey;
420
432
  }
421
433
 
422
434
 
423
- /* Geometry
435
+ function getEvX(ev) {
436
+ if (ev.pageX !== undefined) {
437
+ return ev.pageX;
438
+ }
439
+ var touches = ev.originalEvent.touches;
440
+ if (touches) {
441
+ return touches[0].pageX;
442
+ }
443
+ }
444
+
445
+
446
+ function getEvY(ev) {
447
+ if (ev.pageY !== undefined) {
448
+ return ev.pageY;
449
+ }
450
+ var touches = ev.originalEvent.touches;
451
+ if (touches) {
452
+ return touches[0].pageY;
453
+ }
454
+ }
455
+
456
+
457
+ function getEvIsTouch(ev) {
458
+ return /^touch/.test(ev.type);
459
+ }
460
+
461
+
462
+ function preventSelection(el) {
463
+ el.addClass('fc-unselectable')
464
+ .on('selectstart', preventDefault);
465
+ }
466
+
467
+
468
+ // Stops a mouse/touch event from doing it's native browser action
469
+ function preventDefault(ev) {
470
+ ev.preventDefault();
471
+ }
472
+
473
+
474
+ /* General Geometry Utils
424
475
  ----------------------------------------------------------------------------------------------------------------------*/
425
476
 
426
477
  FC.intersectRects = intersectRects;
@@ -946,22 +997,21 @@ function proxy(obj, methodName) {
946
997
 
947
998
  // Returns a function, that, as long as it continues to be invoked, will not
948
999
  // be triggered. The function will be called after it stops being called for
949
- // N milliseconds.
1000
+ // N milliseconds. If `immediate` is passed, trigger the function on the
1001
+ // leading edge, instead of the trailing.
950
1002
  // https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714
951
- function debounce(func, wait) {
952
- var timeoutId;
953
- var args;
954
- var context;
955
- var timestamp; // of most recent call
1003
+ function debounce(func, wait, immediate) {
1004
+ var timeout, args, context, timestamp, result;
1005
+
956
1006
  var later = function() {
957
1007
  var last = +new Date() - timestamp;
958
- if (last < wait && last > 0) {
959
- timeoutId = setTimeout(later, wait - last);
1008
+ if (last < wait) {
1009
+ timeout = setTimeout(later, wait - last);
960
1010
  }
961
1011
  else {
962
- timeoutId = null;
963
- func.apply(context, args);
964
- if (!timeoutId) {
1012
+ timeout = null;
1013
+ if (!immediate) {
1014
+ result = func.apply(context, args);
965
1015
  context = args = null;
966
1016
  }
967
1017
  }
@@ -971,9 +1021,15 @@ function debounce(func, wait) {
971
1021
  context = this;
972
1022
  args = arguments;
973
1023
  timestamp = +new Date();
974
- if (!timeoutId) {
975
- timeoutId = setTimeout(later, wait);
1024
+ var callNow = immediate && !timeout;
1025
+ if (!timeout) {
1026
+ timeout = setTimeout(later, wait);
1027
+ }
1028
+ if (callNow) {
1029
+ result = func.apply(context, args);
1030
+ context = args = null;
976
1031
  }
1032
+ return result;
977
1033
  };
978
1034
  }
979
1035
 
@@ -1777,23 +1833,25 @@ function extendClass(superClass, members) {
1777
1833
 
1778
1834
 
1779
1835
  function mixIntoClass(theClass, members) {
1780
- copyOwnProps(members.prototype || members, theClass.prototype); // TODO: copyNativeMethods?
1836
+ copyOwnProps(members, theClass.prototype); // TODO: copyNativeMethods?
1781
1837
  }
1782
1838
  ;;
1783
1839
 
1784
- var Emitter = FC.Emitter = Class.extend({
1840
+ var EmitterMixin = FC.EmitterMixin = {
1785
1841
 
1786
1842
  callbackHash: null,
1787
1843
 
1788
1844
 
1789
1845
  on: function(name, callback) {
1790
- this.getCallbacks(name).add(callback);
1846
+ this.loopCallbacks(name, 'add', [ callback ]);
1847
+
1791
1848
  return this; // for chaining
1792
1849
  },
1793
1850
 
1794
1851
 
1795
1852
  off: function(name, callback) {
1796
- this.getCallbacks(name).remove(callback);
1853
+ this.loopCallbacks(name, 'remove', [ callback ]);
1854
+
1797
1855
  return this; // for chaining
1798
1856
  },
1799
1857
 
@@ -1808,30 +1866,104 @@ var Emitter = FC.Emitter = Class.extend({
1808
1866
 
1809
1867
 
1810
1868
  triggerWith: function(name, context, args) {
1811
- var callbacks = this.getCallbacks(name);
1812
-
1813
- callbacks.fireWith(context, args);
1869
+ this.loopCallbacks(name, 'fireWith', [ context, args ]);
1814
1870
 
1815
1871
  return this; // for chaining
1816
1872
  },
1817
1873
 
1818
1874
 
1819
- getCallbacks: function(name) {
1820
- var callbacks;
1875
+ /*
1876
+ Given an event name string with possible namespaces,
1877
+ call the given methodName on all the internal Callback object with the given arguments.
1878
+ */
1879
+ loopCallbacks: function(name, methodName, args) {
1880
+ var parts = name.split('.'); // "click.namespace" -> [ "click", "namespace" ]
1881
+ var i, part;
1882
+ var callbackObj;
1883
+
1884
+ for (i = 0; i < parts.length; i++) {
1885
+ part = parts[i];
1886
+ if (part) { // in case no event name like "click"
1887
+ callbackObj = this.ensureCallbackObj((i ? '.' : '') + part); // put periods in front of namespaces
1888
+ callbackObj[methodName].apply(callbackObj, args);
1889
+ }
1890
+ }
1891
+ },
1892
+
1821
1893
 
1894
+ ensureCallbackObj: function(name) {
1822
1895
  if (!this.callbackHash) {
1823
1896
  this.callbackHash = {};
1824
1897
  }
1825
-
1826
- callbacks = this.callbackHash[name];
1827
- if (!callbacks) {
1828
- callbacks = this.callbackHash[name] = $.Callbacks();
1898
+ if (!this.callbackHash[name]) {
1899
+ this.callbackHash[name] = $.Callbacks();
1829
1900
  }
1830
-
1831
- return callbacks;
1901
+ return this.callbackHash[name];
1832
1902
  }
1833
1903
 
1834
- });
1904
+ };
1905
+ ;;
1906
+
1907
+ /*
1908
+ Utility methods for easily listening to events on another object,
1909
+ and more importantly, easily unlistening from them.
1910
+ */
1911
+ var ListenerMixin = FC.ListenerMixin = (function() {
1912
+ var guid = 0;
1913
+ var ListenerMixin = {
1914
+
1915
+ listenerId: null,
1916
+
1917
+ /*
1918
+ Given an `other` object that has on/off methods, bind the given `callback` to an event by the given name.
1919
+ The `callback` will be called with the `this` context of the object that .listenTo is being called on.
1920
+ Can be called:
1921
+ .listenTo(other, eventName, callback)
1922
+ OR
1923
+ .listenTo(other, {
1924
+ eventName1: callback1,
1925
+ eventName2: callback2
1926
+ })
1927
+ */
1928
+ listenTo: function(other, arg, callback) {
1929
+ if (typeof arg === 'object') { // given dictionary of callbacks
1930
+ for (var eventName in arg) {
1931
+ if (arg.hasOwnProperty(eventName)) {
1932
+ this.listenTo(other, eventName, arg[eventName]);
1933
+ }
1934
+ }
1935
+ }
1936
+ else if (typeof arg === 'string') {
1937
+ other.on(
1938
+ arg + '.' + this.getListenerNamespace(), // use event namespacing to identify this object
1939
+ $.proxy(callback, this) // always use `this` context
1940
+ // the usually-undesired jQuery guid behavior doesn't matter,
1941
+ // because we always unbind via namespace
1942
+ );
1943
+ }
1944
+ },
1945
+
1946
+ /*
1947
+ Causes the current object to stop listening to events on the `other` object.
1948
+ `eventName` is optional. If omitted, will stop listening to ALL events on `other`.
1949
+ */
1950
+ stopListeningTo: function(other, eventName) {
1951
+ other.off((eventName || '') + '.' + this.getListenerNamespace());
1952
+ },
1953
+
1954
+ /*
1955
+ Returns a string, unique to this object, to be used for event namespacing
1956
+ */
1957
+ getListenerNamespace: function() {
1958
+ if (this.listenerId == null) {
1959
+ this.listenerId = guid++;
1960
+ }
1961
+ return '_listener' + this.listenerId;
1962
+ }
1963
+
1964
+ };
1965
+ return ListenerMixin;
1966
+ })();
1835
1967
  ;;
1836
1968
 
1837
1969
  /* A rectangular panel that is absolutely positioned over other content
@@ -1848,12 +1980,11 @@ Options:
1848
1980
  - hide (callback)
1849
1981
  */
1850
1982
 
1851
- var Popover = Class.extend({
1983
+ var Popover = Class.extend(ListenerMixin, {
1852
1984
 
1853
1985
  isHidden: true,
1854
1986
  options: null,
1855
1987
  el: null, // the container element for the popover. generated by this object
1856
- documentMousedownProxy: null, // document mousedown handler bound to `this`
1857
1988
  margin: 10, // the space required between the popover and the edges of the scroll container
1858
1989
 
1859
1990
 
@@ -1907,7 +2038,7 @@ var Popover = Class.extend({
1907
2038
  });
1908
2039
 
1909
2040
  if (options.autoHide) {
1910
- $(document).on('mousedown', this.documentMousedownProxy = proxy(this, 'documentMousedown'));
2041
+ this.listenTo($(document), 'mousedown', this.documentMousedown);
1911
2042
  }
1912
2043
  },
1913
2044
 
@@ -1930,7 +2061,7 @@ var Popover = Class.extend({
1930
2061
  this.el = null;
1931
2062
  }
1932
2063
 
1933
- $(document).off('mousedown', this.documentMousedownProxy);
2064
+ this.stopListeningTo($(document), 'mousedown');
1934
2065
  },
1935
2066
 
1936
2067
 
@@ -2243,257 +2374,399 @@ var CoordCache = FC.CoordCache = Class.extend({
2243
2374
  ----------------------------------------------------------------------------------------------------------------------*/
2244
2375
  // TODO: use Emitter
2245
2376
 
2246
- var DragListener = FC.DragListener = Class.extend({
2377
+ var DragListener = FC.DragListener = Class.extend(ListenerMixin, {
2247
2378
 
2248
2379
  options: null,
2249
2380
 
2250
- isListening: false,
2251
- isDragging: false,
2381
+ // for IE8 bug-fighting behavior
2382
+ subjectEl: null,
2383
+ subjectHref: null,
2252
2384
 
2253
2385
  // coordinates of the initial mousedown
2254
2386
  originX: null,
2255
2387
  originY: null,
2256
2388
 
2257
- // handler attached to the document, bound to the DragListener's `this`
2258
- mousemoveProxy: null,
2259
- mouseupProxy: null,
2260
-
2261
- // for IE8 bug-fighting behavior, for now
2262
- subjectEl: null, // the element being draged. optional
2263
- subjectHref: null,
2264
-
2265
2389
  scrollEl: null,
2266
- scrollBounds: null, // { top, bottom, left, right }
2267
- scrollTopVel: null, // pixels per second
2268
- scrollLeftVel: null, // pixels per second
2269
- scrollIntervalId: null, // ID of setTimeout for scrolling animation loop
2270
- scrollHandlerProxy: null, // this-scoped function for handling when scrollEl is scrolled
2271
2390
 
2272
- scrollSensitivity: 30, // pixels from edge for scrolling to start
2273
- scrollSpeed: 200, // pixels per second, at maximum speed
2274
- scrollIntervalMs: 50, // millisecond wait between scroll increment
2391
+ isInteracting: false,
2392
+ isDistanceSurpassed: false,
2393
+ isDelayEnded: false,
2394
+ isDragging: false,
2395
+ isTouch: false,
2396
+
2397
+ delay: null,
2398
+ delayTimeoutId: null,
2399
+ minDistance: null,
2275
2400
 
2276
2401
 
2277
2402
  constructor: function(options) {
2278
- options = options || {};
2279
- this.options = options;
2280
- this.subjectEl = options.subjectEl;
2403
+ this.options = options || {};
2281
2404
  },
2282
2405
 
2283
2406
 
2284
- // Call this when the user does a mousedown. Will probably lead to startListening
2285
- mousedown: function(ev) {
2286
- if (isPrimaryMouseButton(ev)) {
2407
+ // Interaction (high-level)
2408
+ // -----------------------------------------------------------------------------------------------------------------
2287
2409
 
2288
- ev.preventDefault(); // prevents native selection in most browsers
2289
2410
 
2290
- this.startListening(ev);
2411
+ startInteraction: function(ev, extraOptions) {
2412
+ var isTouch = getEvIsTouch(ev);
2291
2413
 
2292
- // start the drag immediately if there is no minimum distance for a drag start
2293
- if (!this.options.distance) {
2294
- this.startDrag(ev);
2414
+ if (ev.type === 'mousedown') {
2415
+ if (!isPrimaryMouseButton(ev)) {
2416
+ return;
2417
+ }
2418
+ else {
2419
+ ev.preventDefault(); // prevents native selection in most browsers
2295
2420
  }
2296
2421
  }
2297
- },
2298
2422
 
2423
+ if (!this.isInteracting) {
2299
2424
 
2300
- // Call this to start tracking mouse movements
2301
- startListening: function(ev) {
2302
- var scrollParent;
2425
+ // process options
2426
+ extraOptions = extraOptions || {};
2427
+ this.delay = firstDefined(extraOptions.delay, this.options.delay, 0);
2428
+ this.minDistance = firstDefined(extraOptions.distance, this.options.distance, 0);
2429
+ this.subjectEl = this.options.subjectEl;
2303
2430
 
2304
- if (!this.isListening) {
2431
+ this.isInteracting = true;
2432
+ this.isTouch = isTouch;
2433
+ this.isDelayEnded = false;
2434
+ this.isDistanceSurpassed = false;
2305
2435
 
2306
- // grab scroll container and attach handler
2307
- if (ev && this.options.scroll) {
2308
- scrollParent = getScrollParent($(ev.target));
2309
- if (!scrollParent.is(window) && !scrollParent.is(document)) {
2310
- this.scrollEl = scrollParent;
2436
+ this.originX = getEvX(ev);
2437
+ this.originY = getEvY(ev);
2438
+ this.scrollEl = getScrollParent($(ev.target));
2311
2439
 
2312
- // scope to `this`, and use `debounce` to make sure rapid calls don't happen
2313
- this.scrollHandlerProxy = debounce(proxy(this, 'scrollHandler'), 100);
2314
- this.scrollEl.on('scroll', this.scrollHandlerProxy);
2315
- }
2440
+ this.bindHandlers();
2441
+ this.initAutoScroll();
2442
+ this.handleInteractionStart(ev);
2443
+ this.startDelay(ev);
2444
+
2445
+ if (!this.minDistance) {
2446
+ this.handleDistanceSurpassed(ev);
2316
2447
  }
2448
+ }
2449
+ },
2450
+
2451
+
2452
+ handleInteractionStart: function(ev) {
2453
+ this.trigger('interactionStart', ev);
2454
+ },
2455
+
2317
2456
 
2318
- $(document)
2319
- .on('mousemove', this.mousemoveProxy = proxy(this, 'mousemove'))
2320
- .on('mouseup', this.mouseupProxy = proxy(this, 'mouseup'))
2321
- .on('selectstart', this.preventDefault); // prevents native selection in IE<=8
2457
+ endInteraction: function(ev) {
2458
+ if (this.isInteracting) {
2459
+ this.endDrag(ev);
2322
2460
 
2323
- if (ev) {
2324
- this.originX = ev.pageX;
2325
- this.originY = ev.pageY;
2461
+ if (this.delayTimeoutId) {
2462
+ clearTimeout(this.delayTimeoutId);
2463
+ this.delayTimeoutId = null;
2326
2464
  }
2327
- else {
2328
- // if no starting information was given, origin will be the topleft corner of the screen.
2329
- // if so, dx/dy in the future will be the absolute coordinates.
2330
- this.originX = 0;
2331
- this.originY = 0;
2465
+
2466
+ this.destroyAutoScroll();
2467
+ this.unbindHandlers();
2468
+
2469
+ this.isInteracting = false;
2470
+ this.handleInteractionEnd(ev);
2471
+ }
2472
+ },
2473
+
2474
+
2475
+ handleInteractionEnd: function(ev) {
2476
+ this.trigger('interactionEnd', ev);
2477
+ },
2478
+
2479
+
2480
+ // Binding To DOM
2481
+ // -----------------------------------------------------------------------------------------------------------------
2482
+
2483
+
2484
+ bindHandlers: function() {
2485
+ var _this = this;
2486
+ var touchStartIgnores = 1;
2487
+
2488
+ if (this.isTouch) {
2489
+ this.listenTo($(document), {
2490
+ touchmove: this.handleTouchMove,
2491
+ touchend: this.endInteraction,
2492
+ touchcancel: this.endInteraction,
2493
+
2494
+ // Sometimes touchend doesn't fire
2495
+ // (can't figure out why. touchcancel doesn't fire either. has to do with scrolling?)
2496
+ // If another touchstart happens, we know it's bogus, so cancel the drag.
2497
+ // touchend will continue to be broken until user does a shorttap/scroll, but this is best we can do.
2498
+ touchstart: function(ev) {
2499
+ if (touchStartIgnores) { // bindHandlers is called from within a touchstart,
2500
+ touchStartIgnores--; // and we don't want this to fire immediately, so ignore.
2501
+ }
2502
+ else {
2503
+ _this.endInteraction(ev);
2504
+ }
2505
+ }
2506
+ });
2507
+
2508
+ if (this.scrollEl) {
2509
+ this.listenTo(this.scrollEl, 'scroll', this.handleTouchScroll);
2332
2510
  }
2511
+ }
2512
+ else {
2513
+ this.listenTo($(document), {
2514
+ mousemove: this.handleMouseMove,
2515
+ mouseup: this.endInteraction
2516
+ });
2517
+ }
2518
+
2519
+ this.listenTo($(document), {
2520
+ selectstart: preventDefault, // don't allow selection while dragging
2521
+ contextmenu: preventDefault // long taps would open menu on Chrome dev tools
2522
+ });
2523
+ },
2524
+
2525
+
2526
+ unbindHandlers: function() {
2527
+ this.stopListeningTo($(document));
2528
+
2529
+ if (this.scrollEl) {
2530
+ this.stopListeningTo(this.scrollEl);
2531
+ }
2532
+ },
2533
+
2534
+
2535
+ // Drag (high-level)
2536
+ // -----------------------------------------------------------------------------------------------------------------
2333
2537
 
2334
- this.isListening = true;
2335
- this.listenStart(ev);
2538
+
2539
+ // extraOptions ignored if drag already started
2540
+ startDrag: function(ev, extraOptions) {
2541
+ this.startInteraction(ev, extraOptions); // ensure interaction began
2542
+
2543
+ if (!this.isDragging) {
2544
+ this.isDragging = true;
2545
+ this.handleDragStart(ev);
2336
2546
  }
2337
2547
  },
2338
2548
 
2339
2549
 
2340
- // Called when drag listening has started (but a real drag has not necessarily began)
2341
- listenStart: function(ev) {
2342
- this.trigger('listenStart', ev);
2550
+ handleDragStart: function(ev) {
2551
+ this.trigger('dragStart', ev);
2552
+ this.initHrefHack();
2343
2553
  },
2344
2554
 
2345
2555
 
2346
- // Called when the user moves the mouse
2347
- mousemove: function(ev) {
2348
- var dx = ev.pageX - this.originX;
2349
- var dy = ev.pageY - this.originY;
2350
- var minDistance;
2556
+ handleMove: function(ev) {
2557
+ var dx = getEvX(ev) - this.originX;
2558
+ var dy = getEvY(ev) - this.originY;
2559
+ var minDistance = this.minDistance;
2351
2560
  var distanceSq; // current distance from the origin, squared
2352
2561
 
2353
- if (!this.isDragging) { // if not already dragging...
2354
- // then start the drag if the minimum distance criteria is met
2355
- minDistance = this.options.distance || 1;
2562
+ if (!this.isDistanceSurpassed) {
2356
2563
  distanceSq = dx * dx + dy * dy;
2357
2564
  if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem
2358
- this.startDrag(ev);
2565
+ this.handleDistanceSurpassed(ev);
2359
2566
  }
2360
2567
  }
2361
2568
 
2362
2569
  if (this.isDragging) {
2363
- this.drag(dx, dy, ev); // report a drag, even if this mousemove initiated the drag
2570
+ this.handleDrag(dx, dy, ev);
2364
2571
  }
2365
2572
  },
2366
2573
 
2367
2574
 
2368
- // Call this to initiate a legitimate drag.
2369
- // This function is called internally from this class, but can also be called explicitly from outside
2370
- startDrag: function(ev) {
2575
+ // Called while the mouse is being moved and when we know a legitimate drag is taking place
2576
+ handleDrag: function(dx, dy, ev) {
2577
+ this.trigger('drag', dx, dy, ev);
2578
+ this.updateAutoScroll(ev); // will possibly cause scrolling
2579
+ },
2371
2580
 
2372
- if (!this.isListening) { // startDrag must have manually initiated
2373
- this.startListening();
2374
- }
2375
2581
 
2376
- if (!this.isDragging) {
2377
- this.isDragging = true;
2378
- this.dragStart(ev);
2582
+ endDrag: function(ev) {
2583
+ if (this.isDragging) {
2584
+ this.isDragging = false;
2585
+ this.handleDragEnd(ev);
2379
2586
  }
2380
2587
  },
2381
2588
 
2382
2589
 
2383
- // Called when the actual drag has started (went beyond minDistance)
2384
- dragStart: function(ev) {
2385
- var subjectEl = this.subjectEl;
2590
+ handleDragEnd: function(ev) {
2591
+ this.trigger('dragEnd', ev);
2592
+ this.destroyHrefHack();
2593
+ },
2386
2594
 
2387
- this.trigger('dragStart', ev);
2388
2595
 
2389
- // remove a mousedown'd <a>'s href so it is not visited (IE8 bug)
2390
- if ((this.subjectHref = subjectEl ? subjectEl.attr('href') : null)) {
2391
- subjectEl.removeAttr('href');
2596
+ // Delay
2597
+ // -----------------------------------------------------------------------------------------------------------------
2598
+
2599
+
2600
+ startDelay: function(initialEv) {
2601
+ var _this = this;
2602
+
2603
+ if (this.delay) {
2604
+ this.delayTimeoutId = setTimeout(function() {
2605
+ _this.handleDelayEnd(initialEv);
2606
+ }, this.delay);
2607
+ }
2608
+ else {
2609
+ this.handleDelayEnd(initialEv);
2392
2610
  }
2393
2611
  },
2394
2612
 
2395
2613
 
2396
- // Called while the mouse is being moved and when we know a legitimate drag is taking place
2397
- drag: function(dx, dy, ev) {
2398
- this.trigger('drag', dx, dy, ev);
2399
- this.updateScroll(ev); // will possibly cause scrolling
2614
+ handleDelayEnd: function(initialEv) {
2615
+ this.isDelayEnded = true;
2616
+
2617
+ if (this.isDistanceSurpassed) {
2618
+ this.startDrag(initialEv);
2619
+ }
2400
2620
  },
2401
2621
 
2402
2622
 
2403
- // Called when the user does a mouseup
2404
- mouseup: function(ev) {
2405
- this.stopListening(ev);
2623
+ // Distance
2624
+ // -----------------------------------------------------------------------------------------------------------------
2625
+
2626
+
2627
+ handleDistanceSurpassed: function(ev) {
2628
+ this.isDistanceSurpassed = true;
2629
+
2630
+ if (this.isDelayEnded) {
2631
+ this.startDrag(ev);
2632
+ }
2406
2633
  },
2407
2634
 
2408
2635
 
2409
- // Called when the drag is over. Will not cause listening to stop however.
2410
- // A concluding 'cellOut' event will NOT be triggered.
2411
- stopDrag: function(ev) {
2636
+ // Mouse / Touch
2637
+ // -----------------------------------------------------------------------------------------------------------------
2638
+
2639
+
2640
+ handleTouchMove: function(ev) {
2641
+ // prevent inertia and touchmove-scrolling while dragging
2412
2642
  if (this.isDragging) {
2413
- this.stopScrolling();
2414
- this.dragStop(ev);
2415
- this.isDragging = false;
2643
+ ev.preventDefault();
2416
2644
  }
2645
+
2646
+ this.handleMove(ev);
2417
2647
  },
2418
2648
 
2419
2649
 
2420
- // Called when dragging has been stopped
2421
- dragStop: function(ev) {
2422
- var _this = this;
2650
+ handleMouseMove: function(ev) {
2651
+ this.handleMove(ev);
2652
+ },
2423
2653
 
2424
- this.trigger('dragStop', ev);
2425
2654
 
2426
- // restore a mousedown'd <a>'s href (for IE8 bug)
2427
- setTimeout(function() { // must be outside of the click's execution
2428
- if (_this.subjectHref) {
2429
- _this.subjectEl.attr('href', _this.subjectHref);
2430
- }
2431
- }, 0);
2432
- },
2655
+ // Scrolling (unrelated to auto-scroll)
2656
+ // -----------------------------------------------------------------------------------------------------------------
2433
2657
 
2434
2658
 
2435
- // Call this to stop listening to the user's mouse events
2436
- stopListening: function(ev) {
2437
- this.stopDrag(ev); // if there's a current drag, kill it
2659
+ handleTouchScroll: function(ev) {
2660
+ // if the drag is being initiated by touch, but a scroll happens before
2661
+ // the drag-initiating delay is over, cancel the drag
2662
+ if (!this.isDragging) {
2663
+ this.endInteraction(ev);
2664
+ }
2665
+ },
2438
2666
 
2439
- if (this.isListening) {
2440
2667
 
2441
- // remove the scroll handler if there is a scrollEl
2442
- if (this.scrollEl) {
2443
- this.scrollEl.off('scroll', this.scrollHandlerProxy);
2444
- this.scrollHandlerProxy = null;
2445
- }
2668
+ // <A> HREF Hack
2669
+ // -----------------------------------------------------------------------------------------------------------------
2446
2670
 
2447
- $(document)
2448
- .off('mousemove', this.mousemoveProxy)
2449
- .off('mouseup', this.mouseupProxy)
2450
- .off('selectstart', this.preventDefault);
2451
2671
 
2452
- this.mousemoveProxy = null;
2453
- this.mouseupProxy = null;
2672
+ initHrefHack: function() {
2673
+ var subjectEl = this.subjectEl;
2454
2674
 
2455
- this.isListening = false;
2456
- this.listenStop(ev);
2675
+ // remove a mousedown'd <a>'s href so it is not visited (IE8 bug)
2676
+ if ((this.subjectHref = subjectEl ? subjectEl.attr('href') : null)) {
2677
+ subjectEl.removeAttr('href');
2457
2678
  }
2458
2679
  },
2459
2680
 
2460
2681
 
2461
- // Called when drag listening has stopped
2462
- listenStop: function(ev) {
2463
- this.trigger('listenStop', ev);
2682
+ destroyHrefHack: function() {
2683
+ var subjectEl = this.subjectEl;
2684
+ var subjectHref = this.subjectHref;
2685
+
2686
+ // restore a mousedown'd <a>'s href (for IE8 bug)
2687
+ setTimeout(function() { // must be outside of the click's execution
2688
+ if (subjectHref) {
2689
+ subjectEl.attr('href', subjectHref);
2690
+ }
2691
+ }, 0);
2464
2692
  },
2465
2693
 
2466
2694
 
2695
+ // Utils
2696
+ // -----------------------------------------------------------------------------------------------------------------
2697
+
2698
+
2467
2699
  // Triggers a callback. Calls a function in the option hash of the same name.
2468
2700
  // Arguments beyond the first `name` are forwarded on.
2469
2701
  trigger: function(name) {
2470
2702
  if (this.options[name]) {
2471
2703
  this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
2472
2704
  }
2473
- },
2705
+ // makes _methods callable by event name. TODO: kill this
2706
+ if (this['_' + name]) {
2707
+ this['_' + name].apply(this, Array.prototype.slice.call(arguments, 1));
2708
+ }
2709
+ }
2710
+
2711
+
2712
+ });
2713
+
2714
+ ;;
2715
+ /*
2716
+ this.scrollEl is set in DragListener
2717
+ */
2718
+ DragListener.mixin({
2719
+
2720
+ isAutoScroll: false,
2721
+
2722
+ scrollBounds: null, // { top, bottom, left, right }
2723
+ scrollTopVel: null, // pixels per second
2724
+ scrollLeftVel: null, // pixels per second
2725
+ scrollIntervalId: null, // ID of setTimeout for scrolling animation loop
2726
+
2727
+ // defaults
2728
+ scrollSensitivity: 30, // pixels from edge for scrolling to start
2729
+ scrollSpeed: 200, // pixels per second, at maximum speed
2730
+ scrollIntervalMs: 50, // millisecond wait between scroll increment
2731
+
2732
+
2733
+ initAutoScroll: function() {
2734
+ var scrollEl = this.scrollEl;
2474
2735
 
2736
+ this.isAutoScroll =
2737
+ this.options.scroll &&
2738
+ scrollEl &&
2739
+ !scrollEl.is(window) &&
2740
+ !scrollEl.is(document);
2475
2741
 
2476
- // Stops a given mouse event from doing it's native browser action. In our case, text selection.
2477
- preventDefault: function(ev) {
2478
- ev.preventDefault();
2742
+ if (this.isAutoScroll) {
2743
+ // debounce makes sure rapid calls don't happen
2744
+ this.listenTo(scrollEl, 'scroll', debounce(this.handleDebouncedScroll, 100));
2745
+ }
2479
2746
  },
2480
2747
 
2481
2748
 
2482
- /* Scrolling
2483
- ------------------------------------------------------------------------------------------------------------------*/
2749
+ destroyAutoScroll: function() {
2750
+ this.endAutoScroll(); // kill any animation loop
2751
+
2752
+ // remove the scroll handler if there is a scrollEl
2753
+ if (this.isAutoScroll) {
2754
+ this.stopListeningTo(this.scrollEl, 'scroll'); // will probably get removed by unbindHandlers too :(
2755
+ }
2756
+ },
2484
2757
 
2485
2758
 
2486
2759
  // Computes and stores the bounding rectangle of scrollEl
2487
2760
  computeScrollBounds: function() {
2488
- var el = this.scrollEl;
2489
-
2490
- this.scrollBounds = el ? getOuterRect(el) : null;
2761
+ if (this.isAutoScroll) {
2762
+ this.scrollBounds = getOuterRect(this.scrollEl);
2491
2763
  // TODO: use getClientRect in future. but prevents auto scrolling when on top of scrollbars
2764
+ }
2492
2765
  },
2493
2766
 
2494
2767
 
2495
2768
  // Called when the dragging is in progress and scrolling should be updated
2496
- updateScroll: function(ev) {
2769
+ updateAutoScroll: function(ev) {
2497
2770
  var sensitivity = this.scrollSensitivity;
2498
2771
  var bounds = this.scrollBounds;
2499
2772
  var topCloseness, bottomCloseness;
@@ -2504,10 +2777,10 @@ var DragListener = FC.DragListener = Class.extend({
2504
2777
  if (bounds) { // only scroll if scrollEl exists
2505
2778
 
2506
2779
  // compute closeness to edges. valid range is from 0.0 - 1.0
2507
- topCloseness = (sensitivity - (ev.pageY - bounds.top)) / sensitivity;
2508
- bottomCloseness = (sensitivity - (bounds.bottom - ev.pageY)) / sensitivity;
2509
- leftCloseness = (sensitivity - (ev.pageX - bounds.left)) / sensitivity;
2510
- rightCloseness = (sensitivity - (bounds.right - ev.pageX)) / sensitivity;
2780
+ topCloseness = (sensitivity - (getEvY(ev) - bounds.top)) / sensitivity;
2781
+ bottomCloseness = (sensitivity - (bounds.bottom - getEvY(ev))) / sensitivity;
2782
+ leftCloseness = (sensitivity - (getEvX(ev) - bounds.left)) / sensitivity;
2783
+ rightCloseness = (sensitivity - (bounds.right - getEvX(ev))) / sensitivity;
2511
2784
 
2512
2785
  // translate vertical closeness into velocity.
2513
2786
  // mouse must be completely in bounds for velocity to happen.
@@ -2594,38 +2867,36 @@ var DragListener = FC.DragListener = Class.extend({
2594
2867
 
2595
2868
  // if scrolled all the way, which causes the vels to be zero, stop the animation loop
2596
2869
  if (!this.scrollTopVel && !this.scrollLeftVel) {
2597
- this.stopScrolling();
2870
+ this.endAutoScroll();
2598
2871
  }
2599
2872
  },
2600
2873
 
2601
2874
 
2602
2875
  // Kills any existing scrolling animation loop
2603
- stopScrolling: function() {
2876
+ endAutoScroll: function() {
2604
2877
  if (this.scrollIntervalId) {
2605
2878
  clearInterval(this.scrollIntervalId);
2606
2879
  this.scrollIntervalId = null;
2607
2880
 
2608
- // when all done with scrolling, recompute positions since they probably changed
2609
- this.scrollStop();
2881
+ this.handleScrollEnd();
2610
2882
  }
2611
2883
  },
2612
2884
 
2613
2885
 
2614
2886
  // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce)
2615
- scrollHandler: function() {
2887
+ handleDebouncedScroll: function() {
2616
2888
  // recompute all coordinates, but *only* if this is *not* part of our scrolling animation
2617
2889
  if (!this.scrollIntervalId) {
2618
- this.scrollStop();
2890
+ this.handleScrollEnd();
2619
2891
  }
2620
2892
  },
2621
2893
 
2622
2894
 
2623
2895
  // Called when scrolling has stopped, whether through auto scroll, or the user scrolling
2624
- scrollStop: function() {
2896
+ handleScrollEnd: function() {
2625
2897
  }
2626
2898
 
2627
2899
  });
2628
-
2629
2900
  ;;
2630
2901
 
2631
2902
  /* Tracks mouse movements over a component and raises events about which hit the mouse is over.
@@ -2654,18 +2925,18 @@ var HitDragListener = DragListener.extend({
2654
2925
 
2655
2926
  // Called when drag listening starts (but a real drag has not necessarily began).
2656
2927
  // ev might be undefined if dragging was started manually.
2657
- listenStart: function(ev) {
2928
+ handleInteractionStart: function(ev) {
2658
2929
  var subjectEl = this.subjectEl;
2659
2930
  var subjectRect;
2660
2931
  var origPoint;
2661
2932
  var point;
2662
2933
 
2663
- DragListener.prototype.listenStart.apply(this, arguments); // call the super-method
2934
+ DragListener.prototype.handleInteractionStart.apply(this, arguments); // call the super-method
2664
2935
 
2665
2936
  this.computeCoords();
2666
2937
 
2667
2938
  if (ev) {
2668
- origPoint = { left: ev.pageX, top: ev.pageY };
2939
+ origPoint = { left: getEvX(ev), top: getEvY(ev) };
2669
2940
  point = origPoint;
2670
2941
 
2671
2942
  // constrain the point to bounds of the element being dragged
@@ -2701,55 +2972,55 @@ var HitDragListener = DragListener.extend({
2701
2972
  // Recomputes the drag-critical positions of elements
2702
2973
  computeCoords: function() {
2703
2974
  this.component.prepareHits();
2704
- this.computeScrollBounds(); // why is this here???
2975
+ this.computeScrollBounds(); // why is this here??????
2705
2976
  },
2706
2977
 
2707
2978
 
2708
2979
  // Called when the actual drag has started
2709
- dragStart: function(ev) {
2980
+ handleDragStart: function(ev) {
2710
2981
  var hit;
2711
2982
 
2712
- DragListener.prototype.dragStart.apply(this, arguments); // call the super-method
2983
+ DragListener.prototype.handleDragStart.apply(this, arguments); // call the super-method
2713
2984
 
2714
2985
  // might be different from this.origHit if the min-distance is large
2715
- hit = this.queryHit(ev.pageX, ev.pageY);
2986
+ hit = this.queryHit(getEvX(ev), getEvY(ev));
2716
2987
 
2717
2988
  // report the initial hit the mouse is over
2718
2989
  // especially important if no min-distance and drag starts immediately
2719
2990
  if (hit) {
2720
- this.hitOver(hit);
2991
+ this.handleHitOver(hit);
2721
2992
  }
2722
2993
  },
2723
2994
 
2724
2995
 
2725
2996
  // Called when the drag moves
2726
- drag: function(dx, dy, ev) {
2997
+ handleDrag: function(dx, dy, ev) {
2727
2998
  var hit;
2728
2999
 
2729
- DragListener.prototype.drag.apply(this, arguments); // call the super-method
3000
+ DragListener.prototype.handleDrag.apply(this, arguments); // call the super-method
2730
3001
 
2731
- hit = this.queryHit(ev.pageX, ev.pageY);
3002
+ hit = this.queryHit(getEvX(ev), getEvY(ev));
2732
3003
 
2733
3004
  if (!isHitsEqual(hit, this.hit)) { // a different hit than before?
2734
3005
  if (this.hit) {
2735
- this.hitOut();
3006
+ this.handleHitOut();
2736
3007
  }
2737
3008
  if (hit) {
2738
- this.hitOver(hit);
3009
+ this.handleHitOver(hit);
2739
3010
  }
2740
3011
  }
2741
3012
  },
2742
3013
 
2743
3014
 
2744
3015
  // Called when dragging has been stopped
2745
- dragStop: function() {
2746
- this.hitDone();
2747
- DragListener.prototype.dragStop.apply(this, arguments); // call the super-method
3016
+ handleDragEnd: function() {
3017
+ this.handleHitDone();
3018
+ DragListener.prototype.handleDragEnd.apply(this, arguments); // call the super-method
2748
3019
  },
2749
3020
 
2750
3021
 
2751
3022
  // Called when a the mouse has just moved over a new hit
2752
- hitOver: function(hit) {
3023
+ handleHitOver: function(hit) {
2753
3024
  var isOrig = isHitsEqual(hit, this.origHit);
2754
3025
 
2755
3026
  this.hit = hit;
@@ -2759,26 +3030,26 @@ var HitDragListener = DragListener.extend({
2759
3030
 
2760
3031
 
2761
3032
  // Called when the mouse has just moved out of a hit
2762
- hitOut: function() {
3033
+ handleHitOut: function() {
2763
3034
  if (this.hit) {
2764
3035
  this.trigger('hitOut', this.hit);
2765
- this.hitDone();
3036
+ this.handleHitDone();
2766
3037
  this.hit = null;
2767
3038
  }
2768
3039
  },
2769
3040
 
2770
3041
 
2771
3042
  // Called after a hitOut. Also called before a dragStop
2772
- hitDone: function() {
3043
+ handleHitDone: function() {
2773
3044
  if (this.hit) {
2774
3045
  this.trigger('hitDone', this.hit);
2775
3046
  }
2776
3047
  },
2777
3048
 
2778
3049
 
2779
- // Called when drag listening has stopped
2780
- listenStop: function() {
2781
- DragListener.prototype.listenStop.apply(this, arguments); // call the super-method
3050
+ // Called when the interaction ends, whether there was a real drag or not
3051
+ handleInteractionEnd: function() {
3052
+ DragListener.prototype.handleInteractionEnd.apply(this, arguments); // call the super-method
2782
3053
 
2783
3054
  this.origHit = null;
2784
3055
  this.hit = null;
@@ -2788,8 +3059,8 @@ var HitDragListener = DragListener.extend({
2788
3059
 
2789
3060
 
2790
3061
  // Called when scrolling has stopped, whether through auto scroll, or the user scrolling
2791
- scrollStop: function() {
2792
- DragListener.prototype.scrollStop.apply(this, arguments); // call the super-method
3062
+ handleScrollEnd: function() {
3063
+ DragListener.prototype.handleScrollEnd.apply(this, arguments); // call the super-method
2793
3064
 
2794
3065
  this.computeCoords(); // hits' absolute positions will be in new places. recompute
2795
3066
  },
@@ -2844,7 +3115,7 @@ function isHitPropsWithin(subHit, superHit) {
2844
3115
  /* Creates a clone of an element and lets it track the mouse as it moves
2845
3116
  ----------------------------------------------------------------------------------------------------------------------*/
2846
3117
 
2847
- var MouseFollower = Class.extend({
3118
+ var MouseFollower = Class.extend(ListenerMixin, {
2848
3119
 
2849
3120
  options: null,
2850
3121
 
@@ -2856,16 +3127,14 @@ var MouseFollower = Class.extend({
2856
3127
  top0: null,
2857
3128
  left0: null,
2858
3129
 
2859
- // the initial position of the mouse
2860
- mouseY0: null,
2861
- mouseX0: null,
3130
+ // the absolute coordinates of the initiating touch/mouse action
3131
+ y0: null,
3132
+ x0: null,
2862
3133
 
2863
3134
  // the number of pixels the mouse has moved from its initial position
2864
3135
  topDelta: null,
2865
3136
  leftDelta: null,
2866
3137
 
2867
- mousemoveProxy: null, // document mousemove handler, bound to the MouseFollower's `this`
2868
-
2869
3138
  isFollowing: false,
2870
3139
  isHidden: false,
2871
3140
  isAnimating: false, // doing the revert animation?
@@ -2882,8 +3151,8 @@ var MouseFollower = Class.extend({
2882
3151
  if (!this.isFollowing) {
2883
3152
  this.isFollowing = true;
2884
3153
 
2885
- this.mouseY0 = ev.pageY;
2886
- this.mouseX0 = ev.pageX;
3154
+ this.y0 = getEvY(ev);
3155
+ this.x0 = getEvX(ev);
2887
3156
  this.topDelta = 0;
2888
3157
  this.leftDelta = 0;
2889
3158
 
@@ -2891,7 +3160,12 @@ var MouseFollower = Class.extend({
2891
3160
  this.updatePosition();
2892
3161
  }
2893
3162
 
2894
- $(document).on('mousemove', this.mousemoveProxy = proxy(this, 'mousemove'));
3163
+ if (getEvIsTouch(ev)) {
3164
+ this.listenTo($(document), 'touchmove', this.handleMove);
3165
+ }
3166
+ else {
3167
+ this.listenTo($(document), 'mousemove', this.handleMove);
3168
+ }
2895
3169
  }
2896
3170
  },
2897
3171
 
@@ -2916,7 +3190,7 @@ var MouseFollower = Class.extend({
2916
3190
  if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time
2917
3191
  this.isFollowing = false;
2918
3192
 
2919
- $(document).off('mousemove', this.mousemoveProxy);
3193
+ this.stopListeningTo($(document));
2920
3194
 
2921
3195
  if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation?
2922
3196
  this.isAnimating = true;
@@ -2942,6 +3216,7 @@ var MouseFollower = Class.extend({
2942
3216
  if (!el) {
2943
3217
  this.sourceEl.width(); // hack to force IE8 to compute correct bounding box
2944
3218
  el = this.el = this.sourceEl.clone()
3219
+ .addClass(this.options.additionalClass || '')
2945
3220
  .css({
2946
3221
  position: 'absolute',
2947
3222
  visibility: '', // in case original element was hidden (commonly through hideEvents())
@@ -2953,8 +3228,13 @@ var MouseFollower = Class.extend({
2953
3228
  height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value
2954
3229
  opacity: this.options.opacity || '',
2955
3230
  zIndex: this.options.zIndex
2956
- })
2957
- .appendTo(this.parentEl);
3231
+ });
3232
+
3233
+ // we don't want long taps or any mouse interaction causing selection/menus.
3234
+ // would use preventSelection(), but that prevents selectstart, causing problems.
3235
+ el.addClass('fc-unselectable');
3236
+
3237
+ el.appendTo(this.parentEl);
2958
3238
  }
2959
3239
 
2960
3240
  return el;
@@ -2994,9 +3274,9 @@ var MouseFollower = Class.extend({
2994
3274
 
2995
3275
 
2996
3276
  // Gets called when the user moves the mouse
2997
- mousemove: function(ev) {
2998
- this.topDelta = ev.pageY - this.mouseY0;
2999
- this.leftDelta = ev.pageX - this.mouseX0;
3277
+ handleMove: function(ev) {
3278
+ this.topDelta = getEvY(ev) - this.y0;
3279
+ this.leftDelta = getEvX(ev) - this.x0;
3000
3280
 
3001
3281
  if (!this.isHidden) {
3002
3282
  this.updatePosition();
@@ -3031,7 +3311,7 @@ var MouseFollower = Class.extend({
3031
3311
  /* An abstract class comprised of a "grid" of areas that each represent a specific datetime
3032
3312
  ----------------------------------------------------------------------------------------------------------------------*/
3033
3313
 
3034
- var Grid = FC.Grid = Class.extend({
3314
+ var Grid = FC.Grid = Class.extend(ListenerMixin, {
3035
3315
 
3036
3316
  view: null, // a View object
3037
3317
  isRTL: null, // shortcut to the view's isRTL option
@@ -3042,8 +3322,6 @@ var Grid = FC.Grid = Class.extend({
3042
3322
  el: null, // the containing element
3043
3323
  elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.
3044
3324
 
3045
- externalDragStartProxy: null, // binds the Grid's scope to externalDragStart (in DayGrid.events)
3046
-
3047
3325
  // derived from options
3048
3326
  eventTimeFormat: null,
3049
3327
  displayEventTime: null,
@@ -3056,13 +3334,16 @@ var Grid = FC.Grid = Class.extend({
3056
3334
  // TODO: port isTimeScale into same system?
3057
3335
  largeUnit: null,
3058
3336
 
3337
+ dayDragListener: null,
3338
+ segDragListener: null,
3339
+ segResizeListener: null,
3340
+ externalDragListener: null,
3341
+
3059
3342
 
3060
3343
  constructor: function(view) {
3061
3344
  this.view = view;
3062
3345
  this.isRTL = view.opt('isRTL');
3063
-
3064
3346
  this.elsByFill = {};
3065
- this.externalDragStartProxy = proxy(this, 'externalDragStart');
3066
3347
  },
3067
3348
 
3068
3349
 
@@ -3195,26 +3476,37 @@ var Grid = FC.Grid = Class.extend({
3195
3476
  // Sets the container element that the grid should render inside of.
3196
3477
  // Does other DOM-related initializations.
3197
3478
  setElement: function(el) {
3198
- var _this = this;
3199
-
3200
3479
  this.el = el;
3480
+ preventSelection(el);
3481
+
3482
+ if (this.view.calendar.isTouch) {
3483
+ this.bindDayHandler('touchstart', this.dayTouchStart);
3484
+ }
3485
+ else {
3486
+ this.bindDayHandler('mousedown', this.dayMousedown);
3487
+ }
3488
+
3489
+ // attach event-element-related handlers. in Grid.events
3490
+ // same garbage collection note as above.
3491
+ this.bindSegHandlers();
3492
+
3493
+ this.bindGlobalHandlers();
3494
+ },
3495
+
3496
+
3497
+ bindDayHandler: function(name, handler) {
3498
+ var _this = this;
3201
3499
 
3202
3500
  // attach a handler to the grid's root element.
3203
3501
  // jQuery will take care of unregistering them when removeElement gets called.
3204
- el.on('mousedown', function(ev) {
3502
+ this.el.on(name, function(ev) {
3205
3503
  if (
3206
3504
  !$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link
3207
3505
  !$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one)
3208
3506
  ) {
3209
- _this.dayMousedown(ev);
3507
+ return handler.call(_this, ev);
3210
3508
  }
3211
3509
  });
3212
-
3213
- // attach event-element-related handlers. in Grid.events
3214
- // same garbage collection note as above.
3215
- this.bindSegHandlers();
3216
-
3217
- this.bindGlobalHandlers();
3218
3510
  },
3219
3511
 
3220
3512
 
@@ -3222,6 +3514,7 @@ var Grid = FC.Grid = Class.extend({
3222
3514
  // DOES NOT remove any content beforehand (doesn't clear events or call unrenderDates), unlike View
3223
3515
  removeElement: function() {
3224
3516
  this.unbindGlobalHandlers();
3517
+ this.clearDragListeners();
3225
3518
 
3226
3519
  this.el.remove();
3227
3520
 
@@ -3254,18 +3547,39 @@ var Grid = FC.Grid = Class.extend({
3254
3547
 
3255
3548
  // Binds DOM handlers to elements that reside outside the grid, such as the document
3256
3549
  bindGlobalHandlers: function() {
3257
- $(document).on('dragstart sortstart', this.externalDragStartProxy); // jqui
3550
+ this.listenTo($(document), {
3551
+ dragstart: this.externalDragStart, // jqui
3552
+ sortstart: this.externalDragStart // jqui
3553
+ });
3258
3554
  },
3259
3555
 
3260
3556
 
3261
3557
  // Unbinds DOM handlers from elements that reside outside the grid
3262
3558
  unbindGlobalHandlers: function() {
3263
- $(document).off('dragstart sortstart', this.externalDragStartProxy); // jqui
3559
+ this.stopListeningTo($(document));
3264
3560
  },
3265
3561
 
3266
3562
 
3267
3563
  // Process a mousedown on an element that represents a day. For day clicking and selecting.
3268
3564
  dayMousedown: function(ev) {
3565
+ this.clearDragListeners();
3566
+ this.buildDayDragListener().startInteraction(ev, {
3567
+ //distance: 5, // needs more work if we want dayClick to fire correctly
3568
+ });
3569
+ },
3570
+
3571
+
3572
+ dayTouchStart: function(ev) {
3573
+ this.clearDragListeners();
3574
+ this.buildDayDragListener().startInteraction(ev, {
3575
+ delay: this.view.opt('longPressDelay')
3576
+ });
3577
+ },
3578
+
3579
+
3580
+ // Creates a listener that tracks the user's drag across day elements.
3581
+ // For day clicking and selecting.
3582
+ buildDayDragListener: function() {
3269
3583
  var _this = this;
3270
3584
  var view = this.view;
3271
3585
  var isSelectable = view.opt('selectable');
@@ -3275,8 +3589,7 @@ var Grid = FC.Grid = Class.extend({
3275
3589
  // this listener tracks a mousedown on a day element, and a subsequent drag.
3276
3590
  // if the drag ends on the same day, it is a 'dayClick'.
3277
3591
  // if 'selectable' is enabled, this listener also detects selections.
3278
- var dragListener = new HitDragListener(this, {
3279
- //distance: 5, // needs more work if we want dayClick to fire correctly
3592
+ var dragListener = this.dayDragListener = new HitDragListener(this, {
3280
3593
  scroll: view.opt('dragScroll'),
3281
3594
  dragStart: function() {
3282
3595
  view.unselect(); // since we could be rendering a new selection, we want to clear any old one
@@ -3304,7 +3617,7 @@ var Grid = FC.Grid = Class.extend({
3304
3617
  _this.unrenderSelection();
3305
3618
  enableCursor();
3306
3619
  },
3307
- listenStop: function(ev) {
3620
+ interactionEnd: function(ev) {
3308
3621
  if (dayClickHit) {
3309
3622
  view.triggerDayClick(
3310
3623
  _this.getHitSpan(dayClickHit),
@@ -3317,10 +3630,30 @@ var Grid = FC.Grid = Class.extend({
3317
3630
  view.reportSelection(selectionSpan, ev);
3318
3631
  }
3319
3632
  enableCursor();
3633
+ _this.dayDragListener = null;
3320
3634
  }
3321
3635
  });
3322
3636
 
3323
- dragListener.mousedown(ev); // start listening, which will eventually initiate a dragStart
3637
+ return dragListener;
3638
+ },
3639
+
3640
+
3641
+ // Kills all in-progress dragging.
3642
+ // Useful for when public API methods that result in re-rendering are invoked during a drag.
3643
+ // Also useful for when touch devices misbehave and don't fire their touchend.
3644
+ clearDragListeners: function() {
3645
+ if (this.dayDragListener) {
3646
+ this.dayDragListener.endInteraction(); // will clear this.dayDragListener
3647
+ }
3648
+ if (this.segDragListener) {
3649
+ this.segDragListener.endInteraction(); // will clear this.segDragListener
3650
+ }
3651
+ if (this.segResizeListener) {
3652
+ this.segResizeListener.endInteraction(); // will clear this.segResizeListener
3653
+ }
3654
+ if (this.externalDragListener) {
3655
+ this.externalDragListener.endInteraction(); // will clear this.externalDragListener
3656
+ }
3324
3657
  },
3325
3658
 
3326
3659
 
@@ -3330,10 +3663,11 @@ var Grid = FC.Grid = Class.extend({
3330
3663
 
3331
3664
 
3332
3665
  // Renders a mock event at the given event location, which contains zoned start/end properties.
3666
+ // Returns all mock event elements.
3333
3667
  renderEventLocationHelper: function(eventLocation, sourceSeg) {
3334
3668
  var fakeEvent = this.fabricateHelperEvent(eventLocation, sourceSeg);
3335
3669
 
3336
- this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering
3670
+ return this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering
3337
3671
  },
3338
3672
 
3339
3673
 
@@ -3361,6 +3695,7 @@ var Grid = FC.Grid = Class.extend({
3361
3695
 
3362
3696
 
3363
3697
  // Renders a mock event. Given zoned event date properties.
3698
+ // Must return all mock event elements.
3364
3699
  renderHelper: function(eventLocation, sourceSeg) {
3365
3700
  // subclasses must implement
3366
3701
  },
@@ -3640,7 +3975,8 @@ Grid.mixin({
3640
3975
 
3641
3976
  // Unrenders all events currently rendered on the grid
3642
3977
  unrenderEvents: function() {
3643
- this.triggerSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
3978
+ this.handleSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
3979
+ this.clearDragListeners();
3644
3980
 
3645
3981
  this.unrenderFgSegs();
3646
3982
  this.unrenderBgSegs();
@@ -3768,46 +4104,42 @@ Grid.mixin({
3768
4104
 
3769
4105
  // Attaches event-element-related handlers to the container element and leverage bubbling
3770
4106
  bindSegHandlers: function() {
4107
+ if (this.view.calendar.isTouch) {
4108
+ this.bindSegHandler('touchstart', this.handleSegTouchStart);
4109
+ }
4110
+ else {
4111
+ this.bindSegHandler('mouseenter', this.handleSegMouseover);
4112
+ this.bindSegHandler('mouseleave', this.handleSegMouseout);
4113
+ this.bindSegHandler('mousedown', this.handleSegMousedown);
4114
+ }
4115
+
4116
+ this.bindSegHandler('click', this.handleSegClick);
4117
+ },
4118
+
4119
+
4120
+ // Executes a handler for any a user-interaction on a segment.
4121
+ // Handler gets called with (seg, ev), and with the `this` context of the Grid
4122
+ bindSegHandler: function(name, handler) {
3771
4123
  var _this = this;
3772
- var view = this.view;
3773
4124
 
3774
- $.each(
3775
- {
3776
- mouseenter: function(seg, ev) {
3777
- _this.triggerSegMouseover(seg, ev);
3778
- },
3779
- mouseleave: function(seg, ev) {
3780
- _this.triggerSegMouseout(seg, ev);
3781
- },
3782
- click: function(seg, ev) {
3783
- return view.trigger('eventClick', this, seg.event, ev); // can return `false` to cancel
3784
- },
3785
- mousedown: function(seg, ev) {
3786
- if ($(ev.target).is('.fc-resizer') && view.isEventResizable(seg.event)) {
3787
- _this.segResizeMousedown(seg, ev, $(ev.target).is('.fc-start-resizer'));
3788
- }
3789
- else if (view.isEventDraggable(seg.event)) {
3790
- _this.segDragMousedown(seg, ev);
3791
- }
3792
- }
3793
- },
3794
- function(name, func) {
3795
- // attach the handler to the container element and only listen for real event elements via bubbling
3796
- _this.el.on(name, '.fc-event-container > *', function(ev) {
3797
- var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents
3798
-
3799
- // only call the handlers if there is not a drag/resize in progress
3800
- if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) {
3801
- return func.call(this, seg, ev); // `this` will be the event element
3802
- }
3803
- });
4125
+ this.el.on(name, '.fc-event-container > *', function(ev) {
4126
+ var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents
4127
+
4128
+ // only call the handlers if there is not a drag/resize in progress
4129
+ if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) {
4130
+ return handler.call(_this, seg, ev); // context will be the Grid
3804
4131
  }
3805
- );
4132
+ });
4133
+ },
4134
+
4135
+
4136
+ handleSegClick: function(seg, ev) {
4137
+ return this.view.trigger('eventClick', seg.el[0], seg.event, ev); // can return `false` to cancel
3806
4138
  },
3807
4139
 
3808
4140
 
3809
4141
  // Updates internal state and triggers handlers for when an event element is moused over
3810
- triggerSegMouseover: function(seg, ev) {
4142
+ handleSegMouseover: function(seg, ev) {
3811
4143
  if (!this.mousedOverSeg) {
3812
4144
  this.mousedOverSeg = seg;
3813
4145
  this.view.trigger('eventMouseover', seg.el[0], seg.event, ev);
@@ -3817,7 +4149,7 @@ Grid.mixin({
3817
4149
 
3818
4150
  // Updates internal state and triggers handlers for when an event element is moused out.
3819
4151
  // Can be given no arguments, in which case it will mouseout the segment that was previously moused over.
3820
- triggerSegMouseout: function(seg, ev) {
4152
+ handleSegMouseout: function(seg, ev) {
3821
4153
  ev = ev || {}; // if given no args, make a mock mouse event
3822
4154
 
3823
4155
  if (this.mousedOverSeg) {
@@ -3828,45 +4160,112 @@ Grid.mixin({
3828
4160
  },
3829
4161
 
3830
4162
 
4163
+ handleSegTouchStart: function(seg, ev) {
4164
+ var view = this.view;
4165
+ var event = seg.event;
4166
+ var isSelected = view.isEventSelected(event);
4167
+ var isDraggable = view.isEventDraggable(event);
4168
+ var isResizable = view.isEventResizable(event);
4169
+ var isResizing = false;
4170
+ var dragListener;
4171
+
4172
+ if (isSelected && isResizable) {
4173
+ // only allow resizing of the event is selected
4174
+ isResizing = this.startSegResize(seg, ev);
4175
+ }
4176
+
4177
+ if (!isResizing && (isDraggable || isResizable)) { // allowed to be selected?
4178
+ this.clearDragListeners();
4179
+
4180
+ dragListener = isDraggable ?
4181
+ this.buildSegDragListener(seg) :
4182
+ new DragListener(); // seg isn't draggable, but let's use a generic DragListener
4183
+ // simply for the delay, so it can be selected.
4184
+
4185
+ dragListener._dragStart = function() { // TODO: better way of binding
4186
+ // if not previously selected, will fire after a delay. then, select the event
4187
+ if (!isSelected) {
4188
+ view.selectEvent(event);
4189
+ }
4190
+ };
4191
+
4192
+ dragListener.startInteraction(ev, {
4193
+ delay: isSelected ? 0 : this.view.opt('longPressDelay') // do delay if not already selected
4194
+ });
4195
+ }
4196
+ },
4197
+
4198
+
4199
+ handleSegMousedown: function(seg, ev) {
4200
+ var isResizing = this.startSegResize(seg, ev, { distance: 5 });
4201
+
4202
+ if (!isResizing && this.view.isEventDraggable(seg.event)) {
4203
+ this.clearDragListeners();
4204
+ this.buildSegDragListener(seg)
4205
+ .startInteraction(ev, {
4206
+ distance: 5
4207
+ });
4208
+ }
4209
+ },
4210
+
4211
+
4212
+ // returns boolean whether resizing actually started or not.
4213
+ // assumes the seg allows resizing.
4214
+ // `dragOptions` are optional.
4215
+ startSegResize: function(seg, ev, dragOptions) {
4216
+ if ($(ev.target).is('.fc-resizer')) {
4217
+ this.clearDragListeners();
4218
+ this.buildSegResizeListener(seg, $(ev.target).is('.fc-start-resizer'))
4219
+ .startInteraction(ev, dragOptions);
4220
+ return true;
4221
+ }
4222
+ return false;
4223
+ },
4224
+
4225
+
4226
+
3831
4227
  /* Event Dragging
3832
4228
  ------------------------------------------------------------------------------------------------------------------*/
3833
4229
 
3834
4230
 
3835
- // Called when the user does a mousedown on an event, which might lead to dragging.
4231
+ // Builds a listener that will track user-dragging on an event segment.
3836
4232
  // Generic enough to work with any type of Grid.
3837
- segDragMousedown: function(seg, ev) {
4233
+ buildSegDragListener: function(seg) {
3838
4234
  var _this = this;
3839
4235
  var view = this.view;
3840
4236
  var calendar = view.calendar;
3841
4237
  var el = seg.el;
3842
4238
  var event = seg.event;
4239
+ var isDragging;
4240
+ var mouseFollower; // A clone of the original element that will move with the mouse
3843
4241
  var dropLocation; // zoned event date properties
3844
4242
 
3845
- // A clone of the original element that will move with the mouse
3846
- var mouseFollower = new MouseFollower(seg.el, {
3847
- parentEl: view.el,
3848
- opacity: view.opt('dragOpacity'),
3849
- revertDuration: view.opt('dragRevertDuration'),
3850
- zIndex: 2 // one above the .fc-view
3851
- });
3852
-
3853
4243
  // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents
3854
4244
  // of the view.
3855
- var dragListener = new HitDragListener(view, {
3856
- distance: 5,
4245
+ var dragListener = this.segDragListener = new HitDragListener(view, {
3857
4246
  scroll: view.opt('dragScroll'),
3858
4247
  subjectEl: el,
3859
4248
  subjectCenter: true,
3860
- listenStart: function(ev) {
4249
+ interactionStart: function(ev) {
4250
+ isDragging = false;
4251
+ mouseFollower = new MouseFollower(seg.el, {
4252
+ additionalClass: 'fc-dragging',
4253
+ parentEl: view.el,
4254
+ opacity: dragListener.isTouch ? null : view.opt('dragOpacity'),
4255
+ revertDuration: view.opt('dragRevertDuration'),
4256
+ zIndex: 2 // one above the .fc-view
4257
+ });
3861
4258
  mouseFollower.hide(); // don't show until we know this is a real drag
3862
4259
  mouseFollower.start(ev);
3863
4260
  },
3864
4261
  dragStart: function(ev) {
3865
- _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
4262
+ isDragging = true;
4263
+ _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
3866
4264
  _this.segDragStart(seg, ev);
3867
4265
  view.hideEvent(event); // hide all event segments. our mouseFollower will take over
3868
4266
  },
3869
4267
  hitOver: function(hit, isOrig, origHit) {
4268
+ var dragHelperEls;
3870
4269
 
3871
4270
  // starting hit could be forced (DayGrid.limit)
3872
4271
  if (seg.hit) {
@@ -3886,7 +4285,13 @@ Grid.mixin({
3886
4285
  }
3887
4286
 
3888
4287
  // if a valid drop location, have the subclass render a visual indication
3889
- if (dropLocation && view.renderDrag(dropLocation, seg)) {
4288
+ if (dropLocation && (dragHelperEls = view.renderDrag(dropLocation, seg))) {
4289
+
4290
+ dragHelperEls.addClass('fc-dragging');
4291
+ if (!dragListener.isTouch) {
4292
+ _this.applyDragOpacity(dragHelperEls);
4293
+ }
4294
+
3890
4295
  mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own
3891
4296
  }
3892
4297
  else {
@@ -3902,27 +4307,26 @@ Grid.mixin({
3902
4307
  mouseFollower.show(); // show in case we are moving out of all hits
3903
4308
  dropLocation = null;
3904
4309
  },
3905
- hitDone: function() { // Called after a hitOut OR before a dragStop
4310
+ hitDone: function() { // Called after a hitOut OR before a dragEnd
3906
4311
  enableCursor();
3907
4312
  },
3908
- dragStop: function(ev) {
4313
+ interactionEnd: function(ev) {
3909
4314
  // do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
3910
4315
  mouseFollower.stop(!dropLocation, function() {
3911
- view.unrenderDrag();
3912
- view.showEvent(event);
3913
- _this.segDragStop(seg, ev);
3914
-
4316
+ if (isDragging) {
4317
+ view.unrenderDrag();
4318
+ view.showEvent(event);
4319
+ _this.segDragStop(seg, ev);
4320
+ }
3915
4321
  if (dropLocation) {
3916
4322
  view.reportEventDrop(event, dropLocation, this.largeUnit, el, ev);
3917
4323
  }
3918
4324
  });
3919
- },
3920
- listenStop: function() {
3921
- mouseFollower.stop(); // put in listenStop in case there was a mousedown but the drag never started
4325
+ _this.segDragListener = null;
3922
4326
  }
3923
4327
  });
3924
4328
 
3925
- dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart
4329
+ return dragListener;
3926
4330
  },
3927
4331
 
3928
4332
 
@@ -4038,8 +4442,8 @@ Grid.mixin({
4038
4442
  var dropLocation; // a null value signals an unsuccessful drag
4039
4443
 
4040
4444
  // listener that tracks mouse movement over date-associated pixel regions
4041
- var dragListener = new HitDragListener(this, {
4042
- listenStart: function() {
4445
+ var dragListener = _this.externalDragListener = new HitDragListener(this, {
4446
+ interactionStart: function() {
4043
4447
  _this.isDraggingExternal = true;
4044
4448
  },
4045
4449
  hitOver: function(hit) {
@@ -4063,17 +4467,16 @@ Grid.mixin({
4063
4467
  hitOut: function() {
4064
4468
  dropLocation = null; // signal unsuccessful
4065
4469
  },
4066
- hitDone: function() { // Called after a hitOut OR before a dragStop
4470
+ hitDone: function() { // Called after a hitOut OR before a dragEnd
4067
4471
  enableCursor();
4068
4472
  _this.unrenderDrag();
4069
4473
  },
4070
- dragStop: function() {
4474
+ interactionEnd: function(ev) {
4071
4475
  if (dropLocation) { // element was dropped on a valid hit
4072
4476
  _this.view.reportExternalDrop(meta, dropLocation, el, ev, ui);
4073
4477
  }
4074
- },
4075
- listenStop: function() {
4076
4478
  _this.isDraggingExternal = false;
4479
+ _this.externalDragListener = null;
4077
4480
  }
4078
4481
  });
4079
4482
 
@@ -4114,6 +4517,7 @@ Grid.mixin({
4114
4517
  // `dropLocation` contains hypothetical start/end/allDay values the event would have if dropped. end can be null.
4115
4518
  // `seg` is the internal segment object that is being dragged. If dragging an external element, `seg` is null.
4116
4519
  // A truthy returned value indicates this method has rendered a helper element.
4520
+ // Must return elements used for any mock events.
4117
4521
  renderDrag: function(dropLocation, seg) {
4118
4522
  // subclasses must implement
4119
4523
  },
@@ -4129,24 +4533,28 @@ Grid.mixin({
4129
4533
  ------------------------------------------------------------------------------------------------------------------*/
4130
4534
 
4131
4535
 
4132
- // Called when the user does a mousedown on an event's resizer, which might lead to resizing.
4536
+ // Creates a listener that tracks the user as they resize an event segment.
4133
4537
  // Generic enough to work with any type of Grid.
4134
- segResizeMousedown: function(seg, ev, isStart) {
4538
+ buildSegResizeListener: function(seg, isStart) {
4135
4539
  var _this = this;
4136
4540
  var view = this.view;
4137
4541
  var calendar = view.calendar;
4138
4542
  var el = seg.el;
4139
4543
  var event = seg.event;
4140
4544
  var eventEnd = calendar.getEventEnd(event);
4545
+ var isDragging;
4141
4546
  var resizeLocation; // zoned event date properties. falsy if invalid resize
4142
4547
 
4143
4548
  // Tracks mouse movement over the *grid's* coordinate map
4144
- var dragListener = new HitDragListener(this, {
4145
- distance: 5,
4549
+ var dragListener = this.segResizeListener = new HitDragListener(this, {
4146
4550
  scroll: view.opt('dragScroll'),
4147
4551
  subjectEl: el,
4552
+ interactionStart: function() {
4553
+ isDragging = false;
4554
+ },
4148
4555
  dragStart: function(ev) {
4149
- _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
4556
+ isDragging = true;
4557
+ _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
4150
4558
  _this.segResizeStart(seg, ev);
4151
4559
  },
4152
4560
  hitOver: function(hit, isOrig, origHit) {
@@ -4181,16 +4589,18 @@ Grid.mixin({
4181
4589
  view.showEvent(event);
4182
4590
  enableCursor();
4183
4591
  },
4184
- dragStop: function(ev) {
4185
- _this.segResizeStop(seg, ev);
4186
-
4592
+ interactionEnd: function(ev) {
4593
+ if (isDragging) {
4594
+ _this.segResizeStop(seg, ev);
4595
+ }
4187
4596
  if (resizeLocation) { // valid date to resize to?
4188
4597
  view.reportEventResize(event, resizeLocation, this.largeUnit, el, ev);
4189
4598
  }
4599
+ _this.segResizeListener = null;
4190
4600
  }
4191
4601
  });
4192
4602
 
4193
- dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart
4603
+ return dragListener;
4194
4604
  },
4195
4605
 
4196
4606
 
@@ -4267,6 +4677,7 @@ Grid.mixin({
4267
4677
 
4268
4678
  // Renders a visual indication of an event being resized.
4269
4679
  // `range` has the updated dates of the event. `seg` is the original segment object involved in the drag.
4680
+ // Must return elements used for any mock events.
4270
4681
  renderEventResize: function(range, seg) {
4271
4682
  // subclasses must implement
4272
4683
  },
@@ -4312,6 +4723,7 @@ Grid.mixin({
4312
4723
 
4313
4724
  // Generic utility for generating the HTML classNames for an event segment's element
4314
4725
  getSegClasses: function(seg, isDraggable, isResizable) {
4726
+ var view = this.view;
4315
4727
  var event = seg.event;
4316
4728
  var classes = [
4317
4729
  'fc-event',
@@ -4329,6 +4741,11 @@ Grid.mixin({
4329
4741
  classes.push('fc-resizable');
4330
4742
  }
4331
4743
 
4744
+ // event is currently selected? attach a className.
4745
+ if (view.isEventSelected(event)) {
4746
+ classes.push('fc-selected');
4747
+ }
4748
+
4332
4749
  return classes;
4333
4750
  },
4334
4751
 
@@ -5311,10 +5728,7 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
5311
5728
  // if a segment from the same calendar but another component is being dragged, render a helper event
5312
5729
  if (seg && !seg.el.closest(this.el).length) {
5313
5730
 
5314
- this.renderEventLocationHelper(eventLocation, seg);
5315
- this.applyDragOpacity(this.helperEls);
5316
-
5317
- return true; // a helper has been rendered
5731
+ return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements
5318
5732
  }
5319
5733
  },
5320
5734
 
@@ -5333,7 +5747,7 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
5333
5747
  // Renders a visual indication of an event being resized
5334
5748
  renderEventResize: function(eventLocation, seg) {
5335
5749
  this.renderHighlight(this.eventToSpan(eventLocation));
5336
- this.renderEventLocationHelper(eventLocation, seg);
5750
+ return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements
5337
5751
  },
5338
5752
 
5339
5753
 
@@ -5379,7 +5793,9 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
5379
5793
  helperNodes.push(skeletonEl[0]);
5380
5794
  });
5381
5795
 
5382
- this.helperEls = $(helperNodes); // array -> jQuery set
5796
+ return ( // must return the elements rendered
5797
+ this.helperEls = $(helperNodes) // array -> jQuery set
5798
+ );
5383
5799
  },
5384
5800
 
5385
5801
 
@@ -6165,6 +6581,7 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
6165
6581
  labelInterval: null, // duration of how often a label should be displayed for a slot
6166
6582
 
6167
6583
  colEls: null, // cells elements in the day-row background
6584
+ slatContainerEl: null, // div that wraps all the slat rows
6168
6585
  slatEls: null, // elements running horizontally across all columns
6169
6586
  nowIndicatorEls: null,
6170
6587
 
@@ -6184,7 +6601,8 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
6184
6601
  renderDates: function() {
6185
6602
  this.el.html(this.renderHtml());
6186
6603
  this.colEls = this.el.find('.fc-day');
6187
- this.slatEls = this.el.find('.fc-slats tr');
6604
+ this.slatContainerEl = this.el.find('.fc-slats');
6605
+ this.slatEls = this.slatContainerEl.find('tr');
6188
6606
 
6189
6607
  this.colCoordCache = new CoordCache({
6190
6608
  els: this.colEls,
@@ -6463,6 +6881,11 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
6463
6881
  },
6464
6882
 
6465
6883
 
6884
+ getTotalSlatHeight: function() {
6885
+ return this.slatContainerEl.outerHeight();
6886
+ },
6887
+
6888
+
6466
6889
  // Computes the top coordinate, relative to the bounds of the grid, of the given date.
6467
6890
  // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
6468
6891
  computeDateTop: function(date, startOfDayDate) {
@@ -6511,13 +6934,10 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
6511
6934
  renderDrag: function(eventLocation, seg) {
6512
6935
 
6513
6936
  if (seg) { // if there is event information for this drag, render a helper event
6514
- this.renderEventLocationHelper(eventLocation, seg);
6515
-
6516
- for (var i = 0; i < this.helperSegs.length; i++) {
6517
- this.applyDragOpacity(this.helperSegs[i].el);
6518
- }
6519
6937
 
6520
- return true; // signal that a helper has been rendered
6938
+ // returns mock event elements
6939
+ // signal that a helper has been rendered
6940
+ return this.renderEventLocationHelper(eventLocation, seg);
6521
6941
  }
6522
6942
  else {
6523
6943
  // otherwise, just render a highlight
@@ -6539,7 +6959,7 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
6539
6959
 
6540
6960
  // Renders a visual indication of an event being resized
6541
6961
  renderEventResize: function(eventLocation, seg) {
6542
- this.renderEventLocationHelper(eventLocation, seg);
6962
+ return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements
6543
6963
  },
6544
6964
 
6545
6965
 
@@ -6555,7 +6975,7 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
6555
6975
 
6556
6976
  // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag)
6557
6977
  renderHelper: function(event, sourceSeg) {
6558
- this.renderHelperSegs(this.eventToSegs(event), sourceSeg);
6978
+ return this.renderHelperSegs(this.eventToSegs(event), sourceSeg); // returns mock event elements
6559
6979
  },
6560
6980
 
6561
6981
 
@@ -6749,6 +7169,7 @@ TimeGrid.mixin({
6749
7169
 
6750
7170
 
6751
7171
  renderHelperSegs: function(segs, sourceSeg) {
7172
+ var helperEls = [];
6752
7173
  var i, seg;
6753
7174
  var sourceEl;
6754
7175
 
@@ -6766,9 +7187,12 @@ TimeGrid.mixin({
6766
7187
  'margin-right': sourceEl.css('margin-right')
6767
7188
  });
6768
7189
  }
7190
+ helperEls.push(seg.el[0]);
6769
7191
  }
6770
7192
 
6771
7193
  this.helperSegs = segs;
7194
+
7195
+ return $(helperEls); // must return rendered helpers
6772
7196
  },
6773
7197
 
6774
7198
 
@@ -7279,7 +7703,7 @@ function isSlotSegCollision(seg1, seg2) {
7279
7703
  /* An abstract class from which other views inherit from
7280
7704
  ----------------------------------------------------------------------------------------------------------------------*/
7281
7705
 
7282
- var View = FC.View = Class.extend({
7706
+ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
7283
7707
 
7284
7708
  type: null, // subclass' view name (string)
7285
7709
  name: null, // deprecated. use `type` instead
@@ -7306,13 +7730,10 @@ var View = FC.View = Class.extend({
7306
7730
 
7307
7731
  isRTL: false,
7308
7732
  isSelected: false, // boolean whether a range of time is user-selected or not
7733
+ selectedEvent: null,
7309
7734
 
7310
7735
  eventOrderSpecs: null, // criteria for ordering events when they have same date/time
7311
7736
 
7312
- // subclasses can optionally use a scroll container
7313
- scrollerEl: null, // the element that will most likely scroll when content is too tall
7314
- scrollTop: null, // cached vertical scroll value
7315
-
7316
7737
  // classNames styled by jqui themes
7317
7738
  widgetHeaderClass: null,
7318
7739
  widgetContentClass: null,
@@ -7322,9 +7743,6 @@ var View = FC.View = Class.extend({
7322
7743
  nextDayThreshold: null,
7323
7744
  isHiddenDayHash: null,
7324
7745
 
7325
- // document handlers, bound to `this` object
7326
- documentMousedownProxy: null, // TODO: doesn't work with touch
7327
-
7328
7746
  // now indicator
7329
7747
  isNowIndicatorRendered: null,
7330
7748
  initialNowDate: null, // result first getNow call
@@ -7347,8 +7765,6 @@ var View = FC.View = Class.extend({
7347
7765
 
7348
7766
  this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder'));
7349
7767
 
7350
- this.documentMousedownProxy = proxy(this, 'documentMousedown');
7351
-
7352
7768
  this.initialize();
7353
7769
  },
7354
7770
 
@@ -7674,13 +8090,15 @@ var View = FC.View = Class.extend({
7674
8090
 
7675
8091
  // Binds DOM handlers to elements that reside outside the view container, such as the document
7676
8092
  bindGlobalHandlers: function() {
7677
- $(document).on('mousedown', this.documentMousedownProxy);
8093
+ this.listenTo($(document), 'mousedown', this.handleDocumentMousedown);
8094
+ this.listenTo($(document), 'touchstart', this.handleDocumentTouchStart);
8095
+ this.listenTo($(document), 'touchend', this.handleDocumentTouchEnd);
7678
8096
  },
7679
8097
 
7680
8098
 
7681
8099
  // Unbinds DOM handlers from elements that reside outside the view container
7682
8100
  unbindGlobalHandlers: function() {
7683
- $(document).off('mousedown', this.documentMousedownProxy);
8101
+ this.stopListeningTo($(document));
7684
8102
  },
7685
8103
 
7686
8104
 
@@ -7848,27 +8266,6 @@ var View = FC.View = Class.extend({
7848
8266
  ------------------------------------------------------------------------------------------------------------------*/
7849
8267
 
7850
8268
 
7851
- // Given the total height of the view, return the number of pixels that should be used for the scroller.
7852
- // Utility for subclasses.
7853
- computeScrollerHeight: function(totalHeight) {
7854
- var scrollerEl = this.scrollerEl;
7855
- var both;
7856
- var otherHeight; // cumulative height of everything that is not the scrollerEl in the view (header+borders)
7857
-
7858
- both = this.el.add(scrollerEl);
7859
-
7860
- // fuckin IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked
7861
- both.css({
7862
- position: 'relative', // cause a reflow, which will force fresh dimension recalculation
7863
- left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll
7864
- });
7865
- otherHeight = this.el.outerHeight() - scrollerEl.height(); // grab the dimensions
7866
- both.css({ position: '', left: '' }); // undo hack
7867
-
7868
- return totalHeight - otherHeight;
7869
- },
7870
-
7871
-
7872
8269
  // Computes the initial pre-configured scroll state prior to allowing the user to change it.
7873
8270
  // Given the scroll state from the previous rendering. If first time rendering, given null.
7874
8271
  computeInitialScroll: function(previousScrollState) {
@@ -7878,17 +8275,13 @@ var View = FC.View = Class.extend({
7878
8275
 
7879
8276
  // Retrieves the view's current natural scroll state. Can return an arbitrary format.
7880
8277
  queryScroll: function() {
7881
- if (this.scrollerEl) {
7882
- return this.scrollerEl.scrollTop(); // operates on scrollerEl by default
7883
- }
8278
+ // subclasses must implement
7884
8279
  },
7885
8280
 
7886
8281
 
7887
8282
  // Sets the view's scroll state. Will accept the same format computeInitialScroll and queryScroll produce.
7888
8283
  setScroll: function(scrollState) {
7889
- if (this.scrollerEl) {
7890
- return this.scrollerEl.scrollTop(scrollState); // operates on scrollerEl by default
7891
- }
8284
+ // subclasses must implement
7892
8285
  },
7893
8286
 
7894
8287
 
@@ -8103,7 +8496,8 @@ var View = FC.View = Class.extend({
8103
8496
 
8104
8497
 
8105
8498
  // Renders a visual indication of a event or external-element drag over the given drop zone.
8106
- // If an external-element, seg will be `null`
8499
+ // If an external-element, seg will be `null`.
8500
+ // Must return elements used for any mock events.
8107
8501
  renderDrag: function(dropLocation, seg) {
8108
8502
  // subclasses must implement
8109
8503
  },
@@ -8166,7 +8560,7 @@ var View = FC.View = Class.extend({
8166
8560
  },
8167
8561
 
8168
8562
 
8169
- /* Selection
8563
+ /* Selection (time range)
8170
8564
  ------------------------------------------------------------------------------------------------------------------*/
8171
8565
 
8172
8566
 
@@ -8224,13 +8618,69 @@ var View = FC.View = Class.extend({
8224
8618
  },
8225
8619
 
8226
8620
 
8227
- // Handler for unselecting when the user clicks something and the 'unselectAuto' setting is on
8228
- documentMousedown: function(ev) {
8229
- var ignore;
8621
+ /* Event Selection
8622
+ ------------------------------------------------------------------------------------------------------------------*/
8623
+
8624
+
8625
+ selectEvent: function(event) {
8626
+ if (!this.selectedEvent || this.selectedEvent !== event) {
8627
+ this.unselectEvent();
8628
+ this.renderedEventSegEach(function(seg) {
8629
+ seg.el.addClass('fc-selected');
8630
+ }, event);
8631
+ this.selectedEvent = event;
8632
+ }
8633
+ },
8634
+
8635
+
8636
+ unselectEvent: function() {
8637
+ if (this.selectedEvent) {
8638
+ this.renderedEventSegEach(function(seg) {
8639
+ seg.el.removeClass('fc-selected');
8640
+ }, this.selectedEvent);
8641
+ this.selectedEvent = null;
8642
+ }
8643
+ },
8644
+
8645
+
8646
+ isEventSelected: function(event) {
8647
+ // event references might change on refetchEvents(), while selectedEvent doesn't,
8648
+ // so compare IDs
8649
+ return this.selectedEvent && this.selectedEvent._id === event._id;
8650
+ },
8651
+
8652
+
8653
+ /* Mouse / Touch Unselecting (time range & event unselection)
8654
+ ------------------------------------------------------------------------------------------------------------------*/
8655
+ // TODO: move consistently to down/start or up/end?
8656
+
8657
+
8658
+ handleDocumentMousedown: function(ev) {
8659
+ // touch devices fire simulated mouse events on a "click".
8660
+ // only process mousedown if we know this isn't a touch device.
8661
+ if (!this.calendar.isTouch && isPrimaryMouseButton(ev)) {
8662
+ this.processRangeUnselect(ev);
8663
+ this.processEventUnselect(ev);
8664
+ }
8665
+ },
8666
+
8667
+
8668
+ handleDocumentTouchStart: function(ev) {
8669
+ this.processRangeUnselect(ev);
8670
+ },
8230
8671
 
8231
- // is there a selection, and has the user made a proper left click?
8232
- if (this.isSelected && this.opt('unselectAuto') && isPrimaryMouseButton(ev)) {
8233
8672
 
8673
+ handleDocumentTouchEnd: function(ev) {
8674
+ // TODO: don't do this if because of touch-scrolling
8675
+ this.processEventUnselect(ev);
8676
+ },
8677
+
8678
+
8679
+ processRangeUnselect: function(ev) {
8680
+ var ignore;
8681
+
8682
+ // is there a time-range selection?
8683
+ if (this.isSelected && this.opt('unselectAuto')) {
8234
8684
  // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element
8235
8685
  ignore = this.opt('unselectCancel');
8236
8686
  if (!ignore || !$(ev.target).closest(ignore).length) {
@@ -8240,6 +8690,15 @@ var View = FC.View = Class.extend({
8240
8690
  },
8241
8691
 
8242
8692
 
8693
+ processEventUnselect: function(ev) {
8694
+ if (this.selectedEvent) {
8695
+ if (!$(ev.target).closest('.fc-selected').length) {
8696
+ this.unselectEvent();
8697
+ }
8698
+ }
8699
+ },
8700
+
8701
+
8243
8702
  /* Day Click
8244
8703
  ------------------------------------------------------------------------------------------------------------------*/
8245
8704
 
@@ -8354,6 +8813,127 @@ var View = FC.View = Class.extend({
8354
8813
 
8355
8814
  ;;
8356
8815
 
8816
+ /*
8817
+ Embodies a div that has potential scrollbars
8818
+ */
8819
+ var Scroller = FC.Scroller = Class.extend({
8820
+
8821
+ el: null, // the guaranteed outer element
8822
+ scrollEl: null, // the element with the scrollbars
8823
+ overflowX: null,
8824
+ overflowY: null,
8825
+
8826
+
8827
+ constructor: function(options) {
8828
+ options = options || {};
8829
+ this.overflowX = options.overflowX || options.overflow || 'auto';
8830
+ this.overflowY = options.overflowY || options.overflow || 'auto';
8831
+ },
8832
+
8833
+
8834
+ render: function() {
8835
+ this.el = this.renderEl();
8836
+ this.applyOverflow();
8837
+ },
8838
+
8839
+
8840
+ renderEl: function() {
8841
+ return (this.scrollEl = $('<div class="fc-scroller"></div>'));
8842
+ },
8843
+
8844
+
8845
+ // sets to natural height, unlocks overflow
8846
+ clear: function() {
8847
+ this.setHeight('auto');
8848
+ this.applyOverflow();
8849
+ },
8850
+
8851
+
8852
+ destroy: function() {
8853
+ this.el.remove();
8854
+ },
8855
+
8856
+
8857
+ // Overflow
8858
+ // -----------------------------------------------------------------------------------------------------------------
8859
+
8860
+
8861
+ applyOverflow: function() {
8862
+ this.scrollEl.css({
8863
+ 'overflow-x': this.overflowX,
8864
+ 'overflow-y': this.overflowY
8865
+ });
8866
+ },
8867
+
8868
+
8869
+ // Causes any 'auto' overflow values to resolves to 'scroll' or 'hidden'.
8870
+ // Useful for preserving scrollbar widths regardless of future resizes.
8871
+ // Can pass in scrollbarWidths for optimization.
8872
+ lockOverflow: function(scrollbarWidths) {
8873
+ var overflowX = this.overflowX;
8874
+ var overflowY = this.overflowY;
8875
+
8876
+ scrollbarWidths = scrollbarWidths || this.getScrollbarWidths();
8877
+
8878
+ if (overflowX === 'auto') {
8879
+ overflowX = (
8880
+ scrollbarWidths.top || scrollbarWidths.bottom || // horizontal scrollbars?
8881
+ // OR scrolling pane with massless scrollbars?
8882
+ this.scrollEl[0].scrollWidth - 1 > this.scrollEl[0].clientWidth
8883
+ // subtract 1 because of IE off-by-one issue
8884
+ ) ? 'scroll' : 'hidden';
8885
+ }
8886
+
8887
+ if (overflowY === 'auto') {
8888
+ overflowY = (
8889
+ scrollbarWidths.left || scrollbarWidths.right || // vertical scrollbars?
8890
+ // OR scrolling pane with massless scrollbars?
8891
+ this.scrollEl[0].scrollHeight - 1 > this.scrollEl[0].clientHeight
8892
+ // subtract 1 because of IE off-by-one issue
8893
+ ) ? 'scroll' : 'hidden';
8894
+ }
8895
+
8896
+ this.scrollEl.css({ 'overflow-x': overflowX, 'overflow-y': overflowY });
8897
+ },
8898
+
8899
+
8900
+ // Getters / Setters
8901
+ // -----------------------------------------------------------------------------------------------------------------
8902
+
8903
+
8904
+ setHeight: function(height) {
8905
+ this.scrollEl.height(height);
8906
+ },
8907
+
8908
+
8909
+ getScrollTop: function() {
8910
+ return this.scrollEl.scrollTop();
8911
+ },
8912
+
8913
+
8914
+ setScrollTop: function(top) {
8915
+ this.scrollEl.scrollTop(top);
8916
+ },
8917
+
8918
+
8919
+ getClientWidth: function() {
8920
+ return this.scrollEl[0].clientWidth;
8921
+ },
8922
+
8923
+
8924
+ getClientHeight: function() {
8925
+ return this.scrollEl[0].clientHeight;
8926
+ },
8927
+
8928
+
8929
+ getScrollbarWidths: function() {
8930
+ return getScrollbarWidths(this.scrollEl);
8931
+ }
8932
+
8933
+ });
8934
+
8935
+ ;;
8936
+
8357
8937
  var Calendar = FC.Calendar = Class.extend({
8358
8938
 
8359
8939
  dirDefaults: null, // option defaults related to LTR or RTL
@@ -8364,6 +8944,7 @@ var Calendar = FC.Calendar = Class.extend({
8364
8944
  view: null, // current View object
8365
8945
  header: null,
8366
8946
  loadingLevel: 0, // number of simultaneous loading tasks
8947
+ isTouch: false,
8367
8948
 
8368
8949
 
8369
8950
  // a lot of this class' OOP logic is scoped within this constructor function,
@@ -8410,6 +8991,10 @@ var Calendar = FC.Calendar = Class.extend({
8410
8991
  ]);
8411
8992
  populateInstanceComputableOptions(this.options);
8412
8993
 
8994
+ this.isTouch = this.options.isTouch != null ?
8995
+ this.options.isTouch :
8996
+ FC.isTouch;
8997
+
8413
8998
  this.viewSpecCache = {}; // somewhat unrelated
8414
8999
  },
8415
9000
 
@@ -8608,7 +9193,7 @@ var Calendar = FC.Calendar = Class.extend({
8608
9193
  });
8609
9194
 
8610
9195
 
8611
- Calendar.mixin(Emitter);
9196
+ Calendar.mixin(EmitterMixin);
8612
9197
 
8613
9198
 
8614
9199
  function Calendar_constructor(element, overrides) {
@@ -8866,6 +9451,10 @@ function Calendar_constructor(element, overrides) {
8866
9451
  tm = options.theme ? 'ui' : 'fc';
8867
9452
  element.addClass('fc');
8868
9453
 
9454
+ element.addClass(
9455
+ t.isTouch ? 'fc-touch' : 'fc-cursor'
9456
+ );
9457
+
8869
9458
  if (options.isRTL) {
8870
9459
  element.addClass('fc-rtl');
8871
9460
  }
@@ -8908,7 +9497,7 @@ function Calendar_constructor(element, overrides) {
8908
9497
 
8909
9498
  header.removeElement();
8910
9499
  content.remove();
8911
- element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');
9500
+ element.removeClass('fc fc-touch fc-cursor fc-ltr fc-rtl fc-unthemed ui-widget');
8912
9501
 
8913
9502
  if (windowResizeProxy) {
8914
9503
  $(window).unbind('resize', windowResizeProxy);
@@ -9369,7 +9958,9 @@ Calendar.defaults = {
9369
9958
  dayPopoverFormat: 'LL',
9370
9959
 
9371
9960
  handleWindowResize: true,
9372
- windowResizeDelay: 200 // milliseconds before an updateSize happens
9961
+ windowResizeDelay: 200, // milliseconds before an updateSize happens
9962
+
9963
+ longPressDelay: 1000
9373
9964
 
9374
9965
  };
9375
9966
 
@@ -10937,6 +11528,8 @@ function backupEventDates(event) {
10937
11528
 
10938
11529
  var BasicView = FC.BasicView = View.extend({
10939
11530
 
11531
+ scroller: null,
11532
+
10940
11533
  dayGridClass: DayGrid, // class the dayGrid will be instantiated from (overridable by subclasses)
10941
11534
  dayGrid: null, // the main subcomponent that does most of the heavy lifting
10942
11535
 
@@ -10951,6 +11544,11 @@ var BasicView = FC.BasicView = View.extend({
10951
11544
 
10952
11545
  initialize: function() {
10953
11546
  this.dayGrid = this.instantiateDayGrid();
11547
+
11548
+ this.scroller = new Scroller({
11549
+ overflowX: 'hidden',
11550
+ overflowY: 'auto'
11551
+ });
10954
11552
  },
10955
11553
 
10956
11554
 
@@ -11003,9 +11601,12 @@ var BasicView = FC.BasicView = View.extend({
11003
11601
  this.el.addClass('fc-basic-view').html(this.renderSkeletonHtml());
11004
11602
  this.renderHead();
11005
11603
 
11006
- this.scrollerEl = this.el.find('.fc-day-grid-container');
11604
+ this.scroller.render();
11605
+ var dayGridContainerEl = this.scroller.el.addClass('fc-day-grid-container');
11606
+ var dayGridEl = $('<div class="fc-day-grid" />').appendTo(dayGridContainerEl);
11607
+ this.el.find('.fc-body > tr > td').append(dayGridContainerEl);
11007
11608
 
11008
- this.dayGrid.setElement(this.el.find('.fc-day-grid'));
11609
+ this.dayGrid.setElement(dayGridEl);
11009
11610
  this.dayGrid.renderDates(this.hasRigidRows());
11010
11611
  },
11011
11612
 
@@ -11024,6 +11625,7 @@ var BasicView = FC.BasicView = View.extend({
11024
11625
  unrenderDates: function() {
11025
11626
  this.dayGrid.unrenderDates();
11026
11627
  this.dayGrid.removeElement();
11628
+ this.scroller.destroy();
11027
11629
  },
11028
11630
 
11029
11631
 
@@ -11044,11 +11646,7 @@ var BasicView = FC.BasicView = View.extend({
11044
11646
  '</thead>' +
11045
11647
  '<tbody class="fc-body">' +
11046
11648
  '<tr>' +
11047
- '<td class="' + this.widgetContentClass + '">' +
11048
- '<div class="fc-day-grid-container">' +
11049
- '<div class="fc-day-grid"/>' +
11050
- '</div>' +
11051
- '</td>' +
11649
+ '<td class="' + this.widgetContentClass + '"></td>' +
11052
11650
  '</tr>' +
11053
11651
  '</tbody>' +
11054
11652
  '</table>';
@@ -11091,9 +11689,10 @@ var BasicView = FC.BasicView = View.extend({
11091
11689
  setHeight: function(totalHeight, isAuto) {
11092
11690
  var eventLimit = this.opt('eventLimit');
11093
11691
  var scrollerHeight;
11692
+ var scrollbarWidths;
11094
11693
 
11095
11694
  // reset all heights to be natural
11096
- unsetScroller(this.scrollerEl);
11695
+ this.scroller.clear();
11097
11696
  uncompensateScroll(this.headRowEl);
11098
11697
 
11099
11698
  this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed
@@ -11103,6 +11702,8 @@ var BasicView = FC.BasicView = View.extend({
11103
11702
  this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after
11104
11703
  }
11105
11704
 
11705
+ // distribute the height to the rows
11706
+ // (totalHeight is a "recommended" value if isAuto)
11106
11707
  scrollerHeight = this.computeScrollerHeight(totalHeight);
11107
11708
  this.setGridHeight(scrollerHeight, isAuto);
11108
11709
 
@@ -11111,17 +11712,33 @@ var BasicView = FC.BasicView = View.extend({
11111
11712
  this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set
11112
11713
  }
11113
11714
 
11114
- if (!isAuto && setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars?
11715
+ if (!isAuto) { // should we force dimensions of the scroll container?
11115
11716
 
11116
- compensateScroll(this.headRowEl, getScrollbarWidths(this.scrollerEl));
11717
+ this.scroller.setHeight(scrollerHeight);
11718
+ scrollbarWidths = this.scroller.getScrollbarWidths();
11117
11719
 
11118
- // doing the scrollbar compensation might have created text overflow which created more height. redo
11119
- scrollerHeight = this.computeScrollerHeight(totalHeight);
11120
- this.scrollerEl.height(scrollerHeight);
11720
+ if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars?
11721
+
11722
+ compensateScroll(this.headRowEl, scrollbarWidths);
11723
+
11724
+ // doing the scrollbar compensation might have created text overflow which created more height. redo
11725
+ scrollerHeight = this.computeScrollerHeight(totalHeight);
11726
+ this.scroller.setHeight(scrollerHeight);
11727
+ }
11728
+
11729
+ // guarantees the same scrollbar widths
11730
+ this.scroller.lockOverflow(scrollbarWidths);
11121
11731
  }
11122
11732
  },
11123
11733
 
11124
11734
 
11735
+ // given a desired total height of the view, returns what the height of the scroller should be
11736
+ computeScrollerHeight: function(totalHeight) {
11737
+ return totalHeight -
11738
+ subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
11739
+ },
11740
+
11741
+
11125
11742
  // Sets the height of just the DayGrid component in this view
11126
11743
  setGridHeight: function(height, isAuto) {
11127
11744
  if (isAuto) {
@@ -11133,6 +11750,20 @@ var BasicView = FC.BasicView = View.extend({
11133
11750
  },
11134
11751
 
11135
11752
 
11753
+ /* Scroll
11754
+ ------------------------------------------------------------------------------------------------------------------*/
11755
+
11756
+
11757
+ queryScroll: function() {
11758
+ return this.scroller.getScrollTop();
11759
+ },
11760
+
11761
+
11762
+ setScroll: function(top) {
11763
+ this.scroller.setScrollTop(top);
11764
+ },
11765
+
11766
+
11136
11767
  /* Hit Areas
11137
11768
  ------------------------------------------------------------------------------------------------------------------*/
11138
11769
  // forward all hit-related method calls to dayGrid
@@ -11368,6 +11999,8 @@ fcViews.month = {
11368
11999
 
11369
12000
  var AgendaView = FC.AgendaView = View.extend({
11370
12001
 
12002
+ scroller: null,
12003
+
11371
12004
  timeGridClass: TimeGrid, // class used to instantiate the timeGrid. subclasses can override
11372
12005
  timeGrid: null, // the main time-grid subcomponent of this view
11373
12006
 
@@ -11377,11 +12010,10 @@ var AgendaView = FC.AgendaView = View.extend({
11377
12010
  axisWidth: null, // the width of the time axis running down the side
11378
12011
 
11379
12012
  headContainerEl: null, // div that hold's the timeGrid's rendered date header
11380
- noScrollRowEls: null, // set of fake row elements that must compensate when scrollerEl has scrollbars
12013
+ noScrollRowEls: null, // set of fake row elements that must compensate when scroller has scrollbars
11381
12014
 
11382
12015
  // when the time-grid isn't tall enough to occupy the given height, we render an <hr> underneath
11383
12016
  bottomRuleEl: null,
11384
- bottomRuleHeight: null,
11385
12017
 
11386
12018
 
11387
12019
  initialize: function() {
@@ -11390,6 +12022,11 @@ var AgendaView = FC.AgendaView = View.extend({
11390
12022
  if (this.opt('allDaySlot')) { // should we display the "all-day" area?
11391
12023
  this.dayGrid = this.instantiateDayGrid(); // the all-day subcomponent of this view
11392
12024
  }
12025
+
12026
+ this.scroller = new Scroller({
12027
+ overflowX: 'hidden',
12028
+ overflowY: 'auto'
12029
+ });
11393
12030
  },
11394
12031
 
11395
12032
 
@@ -11430,10 +12067,12 @@ var AgendaView = FC.AgendaView = View.extend({
11430
12067
  this.el.addClass('fc-agenda-view').html(this.renderSkeletonHtml());
11431
12068
  this.renderHead();
11432
12069
 
11433
- // the element that wraps the time-grid that will probably scroll
11434
- this.scrollerEl = this.el.find('.fc-time-grid-container');
12070
+ this.scroller.render();
12071
+ var timeGridWrapEl = this.scroller.el.addClass('fc-time-grid-container');
12072
+ var timeGridEl = $('<div class="fc-time-grid" />').appendTo(timeGridWrapEl);
12073
+ this.el.find('.fc-body > tr > td').append(timeGridWrapEl);
11435
12074
 
11436
- this.timeGrid.setElement(this.el.find('.fc-time-grid'));
12075
+ this.timeGrid.setElement(timeGridEl);
11437
12076
  this.timeGrid.renderDates();
11438
12077
 
11439
12078
  // the <hr> that sometimes displays under the time-grid
@@ -11470,6 +12109,8 @@ var AgendaView = FC.AgendaView = View.extend({
11470
12109
  this.dayGrid.unrenderDates();
11471
12110
  this.dayGrid.removeElement();
11472
12111
  }
12112
+
12113
+ this.scroller.destroy();
11473
12114
  },
11474
12115
 
11475
12116
 
@@ -11491,9 +12132,6 @@ var AgendaView = FC.AgendaView = View.extend({
11491
12132
  '<hr class="fc-divider ' + this.widgetHeaderClass + '"/>' :
11492
12133
  ''
11493
12134
  ) +
11494
- '<div class="fc-time-grid-container">' +
11495
- '<div class="fc-time-grid"/>' +
11496
- '</div>' +
11497
12135
  '</td>' +
11498
12136
  '</tr>' +
11499
12137
  '</tbody>' +
@@ -11573,16 +12211,11 @@ var AgendaView = FC.AgendaView = View.extend({
11573
12211
  setHeight: function(totalHeight, isAuto) {
11574
12212
  var eventLimit;
11575
12213
  var scrollerHeight;
11576
-
11577
- if (this.bottomRuleHeight === null) {
11578
- // calculate the height of the rule the very first time
11579
- this.bottomRuleHeight = this.bottomRuleEl.outerHeight();
11580
- }
11581
- this.bottomRuleEl.hide(); // .show() will be called later if this <hr> is necessary
12214
+ var scrollbarWidths;
11582
12215
 
11583
12216
  // reset all dimensions back to the original state
11584
- this.scrollerEl.css('overflow', '');
11585
- unsetScroller(this.scrollerEl);
12217
+ this.bottomRuleEl.hide(); // .show() will be called later if this <hr> is necessary
12218
+ this.scroller.clear(); // sets height to 'auto' and clears overflow
11586
12219
  uncompensateScroll(this.noScrollRowEls);
11587
12220
 
11588
12221
  // limit number of events in the all-day area
@@ -11598,28 +12231,46 @@ var AgendaView = FC.AgendaView = View.extend({
11598
12231
  }
11599
12232
  }
11600
12233
 
11601
- if (!isAuto) { // should we force dimensions of the scroll container, or let the contents be natural height?
12234
+ if (!isAuto) { // should we force dimensions of the scroll container?
11602
12235
 
11603
12236
  scrollerHeight = this.computeScrollerHeight(totalHeight);
11604
- if (setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars?
12237
+ this.scroller.setHeight(scrollerHeight);
12238
+ scrollbarWidths = this.scroller.getScrollbarWidths();
12239
+
12240
+ if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars?
11605
12241
 
11606
12242
  // make the all-day and header rows lines up
11607
- compensateScroll(this.noScrollRowEls, getScrollbarWidths(this.scrollerEl));
12243
+ compensateScroll(this.noScrollRowEls, scrollbarWidths);
11608
12244
 
11609
12245
  // the scrollbar compensation might have changed text flow, which might affect height, so recalculate
11610
12246
  // and reapply the desired height to the scroller.
11611
12247
  scrollerHeight = this.computeScrollerHeight(totalHeight);
11612
- this.scrollerEl.height(scrollerHeight);
12248
+ this.scroller.setHeight(scrollerHeight);
11613
12249
  }
11614
- else { // no scrollbars
11615
- // still, force a height and display the bottom rule (marks the end of day)
11616
- this.scrollerEl.height(scrollerHeight).css('overflow', 'hidden'); // in case <hr> goes outside
12250
+
12251
+ // guarantees the same scrollbar widths
12252
+ this.scroller.lockOverflow(scrollbarWidths);
12253
+
12254
+ // if there's any space below the slats, show the horizontal rule.
12255
+ // this won't cause any new overflow, because lockOverflow already called.
12256
+ if (this.timeGrid.getTotalSlatHeight() < scrollerHeight) {
11617
12257
  this.bottomRuleEl.show();
11618
12258
  }
11619
12259
  }
11620
12260
  },
11621
12261
 
11622
12262
 
12263
+ // given a desired total height of the view, returns what the height of the scroller should be
12264
+ computeScrollerHeight: function(totalHeight) {
12265
+ return totalHeight -
12266
+ subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
12267
+ },
12268
+
12269
+
12270
+ /* Scroll
12271
+ ------------------------------------------------------------------------------------------------------------------*/
12272
+
12273
+
11623
12274
  // Computes the initial pre-configured scroll state prior to allowing the user to change it
11624
12275
  computeInitialScroll: function() {
11625
12276
  var scrollTime = moment.duration(this.opt('scrollTime'));
@@ -11636,6 +12287,16 @@ var AgendaView = FC.AgendaView = View.extend({
11636
12287
  },
11637
12288
 
11638
12289
 
12290
+ queryScroll: function() {
12291
+ return this.scroller.getScrollTop();
12292
+ },
12293
+
12294
+
12295
+ setScroll: function(top) {
12296
+ this.scroller.setScrollTop(top);
12297
+ },
12298
+
12299
+
11639
12300
  /* Hit Areas
11640
12301
  ------------------------------------------------------------------------------------------------------------------*/
11641
12302
  // forward all hit-related method calls to the grids (dayGrid might not be defined)