Explorar el Código

fix iOS 10 touch interaction via GlobalEmitter (#3403)

Adam Shaw hace 9 años
padre
commit
488107744c
Se han modificado 5 ficheros con 110 adiciones y 46 borrados
  1. 1 0
      src.json
  2. 5 0
      src/Calendar.js
  3. 12 22
      src/common/DragListener.js
  4. 92 0
      src/common/GlobalEmitter.js
  5. 0 24
      src/util.js

+ 1 - 0
src.json

@@ -16,6 +16,7 @@
     "common/DragListener.js",
     "common/DragListener.autoscroll.js",
     "common/HitDragListener.js",
+    "common/GlobalEmitter.js",
     "common/MouseFollower.js",
     "common/Grid.js",
     "common/Grid.events.js",

+ 5 - 0
src/Calendar.js

@@ -265,6 +265,9 @@ Calendar.mixin(EmitterMixin);
 function Calendar_constructor(element, overrides) {
 	var t = this;
 
+	// declare the current calendar instance relies on GlobalEmitter. needed for garbage collection.
+	GlobalEmitter.needed();
+
 
 	// Exports
 	// -----------------------------------------------------------------------------------
@@ -608,6 +611,8 @@ function Calendar_constructor(element, overrides) {
 		if (windowResizeProxy) {
 			$(window).unbind('resize', windowResizeProxy);
 		}
+
+		GlobalEmitter.unneeded();
 	}
 
 

+ 12 - 22
src/common/DragListener.js

@@ -26,13 +26,11 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMix
 	delayTimeoutId: null,
 	minDistance: null,
 
-	handleTouchScrollProxy: null, // calls handleTouchScroll, always bound to `this`
-
 
 	constructor: function(options) {
+		this.initMouseIgnoring(500); // init mixin
+
 		this.options = options || {};
-		this.handleTouchScrollProxy = proxy(this, 'handleTouchScroll');
-		this.initMouseIgnoring(500);
 	},
 
 
@@ -127,12 +125,18 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMix
 		var _this = this;
 		var touchStartIgnores = 1;
 
+		// some browsers (Safari in iOS 10) don't allow preventDefault on touch events that are bound after touchstart,
+		// so listen to the GlobalEmitter singleton, which is always bound, instead of the document directly.
+		var globalEmitter = GlobalEmitter.get();
+
 		if (this.isTouch) {
-			this.listenTo($(document), {
+			this.listenTo(globalEmitter, {
 				touchmove: this.handleTouchMove,
 				touchend: this.endInteraction,
 				touchcancel: this.endInteraction,
 
+				scroll: this.handleTouchScroll,
+
 				// Sometimes touchend doesn't fire
 				// (can't figure out why. touchcancel doesn't fire either. has to do with scrolling?)
 				// If another touchstart happens, we know it's bogus, so cancel the drag.
@@ -146,23 +150,15 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMix
 					}
 				}
 			});
-
-			// listen to ALL scroll actions on the page
-			if (
-				!bindAnyScroll(this.handleTouchScrollProxy) && // hopefully this works and short-circuits the rest
-				this.scrollEl // otherwise, attach a single handler to this
-			) {
-				this.listenTo(this.scrollEl, 'scroll', this.handleTouchScroll);
-			}
 		}
 		else {
-			this.listenTo($(document), {
+			this.listenTo(globalEmitter, {
 				mousemove: this.handleMouseMove,
 				mouseup: this.endInteraction
 			});
 		}
 
-		this.listenTo($(document), {
+		this.listenTo(globalEmitter, {
 			selectstart: preventDefault, // don't allow selection while dragging
 			contextmenu: preventDefault // long taps would open menu on Chrome dev tools
 		});
@@ -170,13 +166,7 @@ var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMix
 
 
 	unbindHandlers: function() {
-		this.stopListeningTo($(document));
-
-		// unbind scroll listening
-		unbindAnyScroll(this.handleTouchScrollProxy);
-		if (this.scrollEl) {
-			this.stopListeningTo(this.scrollEl, 'scroll');
-		}
+		this.stopListeningTo(GlobalEmitter.get());
 	},
 
 

+ 92 - 0
src/common/GlobalEmitter.js

@@ -0,0 +1,92 @@
+
+/*
+Listens to document and window-level user-interaction events, like touch events and mouse events,
+and fires these events as-is to whoever is observing a GlobalEmitter.
+Best when used as a singleton via GlobalEmitter.get()
+*/
+var GlobalEmitter = Class.extend(ListenerMixin, EmitterMixin, {
+
+	handleScrollProxy: null,
+
+
+	bind: function() {
+		var _this = this;
+		var doc = $(document);
+
+		$.each([
+			'mousedown', 'mousemove', 'mouseup',
+			'touchstart', 'touchmove', 'touchcancel', 'touchend',
+			'selectstart', 'contextmenu'
+		], function(i, eventName) {
+			_this.listenTo(doc, eventName, function(ev) {
+				_this.trigger(eventName, ev);
+			});
+		});
+
+		// attach a handler to get called when ANY scroll action happens on the page.
+		// this was impossible to do with normal on/off because 'scroll' doesn't bubble.
+		// http://stackoverflow.com/a/32954565/96342
+		window.addEventListener(
+			'scroll',
+			this.handleScrollProxy = proxy(this, 'handleScroll'),
+			true // useCapture
+		);
+	},
+
+
+	unbind: function() {
+		this.stopListeningTo($(document));
+
+		window.removeEventListener(
+			'scroll',
+			this.handleScrollProxy,
+			true // useCapture
+		);
+	},
+
+
+	handleScroll: function(ev) {
+		this.trigger('scroll', ev);
+	}
+
+});
+
+
+// Singleton
+// ---------------------------------------------------------------------------------------------------------------------
+
+(function() {
+	var globalEmitter = null;
+	var neededCount = 0;
+
+
+	// gets the singleton
+	GlobalEmitter.get = function() {
+
+		if (!globalEmitter) {
+			globalEmitter = new GlobalEmitter();
+			globalEmitter.bind();
+		}
+
+		return globalEmitter;
+	};
+
+
+	// called when an object knows it will need a GlobalEmitter in the near future.
+	GlobalEmitter.needed = function() {
+		GlobalEmitter.get(); // ensures globalEmitter
+		neededCount++;
+	};
+
+
+	// called when the object that originally called needed() doesn't need a GlobalEmitter anymore.
+	GlobalEmitter.unneeded = function() {
+		neededCount--;
+
+		if (!neededCount) { // nobody else needs it
+			globalEmitter.unbind();
+			globalEmitter = null;
+		}
+	};
+
+})();

+ 0 - 24
src/util.js

@@ -344,30 +344,6 @@ function preventDefault(ev) {
 }
 
 
-// attach a handler to get called when ANY scroll action happens on the page.
-// this was impossible to do with normal on/off because 'scroll' doesn't bubble.
-// http://stackoverflow.com/a/32954565/96342
-// returns `true` on success.
-function bindAnyScroll(handler) {
-	if (window.addEventListener) {
-		window.addEventListener('scroll', handler, true); // useCapture=true
-		return true;
-	}
-	return false;
-}
-
-
-// undoes bindAnyScroll. must pass in the original function.
-// returns `true` on success.
-function unbindAnyScroll(handler) {
-	if (window.removeEventListener) {
-		window.removeEventListener('scroll', handler, true); // useCapture=true
-		return true;
-	}
-	return false;
-}
-
-
 /* General Geometry Utils
 ----------------------------------------------------------------------------------------------------------------------*/