fullcalendar.io-rails 2.6.1 → 2.7.0

Sign up to get free protection for your applications and to get access to all the features.
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)