fullcalendar-rails 2.6.1.0 → 2.8.0.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/lib/fullcalendar-rails/version.rb +1 -1
  3. data/vendor/assets/javascripts/fullcalendar.js +1481 -522
  4. data/vendor/assets/javascripts/fullcalendar/gcal.js +4 -4
  5. data/vendor/assets/javascripts/fullcalendar/lang-all.js +4 -4
  6. data/vendor/assets/javascripts/fullcalendar/lang/ar-ma.js +1 -1
  7. data/vendor/assets/javascripts/fullcalendar/lang/ar-sa.js +1 -1
  8. data/vendor/assets/javascripts/fullcalendar/lang/ar-tn.js +1 -1
  9. data/vendor/assets/javascripts/fullcalendar/lang/ar.js +1 -1
  10. data/vendor/assets/javascripts/fullcalendar/lang/bg.js +0 -0
  11. data/vendor/assets/javascripts/fullcalendar/lang/ca.js +1 -1
  12. data/vendor/assets/javascripts/fullcalendar/lang/cs.js +0 -0
  13. data/vendor/assets/javascripts/fullcalendar/lang/da.js +0 -0
  14. data/vendor/assets/javascripts/fullcalendar/lang/de-at.js +1 -1
  15. data/vendor/assets/javascripts/fullcalendar/lang/de.js +1 -1
  16. data/vendor/assets/javascripts/fullcalendar/lang/el.js +0 -0
  17. data/vendor/assets/javascripts/fullcalendar/lang/en-au.js +1 -1
  18. data/vendor/assets/javascripts/fullcalendar/lang/en-ca.js +1 -1
  19. data/vendor/assets/javascripts/fullcalendar/lang/en-gb.js +1 -1
  20. data/vendor/assets/javascripts/fullcalendar/lang/en-ie.js +1 -1
  21. data/vendor/assets/javascripts/fullcalendar/lang/en-nz.js +1 -1
  22. data/vendor/assets/javascripts/fullcalendar/lang/es.js +1 -1
  23. data/vendor/assets/javascripts/fullcalendar/lang/eu.js +1 -0
  24. data/vendor/assets/javascripts/fullcalendar/lang/fa.js +1 -1
  25. data/vendor/assets/javascripts/fullcalendar/lang/fi.js +0 -0
  26. data/vendor/assets/javascripts/fullcalendar/lang/fr-ca.js +1 -1
  27. data/vendor/assets/javascripts/fullcalendar/lang/fr-ch.js +1 -1
  28. data/vendor/assets/javascripts/fullcalendar/lang/fr.js +1 -1
  29. data/vendor/assets/javascripts/fullcalendar/lang/gl.js +1 -0
  30. data/vendor/assets/javascripts/fullcalendar/lang/he.js +1 -1
  31. data/vendor/assets/javascripts/fullcalendar/lang/hi.js +1 -1
  32. data/vendor/assets/javascripts/fullcalendar/lang/hr.js +1 -1
  33. data/vendor/assets/javascripts/fullcalendar/lang/hu.js +0 -0
  34. data/vendor/assets/javascripts/fullcalendar/lang/id.js +0 -0
  35. data/vendor/assets/javascripts/fullcalendar/lang/is.js +1 -1
  36. data/vendor/assets/javascripts/fullcalendar/lang/it.js +0 -0
  37. data/vendor/assets/javascripts/fullcalendar/lang/ja.js +1 -1
  38. data/vendor/assets/javascripts/fullcalendar/lang/ko.js +1 -1
  39. data/vendor/assets/javascripts/fullcalendar/lang/lb.js +1 -0
  40. data/vendor/assets/javascripts/fullcalendar/lang/lt.js +1 -1
  41. data/vendor/assets/javascripts/fullcalendar/lang/lv.js +1 -1
  42. data/vendor/assets/javascripts/fullcalendar/lang/nb.js +1 -1
  43. data/vendor/assets/javascripts/fullcalendar/lang/nl.js +1 -1
  44. data/vendor/assets/javascripts/fullcalendar/lang/pl.js +0 -0
  45. data/vendor/assets/javascripts/fullcalendar/lang/pt-br.js +1 -1
  46. data/vendor/assets/javascripts/fullcalendar/lang/pt.js +1 -1
  47. data/vendor/assets/javascripts/fullcalendar/lang/ro.js +1 -1
  48. data/vendor/assets/javascripts/fullcalendar/lang/ru.js +1 -1
  49. data/vendor/assets/javascripts/fullcalendar/lang/sk.js +0 -0
  50. data/vendor/assets/javascripts/fullcalendar/lang/sl.js +1 -1
  51. data/vendor/assets/javascripts/fullcalendar/lang/sr-cyrl.js +1 -1
  52. data/vendor/assets/javascripts/fullcalendar/lang/sr.js +1 -1
  53. data/vendor/assets/javascripts/fullcalendar/lang/sv.js +1 -1
  54. data/vendor/assets/javascripts/fullcalendar/lang/th.js +1 -1
  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 +1 -1
  58. data/vendor/assets/javascripts/fullcalendar/lang/zh-cn.js +1 -1
  59. data/vendor/assets/javascripts/fullcalendar/lang/zh-tw.js +1 -1
  60. data/vendor/assets/stylesheets/fullcalendar.css +179 -42
  61. data/vendor/assets/stylesheets/fullcalendar.print.css +2 -2
  62. metadata +6 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c8b6f7ceba490bcaee40675f5231556b0b58eb58
4
- data.tar.gz: ce31a06b8322051b0927702b710d10988261e9a9
3
+ metadata.gz: bf346a46d7b099097273d5778484477c361d80d0
4
+ data.tar.gz: 1ef451e5d2a42a11c33174170e84394169ebf449
5
5
  SHA512:
6
- metadata.gz: 9a08956c7cbd276bf35e0ea4d098e6f85eb66e36b87469c3852f61fd4e75bda3f6f2be7d46d85dbf9164506078c9677cc6314518ddbe25a5a361b89af20d3bfc
7
- data.tar.gz: fb4f8fcce54c9388d955d7b1f6f2299a7a5821ba0df39b1a8c6be83b15884e3a1e85ea49f5726c043debe4deecc268c7d8cd95d1261e0f3509c94738b18806ef
6
+ metadata.gz: 073212fe38be4290162ce05f714f9154979eafc35227132a311a8be3d260a2eecbb5e48987ef9cf3649e878f2b7c8e0c980963945f309b5e8602f78911930182
7
+ data.tar.gz: fc867f9f0a959c44749a5d2d0401725e235b379206537fc2cd2927b78c6d0a020a81b8cf6960b8229cd2c832f9bc16b60e309e1c501b498decc965e48602895a
@@ -1,5 +1,5 @@
1
1
  module Fullcalendar
2
2
  module Rails
3
- VERSION = "2.6.1.0"
3
+ VERSION = "2.8.0.0"
4
4
  end
5
5
  end
@@ -1,7 +1,7 @@
1
1
  /*!
2
- * FullCalendar v2.6.1
2
+ * FullCalendar v2.8.0
3
3
  * Docs & License: http://fullcalendar.io/
4
- * (c) 2015 Adam Shaw
4
+ * (c) 2016 Adam Shaw
5
5
  */
6
6
 
7
7
  (function(factory) {
@@ -19,8 +19,8 @@
19
19
  ;;
20
20
 
21
21
  var FC = $.fullCalendar = {
22
- version: "2.6.1",
23
- internalApiVersion: 3
22
+ version: "2.8.0",
23
+ internalApiVersion: 4
24
24
  };
25
25
  var fcViews = FC.views = {};
26
26
 
@@ -262,29 +262,25 @@ function matchCellWidths(els) {
262
262
  }
263
263
 
264
264
 
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
- }
265
+ // Given one element that resides inside another,
266
+ // Subtracts the height of the inner element from the outer element.
267
+ function subtractInnerElHeight(outerEl, innerEl) {
268
+ var both = outerEl.add(innerEl);
269
+ var diff;
279
270
 
271
+ // effin' IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked
272
+ both.css({
273
+ position: 'relative', // cause a reflow, which will force fresh dimension recalculation
274
+ left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll
275
+ });
276
+ diff = outerEl.outerHeight() - innerEl.outerHeight(); // grab the dimensions
277
+ both.css({ position: '', left: '' }); // undo hack
280
278
 
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');
279
+ return diff;
284
280
  }
285
281
 
286
282
 
287
- /* General DOM Utilities
283
+ /* Element Geom Utilities
288
284
  ----------------------------------------------------------------------------------------------------------------------*/
289
285
 
290
286
  FC.getOuterRect = getOuterRect;
@@ -309,26 +305,30 @@ function getScrollParent(el) {
309
305
 
310
306
  // Queries the outer bounding area of a jQuery element.
311
307
  // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
312
- function getOuterRect(el) {
308
+ // Origin is optional.
309
+ function getOuterRect(el, origin) {
313
310
  var offset = el.offset();
311
+ var left = offset.left - (origin ? origin.left : 0);
312
+ var top = offset.top - (origin ? origin.top : 0);
314
313
 
315
314
  return {
316
- left: offset.left,
317
- right: offset.left + el.outerWidth(),
318
- top: offset.top,
319
- bottom: offset.top + el.outerHeight()
315
+ left: left,
316
+ right: left + el.outerWidth(),
317
+ top: top,
318
+ bottom: top + el.outerHeight()
320
319
  };
321
320
  }
322
321
 
323
322
 
324
323
  // Queries the area within the margin/border/scrollbars of a jQuery element. Does not go within the padding.
325
324
  // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
325
+ // Origin is optional.
326
326
  // NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
327
- function getClientRect(el) {
327
+ function getClientRect(el, origin) {
328
328
  var offset = el.offset();
329
329
  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;
330
+ var left = offset.left + getCssFloat(el, 'border-left-width') + scrollbarWidths.left - (origin ? origin.left : 0);
331
+ var top = offset.top + getCssFloat(el, 'border-top-width') + scrollbarWidths.top - (origin ? origin.top : 0);
332
332
 
333
333
  return {
334
334
  left: left,
@@ -341,10 +341,13 @@ function getClientRect(el) {
341
341
 
342
342
  // Queries the area within the margin/border/padding of a jQuery element. Assumed not to have scrollbars.
343
343
  // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
344
- function getContentRect(el) {
344
+ // Origin is optional.
345
+ function getContentRect(el, origin) {
345
346
  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');
347
+ var left = offset.left + getCssFloat(el, 'border-left-width') + getCssFloat(el, 'padding-left') -
348
+ (origin ? origin.left : 0);
349
+ var top = offset.top + getCssFloat(el, 'border-top-width') + getCssFloat(el, 'padding-top') -
350
+ (origin ? origin.top : 0);
348
351
 
349
352
  return {
350
353
  left: left,
@@ -414,13 +417,82 @@ function getCssFloat(el, prop) {
414
417
  }
415
418
 
416
419
 
420
+ /* Mouse / Touch Utilities
421
+ ----------------------------------------------------------------------------------------------------------------------*/
422
+
423
+ FC.preventDefault = preventDefault;
424
+
425
+
417
426
  // Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
418
427
  function isPrimaryMouseButton(ev) {
419
428
  return ev.which == 1 && !ev.ctrlKey;
420
429
  }
421
430
 
422
431
 
423
- /* Geometry
432
+ function getEvX(ev) {
433
+ if (ev.pageX !== undefined) {
434
+ return ev.pageX;
435
+ }
436
+ var touches = ev.originalEvent.touches;
437
+ if (touches) {
438
+ return touches[0].pageX;
439
+ }
440
+ }
441
+
442
+
443
+ function getEvY(ev) {
444
+ if (ev.pageY !== undefined) {
445
+ return ev.pageY;
446
+ }
447
+ var touches = ev.originalEvent.touches;
448
+ if (touches) {
449
+ return touches[0].pageY;
450
+ }
451
+ }
452
+
453
+
454
+ function getEvIsTouch(ev) {
455
+ return /^touch/.test(ev.type);
456
+ }
457
+
458
+
459
+ function preventSelection(el) {
460
+ el.addClass('fc-unselectable')
461
+ .on('selectstart', preventDefault);
462
+ }
463
+
464
+
465
+ // Stops a mouse/touch event from doing it's native browser action
466
+ function preventDefault(ev) {
467
+ ev.preventDefault();
468
+ }
469
+
470
+
471
+ // attach a handler to get called when ANY scroll action happens on the page.
472
+ // this was impossible to do with normal on/off because 'scroll' doesn't bubble.
473
+ // http://stackoverflow.com/a/32954565/96342
474
+ // returns `true` on success.
475
+ function bindAnyScroll(handler) {
476
+ if (window.addEventListener) {
477
+ window.addEventListener('scroll', handler, true); // useCapture=true
478
+ return true;
479
+ }
480
+ return false;
481
+ }
482
+
483
+
484
+ // undoes bindAnyScroll. must pass in the original function.
485
+ // returns `true` on success.
486
+ function unbindAnyScroll(handler) {
487
+ if (window.removeEventListener) {
488
+ window.removeEventListener('scroll', handler, true); // useCapture=true
489
+ return true;
490
+ }
491
+ return false;
492
+ }
493
+
494
+
495
+ /* General Geometry Utils
424
496
  ----------------------------------------------------------------------------------------------------------------------*/
425
497
 
426
498
  FC.intersectRects = intersectRects;
@@ -946,22 +1018,21 @@ function proxy(obj, methodName) {
946
1018
 
947
1019
  // Returns a function, that, as long as it continues to be invoked, will not
948
1020
  // be triggered. The function will be called after it stops being called for
949
- // N milliseconds.
1021
+ // N milliseconds. If `immediate` is passed, trigger the function on the
1022
+ // leading edge, instead of the trailing.
950
1023
  // 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
1024
+ function debounce(func, wait, immediate) {
1025
+ var timeout, args, context, timestamp, result;
1026
+
956
1027
  var later = function() {
957
1028
  var last = +new Date() - timestamp;
958
- if (last < wait && last > 0) {
959
- timeoutId = setTimeout(later, wait - last);
1029
+ if (last < wait) {
1030
+ timeout = setTimeout(later, wait - last);
960
1031
  }
961
1032
  else {
962
- timeoutId = null;
963
- func.apply(context, args);
964
- if (!timeoutId) {
1033
+ timeout = null;
1034
+ if (!immediate) {
1035
+ result = func.apply(context, args);
965
1036
  context = args = null;
966
1037
  }
967
1038
  }
@@ -971,12 +1042,32 @@ function debounce(func, wait) {
971
1042
  context = this;
972
1043
  args = arguments;
973
1044
  timestamp = +new Date();
974
- if (!timeoutId) {
975
- timeoutId = setTimeout(later, wait);
1045
+ var callNow = immediate && !timeout;
1046
+ if (!timeout) {
1047
+ timeout = setTimeout(later, wait);
1048
+ }
1049
+ if (callNow) {
1050
+ result = func.apply(context, args);
1051
+ context = args = null;
976
1052
  }
1053
+ return result;
977
1054
  };
978
1055
  }
979
1056
 
1057
+
1058
+ // HACK around jQuery's now A+ promises: execute callback synchronously if already resolved.
1059
+ // thenFunc shouldn't accept args.
1060
+ // similar to whenResources in Scheduler plugin.
1061
+ function syncThen(promise, thenFunc) {
1062
+ // not a promise, or an already-resolved promise?
1063
+ if (!promise || !promise.then || promise.state() === 'resolved') {
1064
+ return $.when(thenFunc()); // resolve immediately
1065
+ }
1066
+ else if (thenFunc) {
1067
+ return promise.then(thenFunc);
1068
+ }
1069
+ }
1070
+
980
1071
  ;;
981
1072
 
982
1073
  var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/;
@@ -1777,61 +1868,162 @@ function extendClass(superClass, members) {
1777
1868
 
1778
1869
 
1779
1870
  function mixIntoClass(theClass, members) {
1780
- copyOwnProps(members.prototype || members, theClass.prototype); // TODO: copyNativeMethods?
1871
+ copyOwnProps(members, theClass.prototype); // TODO: copyNativeMethods?
1781
1872
  }
1782
1873
  ;;
1783
1874
 
1784
- var Emitter = FC.Emitter = Class.extend({
1875
+ var EmitterMixin = FC.EmitterMixin = {
1876
+
1877
+ // jQuery-ification via $(this) allows a non-DOM object to have
1878
+ // the same event handling capabilities (including namespaces).
1785
1879
 
1786
- callbackHash: null,
1787
1880
 
1881
+ on: function(types, handler) {
1882
+
1883
+ // handlers are always called with an "event" object as their first param.
1884
+ // sneak the `this` context and arguments into the extra parameter object
1885
+ // and forward them on to the original handler.
1886
+ var intercept = function(ev, extra) {
1887
+ return handler.apply(
1888
+ extra.context || this,
1889
+ extra.args || []
1890
+ );
1891
+ };
1892
+
1893
+ // mimick jQuery's internal "proxy" system (risky, I know)
1894
+ // causing all functions with the same .guid to appear to be the same.
1895
+ // https://github.com/jquery/jquery/blob/2.2.4/src/core.js#L448
1896
+ // this is needed for calling .off with the original non-intercept handler.
1897
+ if (!handler.guid) {
1898
+ handler.guid = $.guid++;
1899
+ }
1900
+ intercept.guid = handler.guid;
1901
+
1902
+ $(this).on(types, intercept);
1788
1903
 
1789
- on: function(name, callback) {
1790
- this.getCallbacks(name).add(callback);
1791
1904
  return this; // for chaining
1792
1905
  },
1793
1906
 
1794
1907
 
1795
- off: function(name, callback) {
1796
- this.getCallbacks(name).remove(callback);
1908
+ off: function(types, handler) {
1909
+ $(this).off(types, handler);
1910
+
1797
1911
  return this; // for chaining
1798
1912
  },
1799
1913
 
1800
1914
 
1801
- trigger: function(name) { // args...
1802
- var args = Array.prototype.slice.call(arguments, 1);
1915
+ trigger: function(types) {
1916
+ var args = Array.prototype.slice.call(arguments, 1); // arguments after the first
1803
1917
 
1804
- this.triggerWith(name, this, args);
1918
+ // pass in "extra" info to the intercept
1919
+ $(this).triggerHandler(types, { args: args });
1805
1920
 
1806
1921
  return this; // for chaining
1807
1922
  },
1808
1923
 
1809
1924
 
1810
- triggerWith: function(name, context, args) {
1811
- var callbacks = this.getCallbacks(name);
1925
+ triggerWith: function(types, context, args) {
1812
1926
 
1813
- callbacks.fireWith(context, args);
1927
+ // `triggerHandler` is less reliant on the DOM compared to `trigger`.
1928
+ // pass in "extra" info to the intercept.
1929
+ $(this).triggerHandler(types, { context: context, args: args });
1814
1930
 
1815
1931
  return this; // for chaining
1816
- },
1932
+ }
1817
1933
 
1934
+ };
1818
1935
 
1819
- getCallbacks: function(name) {
1820
- var callbacks;
1936
+ ;;
1821
1937
 
1822
- if (!this.callbackHash) {
1823
- this.callbackHash = {};
1824
- }
1938
+ /*
1939
+ Utility methods for easily listening to events on another object,
1940
+ and more importantly, easily unlistening from them.
1941
+ */
1942
+ var ListenerMixin = FC.ListenerMixin = (function() {
1943
+ var guid = 0;
1944
+ var ListenerMixin = {
1945
+
1946
+ listenerId: null,
1947
+
1948
+ /*
1949
+ Given an `other` object that has on/off methods, bind the given `callback` to an event by the given name.
1950
+ The `callback` will be called with the `this` context of the object that .listenTo is being called on.
1951
+ Can be called:
1952
+ .listenTo(other, eventName, callback)
1953
+ OR
1954
+ .listenTo(other, {
1955
+ eventName1: callback1,
1956
+ eventName2: callback2
1957
+ })
1958
+ */
1959
+ listenTo: function(other, arg, callback) {
1960
+ if (typeof arg === 'object') { // given dictionary of callbacks
1961
+ for (var eventName in arg) {
1962
+ if (arg.hasOwnProperty(eventName)) {
1963
+ this.listenTo(other, eventName, arg[eventName]);
1964
+ }
1965
+ }
1966
+ }
1967
+ else if (typeof arg === 'string') {
1968
+ other.on(
1969
+ arg + '.' + this.getListenerNamespace(), // use event namespacing to identify this object
1970
+ $.proxy(callback, this) // always use `this` context
1971
+ // the usually-undesired jQuery guid behavior doesn't matter,
1972
+ // because we always unbind via namespace
1973
+ );
1974
+ }
1975
+ },
1825
1976
 
1826
- callbacks = this.callbackHash[name];
1827
- if (!callbacks) {
1828
- callbacks = this.callbackHash[name] = $.Callbacks();
1977
+ /*
1978
+ Causes the current object to stop listening to events on the `other` object.
1979
+ `eventName` is optional. If omitted, will stop listening to ALL events on `other`.
1980
+ */
1981
+ stopListeningTo: function(other, eventName) {
1982
+ other.off((eventName || '') + '.' + this.getListenerNamespace());
1983
+ },
1984
+
1985
+ /*
1986
+ Returns a string, unique to this object, to be used for event namespacing
1987
+ */
1988
+ getListenerNamespace: function() {
1989
+ if (this.listenerId == null) {
1990
+ this.listenerId = guid++;
1991
+ }
1992
+ return '_listener' + this.listenerId;
1829
1993
  }
1830
1994
 
1831
- return callbacks;
1995
+ };
1996
+ return ListenerMixin;
1997
+ })();
1998
+ ;;
1999
+
2000
+ // simple class for toggle a `isIgnoringMouse` flag on delay
2001
+ // initMouseIgnoring must first be called, with a millisecond delay setting.
2002
+ var MouseIgnorerMixin = {
2003
+
2004
+ isIgnoringMouse: false, // bool
2005
+ delayUnignoreMouse: null, // method
2006
+
2007
+
2008
+ initMouseIgnoring: function(delay) {
2009
+ this.delayUnignoreMouse = debounce(proxy(this, 'unignoreMouse'), delay || 1000);
2010
+ },
2011
+
2012
+
2013
+ // temporarily ignore mouse actions on segments
2014
+ tempIgnoreMouse: function() {
2015
+ this.isIgnoringMouse = true;
2016
+ this.delayUnignoreMouse();
2017
+ },
2018
+
2019
+
2020
+ // delayUnignoreMouse eventually calls this
2021
+ unignoreMouse: function() {
2022
+ this.isIgnoringMouse = false;
1832
2023
  }
1833
2024
 
1834
- });
2025
+ };
2026
+
1835
2027
  ;;
1836
2028
 
1837
2029
  /* A rectangular panel that is absolutely positioned over other content
@@ -1848,12 +2040,11 @@ Options:
1848
2040
  - hide (callback)
1849
2041
  */
1850
2042
 
1851
- var Popover = Class.extend({
2043
+ var Popover = Class.extend(ListenerMixin, {
1852
2044
 
1853
2045
  isHidden: true,
1854
2046
  options: null,
1855
2047
  el: null, // the container element for the popover. generated by this object
1856
- documentMousedownProxy: null, // document mousedown handler bound to `this`
1857
2048
  margin: 10, // the space required between the popover and the edges of the scroll container
1858
2049
 
1859
2050
 
@@ -1907,7 +2098,7 @@ var Popover = Class.extend({
1907
2098
  });
1908
2099
 
1909
2100
  if (options.autoHide) {
1910
- $(document).on('mousedown', this.documentMousedownProxy = proxy(this, 'documentMousedown'));
2101
+ this.listenTo($(document), 'mousedown', this.documentMousedown);
1911
2102
  }
1912
2103
  },
1913
2104
 
@@ -1930,7 +2121,7 @@ var Popover = Class.extend({
1930
2121
  this.el = null;
1931
2122
  }
1932
2123
 
1933
- $(document).off('mousedown', this.documentMousedownProxy);
2124
+ this.stopListeningTo($(document), 'mousedown');
1934
2125
  },
1935
2126
 
1936
2127
 
@@ -2243,257 +2434,421 @@ var CoordCache = FC.CoordCache = Class.extend({
2243
2434
  ----------------------------------------------------------------------------------------------------------------------*/
2244
2435
  // TODO: use Emitter
2245
2436
 
2246
- var DragListener = FC.DragListener = Class.extend({
2437
+ var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMixin, {
2247
2438
 
2248
2439
  options: null,
2249
2440
 
2250
- isListening: false,
2251
- isDragging: false,
2441
+ // for IE8 bug-fighting behavior
2442
+ subjectEl: null,
2443
+ subjectHref: null,
2252
2444
 
2253
2445
  // coordinates of the initial mousedown
2254
2446
  originX: null,
2255
2447
  originY: null,
2256
2448
 
2257
- // handler attached to the document, bound to the DragListener's `this`
2258
- mousemoveProxy: null,
2259
- mouseupProxy: null,
2449
+ // the wrapping element that scrolls, or MIGHT scroll if there's overflow.
2450
+ // TODO: do this for wrappers that have overflow:hidden as well.
2451
+ scrollEl: null,
2260
2452
 
2261
- // for IE8 bug-fighting behavior, for now
2262
- subjectEl: null, // the element being draged. optional
2263
- subjectHref: null,
2453
+ isInteracting: false,
2454
+ isDistanceSurpassed: false,
2455
+ isDelayEnded: false,
2456
+ isDragging: false,
2457
+ isTouch: false,
2264
2458
 
2265
- 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
2459
+ delay: null,
2460
+ delayTimeoutId: null,
2461
+ minDistance: null,
2271
2462
 
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
2463
+ handleTouchScrollProxy: null, // calls handleTouchScroll, always bound to `this`
2275
2464
 
2276
2465
 
2277
2466
  constructor: function(options) {
2278
- options = options || {};
2279
- this.options = options;
2280
- this.subjectEl = options.subjectEl;
2467
+ this.options = options || {};
2468
+ this.handleTouchScrollProxy = proxy(this, 'handleTouchScroll');
2469
+ this.initMouseIgnoring(500);
2281
2470
  },
2282
2471
 
2283
2472
 
2284
- // Call this when the user does a mousedown. Will probably lead to startListening
2285
- mousedown: function(ev) {
2286
- if (isPrimaryMouseButton(ev)) {
2473
+ // Interaction (high-level)
2474
+ // -----------------------------------------------------------------------------------------------------------------
2475
+
2476
+
2477
+ startInteraction: function(ev, extraOptions) {
2478
+ var isTouch = getEvIsTouch(ev);
2479
+
2480
+ if (ev.type === 'mousedown') {
2481
+ if (this.isIgnoringMouse) {
2482
+ return;
2483
+ }
2484
+ else if (!isPrimaryMouseButton(ev)) {
2485
+ return;
2486
+ }
2487
+ else {
2488
+ ev.preventDefault(); // prevents native selection in most browsers
2489
+ }
2490
+ }
2287
2491
 
2288
- ev.preventDefault(); // prevents native selection in most browsers
2492
+ if (!this.isInteracting) {
2289
2493
 
2290
- this.startListening(ev);
2494
+ // process options
2495
+ extraOptions = extraOptions || {};
2496
+ this.delay = firstDefined(extraOptions.delay, this.options.delay, 0);
2497
+ this.minDistance = firstDefined(extraOptions.distance, this.options.distance, 0);
2498
+ this.subjectEl = this.options.subjectEl;
2291
2499
 
2292
- // start the drag immediately if there is no minimum distance for a drag start
2293
- if (!this.options.distance) {
2294
- this.startDrag(ev);
2500
+ this.isInteracting = true;
2501
+ this.isTouch = isTouch;
2502
+ this.isDelayEnded = false;
2503
+ this.isDistanceSurpassed = false;
2504
+
2505
+ this.originX = getEvX(ev);
2506
+ this.originY = getEvY(ev);
2507
+ this.scrollEl = getScrollParent($(ev.target));
2508
+
2509
+ this.bindHandlers();
2510
+ this.initAutoScroll();
2511
+ this.handleInteractionStart(ev);
2512
+ this.startDelay(ev);
2513
+
2514
+ if (!this.minDistance) {
2515
+ this.handleDistanceSurpassed(ev);
2295
2516
  }
2296
2517
  }
2297
2518
  },
2298
2519
 
2299
2520
 
2300
- // Call this to start tracking mouse movements
2301
- startListening: function(ev) {
2302
- var scrollParent;
2521
+ handleInteractionStart: function(ev) {
2522
+ this.trigger('interactionStart', ev);
2523
+ },
2303
2524
 
2304
- if (!this.isListening) {
2305
2525
 
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;
2526
+ endInteraction: function(ev, isCancelled) {
2527
+ if (this.isInteracting) {
2528
+ this.endDrag(ev);
2311
2529
 
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
- }
2530
+ if (this.delayTimeoutId) {
2531
+ clearTimeout(this.delayTimeoutId);
2532
+ this.delayTimeoutId = null;
2316
2533
  }
2317
2534
 
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
2535
+ this.destroyAutoScroll();
2536
+ this.unbindHandlers();
2537
+
2538
+ this.isInteracting = false;
2539
+ this.handleInteractionEnd(ev, isCancelled);
2322
2540
 
2323
- if (ev) {
2324
- this.originX = ev.pageX;
2325
- this.originY = ev.pageY;
2541
+ // a touchstart+touchend on the same element will result in the following addition simulated events:
2542
+ // mouseover + mouseout + click
2543
+ // let's ignore these bogus events
2544
+ if (this.isTouch) {
2545
+ this.tempIgnoreMouse();
2326
2546
  }
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;
2547
+ }
2548
+ },
2549
+
2550
+
2551
+ handleInteractionEnd: function(ev, isCancelled) {
2552
+ this.trigger('interactionEnd', ev, isCancelled || false);
2553
+ },
2554
+
2555
+
2556
+ // Binding To DOM
2557
+ // -----------------------------------------------------------------------------------------------------------------
2558
+
2559
+
2560
+ bindHandlers: function() {
2561
+ var _this = this;
2562
+ var touchStartIgnores = 1;
2563
+
2564
+ if (this.isTouch) {
2565
+ this.listenTo($(document), {
2566
+ touchmove: this.handleTouchMove,
2567
+ touchend: this.endInteraction,
2568
+ touchcancel: this.endInteraction,
2569
+
2570
+ // Sometimes touchend doesn't fire
2571
+ // (can't figure out why. touchcancel doesn't fire either. has to do with scrolling?)
2572
+ // If another touchstart happens, we know it's bogus, so cancel the drag.
2573
+ // touchend will continue to be broken until user does a shorttap/scroll, but this is best we can do.
2574
+ touchstart: function(ev) {
2575
+ if (touchStartIgnores) { // bindHandlers is called from within a touchstart,
2576
+ touchStartIgnores--; // and we don't want this to fire immediately, so ignore.
2577
+ }
2578
+ else {
2579
+ _this.endInteraction(ev, true); // isCancelled=true
2580
+ }
2581
+ }
2582
+ });
2583
+
2584
+ // listen to ALL scroll actions on the page
2585
+ if (
2586
+ !bindAnyScroll(this.handleTouchScrollProxy) && // hopefully this works and short-circuits the rest
2587
+ this.scrollEl // otherwise, attach a single handler to this
2588
+ ) {
2589
+ this.listenTo(this.scrollEl, 'scroll', this.handleTouchScroll);
2332
2590
  }
2591
+ }
2592
+ else {
2593
+ this.listenTo($(document), {
2594
+ mousemove: this.handleMouseMove,
2595
+ mouseup: this.endInteraction
2596
+ });
2597
+ }
2598
+
2599
+ this.listenTo($(document), {
2600
+ selectstart: preventDefault, // don't allow selection while dragging
2601
+ contextmenu: preventDefault // long taps would open menu on Chrome dev tools
2602
+ });
2603
+ },
2604
+
2605
+
2606
+ unbindHandlers: function() {
2607
+ this.stopListeningTo($(document));
2608
+
2609
+ // unbind scroll listening
2610
+ unbindAnyScroll(this.handleTouchScrollProxy);
2611
+ if (this.scrollEl) {
2612
+ this.stopListeningTo(this.scrollEl, 'scroll');
2613
+ }
2614
+ },
2615
+
2333
2616
 
2334
- this.isListening = true;
2335
- this.listenStart(ev);
2617
+ // Drag (high-level)
2618
+ // -----------------------------------------------------------------------------------------------------------------
2619
+
2620
+
2621
+ // extraOptions ignored if drag already started
2622
+ startDrag: function(ev, extraOptions) {
2623
+ this.startInteraction(ev, extraOptions); // ensure interaction began
2624
+
2625
+ if (!this.isDragging) {
2626
+ this.isDragging = true;
2627
+ this.handleDragStart(ev);
2336
2628
  }
2337
2629
  },
2338
2630
 
2339
2631
 
2340
- // Called when drag listening has started (but a real drag has not necessarily began)
2341
- listenStart: function(ev) {
2342
- this.trigger('listenStart', ev);
2632
+ handleDragStart: function(ev) {
2633
+ this.trigger('dragStart', ev);
2634
+ this.initHrefHack();
2343
2635
  },
2344
2636
 
2345
2637
 
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;
2638
+ handleMove: function(ev) {
2639
+ var dx = getEvX(ev) - this.originX;
2640
+ var dy = getEvY(ev) - this.originY;
2641
+ var minDistance = this.minDistance;
2351
2642
  var distanceSq; // current distance from the origin, squared
2352
2643
 
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;
2644
+ if (!this.isDistanceSurpassed) {
2356
2645
  distanceSq = dx * dx + dy * dy;
2357
2646
  if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem
2358
- this.startDrag(ev);
2647
+ this.handleDistanceSurpassed(ev);
2359
2648
  }
2360
2649
  }
2361
2650
 
2362
2651
  if (this.isDragging) {
2363
- this.drag(dx, dy, ev); // report a drag, even if this mousemove initiated the drag
2652
+ this.handleDrag(dx, dy, ev);
2364
2653
  }
2365
2654
  },
2366
2655
 
2367
2656
 
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) {
2657
+ // Called while the mouse is being moved and when we know a legitimate drag is taking place
2658
+ handleDrag: function(dx, dy, ev) {
2659
+ this.trigger('drag', dx, dy, ev);
2660
+ this.updateAutoScroll(ev); // will possibly cause scrolling
2661
+ },
2371
2662
 
2372
- if (!this.isListening) { // startDrag must have manually initiated
2373
- this.startListening();
2374
- }
2375
2663
 
2376
- if (!this.isDragging) {
2377
- this.isDragging = true;
2378
- this.dragStart(ev);
2664
+ endDrag: function(ev) {
2665
+ if (this.isDragging) {
2666
+ this.isDragging = false;
2667
+ this.handleDragEnd(ev);
2379
2668
  }
2380
2669
  },
2381
2670
 
2382
2671
 
2383
- // Called when the actual drag has started (went beyond minDistance)
2384
- dragStart: function(ev) {
2385
- var subjectEl = this.subjectEl;
2672
+ handleDragEnd: function(ev) {
2673
+ this.trigger('dragEnd', ev);
2674
+ this.destroyHrefHack();
2675
+ },
2386
2676
 
2387
- this.trigger('dragStart', ev);
2388
2677
 
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');
2678
+ // Delay
2679
+ // -----------------------------------------------------------------------------------------------------------------
2680
+
2681
+
2682
+ startDelay: function(initialEv) {
2683
+ var _this = this;
2684
+
2685
+ if (this.delay) {
2686
+ this.delayTimeoutId = setTimeout(function() {
2687
+ _this.handleDelayEnd(initialEv);
2688
+ }, this.delay);
2689
+ }
2690
+ else {
2691
+ this.handleDelayEnd(initialEv);
2392
2692
  }
2393
2693
  },
2394
2694
 
2395
2695
 
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
2696
+ handleDelayEnd: function(initialEv) {
2697
+ this.isDelayEnded = true;
2698
+
2699
+ if (this.isDistanceSurpassed) {
2700
+ this.startDrag(initialEv);
2701
+ }
2400
2702
  },
2401
2703
 
2402
2704
 
2403
- // Called when the user does a mouseup
2404
- mouseup: function(ev) {
2405
- this.stopListening(ev);
2705
+ // Distance
2706
+ // -----------------------------------------------------------------------------------------------------------------
2707
+
2708
+
2709
+ handleDistanceSurpassed: function(ev) {
2710
+ this.isDistanceSurpassed = true;
2711
+
2712
+ if (this.isDelayEnded) {
2713
+ this.startDrag(ev);
2714
+ }
2406
2715
  },
2407
2716
 
2408
2717
 
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) {
2718
+ // Mouse / Touch
2719
+ // -----------------------------------------------------------------------------------------------------------------
2720
+
2721
+
2722
+ handleTouchMove: function(ev) {
2723
+ // prevent inertia and touchmove-scrolling while dragging
2412
2724
  if (this.isDragging) {
2413
- this.stopScrolling();
2414
- this.dragStop(ev);
2415
- this.isDragging = false;
2725
+ ev.preventDefault();
2416
2726
  }
2727
+
2728
+ this.handleMove(ev);
2417
2729
  },
2418
2730
 
2419
2731
 
2420
- // Called when dragging has been stopped
2421
- dragStop: function(ev) {
2422
- var _this = this;
2732
+ handleMouseMove: function(ev) {
2733
+ this.handleMove(ev);
2734
+ },
2423
2735
 
2424
- this.trigger('dragStop', ev);
2425
2736
 
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
- },
2737
+ // Scrolling (unrelated to auto-scroll)
2738
+ // -----------------------------------------------------------------------------------------------------------------
2433
2739
 
2434
2740
 
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
2741
+ handleTouchScroll: function(ev) {
2742
+ // if the drag is being initiated by touch, but a scroll happens before
2743
+ // the drag-initiating delay is over, cancel the drag
2744
+ if (!this.isDragging) {
2745
+ this.endInteraction(ev, true); // isCancelled=true
2746
+ }
2747
+ },
2438
2748
 
2439
- if (this.isListening) {
2440
2749
 
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
- }
2750
+ // <A> HREF Hack
2751
+ // -----------------------------------------------------------------------------------------------------------------
2446
2752
 
2447
- $(document)
2448
- .off('mousemove', this.mousemoveProxy)
2449
- .off('mouseup', this.mouseupProxy)
2450
- .off('selectstart', this.preventDefault);
2451
2753
 
2452
- this.mousemoveProxy = null;
2453
- this.mouseupProxy = null;
2754
+ initHrefHack: function() {
2755
+ var subjectEl = this.subjectEl;
2454
2756
 
2455
- this.isListening = false;
2456
- this.listenStop(ev);
2757
+ // remove a mousedown'd <a>'s href so it is not visited (IE8 bug)
2758
+ if ((this.subjectHref = subjectEl ? subjectEl.attr('href') : null)) {
2759
+ subjectEl.removeAttr('href');
2457
2760
  }
2458
2761
  },
2459
2762
 
2460
2763
 
2461
- // Called when drag listening has stopped
2462
- listenStop: function(ev) {
2463
- this.trigger('listenStop', ev);
2764
+ destroyHrefHack: function() {
2765
+ var subjectEl = this.subjectEl;
2766
+ var subjectHref = this.subjectHref;
2767
+
2768
+ // restore a mousedown'd <a>'s href (for IE8 bug)
2769
+ setTimeout(function() { // must be outside of the click's execution
2770
+ if (subjectHref) {
2771
+ subjectEl.attr('href', subjectHref);
2772
+ }
2773
+ }, 0);
2464
2774
  },
2465
2775
 
2466
2776
 
2777
+ // Utils
2778
+ // -----------------------------------------------------------------------------------------------------------------
2779
+
2780
+
2467
2781
  // Triggers a callback. Calls a function in the option hash of the same name.
2468
2782
  // Arguments beyond the first `name` are forwarded on.
2469
2783
  trigger: function(name) {
2470
2784
  if (this.options[name]) {
2471
2785
  this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
2472
2786
  }
2473
- },
2787
+ // makes _methods callable by event name. TODO: kill this
2788
+ if (this['_' + name]) {
2789
+ this['_' + name].apply(this, Array.prototype.slice.call(arguments, 1));
2790
+ }
2791
+ }
2792
+
2793
+
2794
+ });
2795
+
2796
+ ;;
2797
+ /*
2798
+ this.scrollEl is set in DragListener
2799
+ */
2800
+ DragListener.mixin({
2801
+
2802
+ isAutoScroll: false,
2803
+
2804
+ scrollBounds: null, // { top, bottom, left, right }
2805
+ scrollTopVel: null, // pixels per second
2806
+ scrollLeftVel: null, // pixels per second
2807
+ scrollIntervalId: null, // ID of setTimeout for scrolling animation loop
2808
+
2809
+ // defaults
2810
+ scrollSensitivity: 30, // pixels from edge for scrolling to start
2811
+ scrollSpeed: 200, // pixels per second, at maximum speed
2812
+ scrollIntervalMs: 50, // millisecond wait between scroll increment
2813
+
2814
+
2815
+ initAutoScroll: function() {
2816
+ var scrollEl = this.scrollEl;
2474
2817
 
2818
+ this.isAutoScroll =
2819
+ this.options.scroll &&
2820
+ scrollEl &&
2821
+ !scrollEl.is(window) &&
2822
+ !scrollEl.is(document);
2475
2823
 
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();
2824
+ if (this.isAutoScroll) {
2825
+ // debounce makes sure rapid calls don't happen
2826
+ this.listenTo(scrollEl, 'scroll', debounce(this.handleDebouncedScroll, 100));
2827
+ }
2479
2828
  },
2480
2829
 
2481
2830
 
2482
- /* Scrolling
2483
- ------------------------------------------------------------------------------------------------------------------*/
2831
+ destroyAutoScroll: function() {
2832
+ this.endAutoScroll(); // kill any animation loop
2833
+
2834
+ // remove the scroll handler if there is a scrollEl
2835
+ if (this.isAutoScroll) {
2836
+ this.stopListeningTo(this.scrollEl, 'scroll'); // will probably get removed by unbindHandlers too :(
2837
+ }
2838
+ },
2484
2839
 
2485
2840
 
2486
2841
  // Computes and stores the bounding rectangle of scrollEl
2487
2842
  computeScrollBounds: function() {
2488
- var el = this.scrollEl;
2489
-
2490
- this.scrollBounds = el ? getOuterRect(el) : null;
2843
+ if (this.isAutoScroll) {
2844
+ this.scrollBounds = getOuterRect(this.scrollEl);
2491
2845
  // TODO: use getClientRect in future. but prevents auto scrolling when on top of scrollbars
2846
+ }
2492
2847
  },
2493
2848
 
2494
2849
 
2495
2850
  // Called when the dragging is in progress and scrolling should be updated
2496
- updateScroll: function(ev) {
2851
+ updateAutoScroll: function(ev) {
2497
2852
  var sensitivity = this.scrollSensitivity;
2498
2853
  var bounds = this.scrollBounds;
2499
2854
  var topCloseness, bottomCloseness;
@@ -2504,10 +2859,10 @@ var DragListener = FC.DragListener = Class.extend({
2504
2859
  if (bounds) { // only scroll if scrollEl exists
2505
2860
 
2506
2861
  // 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;
2862
+ topCloseness = (sensitivity - (getEvY(ev) - bounds.top)) / sensitivity;
2863
+ bottomCloseness = (sensitivity - (bounds.bottom - getEvY(ev))) / sensitivity;
2864
+ leftCloseness = (sensitivity - (getEvX(ev) - bounds.left)) / sensitivity;
2865
+ rightCloseness = (sensitivity - (bounds.right - getEvX(ev))) / sensitivity;
2511
2866
 
2512
2867
  // translate vertical closeness into velocity.
2513
2868
  // mouse must be completely in bounds for velocity to happen.
@@ -2594,38 +2949,36 @@ var DragListener = FC.DragListener = Class.extend({
2594
2949
 
2595
2950
  // if scrolled all the way, which causes the vels to be zero, stop the animation loop
2596
2951
  if (!this.scrollTopVel && !this.scrollLeftVel) {
2597
- this.stopScrolling();
2952
+ this.endAutoScroll();
2598
2953
  }
2599
2954
  },
2600
2955
 
2601
2956
 
2602
2957
  // Kills any existing scrolling animation loop
2603
- stopScrolling: function() {
2958
+ endAutoScroll: function() {
2604
2959
  if (this.scrollIntervalId) {
2605
2960
  clearInterval(this.scrollIntervalId);
2606
2961
  this.scrollIntervalId = null;
2607
2962
 
2608
- // when all done with scrolling, recompute positions since they probably changed
2609
- this.scrollStop();
2963
+ this.handleScrollEnd();
2610
2964
  }
2611
2965
  },
2612
2966
 
2613
2967
 
2614
2968
  // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce)
2615
- scrollHandler: function() {
2969
+ handleDebouncedScroll: function() {
2616
2970
  // recompute all coordinates, but *only* if this is *not* part of our scrolling animation
2617
2971
  if (!this.scrollIntervalId) {
2618
- this.scrollStop();
2972
+ this.handleScrollEnd();
2619
2973
  }
2620
2974
  },
2621
2975
 
2622
2976
 
2623
2977
  // Called when scrolling has stopped, whether through auto scroll, or the user scrolling
2624
- scrollStop: function() {
2978
+ handleScrollEnd: function() {
2625
2979
  }
2626
2980
 
2627
2981
  });
2628
-
2629
2982
  ;;
2630
2983
 
2631
2984
  /* Tracks mouse movements over a component and raises events about which hit the mouse is over.
@@ -2654,18 +3007,16 @@ var HitDragListener = DragListener.extend({
2654
3007
 
2655
3008
  // Called when drag listening starts (but a real drag has not necessarily began).
2656
3009
  // ev might be undefined if dragging was started manually.
2657
- listenStart: function(ev) {
3010
+ handleInteractionStart: function(ev) {
2658
3011
  var subjectEl = this.subjectEl;
2659
3012
  var subjectRect;
2660
3013
  var origPoint;
2661
3014
  var point;
2662
3015
 
2663
- DragListener.prototype.listenStart.apply(this, arguments); // call the super-method
2664
-
2665
3016
  this.computeCoords();
2666
3017
 
2667
3018
  if (ev) {
2668
- origPoint = { left: ev.pageX, top: ev.pageY };
3019
+ origPoint = { left: getEvX(ev), top: getEvY(ev) };
2669
3020
  point = origPoint;
2670
3021
 
2671
3022
  // constrain the point to bounds of the element being dragged
@@ -2695,61 +3046,64 @@ var HitDragListener = DragListener.extend({
2695
3046
  this.origHit = null;
2696
3047
  this.coordAdjust = null;
2697
3048
  }
3049
+
3050
+ // call the super-method. do it after origHit has been computed
3051
+ DragListener.prototype.handleInteractionStart.apply(this, arguments);
2698
3052
  },
2699
3053
 
2700
3054
 
2701
3055
  // Recomputes the drag-critical positions of elements
2702
3056
  computeCoords: function() {
2703
3057
  this.component.prepareHits();
2704
- this.computeScrollBounds(); // why is this here???
3058
+ this.computeScrollBounds(); // why is this here??????
2705
3059
  },
2706
3060
 
2707
3061
 
2708
3062
  // Called when the actual drag has started
2709
- dragStart: function(ev) {
3063
+ handleDragStart: function(ev) {
2710
3064
  var hit;
2711
3065
 
2712
- DragListener.prototype.dragStart.apply(this, arguments); // call the super-method
3066
+ DragListener.prototype.handleDragStart.apply(this, arguments); // call the super-method
2713
3067
 
2714
3068
  // might be different from this.origHit if the min-distance is large
2715
- hit = this.queryHit(ev.pageX, ev.pageY);
3069
+ hit = this.queryHit(getEvX(ev), getEvY(ev));
2716
3070
 
2717
3071
  // report the initial hit the mouse is over
2718
3072
  // especially important if no min-distance and drag starts immediately
2719
3073
  if (hit) {
2720
- this.hitOver(hit);
3074
+ this.handleHitOver(hit);
2721
3075
  }
2722
3076
  },
2723
3077
 
2724
3078
 
2725
3079
  // Called when the drag moves
2726
- drag: function(dx, dy, ev) {
3080
+ handleDrag: function(dx, dy, ev) {
2727
3081
  var hit;
2728
3082
 
2729
- DragListener.prototype.drag.apply(this, arguments); // call the super-method
3083
+ DragListener.prototype.handleDrag.apply(this, arguments); // call the super-method
2730
3084
 
2731
- hit = this.queryHit(ev.pageX, ev.pageY);
3085
+ hit = this.queryHit(getEvX(ev), getEvY(ev));
2732
3086
 
2733
3087
  if (!isHitsEqual(hit, this.hit)) { // a different hit than before?
2734
3088
  if (this.hit) {
2735
- this.hitOut();
3089
+ this.handleHitOut();
2736
3090
  }
2737
3091
  if (hit) {
2738
- this.hitOver(hit);
3092
+ this.handleHitOver(hit);
2739
3093
  }
2740
3094
  }
2741
3095
  },
2742
3096
 
2743
3097
 
2744
3098
  // Called when dragging has been stopped
2745
- dragStop: function() {
2746
- this.hitDone();
2747
- DragListener.prototype.dragStop.apply(this, arguments); // call the super-method
3099
+ handleDragEnd: function() {
3100
+ this.handleHitDone();
3101
+ DragListener.prototype.handleDragEnd.apply(this, arguments); // call the super-method
2748
3102
  },
2749
3103
 
2750
3104
 
2751
3105
  // Called when a the mouse has just moved over a new hit
2752
- hitOver: function(hit) {
3106
+ handleHitOver: function(hit) {
2753
3107
  var isOrig = isHitsEqual(hit, this.origHit);
2754
3108
 
2755
3109
  this.hit = hit;
@@ -2759,26 +3113,26 @@ var HitDragListener = DragListener.extend({
2759
3113
 
2760
3114
 
2761
3115
  // Called when the mouse has just moved out of a hit
2762
- hitOut: function() {
3116
+ handleHitOut: function() {
2763
3117
  if (this.hit) {
2764
3118
  this.trigger('hitOut', this.hit);
2765
- this.hitDone();
3119
+ this.handleHitDone();
2766
3120
  this.hit = null;
2767
3121
  }
2768
3122
  },
2769
3123
 
2770
3124
 
2771
3125
  // Called after a hitOut. Also called before a dragStop
2772
- hitDone: function() {
3126
+ handleHitDone: function() {
2773
3127
  if (this.hit) {
2774
3128
  this.trigger('hitDone', this.hit);
2775
3129
  }
2776
3130
  },
2777
3131
 
2778
3132
 
2779
- // Called when drag listening has stopped
2780
- listenStop: function() {
2781
- DragListener.prototype.listenStop.apply(this, arguments); // call the super-method
3133
+ // Called when the interaction ends, whether there was a real drag or not
3134
+ handleInteractionEnd: function() {
3135
+ DragListener.prototype.handleInteractionEnd.apply(this, arguments); // call the super-method
2782
3136
 
2783
3137
  this.origHit = null;
2784
3138
  this.hit = null;
@@ -2788,8 +3142,8 @@ var HitDragListener = DragListener.extend({
2788
3142
 
2789
3143
 
2790
3144
  // 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
3145
+ handleScrollEnd: function() {
3146
+ DragListener.prototype.handleScrollEnd.apply(this, arguments); // call the super-method
2793
3147
 
2794
3148
  this.computeCoords(); // hits' absolute positions will be in new places. recompute
2795
3149
  },
@@ -2844,7 +3198,7 @@ function isHitPropsWithin(subHit, superHit) {
2844
3198
  /* Creates a clone of an element and lets it track the mouse as it moves
2845
3199
  ----------------------------------------------------------------------------------------------------------------------*/
2846
3200
 
2847
- var MouseFollower = Class.extend({
3201
+ var MouseFollower = Class.extend(ListenerMixin, {
2848
3202
 
2849
3203
  options: null,
2850
3204
 
@@ -2856,16 +3210,14 @@ var MouseFollower = Class.extend({
2856
3210
  top0: null,
2857
3211
  left0: null,
2858
3212
 
2859
- // the initial position of the mouse
2860
- mouseY0: null,
2861
- mouseX0: null,
3213
+ // the absolute coordinates of the initiating touch/mouse action
3214
+ y0: null,
3215
+ x0: null,
2862
3216
 
2863
3217
  // the number of pixels the mouse has moved from its initial position
2864
3218
  topDelta: null,
2865
3219
  leftDelta: null,
2866
3220
 
2867
- mousemoveProxy: null, // document mousemove handler, bound to the MouseFollower's `this`
2868
-
2869
3221
  isFollowing: false,
2870
3222
  isHidden: false,
2871
3223
  isAnimating: false, // doing the revert animation?
@@ -2882,8 +3234,8 @@ var MouseFollower = Class.extend({
2882
3234
  if (!this.isFollowing) {
2883
3235
  this.isFollowing = true;
2884
3236
 
2885
- this.mouseY0 = ev.pageY;
2886
- this.mouseX0 = ev.pageX;
3237
+ this.y0 = getEvY(ev);
3238
+ this.x0 = getEvX(ev);
2887
3239
  this.topDelta = 0;
2888
3240
  this.leftDelta = 0;
2889
3241
 
@@ -2891,7 +3243,12 @@ var MouseFollower = Class.extend({
2891
3243
  this.updatePosition();
2892
3244
  }
2893
3245
 
2894
- $(document).on('mousemove', this.mousemoveProxy = proxy(this, 'mousemove'));
3246
+ if (getEvIsTouch(ev)) {
3247
+ this.listenTo($(document), 'touchmove', this.handleMove);
3248
+ }
3249
+ else {
3250
+ this.listenTo($(document), 'mousemove', this.handleMove);
3251
+ }
2895
3252
  }
2896
3253
  },
2897
3254
 
@@ -2916,7 +3273,7 @@ var MouseFollower = Class.extend({
2916
3273
  if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time
2917
3274
  this.isFollowing = false;
2918
3275
 
2919
- $(document).off('mousemove', this.mousemoveProxy);
3276
+ this.stopListeningTo($(document));
2920
3277
 
2921
3278
  if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation?
2922
3279
  this.isAnimating = true;
@@ -2942,6 +3299,7 @@ var MouseFollower = Class.extend({
2942
3299
  if (!el) {
2943
3300
  this.sourceEl.width(); // hack to force IE8 to compute correct bounding box
2944
3301
  el = this.el = this.sourceEl.clone()
3302
+ .addClass(this.options.additionalClass || '')
2945
3303
  .css({
2946
3304
  position: 'absolute',
2947
3305
  visibility: '', // in case original element was hidden (commonly through hideEvents())
@@ -2953,8 +3311,13 @@ var MouseFollower = Class.extend({
2953
3311
  height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value
2954
3312
  opacity: this.options.opacity || '',
2955
3313
  zIndex: this.options.zIndex
2956
- })
2957
- .appendTo(this.parentEl);
3314
+ });
3315
+
3316
+ // we don't want long taps or any mouse interaction causing selection/menus.
3317
+ // would use preventSelection(), but that prevents selectstart, causing problems.
3318
+ el.addClass('fc-unselectable');
3319
+
3320
+ el.appendTo(this.parentEl);
2958
3321
  }
2959
3322
 
2960
3323
  return el;
@@ -2994,9 +3357,9 @@ var MouseFollower = Class.extend({
2994
3357
 
2995
3358
 
2996
3359
  // 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;
3360
+ handleMove: function(ev) {
3361
+ this.topDelta = getEvY(ev) - this.y0;
3362
+ this.leftDelta = getEvX(ev) - this.x0;
3000
3363
 
3001
3364
  if (!this.isHidden) {
3002
3365
  this.updatePosition();
@@ -3031,7 +3394,7 @@ var MouseFollower = Class.extend({
3031
3394
  /* An abstract class comprised of a "grid" of areas that each represent a specific datetime
3032
3395
  ----------------------------------------------------------------------------------------------------------------------*/
3033
3396
 
3034
- var Grid = FC.Grid = Class.extend({
3397
+ var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, {
3035
3398
 
3036
3399
  view: null, // a View object
3037
3400
  isRTL: null, // shortcut to the view's isRTL option
@@ -3042,8 +3405,6 @@ var Grid = FC.Grid = Class.extend({
3042
3405
  el: null, // the containing element
3043
3406
  elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.
3044
3407
 
3045
- externalDragStartProxy: null, // binds the Grid's scope to externalDragStart (in DayGrid.events)
3046
-
3047
3408
  // derived from options
3048
3409
  eventTimeFormat: null,
3049
3410
  displayEventTime: null,
@@ -3056,13 +3417,19 @@ var Grid = FC.Grid = Class.extend({
3056
3417
  // TODO: port isTimeScale into same system?
3057
3418
  largeUnit: null,
3058
3419
 
3420
+ dayDragListener: null,
3421
+ segDragListener: null,
3422
+ segResizeListener: null,
3423
+ externalDragListener: null,
3424
+
3059
3425
 
3060
3426
  constructor: function(view) {
3061
3427
  this.view = view;
3062
3428
  this.isRTL = view.opt('isRTL');
3063
-
3064
3429
  this.elsByFill = {};
3065
- this.externalDragStartProxy = proxy(this, 'externalDragStart');
3430
+
3431
+ this.dayDragListener = this.buildDayDragListener();
3432
+ this.initMouseIgnoring();
3066
3433
  },
3067
3434
 
3068
3435
 
@@ -3195,26 +3562,33 @@ var Grid = FC.Grid = Class.extend({
3195
3562
  // Sets the container element that the grid should render inside of.
3196
3563
  // Does other DOM-related initializations.
3197
3564
  setElement: function(el) {
3198
- var _this = this;
3199
-
3200
3565
  this.el = el;
3566
+ preventSelection(el);
3567
+
3568
+ this.bindDayHandler('touchstart', this.dayTouchStart);
3569
+ this.bindDayHandler('mousedown', this.dayMousedown);
3570
+
3571
+ // attach event-element-related handlers. in Grid.events
3572
+ // same garbage collection note as above.
3573
+ this.bindSegHandlers();
3574
+
3575
+ this.bindGlobalHandlers();
3576
+ },
3577
+
3578
+
3579
+ bindDayHandler: function(name, handler) {
3580
+ var _this = this;
3201
3581
 
3202
3582
  // attach a handler to the grid's root element.
3203
3583
  // jQuery will take care of unregistering them when removeElement gets called.
3204
- el.on('mousedown', function(ev) {
3584
+ this.el.on(name, function(ev) {
3205
3585
  if (
3206
3586
  !$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link
3207
3587
  !$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one)
3208
3588
  ) {
3209
- _this.dayMousedown(ev);
3589
+ return handler.call(_this, ev);
3210
3590
  }
3211
3591
  });
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
3592
  },
3219
3593
 
3220
3594
 
@@ -3222,6 +3596,7 @@ var Grid = FC.Grid = Class.extend({
3222
3596
  // DOES NOT remove any content beforehand (doesn't clear events or call unrenderDates), unlike View
3223
3597
  removeElement: function() {
3224
3598
  this.unbindGlobalHandlers();
3599
+ this.clearDragListeners();
3225
3600
 
3226
3601
  this.el.remove();
3227
3602
 
@@ -3254,18 +3629,47 @@ var Grid = FC.Grid = Class.extend({
3254
3629
 
3255
3630
  // Binds DOM handlers to elements that reside outside the grid, such as the document
3256
3631
  bindGlobalHandlers: function() {
3257
- $(document).on('dragstart sortstart', this.externalDragStartProxy); // jqui
3632
+ this.listenTo($(document), {
3633
+ dragstart: this.externalDragStart, // jqui
3634
+ sortstart: this.externalDragStart // jqui
3635
+ });
3258
3636
  },
3259
3637
 
3260
3638
 
3261
3639
  // Unbinds DOM handlers from elements that reside outside the grid
3262
3640
  unbindGlobalHandlers: function() {
3263
- $(document).off('dragstart sortstart', this.externalDragStartProxy); // jqui
3641
+ this.stopListeningTo($(document));
3264
3642
  },
3265
3643
 
3266
3644
 
3267
3645
  // Process a mousedown on an element that represents a day. For day clicking and selecting.
3268
3646
  dayMousedown: function(ev) {
3647
+ if (!this.isIgnoringMouse) {
3648
+ this.dayDragListener.startInteraction(ev, {
3649
+ //distance: 5, // needs more work if we want dayClick to fire correctly
3650
+ });
3651
+ }
3652
+ },
3653
+
3654
+
3655
+ dayTouchStart: function(ev) {
3656
+ var view = this.view;
3657
+
3658
+ // HACK to prevent a user's clickaway for unselecting a range or an event
3659
+ // from causing a dayClick.
3660
+ if (view.isSelected || view.selectedEvent) {
3661
+ this.tempIgnoreMouse();
3662
+ }
3663
+
3664
+ this.dayDragListener.startInteraction(ev, {
3665
+ delay: this.view.opt('longPressDelay')
3666
+ });
3667
+ },
3668
+
3669
+
3670
+ // Creates a listener that tracks the user's drag across day elements.
3671
+ // For day clicking and selecting.
3672
+ buildDayDragListener: function() {
3269
3673
  var _this = this;
3270
3674
  var view = this.view;
3271
3675
  var isSelectable = view.opt('selectable');
@@ -3276,14 +3680,21 @@ var Grid = FC.Grid = Class.extend({
3276
3680
  // if the drag ends on the same day, it is a 'dayClick'.
3277
3681
  // if 'selectable' is enabled, this listener also detects selections.
3278
3682
  var dragListener = new HitDragListener(this, {
3279
- //distance: 5, // needs more work if we want dayClick to fire correctly
3280
3683
  scroll: view.opt('dragScroll'),
3684
+ interactionStart: function() {
3685
+ dayClickHit = dragListener.origHit; // for dayClick, where no dragging happens
3686
+ },
3281
3687
  dragStart: function() {
3282
3688
  view.unselect(); // since we could be rendering a new selection, we want to clear any old one
3283
3689
  },
3284
3690
  hitOver: function(hit, isOrig, origHit) {
3285
3691
  if (origHit) { // click needs to have started on a hit
3286
- dayClickHit = isOrig ? hit : null; // single-hit selection is a day click
3692
+
3693
+ // if user dragged to another cell at any point, it can no longer be a dayClick
3694
+ if (!isOrig) {
3695
+ dayClickHit = null;
3696
+ }
3697
+
3287
3698
  if (isSelectable) {
3288
3699
  selectionSpan = _this.computeSelection(
3289
3700
  _this.getHitSpan(origHit),
@@ -3304,23 +3715,46 @@ var Grid = FC.Grid = Class.extend({
3304
3715
  _this.unrenderSelection();
3305
3716
  enableCursor();
3306
3717
  },
3307
- listenStop: function(ev) {
3308
- if (dayClickHit) {
3309
- view.triggerDayClick(
3310
- _this.getHitSpan(dayClickHit),
3311
- _this.getHitEl(dayClickHit),
3312
- ev
3313
- );
3314
- }
3315
- if (selectionSpan) {
3316
- // the selection will already have been rendered. just report it
3317
- view.reportSelection(selectionSpan, ev);
3718
+ interactionEnd: function(ev, isCancelled) {
3719
+ if (!isCancelled) {
3720
+ if (
3721
+ dayClickHit &&
3722
+ !_this.isIgnoringMouse // see hack in dayTouchStart
3723
+ ) {
3724
+ view.triggerDayClick(
3725
+ _this.getHitSpan(dayClickHit),
3726
+ _this.getHitEl(dayClickHit),
3727
+ ev
3728
+ );
3729
+ }
3730
+ if (selectionSpan) {
3731
+ // the selection will already have been rendered. just report it
3732
+ view.reportSelection(selectionSpan, ev);
3733
+ }
3734
+ enableCursor();
3318
3735
  }
3319
- enableCursor();
3320
3736
  }
3321
3737
  });
3322
3738
 
3323
- dragListener.mousedown(ev); // start listening, which will eventually initiate a dragStart
3739
+ return dragListener;
3740
+ },
3741
+
3742
+
3743
+ // Kills all in-progress dragging.
3744
+ // Useful for when public API methods that result in re-rendering are invoked during a drag.
3745
+ // Also useful for when touch devices misbehave and don't fire their touchend.
3746
+ clearDragListeners: function() {
3747
+ this.dayDragListener.endInteraction();
3748
+
3749
+ if (this.segDragListener) {
3750
+ this.segDragListener.endInteraction(); // will clear this.segDragListener
3751
+ }
3752
+ if (this.segResizeListener) {
3753
+ this.segResizeListener.endInteraction(); // will clear this.segResizeListener
3754
+ }
3755
+ if (this.externalDragListener) {
3756
+ this.externalDragListener.endInteraction(); // will clear this.externalDragListener
3757
+ }
3324
3758
  },
3325
3759
 
3326
3760
 
@@ -3330,10 +3764,11 @@ var Grid = FC.Grid = Class.extend({
3330
3764
 
3331
3765
 
3332
3766
  // Renders a mock event at the given event location, which contains zoned start/end properties.
3767
+ // Returns all mock event elements.
3333
3768
  renderEventLocationHelper: function(eventLocation, sourceSeg) {
3334
3769
  var fakeEvent = this.fabricateHelperEvent(eventLocation, sourceSeg);
3335
3770
 
3336
- this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering
3771
+ return this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering
3337
3772
  },
3338
3773
 
3339
3774
 
@@ -3361,6 +3796,7 @@ var Grid = FC.Grid = Class.extend({
3361
3796
 
3362
3797
 
3363
3798
  // Renders a mock event. Given zoned event date properties.
3799
+ // Must return all mock event elements.
3364
3800
  renderHelper: function(eventLocation, sourceSeg) {
3365
3801
  // subclasses must implement
3366
3802
  },
@@ -3538,7 +3974,7 @@ var Grid = FC.Grid = Class.extend({
3538
3974
  fillSegTag: 'div', // subclasses can override
3539
3975
 
3540
3976
 
3541
- // Builds the HTML needed for one fill segment. Generic enought o work with different types.
3977
+ // Builds the HTML needed for one fill segment. Generic enough to work with different types.
3542
3978
  fillSegHtml: function(type, seg) {
3543
3979
 
3544
3980
  // custom hooks per-type
@@ -3640,7 +4076,8 @@ Grid.mixin({
3640
4076
 
3641
4077
  // Unrenders all events currently rendered on the grid
3642
4078
  unrenderEvents: function() {
3643
- this.triggerSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
4079
+ this.handleSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
4080
+ this.clearDragListeners();
3644
4081
 
3645
4082
  this.unrenderFgSegs();
3646
4083
  this.unrenderBgSegs();
@@ -3768,48 +4205,44 @@ Grid.mixin({
3768
4205
 
3769
4206
  // Attaches event-element-related handlers to the container element and leverage bubbling
3770
4207
  bindSegHandlers: function() {
4208
+ this.bindSegHandler('touchstart', this.handleSegTouchStart);
4209
+ this.bindSegHandler('touchend', this.handleSegTouchEnd);
4210
+ this.bindSegHandler('mouseenter', this.handleSegMouseover);
4211
+ this.bindSegHandler('mouseleave', this.handleSegMouseout);
4212
+ this.bindSegHandler('mousedown', this.handleSegMousedown);
4213
+ this.bindSegHandler('click', this.handleSegClick);
4214
+ },
4215
+
4216
+
4217
+ // Executes a handler for any a user-interaction on a segment.
4218
+ // Handler gets called with (seg, ev), and with the `this` context of the Grid
4219
+ bindSegHandler: function(name, handler) {
3771
4220
  var _this = this;
3772
- var view = this.view;
3773
4221
 
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
- });
4222
+ this.el.on(name, '.fc-event-container > *', function(ev) {
4223
+ var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents
4224
+
4225
+ // only call the handlers if there is not a drag/resize in progress
4226
+ if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) {
4227
+ return handler.call(_this, seg, ev); // context will be the Grid
3804
4228
  }
3805
- );
4229
+ });
4230
+ },
4231
+
4232
+
4233
+ handleSegClick: function(seg, ev) {
4234
+ return this.view.trigger('eventClick', seg.el[0], seg.event, ev); // can return `false` to cancel
3806
4235
  },
3807
4236
 
3808
4237
 
3809
4238
  // Updates internal state and triggers handlers for when an event element is moused over
3810
- triggerSegMouseover: function(seg, ev) {
3811
- if (!this.mousedOverSeg) {
4239
+ handleSegMouseover: function(seg, ev) {
4240
+ if (
4241
+ !this.isIgnoringMouse &&
4242
+ !this.mousedOverSeg
4243
+ ) {
3812
4244
  this.mousedOverSeg = seg;
4245
+ seg.el.addClass('fc-allow-mouse-resize');
3813
4246
  this.view.trigger('eventMouseover', seg.el[0], seg.event, ev);
3814
4247
  }
3815
4248
  },
@@ -3817,56 +4250,132 @@ Grid.mixin({
3817
4250
 
3818
4251
  // Updates internal state and triggers handlers for when an event element is moused out.
3819
4252
  // Can be given no arguments, in which case it will mouseout the segment that was previously moused over.
3820
- triggerSegMouseout: function(seg, ev) {
4253
+ handleSegMouseout: function(seg, ev) {
3821
4254
  ev = ev || {}; // if given no args, make a mock mouse event
3822
4255
 
3823
4256
  if (this.mousedOverSeg) {
3824
4257
  seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment
3825
4258
  this.mousedOverSeg = null;
4259
+ seg.el.removeClass('fc-allow-mouse-resize');
3826
4260
  this.view.trigger('eventMouseout', seg.el[0], seg.event, ev);
3827
4261
  }
3828
4262
  },
3829
4263
 
3830
4264
 
4265
+ handleSegMousedown: function(seg, ev) {
4266
+ var isResizing = this.startSegResize(seg, ev, { distance: 5 });
4267
+
4268
+ if (!isResizing && this.view.isEventDraggable(seg.event)) {
4269
+ this.buildSegDragListener(seg)
4270
+ .startInteraction(ev, {
4271
+ distance: 5
4272
+ });
4273
+ }
4274
+ },
4275
+
4276
+
4277
+ handleSegTouchStart: function(seg, ev) {
4278
+ var view = this.view;
4279
+ var event = seg.event;
4280
+ var isSelected = view.isEventSelected(event);
4281
+ var isDraggable = view.isEventDraggable(event);
4282
+ var isResizable = view.isEventResizable(event);
4283
+ var isResizing = false;
4284
+ var dragListener;
4285
+
4286
+ if (isSelected && isResizable) {
4287
+ // only allow resizing of the event is selected
4288
+ isResizing = this.startSegResize(seg, ev);
4289
+ }
4290
+
4291
+ if (!isResizing && (isDraggable || isResizable)) { // allowed to be selected?
4292
+
4293
+ dragListener = isDraggable ?
4294
+ this.buildSegDragListener(seg) :
4295
+ this.buildSegSelectListener(seg); // seg isn't draggable, but still needs to be selected
4296
+
4297
+ dragListener.startInteraction(ev, { // won't start if already started
4298
+ delay: isSelected ? 0 : this.view.opt('longPressDelay') // do delay if not already selected
4299
+ });
4300
+ }
4301
+
4302
+ // a long tap simulates a mouseover. ignore this bogus mouseover.
4303
+ this.tempIgnoreMouse();
4304
+ },
4305
+
4306
+
4307
+ handleSegTouchEnd: function(seg, ev) {
4308
+ // touchstart+touchend = click, which simulates a mouseover.
4309
+ // ignore this bogus mouseover.
4310
+ this.tempIgnoreMouse();
4311
+ },
4312
+
4313
+
4314
+ // returns boolean whether resizing actually started or not.
4315
+ // assumes the seg allows resizing.
4316
+ // `dragOptions` are optional.
4317
+ startSegResize: function(seg, ev, dragOptions) {
4318
+ if ($(ev.target).is('.fc-resizer')) {
4319
+ this.buildSegResizeListener(seg, $(ev.target).is('.fc-start-resizer'))
4320
+ .startInteraction(ev, dragOptions);
4321
+ return true;
4322
+ }
4323
+ return false;
4324
+ },
4325
+
4326
+
4327
+
3831
4328
  /* Event Dragging
3832
4329
  ------------------------------------------------------------------------------------------------------------------*/
3833
4330
 
3834
4331
 
3835
- // Called when the user does a mousedown on an event, which might lead to dragging.
4332
+ // Builds a listener that will track user-dragging on an event segment.
3836
4333
  // Generic enough to work with any type of Grid.
3837
- segDragMousedown: function(seg, ev) {
4334
+ // Has side effect of setting/unsetting `segDragListener`
4335
+ buildSegDragListener: function(seg) {
3838
4336
  var _this = this;
3839
4337
  var view = this.view;
3840
4338
  var calendar = view.calendar;
3841
4339
  var el = seg.el;
3842
4340
  var event = seg.event;
4341
+ var isDragging;
4342
+ var mouseFollower; // A clone of the original element that will move with the mouse
3843
4343
  var dropLocation; // zoned event date properties
3844
4344
 
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
- });
4345
+ if (this.segDragListener) {
4346
+ return this.segDragListener;
4347
+ }
3852
4348
 
3853
4349
  // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents
3854
4350
  // of the view.
3855
- var dragListener = new HitDragListener(view, {
3856
- distance: 5,
4351
+ var dragListener = this.segDragListener = new HitDragListener(view, {
3857
4352
  scroll: view.opt('dragScroll'),
3858
4353
  subjectEl: el,
3859
4354
  subjectCenter: true,
3860
- listenStart: function(ev) {
4355
+ interactionStart: function(ev) {
4356
+ isDragging = false;
4357
+ mouseFollower = new MouseFollower(seg.el, {
4358
+ additionalClass: 'fc-dragging',
4359
+ parentEl: view.el,
4360
+ opacity: dragListener.isTouch ? null : view.opt('dragOpacity'),
4361
+ revertDuration: view.opt('dragRevertDuration'),
4362
+ zIndex: 2 // one above the .fc-view
4363
+ });
3861
4364
  mouseFollower.hide(); // don't show until we know this is a real drag
3862
4365
  mouseFollower.start(ev);
3863
4366
  },
3864
4367
  dragStart: function(ev) {
3865
- _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
4368
+ if (dragListener.isTouch && !view.isEventSelected(event)) {
4369
+ // if not previously selected, will fire after a delay. then, select the event
4370
+ view.selectEvent(event);
4371
+ }
4372
+ isDragging = true;
4373
+ _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
3866
4374
  _this.segDragStart(seg, ev);
3867
4375
  view.hideEvent(event); // hide all event segments. our mouseFollower will take over
3868
4376
  },
3869
4377
  hitOver: function(hit, isOrig, origHit) {
4378
+ var dragHelperEls;
3870
4379
 
3871
4380
  // starting hit could be forced (DayGrid.limit)
3872
4381
  if (seg.hit) {
@@ -3886,7 +4395,13 @@ Grid.mixin({
3886
4395
  }
3887
4396
 
3888
4397
  // if a valid drop location, have the subclass render a visual indication
3889
- if (dropLocation && view.renderDrag(dropLocation, seg)) {
4398
+ if (dropLocation && (dragHelperEls = view.renderDrag(dropLocation, seg))) {
4399
+
4400
+ dragHelperEls.addClass('fc-dragging');
4401
+ if (!dragListener.isTouch) {
4402
+ _this.applyDragOpacity(dragHelperEls);
4403
+ }
4404
+
3890
4405
  mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own
3891
4406
  }
3892
4407
  else {
@@ -3902,27 +4417,54 @@ Grid.mixin({
3902
4417
  mouseFollower.show(); // show in case we are moving out of all hits
3903
4418
  dropLocation = null;
3904
4419
  },
3905
- hitDone: function() { // Called after a hitOut OR before a dragStop
4420
+ hitDone: function() { // Called after a hitOut OR before a dragEnd
3906
4421
  enableCursor();
3907
4422
  },
3908
- dragStop: function(ev) {
4423
+ interactionEnd: function(ev) {
3909
4424
  // do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
3910
4425
  mouseFollower.stop(!dropLocation, function() {
3911
- view.unrenderDrag();
3912
- view.showEvent(event);
3913
- _this.segDragStop(seg, ev);
3914
-
4426
+ if (isDragging) {
4427
+ view.unrenderDrag();
4428
+ view.showEvent(event);
4429
+ _this.segDragStop(seg, ev);
4430
+ }
3915
4431
  if (dropLocation) {
3916
4432
  view.reportEventDrop(event, dropLocation, this.largeUnit, el, ev);
3917
4433
  }
3918
4434
  });
4435
+ _this.segDragListener = null;
4436
+ }
4437
+ });
4438
+
4439
+ return dragListener;
4440
+ },
4441
+
4442
+
4443
+ // seg isn't draggable, but let's use a generic DragListener
4444
+ // simply for the delay, so it can be selected.
4445
+ // Has side effect of setting/unsetting `segDragListener`
4446
+ buildSegSelectListener: function(seg) {
4447
+ var _this = this;
4448
+ var view = this.view;
4449
+ var event = seg.event;
4450
+
4451
+ if (this.segDragListener) {
4452
+ return this.segDragListener;
4453
+ }
4454
+
4455
+ var dragListener = this.segDragListener = new DragListener({
4456
+ dragStart: function(ev) {
4457
+ if (dragListener.isTouch && !view.isEventSelected(event)) {
4458
+ // if not previously selected, will fire after a delay. then, select the event
4459
+ view.selectEvent(event);
4460
+ }
3919
4461
  },
3920
- listenStop: function() {
3921
- mouseFollower.stop(); // put in listenStop in case there was a mousedown but the drag never started
4462
+ interactionEnd: function(ev) {
4463
+ _this.segDragListener = null;
3922
4464
  }
3923
4465
  });
3924
4466
 
3925
- dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart
4467
+ return dragListener;
3926
4468
  },
3927
4469
 
3928
4470
 
@@ -4038,8 +4580,8 @@ Grid.mixin({
4038
4580
  var dropLocation; // a null value signals an unsuccessful drag
4039
4581
 
4040
4582
  // listener that tracks mouse movement over date-associated pixel regions
4041
- var dragListener = new HitDragListener(this, {
4042
- listenStart: function() {
4583
+ var dragListener = _this.externalDragListener = new HitDragListener(this, {
4584
+ interactionStart: function() {
4043
4585
  _this.isDraggingExternal = true;
4044
4586
  },
4045
4587
  hitOver: function(hit) {
@@ -4063,17 +4605,16 @@ Grid.mixin({
4063
4605
  hitOut: function() {
4064
4606
  dropLocation = null; // signal unsuccessful
4065
4607
  },
4066
- hitDone: function() { // Called after a hitOut OR before a dragStop
4608
+ hitDone: function() { // Called after a hitOut OR before a dragEnd
4067
4609
  enableCursor();
4068
4610
  _this.unrenderDrag();
4069
4611
  },
4070
- dragStop: function() {
4612
+ interactionEnd: function(ev) {
4071
4613
  if (dropLocation) { // element was dropped on a valid hit
4072
4614
  _this.view.reportExternalDrop(meta, dropLocation, el, ev, ui);
4073
4615
  }
4074
- },
4075
- listenStop: function() {
4076
4616
  _this.isDraggingExternal = false;
4617
+ _this.externalDragListener = null;
4077
4618
  }
4078
4619
  });
4079
4620
 
@@ -4114,6 +4655,7 @@ Grid.mixin({
4114
4655
  // `dropLocation` contains hypothetical start/end/allDay values the event would have if dropped. end can be null.
4115
4656
  // `seg` is the internal segment object that is being dragged. If dragging an external element, `seg` is null.
4116
4657
  // A truthy returned value indicates this method has rendered a helper element.
4658
+ // Must return elements used for any mock events.
4117
4659
  renderDrag: function(dropLocation, seg) {
4118
4660
  // subclasses must implement
4119
4661
  },
@@ -4129,24 +4671,28 @@ Grid.mixin({
4129
4671
  ------------------------------------------------------------------------------------------------------------------*/
4130
4672
 
4131
4673
 
4132
- // Called when the user does a mousedown on an event's resizer, which might lead to resizing.
4674
+ // Creates a listener that tracks the user as they resize an event segment.
4133
4675
  // Generic enough to work with any type of Grid.
4134
- segResizeMousedown: function(seg, ev, isStart) {
4676
+ buildSegResizeListener: function(seg, isStart) {
4135
4677
  var _this = this;
4136
4678
  var view = this.view;
4137
4679
  var calendar = view.calendar;
4138
4680
  var el = seg.el;
4139
4681
  var event = seg.event;
4140
4682
  var eventEnd = calendar.getEventEnd(event);
4683
+ var isDragging;
4141
4684
  var resizeLocation; // zoned event date properties. falsy if invalid resize
4142
4685
 
4143
4686
  // Tracks mouse movement over the *grid's* coordinate map
4144
- var dragListener = new HitDragListener(this, {
4145
- distance: 5,
4687
+ var dragListener = this.segResizeListener = new HitDragListener(this, {
4146
4688
  scroll: view.opt('dragScroll'),
4147
4689
  subjectEl: el,
4690
+ interactionStart: function() {
4691
+ isDragging = false;
4692
+ },
4148
4693
  dragStart: function(ev) {
4149
- _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
4694
+ isDragging = true;
4695
+ _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
4150
4696
  _this.segResizeStart(seg, ev);
4151
4697
  },
4152
4698
  hitOver: function(hit, isOrig, origHit) {
@@ -4181,16 +4727,18 @@ Grid.mixin({
4181
4727
  view.showEvent(event);
4182
4728
  enableCursor();
4183
4729
  },
4184
- dragStop: function(ev) {
4185
- _this.segResizeStop(seg, ev);
4186
-
4730
+ interactionEnd: function(ev) {
4731
+ if (isDragging) {
4732
+ _this.segResizeStop(seg, ev);
4733
+ }
4187
4734
  if (resizeLocation) { // valid date to resize to?
4188
4735
  view.reportEventResize(event, resizeLocation, this.largeUnit, el, ev);
4189
4736
  }
4737
+ _this.segResizeListener = null;
4190
4738
  }
4191
4739
  });
4192
4740
 
4193
- dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart
4741
+ return dragListener;
4194
4742
  },
4195
4743
 
4196
4744
 
@@ -4267,6 +4815,7 @@ Grid.mixin({
4267
4815
 
4268
4816
  // Renders a visual indication of an event being resized.
4269
4817
  // `range` has the updated dates of the event. `seg` is the original segment object involved in the drag.
4818
+ // Must return elements used for any mock events.
4270
4819
  renderEventResize: function(range, seg) {
4271
4820
  // subclasses must implement
4272
4821
  },
@@ -4312,6 +4861,7 @@ Grid.mixin({
4312
4861
 
4313
4862
  // Generic utility for generating the HTML classNames for an event segment's element
4314
4863
  getSegClasses: function(seg, isDraggable, isResizable) {
4864
+ var view = this.view;
4315
4865
  var event = seg.event;
4316
4866
  var classes = [
4317
4867
  'fc-event',
@@ -4329,6 +4879,11 @@ Grid.mixin({
4329
4879
  classes.push('fc-resizable');
4330
4880
  }
4331
4881
 
4882
+ // event is currently selected? attach a className.
4883
+ if (view.isEventSelected(event)) {
4884
+ classes.push('fc-selected');
4885
+ }
4886
+
4332
4887
  return classes;
4333
4888
  },
4334
4889
 
@@ -5311,10 +5866,7 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
5311
5866
  // if a segment from the same calendar but another component is being dragged, render a helper event
5312
5867
  if (seg && !seg.el.closest(this.el).length) {
5313
5868
 
5314
- this.renderEventLocationHelper(eventLocation, seg);
5315
- this.applyDragOpacity(this.helperEls);
5316
-
5317
- return true; // a helper has been rendered
5869
+ return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements
5318
5870
  }
5319
5871
  },
5320
5872
 
@@ -5333,7 +5885,7 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
5333
5885
  // Renders a visual indication of an event being resized
5334
5886
  renderEventResize: function(eventLocation, seg) {
5335
5887
  this.renderHighlight(this.eventToSpan(eventLocation));
5336
- this.renderEventLocationHelper(eventLocation, seg);
5888
+ return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements
5337
5889
  },
5338
5890
 
5339
5891
 
@@ -5379,7 +5931,9 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
5379
5931
  helperNodes.push(skeletonEl[0]);
5380
5932
  });
5381
5933
 
5382
- this.helperEls = $(helperNodes); // array -> jQuery set
5934
+ return ( // must return the elements rendered
5935
+ this.helperEls = $(helperNodes) // array -> jQuery set
5936
+ );
5383
5937
  },
5384
5938
 
5385
5939
 
@@ -6165,6 +6719,7 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
6165
6719
  labelInterval: null, // duration of how often a label should be displayed for a slot
6166
6720
 
6167
6721
  colEls: null, // cells elements in the day-row background
6722
+ slatContainerEl: null, // div that wraps all the slat rows
6168
6723
  slatEls: null, // elements running horizontally across all columns
6169
6724
  nowIndicatorEls: null,
6170
6725
 
@@ -6184,7 +6739,8 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
6184
6739
  renderDates: function() {
6185
6740
  this.el.html(this.renderHtml());
6186
6741
  this.colEls = this.el.find('.fc-day');
6187
- this.slatEls = this.el.find('.fc-slats tr');
6742
+ this.slatContainerEl = this.el.find('.fc-slats');
6743
+ this.slatEls = this.slatContainerEl.find('tr');
6188
6744
 
6189
6745
  this.colCoordCache = new CoordCache({
6190
6746
  els: this.colEls,
@@ -6463,6 +7019,11 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
6463
7019
  },
6464
7020
 
6465
7021
 
7022
+ getTotalSlatHeight: function() {
7023
+ return this.slatContainerEl.outerHeight();
7024
+ },
7025
+
7026
+
6466
7027
  // Computes the top coordinate, relative to the bounds of the grid, of the given date.
6467
7028
  // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
6468
7029
  computeDateTop: function(date, startOfDayDate) {
@@ -6511,13 +7072,10 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
6511
7072
  renderDrag: function(eventLocation, seg) {
6512
7073
 
6513
7074
  if (seg) { // if there is event information for this drag, render a helper event
6514
- this.renderEventLocationHelper(eventLocation, seg);
6515
7075
 
6516
- for (var i = 0; i < this.helperSegs.length; i++) {
6517
- this.applyDragOpacity(this.helperSegs[i].el);
6518
- }
6519
-
6520
- return true; // signal that a helper has been rendered
7076
+ // returns mock event elements
7077
+ // signal that a helper has been rendered
7078
+ return this.renderEventLocationHelper(eventLocation, seg);
6521
7079
  }
6522
7080
  else {
6523
7081
  // otherwise, just render a highlight
@@ -6539,7 +7097,7 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
6539
7097
 
6540
7098
  // Renders a visual indication of an event being resized
6541
7099
  renderEventResize: function(eventLocation, seg) {
6542
- this.renderEventLocationHelper(eventLocation, seg);
7100
+ return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements
6543
7101
  },
6544
7102
 
6545
7103
 
@@ -6555,7 +7113,7 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
6555
7113
 
6556
7114
  // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag)
6557
7115
  renderHelper: function(event, sourceSeg) {
6558
- this.renderHelperSegs(this.eventToSegs(event), sourceSeg);
7116
+ return this.renderHelperSegs(this.eventToSegs(event), sourceSeg); // returns mock event elements
6559
7117
  },
6560
7118
 
6561
7119
 
@@ -6749,6 +7307,7 @@ TimeGrid.mixin({
6749
7307
 
6750
7308
 
6751
7309
  renderHelperSegs: function(segs, sourceSeg) {
7310
+ var helperEls = [];
6752
7311
  var i, seg;
6753
7312
  var sourceEl;
6754
7313
 
@@ -6766,9 +7325,12 @@ TimeGrid.mixin({
6766
7325
  'margin-right': sourceEl.css('margin-right')
6767
7326
  });
6768
7327
  }
7328
+ helperEls.push(seg.el[0]);
6769
7329
  }
6770
7330
 
6771
7331
  this.helperSegs = segs;
7332
+
7333
+ return $(helperEls); // must return rendered helpers
6772
7334
  },
6773
7335
 
6774
7336
 
@@ -7279,7 +7841,7 @@ function isSlotSegCollision(seg1, seg2) {
7279
7841
  /* An abstract class from which other views inherit from
7280
7842
  ----------------------------------------------------------------------------------------------------------------------*/
7281
7843
 
7282
- var View = FC.View = Class.extend({
7844
+ var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
7283
7845
 
7284
7846
  type: null, // subclass' view name (string)
7285
7847
  name: null, // deprecated. use `type` instead
@@ -7306,13 +7868,10 @@ var View = FC.View = Class.extend({
7306
7868
 
7307
7869
  isRTL: false,
7308
7870
  isSelected: false, // boolean whether a range of time is user-selected or not
7871
+ selectedEvent: null,
7309
7872
 
7310
7873
  eventOrderSpecs: null, // criteria for ordering events when they have same date/time
7311
7874
 
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
7875
  // classNames styled by jqui themes
7317
7876
  widgetHeaderClass: null,
7318
7877
  widgetContentClass: null,
@@ -7322,9 +7881,6 @@ var View = FC.View = Class.extend({
7322
7881
  nextDayThreshold: null,
7323
7882
  isHiddenDayHash: null,
7324
7883
 
7325
- // document handlers, bound to `this` object
7326
- documentMousedownProxy: null, // TODO: doesn't work with touch
7327
-
7328
7884
  // now indicator
7329
7885
  isNowIndicatorRendered: null,
7330
7886
  initialNowDate: null, // result first getNow call
@@ -7347,8 +7903,6 @@ var View = FC.View = Class.extend({
7347
7903
 
7348
7904
  this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder'));
7349
7905
 
7350
- this.documentMousedownProxy = proxy(this, 'documentMousedown');
7351
-
7352
7906
  this.initialize();
7353
7907
  },
7354
7908
 
@@ -7566,15 +8120,14 @@ var View = FC.View = Class.extend({
7566
8120
 
7567
8121
  this.calendar.freezeContentHeight();
7568
8122
 
7569
- return this.clear().then(function() { // clear the content first (async)
8123
+ return syncThen(this.clear(), function() { // clear the content first
7570
8124
  return (
7571
8125
  _this.displaying =
7572
- $.when(_this.displayView(date)) // displayView might return a promise
7573
- .then(function() {
7574
- _this.forceScroll(_this.computeInitialScroll(scrollState));
7575
- _this.calendar.unfreezeContentHeight();
7576
- _this.triggerRender();
7577
- })
8126
+ syncThen(_this.displayView(date), function() { // displayView might return a promise
8127
+ _this.forceScroll(_this.computeInitialScroll(scrollState));
8128
+ _this.calendar.unfreezeContentHeight();
8129
+ _this.triggerRender();
8130
+ })
7578
8131
  );
7579
8132
  });
7580
8133
  },
@@ -7588,7 +8141,7 @@ var View = FC.View = Class.extend({
7588
8141
  var displaying = this.displaying;
7589
8142
 
7590
8143
  if (displaying) { // previously displayed, or in the process of being displayed?
7591
- return displaying.then(function() { // wait for the display to finish
8144
+ return syncThen(displaying, function() { // wait for the display to finish
7592
8145
  _this.displaying = null;
7593
8146
  _this.clearEvents();
7594
8147
  return _this.clearView(); // might return a promise. chain it
@@ -7674,13 +8227,14 @@ var View = FC.View = Class.extend({
7674
8227
 
7675
8228
  // Binds DOM handlers to elements that reside outside the view container, such as the document
7676
8229
  bindGlobalHandlers: function() {
7677
- $(document).on('mousedown', this.documentMousedownProxy);
8230
+ this.listenTo($(document), 'mousedown', this.handleDocumentMousedown);
8231
+ this.listenTo($(document), 'touchstart', this.processUnselect);
7678
8232
  },
7679
8233
 
7680
8234
 
7681
8235
  // Unbinds DOM handlers from elements that reside outside the view container
7682
8236
  unbindGlobalHandlers: function() {
7683
- $(document).off('mousedown', this.documentMousedownProxy);
8237
+ this.stopListeningTo($(document));
7684
8238
  },
7685
8239
 
7686
8240
 
@@ -7845,28 +8399,7 @@ var View = FC.View = Class.extend({
7845
8399
 
7846
8400
 
7847
8401
  /* Scroller
7848
- ------------------------------------------------------------------------------------------------------------------*/
7849
-
7850
-
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
- },
8402
+ ------------------------------------------------------------------------------------------------------------------*/
7870
8403
 
7871
8404
 
7872
8405
  // Computes the initial pre-configured scroll state prior to allowing the user to change it.
@@ -7878,17 +8411,13 @@ var View = FC.View = Class.extend({
7878
8411
 
7879
8412
  // Retrieves the view's current natural scroll state. Can return an arbitrary format.
7880
8413
  queryScroll: function() {
7881
- if (this.scrollerEl) {
7882
- return this.scrollerEl.scrollTop(); // operates on scrollerEl by default
7883
- }
8414
+ // subclasses must implement
7884
8415
  },
7885
8416
 
7886
8417
 
7887
8418
  // Sets the view's scroll state. Will accept the same format computeInitialScroll and queryScroll produce.
7888
8419
  setScroll: function(scrollState) {
7889
- if (this.scrollerEl) {
7890
- return this.scrollerEl.scrollTop(scrollState); // operates on scrollerEl by default
7891
- }
8420
+ // subclasses must implement
7892
8421
  },
7893
8422
 
7894
8423
 
@@ -8103,7 +8632,8 @@ var View = FC.View = Class.extend({
8103
8632
 
8104
8633
 
8105
8634
  // 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`
8635
+ // If an external-element, seg will be `null`.
8636
+ // Must return elements used for any mock events.
8107
8637
  renderDrag: function(dropLocation, seg) {
8108
8638
  // subclasses must implement
8109
8639
  },
@@ -8166,7 +8696,7 @@ var View = FC.View = Class.extend({
8166
8696
  },
8167
8697
 
8168
8698
 
8169
- /* Selection
8699
+ /* Selection (time range)
8170
8700
  ------------------------------------------------------------------------------------------------------------------*/
8171
8701
 
8172
8702
 
@@ -8224,13 +8754,62 @@ var View = FC.View = Class.extend({
8224
8754
  },
8225
8755
 
8226
8756
 
8227
- // Handler for unselecting when the user clicks something and the 'unselectAuto' setting is on
8228
- documentMousedown: function(ev) {
8229
- var ignore;
8757
+ /* Event Selection
8758
+ ------------------------------------------------------------------------------------------------------------------*/
8759
+
8760
+
8761
+ selectEvent: function(event) {
8762
+ if (!this.selectedEvent || this.selectedEvent !== event) {
8763
+ this.unselectEvent();
8764
+ this.renderedEventSegEach(function(seg) {
8765
+ seg.el.addClass('fc-selected');
8766
+ }, event);
8767
+ this.selectedEvent = event;
8768
+ }
8769
+ },
8770
+
8771
+
8772
+ unselectEvent: function() {
8773
+ if (this.selectedEvent) {
8774
+ this.renderedEventSegEach(function(seg) {
8775
+ seg.el.removeClass('fc-selected');
8776
+ }, this.selectedEvent);
8777
+ this.selectedEvent = null;
8778
+ }
8779
+ },
8780
+
8781
+
8782
+ isEventSelected: function(event) {
8783
+ // event references might change on refetchEvents(), while selectedEvent doesn't,
8784
+ // so compare IDs
8785
+ return this.selectedEvent && this.selectedEvent._id === event._id;
8786
+ },
8787
+
8788
+
8789
+ /* Mouse / Touch Unselecting (time range & event unselection)
8790
+ ------------------------------------------------------------------------------------------------------------------*/
8791
+ // TODO: move consistently to down/start or up/end?
8792
+ // TODO: don't kill previous selection if touch scrolling
8793
+
8794
+
8795
+ handleDocumentMousedown: function(ev) {
8796
+ if (isPrimaryMouseButton(ev)) {
8797
+ this.processUnselect(ev);
8798
+ }
8799
+ },
8800
+
8801
+
8802
+ processUnselect: function(ev) {
8803
+ this.processRangeUnselect(ev);
8804
+ this.processEventUnselect(ev);
8805
+ },
8806
+
8230
8807
 
8231
- // is there a selection, and has the user made a proper left click?
8232
- if (this.isSelected && this.opt('unselectAuto') && isPrimaryMouseButton(ev)) {
8808
+ processRangeUnselect: function(ev) {
8809
+ var ignore;
8233
8810
 
8811
+ // is there a time-range selection?
8812
+ if (this.isSelected && this.opt('unselectAuto')) {
8234
8813
  // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element
8235
8814
  ignore = this.opt('unselectCancel');
8236
8815
  if (!ignore || !$(ev.target).closest(ignore).length) {
@@ -8240,6 +8819,15 @@ var View = FC.View = Class.extend({
8240
8819
  },
8241
8820
 
8242
8821
 
8822
+ processEventUnselect: function(ev) {
8823
+ if (this.selectedEvent) {
8824
+ if (!$(ev.target).closest('.fc-selected').length) {
8825
+ this.unselectEvent();
8826
+ }
8827
+ }
8828
+ },
8829
+
8830
+
8243
8831
  /* Day Click
8244
8832
  ------------------------------------------------------------------------------------------------------------------*/
8245
8833
 
@@ -8354,6 +8942,127 @@ var View = FC.View = Class.extend({
8354
8942
 
8355
8943
  ;;
8356
8944
 
8945
+ /*
8946
+ Embodies a div that has potential scrollbars
8947
+ */
8948
+ var Scroller = FC.Scroller = Class.extend({
8949
+
8950
+ el: null, // the guaranteed outer element
8951
+ scrollEl: null, // the element with the scrollbars
8952
+ overflowX: null,
8953
+ overflowY: null,
8954
+
8955
+
8956
+ constructor: function(options) {
8957
+ options = options || {};
8958
+ this.overflowX = options.overflowX || options.overflow || 'auto';
8959
+ this.overflowY = options.overflowY || options.overflow || 'auto';
8960
+ },
8961
+
8962
+
8963
+ render: function() {
8964
+ this.el = this.renderEl();
8965
+ this.applyOverflow();
8966
+ },
8967
+
8968
+
8969
+ renderEl: function() {
8970
+ return (this.scrollEl = $('<div class="fc-scroller"></div>'));
8971
+ },
8972
+
8973
+
8974
+ // sets to natural height, unlocks overflow
8975
+ clear: function() {
8976
+ this.setHeight('auto');
8977
+ this.applyOverflow();
8978
+ },
8979
+
8980
+
8981
+ destroy: function() {
8982
+ this.el.remove();
8983
+ },
8984
+
8985
+
8986
+ // Overflow
8987
+ // -----------------------------------------------------------------------------------------------------------------
8988
+
8989
+
8990
+ applyOverflow: function() {
8991
+ this.scrollEl.css({
8992
+ 'overflow-x': this.overflowX,
8993
+ 'overflow-y': this.overflowY
8994
+ });
8995
+ },
8996
+
8997
+
8998
+ // Causes any 'auto' overflow values to resolves to 'scroll' or 'hidden'.
8999
+ // Useful for preserving scrollbar widths regardless of future resizes.
9000
+ // Can pass in scrollbarWidths for optimization.
9001
+ lockOverflow: function(scrollbarWidths) {
9002
+ var overflowX = this.overflowX;
9003
+ var overflowY = this.overflowY;
9004
+
9005
+ scrollbarWidths = scrollbarWidths || this.getScrollbarWidths();
9006
+
9007
+ if (overflowX === 'auto') {
9008
+ overflowX = (
9009
+ scrollbarWidths.top || scrollbarWidths.bottom || // horizontal scrollbars?
9010
+ // OR scrolling pane with massless scrollbars?
9011
+ this.scrollEl[0].scrollWidth - 1 > this.scrollEl[0].clientWidth
9012
+ // subtract 1 because of IE off-by-one issue
9013
+ ) ? 'scroll' : 'hidden';
9014
+ }
9015
+
9016
+ if (overflowY === 'auto') {
9017
+ overflowY = (
9018
+ scrollbarWidths.left || scrollbarWidths.right || // vertical scrollbars?
9019
+ // OR scrolling pane with massless scrollbars?
9020
+ this.scrollEl[0].scrollHeight - 1 > this.scrollEl[0].clientHeight
9021
+ // subtract 1 because of IE off-by-one issue
9022
+ ) ? 'scroll' : 'hidden';
9023
+ }
9024
+
9025
+ this.scrollEl.css({ 'overflow-x': overflowX, 'overflow-y': overflowY });
9026
+ },
9027
+
9028
+
9029
+ // Getters / Setters
9030
+ // -----------------------------------------------------------------------------------------------------------------
9031
+
9032
+
9033
+ setHeight: function(height) {
9034
+ this.scrollEl.height(height);
9035
+ },
9036
+
9037
+
9038
+ getScrollTop: function() {
9039
+ return this.scrollEl.scrollTop();
9040
+ },
9041
+
9042
+
9043
+ setScrollTop: function(top) {
9044
+ this.scrollEl.scrollTop(top);
9045
+ },
9046
+
9047
+
9048
+ getClientWidth: function() {
9049
+ return this.scrollEl[0].clientWidth;
9050
+ },
9051
+
9052
+
9053
+ getClientHeight: function() {
9054
+ return this.scrollEl[0].clientHeight;
9055
+ },
9056
+
9057
+
9058
+ getScrollbarWidths: function() {
9059
+ return getScrollbarWidths(this.scrollEl);
9060
+ }
9061
+
9062
+ });
9063
+
9064
+ ;;
9065
+
8357
9066
  var Calendar = FC.Calendar = Class.extend({
8358
9067
 
8359
9068
  dirDefaults: null, // option defaults related to LTR or RTL
@@ -8608,7 +9317,7 @@ var Calendar = FC.Calendar = Class.extend({
8608
9317
  });
8609
9318
 
8610
9319
 
8611
- Calendar.mixin(Emitter);
9320
+ Calendar.mixin(EmitterMixin);
8612
9321
 
8613
9322
 
8614
9323
  function Calendar_constructor(element, overrides) {
@@ -8625,6 +9334,7 @@ function Calendar_constructor(element, overrides) {
8625
9334
  t.render = render;
8626
9335
  t.destroy = destroy;
8627
9336
  t.refetchEvents = refetchEvents;
9337
+ t.refetchEventSources = refetchEventSources;
8628
9338
  t.reportEvents = reportEvents;
8629
9339
  t.reportEventChange = reportEventChange;
8630
9340
  t.rerenderEvents = renderEvents; // `renderEvents` serves as a rerender. an API method
@@ -8815,6 +9525,7 @@ function Calendar_constructor(element, overrides) {
8815
9525
  EventManager.call(t, options);
8816
9526
  var isFetchNeeded = t.isFetchNeeded;
8817
9527
  var fetchEvents = t.fetchEvents;
9528
+ var fetchEventSources = t.fetchEventSources;
8818
9529
 
8819
9530
 
8820
9531
 
@@ -9054,11 +9765,16 @@ function Calendar_constructor(element, overrides) {
9054
9765
 
9055
9766
 
9056
9767
  function refetchEvents() { // can be called as an API method
9057
- destroyEvents(); // so that events are cleared before user starts waiting for AJAX
9058
9768
  fetchAndRenderEvents();
9059
9769
  }
9060
9770
 
9061
9771
 
9772
+ // TODO: move this into EventManager?
9773
+ function refetchEventSources(matchInputs) {
9774
+ fetchEventSources(t.getEventSourcesByMatchArray(matchInputs));
9775
+ }
9776
+
9777
+
9062
9778
  function renderEvents() { // destroys old events if previously rendered
9063
9779
  if (elementVisible()) {
9064
9780
  freezeContentHeight();
@@ -9066,13 +9782,6 @@ function Calendar_constructor(element, overrides) {
9066
9782
  unfreezeContentHeight();
9067
9783
  }
9068
9784
  }
9069
-
9070
-
9071
- function destroyEvents() {
9072
- freezeContentHeight();
9073
- currentView.clearEvents();
9074
- unfreezeContentHeight();
9075
- }
9076
9785
 
9077
9786
 
9078
9787
  function getAndRenderEvents() {
@@ -9283,7 +9992,7 @@ function Calendar_constructor(element, overrides) {
9283
9992
 
9284
9993
  Calendar.defaults = {
9285
9994
 
9286
- titleRangeSeparator: ' \u2014 ', // emphasized dash
9995
+ titleRangeSeparator: ' \u2013 ', // en dash
9287
9996
  monthYearFormat: 'MMMM YYYY', // required for en. other languages rely on datepicker computable option
9288
9997
 
9289
9998
  defaultTimedEventDuration: '02:00:00',
@@ -9369,7 +10078,9 @@ Calendar.defaults = {
9369
10078
  dayPopoverFormat: 'LL',
9370
10079
 
9371
10080
  handleWindowResize: true,
9372
- windowResizeDelay: 200 // milliseconds before an updateSize happens
10081
+ windowResizeDelay: 200, // milliseconds before an updateSize happens
10082
+
10083
+ longPressDelay: 1000
9373
10084
 
9374
10085
  };
9375
10086
 
@@ -9830,14 +10541,14 @@ function Header(calendar, options) {
9830
10541
 
9831
10542
  function disableButton(buttonName) {
9832
10543
  el.find('.fc-' + buttonName + '-button')
9833
- .attr('disabled', 'disabled')
10544
+ .prop('disabled', true)
9834
10545
  .addClass(tm + '-state-disabled');
9835
10546
  }
9836
10547
 
9837
10548
 
9838
10549
  function enableButton(buttonName) {
9839
10550
  el.find('.fc-' + buttonName + '-button')
9840
- .removeAttr('disabled')
10551
+ .prop('disabled', false)
9841
10552
  .removeClass(tm + '-state-disabled');
9842
10553
  }
9843
10554
 
@@ -9868,8 +10579,14 @@ function EventManager(options) { // assumed to be a calendar
9868
10579
  // exports
9869
10580
  t.isFetchNeeded = isFetchNeeded;
9870
10581
  t.fetchEvents = fetchEvents;
10582
+ t.fetchEventSources = fetchEventSources;
10583
+ t.getEventSources = getEventSources;
10584
+ t.getEventSourceById = getEventSourceById;
10585
+ t.getEventSourcesByMatchArray = getEventSourcesByMatchArray;
10586
+ t.getEventSourcesByMatch = getEventSourcesByMatch;
9871
10587
  t.addEventSource = addEventSource;
9872
10588
  t.removeEventSource = removeEventSource;
10589
+ t.removeEventSources = removeEventSources;
9873
10590
  t.updateEvent = updateEvent;
9874
10591
  t.renderEvent = renderEvent;
9875
10592
  t.removeEvents = removeEvents;
@@ -9887,8 +10604,7 @@ function EventManager(options) { // assumed to be a calendar
9887
10604
  var stickySource = { events: [] };
9888
10605
  var sources = [ stickySource ];
9889
10606
  var rangeStart, rangeEnd;
9890
- var currentFetchID = 0;
9891
- var pendingSourceCnt = 0;
10607
+ var pendingSourceCnt = 0; // outstanding fetch requests, max one per source
9892
10608
  var cache = []; // holds events that have already been expanded
9893
10609
 
9894
10610
 
@@ -9918,23 +10634,58 @@ function EventManager(options) { // assumed to be a calendar
9918
10634
  function fetchEvents(start, end) {
9919
10635
  rangeStart = start;
9920
10636
  rangeEnd = end;
9921
- cache = [];
9922
- var fetchID = ++currentFetchID;
9923
- var len = sources.length;
9924
- pendingSourceCnt = len;
9925
- for (var i=0; i<len; i++) {
9926
- fetchEventSource(sources[i], fetchID);
10637
+ fetchEventSources(sources, 'reset');
10638
+ }
10639
+
10640
+
10641
+ // expects an array of event source objects (the originals, not copies)
10642
+ // `specialFetchType` is an optimization parameter that affects purging of the event cache.
10643
+ function fetchEventSources(specificSources, specialFetchType) {
10644
+ var i, source;
10645
+
10646
+ if (specialFetchType === 'reset') {
10647
+ cache = [];
10648
+ }
10649
+ else if (specialFetchType !== 'add') {
10650
+ cache = excludeEventsBySources(cache, specificSources);
10651
+ }
10652
+
10653
+ for (i = 0; i < specificSources.length; i++) {
10654
+ source = specificSources[i];
10655
+
10656
+ // already-pending sources have already been accounted for in pendingSourceCnt
10657
+ if (source._status !== 'pending') {
10658
+ pendingSourceCnt++;
10659
+ }
10660
+
10661
+ source._fetchId = (source._fetchId || 0) + 1;
10662
+ source._status = 'pending';
10663
+ }
10664
+
10665
+ for (i = 0; i < specificSources.length; i++) {
10666
+ source = specificSources[i];
10667
+
10668
+ tryFetchEventSource(source, source._fetchId);
9927
10669
  }
9928
10670
  }
9929
-
9930
-
9931
- function fetchEventSource(source, fetchID) {
10671
+
10672
+
10673
+ // fetches an event source and processes its result ONLY if it is still the current fetch.
10674
+ // caller is responsible for incrementing pendingSourceCnt first.
10675
+ function tryFetchEventSource(source, fetchId) {
9932
10676
  _fetchEventSource(source, function(eventInputs) {
9933
10677
  var isArraySource = $.isArray(source.events);
9934
10678
  var i, eventInput;
9935
10679
  var abstractEvent;
9936
10680
 
9937
- if (fetchID == currentFetchID) {
10681
+ if (
10682
+ // is this the source's most recent fetch?
10683
+ // if not, rely on an upcoming fetch of this source to decrement pendingSourceCnt
10684
+ fetchId === source._fetchId &&
10685
+ // event source no longer valid?
10686
+ source._status !== 'rejected'
10687
+ ) {
10688
+ source._status = 'resolved';
9938
10689
 
9939
10690
  if (eventInputs) {
9940
10691
  for (i = 0; i < eventInputs.length; i++) {
@@ -9956,13 +10707,29 @@ function EventManager(options) { // assumed to be a calendar
9956
10707
  }
9957
10708
  }
9958
10709
 
9959
- pendingSourceCnt--;
9960
- if (!pendingSourceCnt) {
9961
- reportEvents(cache);
9962
- }
10710
+ decrementPendingSourceCnt();
9963
10711
  }
9964
10712
  });
9965
10713
  }
10714
+
10715
+
10716
+ function rejectEventSource(source) {
10717
+ var wasPending = source._status === 'pending';
10718
+
10719
+ source._status = 'rejected';
10720
+
10721
+ if (wasPending) {
10722
+ decrementPendingSourceCnt();
10723
+ }
10724
+ }
10725
+
10726
+
10727
+ function decrementPendingSourceCnt() {
10728
+ pendingSourceCnt--;
10729
+ if (!pendingSourceCnt) {
10730
+ reportEvents(cache);
10731
+ }
10732
+ }
9966
10733
 
9967
10734
 
9968
10735
  function _fetchEventSource(source, callback) {
@@ -10078,14 +10845,13 @@ function EventManager(options) { // assumed to be a calendar
10078
10845
 
10079
10846
  /* Sources
10080
10847
  -----------------------------------------------------------------------------*/
10081
-
10848
+
10082
10849
 
10083
10850
  function addEventSource(sourceInput) {
10084
10851
  var source = buildEventSource(sourceInput);
10085
10852
  if (source) {
10086
10853
  sources.push(source);
10087
- pendingSourceCnt++;
10088
- fetchEventSource(source, currentFetchID); // will eventually call reportEvents
10854
+ fetchEventSources([ source ], 'add'); // will eventually call reportEvents
10089
10855
  }
10090
10856
  }
10091
10857
 
@@ -10135,19 +10901,120 @@ function EventManager(options) { // assumed to be a calendar
10135
10901
  }
10136
10902
 
10137
10903
 
10138
- function removeEventSource(source) {
10139
- sources = $.grep(sources, function(src) {
10140
- return !isSourcesEqual(src, source);
10141
- });
10142
- // remove all client events from that source
10143
- cache = $.grep(cache, function(e) {
10144
- return !isSourcesEqual(e.source, source);
10145
- });
10904
+ function removeEventSource(matchInput) {
10905
+ removeSpecificEventSources(
10906
+ getEventSourcesByMatch(matchInput)
10907
+ );
10908
+ }
10909
+
10910
+
10911
+ // if called with no arguments, removes all.
10912
+ function removeEventSources(matchInputs) {
10913
+ if (matchInputs == null) {
10914
+ removeSpecificEventSources(sources, true); // isAll=true
10915
+ }
10916
+ else {
10917
+ removeSpecificEventSources(
10918
+ getEventSourcesByMatchArray(matchInputs)
10919
+ );
10920
+ }
10921
+ }
10922
+
10923
+
10924
+ function removeSpecificEventSources(targetSources, isAll) {
10925
+ var i;
10926
+
10927
+ // cancel pending requests
10928
+ for (i = 0; i < targetSources.length; i++) {
10929
+ rejectEventSource(targetSources[i]);
10930
+ }
10931
+
10932
+ if (isAll) { // an optimization
10933
+ sources = [];
10934
+ cache = [];
10935
+ }
10936
+ else {
10937
+ // remove from persisted source list
10938
+ sources = $.grep(sources, function(source) {
10939
+ for (i = 0; i < targetSources.length; i++) {
10940
+ if (source === targetSources[i]) {
10941
+ return false; // exclude
10942
+ }
10943
+ }
10944
+ return true; // include
10945
+ });
10946
+
10947
+ cache = excludeEventsBySources(cache, targetSources);
10948
+ }
10949
+
10146
10950
  reportEvents(cache);
10147
10951
  }
10148
10952
 
10149
10953
 
10150
- function isSourcesEqual(source1, source2) {
10954
+ function getEventSources() {
10955
+ return sources.slice(1); // returns a shallow copy of sources with stickySource removed
10956
+ }
10957
+
10958
+
10959
+ function getEventSourceById(id) {
10960
+ return $.grep(sources, function(source) {
10961
+ return source.id && source.id === id;
10962
+ })[0];
10963
+ }
10964
+
10965
+
10966
+ // like getEventSourcesByMatch, but accepts multple match criteria (like multiple IDs)
10967
+ function getEventSourcesByMatchArray(matchInputs) {
10968
+
10969
+ // coerce into an array
10970
+ if (!matchInputs) {
10971
+ matchInputs = [];
10972
+ }
10973
+ else if (!$.isArray(matchInputs)) {
10974
+ matchInputs = [ matchInputs ];
10975
+ }
10976
+
10977
+ var matchingSources = [];
10978
+ var i;
10979
+
10980
+ // resolve raw inputs to real event source objects
10981
+ for (i = 0; i < matchInputs.length; i++) {
10982
+ matchingSources.push.apply( // append
10983
+ matchingSources,
10984
+ getEventSourcesByMatch(matchInputs[i])
10985
+ );
10986
+ }
10987
+
10988
+ return matchingSources;
10989
+ }
10990
+
10991
+
10992
+ // matchInput can either by a real event source object, an ID, or the function/URL for the source.
10993
+ // returns an array of matching source objects.
10994
+ function getEventSourcesByMatch(matchInput) {
10995
+ var i, source;
10996
+
10997
+ // given an proper event source object
10998
+ for (i = 0; i < sources.length; i++) {
10999
+ source = sources[i];
11000
+ if (source === matchInput) {
11001
+ return [ source ];
11002
+ }
11003
+ }
11004
+
11005
+ // an ID match
11006
+ source = getEventSourceById(matchInput);
11007
+ if (source) {
11008
+ return [ source ];
11009
+ }
11010
+
11011
+ return $.grep(sources, function(source) {
11012
+ return isSourcesEquivalent(matchInput, source);
11013
+ });
11014
+ }
11015
+
11016
+
11017
+ function isSourcesEquivalent(source1, source2) {
10151
11018
  return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2);
10152
11019
  }
10153
11020
 
@@ -10160,6 +11027,20 @@ function EventManager(options) { // assumed to be a calendar
10160
11027
  ) ||
10161
11028
  source; // the given argument *is* the primitive
10162
11029
  }
11030
+
11031
+
11032
+ // util
11033
+ // returns a filtered array without events that are part of any of the given sources
11034
+ function excludeEventsBySources(specificEvents, specificSources) {
11035
+ return $.grep(specificEvents, function(event) {
11036
+ for (var i = 0; i < specificSources.length; i++) {
11037
+ if (event.source === specificSources[i]) {
11038
+ return false; // exclude
11039
+ }
11040
+ }
11041
+ return true; // keep
11042
+ });
11043
+ }
10163
11044
 
10164
11045
 
10165
11046
 
@@ -10368,6 +11249,8 @@ function EventManager(options) { // assumed to be a calendar
10368
11249
  assignDatesToEvent(start, end, allDay, out);
10369
11250
  }
10370
11251
 
11252
+ t.normalizeEvent(out); // hook for external use. a prototype method
11253
+
10371
11254
  return out;
10372
11255
  }
10373
11256
 
@@ -10900,6 +11783,12 @@ function EventManager(options) { // assumed to be a calendar
10900
11783
  }
10901
11784
 
10902
11785
 
11786
+ // hook for external libs to manipulate event properties upon creation.
11787
+ // should manipulate the event in-place.
11788
+ Calendar.prototype.normalizeEvent = function(event) {
11789
+ };
11790
+
11791
+
10903
11792
  // Returns a list of events that the given event should be compared against when being considered for a move to
10904
11793
  // the specified span. Attached to the Calendar's prototype because EventManager is a mixin for a Calendar.
10905
11794
  Calendar.prototype.getPeerEvents = function(span, event) {
@@ -10937,6 +11826,8 @@ function backupEventDates(event) {
10937
11826
 
10938
11827
  var BasicView = FC.BasicView = View.extend({
10939
11828
 
11829
+ scroller: null,
11830
+
10940
11831
  dayGridClass: DayGrid, // class the dayGrid will be instantiated from (overridable by subclasses)
10941
11832
  dayGrid: null, // the main subcomponent that does most of the heavy lifting
10942
11833
 
@@ -10951,6 +11842,11 @@ var BasicView = FC.BasicView = View.extend({
10951
11842
 
10952
11843
  initialize: function() {
10953
11844
  this.dayGrid = this.instantiateDayGrid();
11845
+
11846
+ this.scroller = new Scroller({
11847
+ overflowX: 'hidden',
11848
+ overflowY: 'auto'
11849
+ });
10954
11850
  },
10955
11851
 
10956
11852
 
@@ -11003,9 +11899,12 @@ var BasicView = FC.BasicView = View.extend({
11003
11899
  this.el.addClass('fc-basic-view').html(this.renderSkeletonHtml());
11004
11900
  this.renderHead();
11005
11901
 
11006
- this.scrollerEl = this.el.find('.fc-day-grid-container');
11902
+ this.scroller.render();
11903
+ var dayGridContainerEl = this.scroller.el.addClass('fc-day-grid-container');
11904
+ var dayGridEl = $('<div class="fc-day-grid" />').appendTo(dayGridContainerEl);
11905
+ this.el.find('.fc-body > tr > td').append(dayGridContainerEl);
11007
11906
 
11008
- this.dayGrid.setElement(this.el.find('.fc-day-grid'));
11907
+ this.dayGrid.setElement(dayGridEl);
11009
11908
  this.dayGrid.renderDates(this.hasRigidRows());
11010
11909
  },
11011
11910
 
@@ -11024,6 +11923,7 @@ var BasicView = FC.BasicView = View.extend({
11024
11923
  unrenderDates: function() {
11025
11924
  this.dayGrid.unrenderDates();
11026
11925
  this.dayGrid.removeElement();
11926
+ this.scroller.destroy();
11027
11927
  },
11028
11928
 
11029
11929
 
@@ -11044,11 +11944,7 @@ var BasicView = FC.BasicView = View.extend({
11044
11944
  '</thead>' +
11045
11945
  '<tbody class="fc-body">' +
11046
11946
  '<tr>' +
11047
- '<td class="' + this.widgetContentClass + '">' +
11048
- '<div class="fc-day-grid-container">' +
11049
- '<div class="fc-day-grid"/>' +
11050
- '</div>' +
11051
- '</td>' +
11947
+ '<td class="' + this.widgetContentClass + '"></td>' +
11052
11948
  '</tr>' +
11053
11949
  '</tbody>' +
11054
11950
  '</table>';
@@ -11091,9 +11987,10 @@ var BasicView = FC.BasicView = View.extend({
11091
11987
  setHeight: function(totalHeight, isAuto) {
11092
11988
  var eventLimit = this.opt('eventLimit');
11093
11989
  var scrollerHeight;
11990
+ var scrollbarWidths;
11094
11991
 
11095
11992
  // reset all heights to be natural
11096
- unsetScroller(this.scrollerEl);
11993
+ this.scroller.clear();
11097
11994
  uncompensateScroll(this.headRowEl);
11098
11995
 
11099
11996
  this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed
@@ -11103,6 +12000,8 @@ var BasicView = FC.BasicView = View.extend({
11103
12000
  this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after
11104
12001
  }
11105
12002
 
12003
+ // distribute the height to the rows
12004
+ // (totalHeight is a "recommended" value if isAuto)
11106
12005
  scrollerHeight = this.computeScrollerHeight(totalHeight);
11107
12006
  this.setGridHeight(scrollerHeight, isAuto);
11108
12007
 
@@ -11111,17 +12010,33 @@ var BasicView = FC.BasicView = View.extend({
11111
12010
  this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set
11112
12011
  }
11113
12012
 
11114
- if (!isAuto && setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars?
12013
+ if (!isAuto) { // should we force dimensions of the scroll container?
11115
12014
 
11116
- compensateScroll(this.headRowEl, getScrollbarWidths(this.scrollerEl));
12015
+ this.scroller.setHeight(scrollerHeight);
12016
+ scrollbarWidths = this.scroller.getScrollbarWidths();
11117
12017
 
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);
12018
+ if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars?
12019
+
12020
+ compensateScroll(this.headRowEl, scrollbarWidths);
12021
+
12022
+ // doing the scrollbar compensation might have created text overflow which created more height. redo
12023
+ scrollerHeight = this.computeScrollerHeight(totalHeight);
12024
+ this.scroller.setHeight(scrollerHeight);
12025
+ }
12026
+
12027
+ // guarantees the same scrollbar widths
12028
+ this.scroller.lockOverflow(scrollbarWidths);
11121
12029
  }
11122
12030
  },
11123
12031
 
11124
12032
 
12033
+ // given a desired total height of the view, returns what the height of the scroller should be
12034
+ computeScrollerHeight: function(totalHeight) {
12035
+ return totalHeight -
12036
+ subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
12037
+ },
12038
+
12039
+
11125
12040
  // Sets the height of just the DayGrid component in this view
11126
12041
  setGridHeight: function(height, isAuto) {
11127
12042
  if (isAuto) {
@@ -11133,6 +12048,20 @@ var BasicView = FC.BasicView = View.extend({
11133
12048
  },
11134
12049
 
11135
12050
 
12051
+ /* Scroll
12052
+ ------------------------------------------------------------------------------------------------------------------*/
12053
+
12054
+
12055
+ queryScroll: function() {
12056
+ return this.scroller.getScrollTop();
12057
+ },
12058
+
12059
+
12060
+ setScroll: function(top) {
12061
+ this.scroller.setScrollTop(top);
12062
+ },
12063
+
12064
+
11136
12065
  /* Hit Areas
11137
12066
  ------------------------------------------------------------------------------------------------------------------*/
11138
12067
  // forward all hit-related method calls to dayGrid
@@ -11368,6 +12297,8 @@ fcViews.month = {
11368
12297
 
11369
12298
  var AgendaView = FC.AgendaView = View.extend({
11370
12299
 
12300
+ scroller: null,
12301
+
11371
12302
  timeGridClass: TimeGrid, // class used to instantiate the timeGrid. subclasses can override
11372
12303
  timeGrid: null, // the main time-grid subcomponent of this view
11373
12304
 
@@ -11377,11 +12308,10 @@ var AgendaView = FC.AgendaView = View.extend({
11377
12308
  axisWidth: null, // the width of the time axis running down the side
11378
12309
 
11379
12310
  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
12311
+ noScrollRowEls: null, // set of fake row elements that must compensate when scroller has scrollbars
11381
12312
 
11382
12313
  // when the time-grid isn't tall enough to occupy the given height, we render an <hr> underneath
11383
12314
  bottomRuleEl: null,
11384
- bottomRuleHeight: null,
11385
12315
 
11386
12316
 
11387
12317
  initialize: function() {
@@ -11390,6 +12320,11 @@ var AgendaView = FC.AgendaView = View.extend({
11390
12320
  if (this.opt('allDaySlot')) { // should we display the "all-day" area?
11391
12321
  this.dayGrid = this.instantiateDayGrid(); // the all-day subcomponent of this view
11392
12322
  }
12323
+
12324
+ this.scroller = new Scroller({
12325
+ overflowX: 'hidden',
12326
+ overflowY: 'auto'
12327
+ });
11393
12328
  },
11394
12329
 
11395
12330
 
@@ -11430,10 +12365,12 @@ var AgendaView = FC.AgendaView = View.extend({
11430
12365
  this.el.addClass('fc-agenda-view').html(this.renderSkeletonHtml());
11431
12366
  this.renderHead();
11432
12367
 
11433
- // the element that wraps the time-grid that will probably scroll
11434
- this.scrollerEl = this.el.find('.fc-time-grid-container');
12368
+ this.scroller.render();
12369
+ var timeGridWrapEl = this.scroller.el.addClass('fc-time-grid-container');
12370
+ var timeGridEl = $('<div class="fc-time-grid" />').appendTo(timeGridWrapEl);
12371
+ this.el.find('.fc-body > tr > td').append(timeGridWrapEl);
11435
12372
 
11436
- this.timeGrid.setElement(this.el.find('.fc-time-grid'));
12373
+ this.timeGrid.setElement(timeGridEl);
11437
12374
  this.timeGrid.renderDates();
11438
12375
 
11439
12376
  // the <hr> that sometimes displays under the time-grid
@@ -11470,6 +12407,8 @@ var AgendaView = FC.AgendaView = View.extend({
11470
12407
  this.dayGrid.unrenderDates();
11471
12408
  this.dayGrid.removeElement();
11472
12409
  }
12410
+
12411
+ this.scroller.destroy();
11473
12412
  },
11474
12413
 
11475
12414
 
@@ -11491,9 +12430,6 @@ var AgendaView = FC.AgendaView = View.extend({
11491
12430
  '<hr class="fc-divider ' + this.widgetHeaderClass + '"/>' :
11492
12431
  ''
11493
12432
  ) +
11494
- '<div class="fc-time-grid-container">' +
11495
- '<div class="fc-time-grid"/>' +
11496
- '</div>' +
11497
12433
  '</td>' +
11498
12434
  '</tr>' +
11499
12435
  '</tbody>' +
@@ -11573,16 +12509,11 @@ var AgendaView = FC.AgendaView = View.extend({
11573
12509
  setHeight: function(totalHeight, isAuto) {
11574
12510
  var eventLimit;
11575
12511
  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
12512
+ var scrollbarWidths;
11582
12513
 
11583
12514
  // reset all dimensions back to the original state
11584
- this.scrollerEl.css('overflow', '');
11585
- unsetScroller(this.scrollerEl);
12515
+ this.bottomRuleEl.hide(); // .show() will be called later if this <hr> is necessary
12516
+ this.scroller.clear(); // sets height to 'auto' and clears overflow
11586
12517
  uncompensateScroll(this.noScrollRowEls);
11587
12518
 
11588
12519
  // limit number of events in the all-day area
@@ -11598,28 +12529,46 @@ var AgendaView = FC.AgendaView = View.extend({
11598
12529
  }
11599
12530
  }
11600
12531
 
11601
- if (!isAuto) { // should we force dimensions of the scroll container, or let the contents be natural height?
12532
+ if (!isAuto) { // should we force dimensions of the scroll container?
11602
12533
 
11603
12534
  scrollerHeight = this.computeScrollerHeight(totalHeight);
11604
- if (setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars?
12535
+ this.scroller.setHeight(scrollerHeight);
12536
+ scrollbarWidths = this.scroller.getScrollbarWidths();
12537
+
12538
+ if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars?
11605
12539
 
11606
12540
  // make the all-day and header rows lines up
11607
- compensateScroll(this.noScrollRowEls, getScrollbarWidths(this.scrollerEl));
12541
+ compensateScroll(this.noScrollRowEls, scrollbarWidths);
11608
12542
 
11609
12543
  // the scrollbar compensation might have changed text flow, which might affect height, so recalculate
11610
12544
  // and reapply the desired height to the scroller.
11611
12545
  scrollerHeight = this.computeScrollerHeight(totalHeight);
11612
- this.scrollerEl.height(scrollerHeight);
12546
+ this.scroller.setHeight(scrollerHeight);
11613
12547
  }
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
12548
+
12549
+ // guarantees the same scrollbar widths
12550
+ this.scroller.lockOverflow(scrollbarWidths);
12551
+
12552
+ // if there's any space below the slats, show the horizontal rule.
12553
+ // this won't cause any new overflow, because lockOverflow already called.
12554
+ if (this.timeGrid.getTotalSlatHeight() < scrollerHeight) {
11617
12555
  this.bottomRuleEl.show();
11618
12556
  }
11619
12557
  }
11620
12558
  },
11621
12559
 
11622
12560
 
12561
+ // given a desired total height of the view, returns what the height of the scroller should be
12562
+ computeScrollerHeight: function(totalHeight) {
12563
+ return totalHeight -
12564
+ subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
12565
+ },
12566
+
12567
+
12568
+ /* Scroll
12569
+ ------------------------------------------------------------------------------------------------------------------*/
12570
+
12571
+
11623
12572
  // Computes the initial pre-configured scroll state prior to allowing the user to change it
11624
12573
  computeInitialScroll: function() {
11625
12574
  var scrollTime = moment.duration(this.opt('scrollTime'));
@@ -11636,6 +12585,16 @@ var AgendaView = FC.AgendaView = View.extend({
11636
12585
  },
11637
12586
 
11638
12587
 
12588
+ queryScroll: function() {
12589
+ return this.scroller.getScrollTop();
12590
+ },
12591
+
12592
+
12593
+ setScroll: function(top) {
12594
+ this.scroller.setScrollTop(top);
12595
+ },
12596
+
12597
+
11639
12598
  /* Hit Areas
11640
12599
  ------------------------------------------------------------------------------------------------------------------*/
11641
12600
  // forward all hit-related method calls to the grids (dayGrid might not be defined)