Repository: chartjs/chartjs-plugin-zoom
Pull request #382 (2020-09-07 21:43:19)

Added optional modifier keys
Often the chart captures scroll events in an undesired way, so this PR makes it possible to set an optional `wheelModifierKey` that must be pressed to activate mouse wheel zoom.

A `modifierKey` can also be set for pan. By default this is only checked when the input comes via mouse, but can be set for touch and other inputs with `requireModifierNonMouse: true`

`zoom.onWheelModifierKeyFailed` is fired when a zoom does not occur because the modifier key is not present (e.g. to allow the consumer to display "Press ctrl to zoom"). A similar `pan.onModifierKeyFailed` is also available.

Although not necessarily the requested fix, it will help with the following issues:
#377, #370, #335, #308 (Actually I see the collaborators offer to accept PRs on this issue there). Not looking any further...

Relationship to other pull requests:
Might be incompatible with #380.
A more flexible version of #368 


BarryPSmith: @jledentu I see on issue #308 you suggest the name modifierKey for both the pan and zoom modifiers. I have used 'wheelModifierKey' for the zoom key in this PR because it only affects wheel zoom, but it would be an easy change to have them both 'modifierKey'.
NicolasGoeddel: Cool. When can I expect the merge request to be accepted?
benmccann: @BarryPSmith this will need to be rebased
benmccann: Actually, thinking about this more, I think a better way to handle this might be a generic function that let's you filter whether the zoom occurs. That seems to handle more use cases. If I recall correctly that might already exist too in which case maybe we just need an example of using it for this usecase
NicolasGoeddel: Okay, why the heck do you delete comments?
benmccann: The comments are meant for code review. If you want to express interest you can simply leave a thumbs up on the PR
BarryPSmith: It looks like enough has changed that it'll be easier to rewrite the PR. That's the impression I get looking at the conflict files that rebase has generated. At least it's a fairly simple set of changes to the functional code.

I'll throw away the zoom tests, given the modified test/utils.js has migrated into a different repo.

At least the weather is bad here so I have time.
kurkle: some lint issues, `npm run lint` should reveal them lovally. 
kurkle: > Test coverage decrease: In order to test the new code one must be able to send mouse events with keys attached. That is not possible with the current test framework - util.js of chartjs-test-utils must be updated. See test/util.js of [d5cb18f](https://github.com/chartjs/chartjs-plugin-zoom/commit/d5cb18f90bb720e50cd930a047db9a5e6e8e0702)

Ok, I'll take a look at the tests later. I think it needs to be a WheelEvent for the delta stuff.

Patch:
diff --git a/README.md b/README.md
index a7e163c..9058cd5 100644
--- a/README.md
+++ b/README.md
@@ -62,7 +62,10 @@ plugins: {
 			// Function called while the user is panning
 			onPan: function({chart}) { console.log(`I'm panning!!!`); },
 			// Function called once panning is completed
-			onPanComplete: function({chart}) { console.log(`I was panned!!!`); }
+			onPanComplete: function({chart}) { console.log(`I was panned!!!`); },
+			// Function called when pan fails because modifier key was not detected.
+			// event is the a hammer event that failed - see https://hammerjs.github.io/api#event-object
+			onPanRejected: function({chart, event}) { console.log(`I didn't start panning!`); }
 		},
 
 		// Container for zoom options
@@ -114,7 +117,9 @@ plugins: {
 			// Function called while the user is zooming
 			onZoom: function({chart}) { console.log(`I'm zooming!!!`); },
 			// Function called once zooming is completed
-			onZoomComplete: function({chart}) { console.log(`I was zoomed!!!`); }
+			onZoomComplete: function({chart}) { console.log(`I was zoomed!!!`); },
+			// Function called when wheel input occurs without modifier key
+			onZoomRejected: function({chart, event}) { console.log(`I didn't start zooming!`); }
 		}
 	}
 }
diff --git a/src/plugin.js b/src/plugin.js
index eb1040c..6e63d9b 100644
--- a/src/plugin.js
+++ b/src/plugin.js
@@ -73,7 +73,7 @@ function directionEnabled(mode, dir, chart) {
 
 function rangeMaxLimiter(zoomPanOptions, newMax) {
   if (zoomPanOptions.scaleAxes && zoomPanOptions.rangeMax &&
-			!isNullOrUndef(zoomPanOptions.rangeMax[zoomPanOptions.scaleAxes])) {
+      !isNullOrUndef(zoomPanOptions.rangeMax[zoomPanOptions.scaleAxes])) {
     const rangeMax = zoomPanOptions.rangeMax[zoomPanOptions.scaleAxes];
     if (newMax > rangeMax) {
       newMax = rangeMax;
@@ -84,7 +84,7 @@ function rangeMaxLimiter(zoomPanOptions, newMax) {
 
 function rangeMinLimiter(zoomPanOptions, newMin) {
   if (zoomPanOptions.scaleAxes && zoomPanOptions.rangeMin &&
-			!isNullOrUndef(zoomPanOptions.rangeMin[zoomPanOptions.scaleAxes])) {
+      !isNullOrUndef(zoomPanOptions.rangeMin[zoomPanOptions.scaleAxes])) {
     const rangeMin = zoomPanOptions.rangeMin[zoomPanOptions.scaleAxes];
     if (newMin < rangeMin) {
       newMin = rangeMin;
@@ -321,13 +321,15 @@ var zoomPlugin = {
       enabled: false,
       mode: 'xy',
       speed: 20,
-      threshold: 10
+      threshold: 10,
+      modifierKey: null,
     },
     zoom: {
       enabled: false,
       mode: 'xy',
       sensitivity: 3,
-      speed: 0.1
+      speed: 0.1,
+      wheelModifierKey: null
     }
   },
 
@@ -405,6 +407,21 @@ var zoomPlugin = {
 
     var _scrollTimeout = null;
     chartInstance.$zoom._wheelHandler = function(event) {
+      var zoomOptions = chartInstance.$zoom._options.zoom;
+
+      // Before preventDefault, check if the modifier key required and pressed
+      if (zoomOptions
+          && zoomOptions.wheelModifierKey
+          && !event[zoomOptions.wheelModifierKey + 'Key']) {
+        if (typeof zoomOptions.onzoomRejected === 'function') {
+          zoomOptions.onzoomRejected({
+            chart: chartInstance,
+            event: event
+          });
+        }
+        return;
+      }
+
       // Prevent the event from triggering the default behavior (eg. Content scrolling).
       if (event.cancelable) {
         event.preventDefault();
@@ -425,7 +442,6 @@ var zoomPlugin = {
         y: offsetY
       };
 
-      var zoomOptions = chartInstance.$zoom._options.zoom;
       var speedPercent = zoomOptions.speed;
 
       if (event.deltaY >= 0) {
@@ -442,6 +458,28 @@ var zoomPlugin = {
     };
 
     if (Hammer) {
+      var panEnabler = function(recognizer, event) {
+        const panOptions = chartInstance.$zoom._options.pan;
+        if (!panOptions || !panOptions.enabled) {
+          return false;
+        }
+        if (!event || !event.srcEvent) { // Sometimes Hammer queries this with a null event.
+          return true;
+        }
+        const requireModifier = panOptions.modifierKey
+          && (event.pointerType === 'mouse');
+        if (requireModifier && !event.srcEvent[panOptions.modifierKey + 'Key']) {
+          if (typeof panOptions.onPanRejected === 'function') {
+            panOptions.onPanRejected({
+              chart: chartInstance,
+              event: event
+            });
+          }
+          return false;
+        }
+        return true;
+      };
+
       var zoomOptions = chartInstance.$zoom._options.zoom;
       var panOptions = chartInstance.$zoom._options.pan;
       var mc = new Hammer.Manager(node);
@@ -450,7 +488,8 @@ var zoomPlugin = {
       }
       if (panOptions && panOptions.enabled) {
         mc.add(new Hammer.Pan({
-          threshold: panThreshold
+          threshold: panThreshold,
+          enable: panEnabler
         }));
       }
 


Test patch:
diff --git a/test/specs/defaults.spec.js b/test/specs/defaults.spec.js
index eb3376b..b161968 100644
--- a/test/specs/defaults.spec.js
+++ b/test/specs/defaults.spec.js
@@ -6,13 +6,15 @@ describe('defaults', function() {
       enabled: false,
       mode: 'xy',
       speed: 20,
-      threshold: 10
+      threshold: 10,
+      modifierKey: null,
     },
     zoom: {
       enabled: false,
       mode: 'xy',
       sensitivity: 3,
-      speed: 0.1
+      speed: 0.1,
+      wheelModifierKey: null
     }
   };