fullcalendar-rails 2.6.1.0 → 2.8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/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)