From 75bdd456b589db7560fdd2d25032d210b8457eeb Mon Sep 17 00:00:00 2001 From: kurkle Date: Wed, 30 Jun 2021 14:21:56 +0300 Subject: [PATCH 1/4] Add modifierKey option for drag-to-zoom --- docs/guide/options.md | 3 +- src/handlers.js | 8 +++-- test/specs/zoom.spec.js | 69 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/docs/guide/options.md b/docs/guide/options.md index eb4f99b6..f45a50e0 100644 --- a/docs/guide/options.md +++ b/docs/guide/options.md @@ -65,7 +65,7 @@ const chart = new Chart('id', { | ---- | -----| ------- | ----------- | `enabled` | `boolean` | `false` | Enable zooming via mouse wheel | `speed` | `number` | `0.1` | Factor of zoom speed via mouse wheel -| `modifierKey` | `'ctrl'`\|`'alt'`\|`'shift'`\|`'meta'` | `null` | Modifier key required for zooming with mouse +| `modifierKey` | `'ctrl'`\|`'alt'`\|`'shift'`\|`'meta'` | `null` | Modifier key required for zooming via mouse wheel #### Drag options @@ -76,6 +76,7 @@ const chart = new Chart('id', { | `borderColor` | `Color` | `'rgba(225,225,225)'` | Stroke color | `borderWidth` | `number` | `0` | Stroke width | `threshold` | `number` | `0` | Minimal zoom distance required before actually applying zoom +| `modifierKey` | `'ctrl'`\|`'alt'`\|`'shift'`\|`'meta'` | `null` | Modifier key required for drag-to-zoom #### Pinch options diff --git a/src/handlers.js b/src/handlers.js index ec49d634..5c3053a7 100644 --- a/src/handlers.js +++ b/src/handlers.js @@ -3,6 +3,9 @@ import {zoom, zoomRect} from './core'; import {callback as call} from 'chart.js/helpers'; import {getState} from './state'; +const modifierKeyPressed = (key, event) => key && event[key + 'Key']; +const modifierKeyNotPressed = (key, event) => key && !event[key + 'Key']; + function removeHandler(chart, type) { const {handlers} = getState(chart); const handler = handlers[type]; @@ -48,7 +51,8 @@ export function mouseDown(chart, event) { const state = getState(chart); const {pan: panOptions, zoom: zoomOptions} = state.options; const panKey = panOptions && panOptions.modifierKey; - if (panKey && event[panKey + 'Key']) { + const dragKey = zoomOptions && zoomOptions.drag && zoomOptions.drag.modifierKey; + if (modifierKeyPressed(panKey, event) || modifierKeyNotPressed(dragKey, event)) { return call(zoomOptions.onZoomRejected, [{chart, event}]); } @@ -121,7 +125,7 @@ export function mouseUp(chart, event) { function wheelPreconditions(chart, event, zoomOptions) { const {wheel: wheelOptions, onZoomRejected} = zoomOptions; // Before preventDefault, check if the modifier key required and pressed - if (wheelOptions.modifierKey && !event[wheelOptions.modifierKey + 'Key']) { + if (modifierKeyNotPressed(wheelOptions.modifierKey, event)) { call(onZoomRejected, [{chart, event}]); return; } diff --git a/test/specs/zoom.spec.js b/test/specs/zoom.spec.js index f8c58ba0..109cebe5 100644 --- a/test/specs/zoom.spec.js +++ b/test/specs/zoom.spec.js @@ -396,6 +396,75 @@ describe('zoom', function() { } }); + describe('drag with modifierKey', function() { + for (const key of ['ctrl', 'alt', 'shift', 'meta']) { + for (const pressed of [true, false]) { + let chart, scaleX, scaleY; + it(`should ${pressed ? '' : 'not '}change ${pressed ? 'with' : 'without'} key ${key}`, async function() { + const rejectedSpy = jasmine.createSpy('wheelFailed'); + chart = window.acquireChart({ + type: 'line', + data, + options: { + scales: { + x: { + type: 'linear', + min: 0, + max: 10 + }, + y: { + type: 'linear' + } + }, + plugins: { + zoom: { + zoom: { + drag: { + enabled: true, + modifierKey: key, + }, + mode: 'x', + onZoomRejected: rejectedSpy + } + } + } + } + }); + + scaleX = chart.scales.x; + scaleY = chart.scales.y; + + const oldMinX = scaleX.options.min; + const oldMaxX = scaleX.options.max; + + const pt = { + x: scaleX.getPixelForValue(1.5), + y: scaleY.getPixelForValue(1.1), + }; + const pt2 = {x: pt.x + 20, y: pt.y + 20}; + const init = {}; + if (pressed) { + init[key + 'Key'] = true; + } + + jasmine.dispatchEvent(chart, 'mousedown', pt, init); + jasmine.dispatchEvent(chart, 'mousemove', pt2, init); + jasmine.dispatchEvent(chart, 'mouseup', pt2, init); + + if (pressed) { + expect(scaleX.options.min).not.toEqual(oldMinX); + expect(scaleX.options.max).not.toEqual(oldMaxX); + expect(rejectedSpy).not.toHaveBeenCalled(); + } else { + expect(scaleX.options.min).toEqual(oldMinX); + expect(scaleX.options.max).toEqual(oldMaxX); + expect(rejectedSpy).toHaveBeenCalled(); + } + }); + } + } + }); + describe('with overScaleMode = y and mode = xy', function() { const config = { type: 'line', From b2cf10064554374f29c6c2ebc1907988e9b9e6b6 Mon Sep 17 00:00:00 2001 From: kurkle Date: Wed, 30 Jun 2021 19:23:20 +0300 Subject: [PATCH 2/4] Add some missing things --- src/hammer.js | 12 ++++++++---- src/handlers.js | 6 +++--- src/plugin.js | 3 ++- test/specs/defaults.spec.js | 3 ++- test/specs/zoom.spec.js | 1 + types/options.d.ts | 5 +++++ 6 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/hammer.js b/src/hammer.js index 69327f3f..abf40362 100644 --- a/src/hammer.js +++ b/src/hammer.js @@ -1,21 +1,25 @@ import {callback as call} from 'chart.js/helpers'; import Hammer from 'hammerjs'; import {pan, zoom} from './core'; +import {modifierKeyNotPressed, modifierKeyPressed} from './handlers'; import {getState} from './state'; import {directionEnabled, getEnabledScalesByPoint} from './utils'; function createEnabler(chart, state) { return function(recognizer, event) { - const panOptions = state.options.pan; + const {pan: panOptions, zoom: zoomOptions = {}} = state.options; if (!panOptions || !panOptions.enabled) { return false; } if (!event || !event.srcEvent) { // Sometimes Hammer queries this with a null event. return true; } - const modifierKey = panOptions.modifierKey; - const requireModifier = modifierKey && (event.pointerType === 'mouse'); - if (!state.panning && requireModifier && !event.srcEvent[modifierKey + 'Key']) { + const panModifierKey = panOptions.modifierKey; + const zoomModifierKey = zoomOptions.enabled && zoomOptions.modifierKey; + if (!state.panning && event.pointerType === 'mouse' && ( + modifierKeyNotPressed(panModifierKey, event.srcEvent) || + modifierKeyPressed(zoomModifierKey, event.srcEvent)) + ) { call(panOptions.onPanRejected, [{chart, event}]); return false; } diff --git a/src/handlers.js b/src/handlers.js index 5c3053a7..30ee5fc5 100644 --- a/src/handlers.js +++ b/src/handlers.js @@ -3,8 +3,8 @@ import {zoom, zoomRect} from './core'; import {callback as call} from 'chart.js/helpers'; import {getState} from './state'; -const modifierKeyPressed = (key, event) => key && event[key + 'Key']; -const modifierKeyNotPressed = (key, event) => key && !event[key + 'Key']; +export const modifierKeyPressed = (key, event) => key && event[key + 'Key']; +export const modifierKeyNotPressed = (key, event) => key && !event[key + 'Key']; function removeHandler(chart, type) { const {handlers} = getState(chart); @@ -50,7 +50,7 @@ function zoomStart(chart, event, zoomOptions) { export function mouseDown(chart, event) { const state = getState(chart); const {pan: panOptions, zoom: zoomOptions} = state.options; - const panKey = panOptions && panOptions.modifierKey; + const panKey = panOptions && panOptions.enabled && panOptions.modifierKey; const dragKey = zoomOptions && zoomOptions.drag && zoomOptions.drag.modifierKey; if (modifierKeyPressed(panKey, event) || modifierKeyNotPressed(dragKey, event)) { return call(zoomOptions.onZoomRejected, [{chart, event}]); diff --git a/src/plugin.js b/src/plugin.js index 5d98b1dd..3338f76e 100644 --- a/src/plugin.js +++ b/src/plugin.js @@ -25,7 +25,8 @@ export default { modifierKey: null }, drag: { - enabled: false + enabled: false, + modifierKey: null }, pinch: { enabled: false diff --git a/test/specs/defaults.spec.js b/test/specs/defaults.spec.js index 066f3871..5e016a32 100644 --- a/test/specs/defaults.spec.js +++ b/test/specs/defaults.spec.js @@ -13,7 +13,8 @@ describe('defaults', function() { modifierKey: null }, drag: { - enabled: false + enabled: false, + modifierKey: null }, pinch: { enabled: false diff --git a/test/specs/zoom.spec.js b/test/specs/zoom.spec.js index 109cebe5..af78d29b 100644 --- a/test/specs/zoom.spec.js +++ b/test/specs/zoom.spec.js @@ -346,6 +346,7 @@ describe('zoom', function() { plugins: { zoom: { pan: { + enabled: true, modifierKey: key, }, zoom: { diff --git a/types/options.d.ts b/types/options.d.ts index eccdaadc..6fe4b2e9 100644 --- a/types/options.d.ts +++ b/types/options.d.ts @@ -47,6 +47,11 @@ export interface DragOptions { * Background color of the drag area */ backgroundColor?: Color; + + /** + * Modifier key required for drag-to-zoom + */ + modifierKey?: Key; } export interface PinchOptions { From bf0c638839f0b658a528d45e2da1436ad5a1006b Mon Sep 17 00:00:00 2001 From: kurkle Date: Wed, 30 Jun 2021 19:29:54 +0300 Subject: [PATCH 3/4] Tiny optimization --- src/hammer.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/hammer.js b/src/hammer.js index abf40362..15e59e12 100644 --- a/src/hammer.js +++ b/src/hammer.js @@ -11,14 +11,15 @@ function createEnabler(chart, state) { if (!panOptions || !panOptions.enabled) { return false; } - if (!event || !event.srcEvent) { // Sometimes Hammer queries this with a null event. + const srcEvent = event && event.srcEvent; + if (!srcEvent) { // Sometimes Hammer queries this with a null event. return true; } const panModifierKey = panOptions.modifierKey; const zoomModifierKey = zoomOptions.enabled && zoomOptions.modifierKey; if (!state.panning && event.pointerType === 'mouse' && ( - modifierKeyNotPressed(panModifierKey, event.srcEvent) || - modifierKeyPressed(zoomModifierKey, event.srcEvent)) + modifierKeyNotPressed(panModifierKey, srcEvent) || + modifierKeyPressed(zoomModifierKey, srcEvent)) ) { call(panOptions.onPanRejected, [{chart, event}]); return false; From 3eac45dbb2337266d7ed35bca7506f73cd632728 Mon Sep 17 00:00:00 2001 From: kurkle Date: Wed, 30 Jun 2021 19:53:19 +0300 Subject: [PATCH 4/4] Move things to utils --- src/hammer.js | 8 ++------ src/handlers.js | 16 +++++----------- src/utils.js | 4 ++++ 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/hammer.js b/src/hammer.js index 15e59e12..197b236c 100644 --- a/src/hammer.js +++ b/src/hammer.js @@ -1,9 +1,8 @@ import {callback as call} from 'chart.js/helpers'; import Hammer from 'hammerjs'; import {pan, zoom} from './core'; -import {modifierKeyNotPressed, modifierKeyPressed} from './handlers'; import {getState} from './state'; -import {directionEnabled, getEnabledScalesByPoint} from './utils'; +import {directionEnabled, getEnabledScalesByPoint, getModifierKey, keyNotPressed, keyPressed} from './utils'; function createEnabler(chart, state) { return function(recognizer, event) { @@ -15,11 +14,8 @@ function createEnabler(chart, state) { if (!srcEvent) { // Sometimes Hammer queries this with a null event. return true; } - const panModifierKey = panOptions.modifierKey; - const zoomModifierKey = zoomOptions.enabled && zoomOptions.modifierKey; if (!state.panning && event.pointerType === 'mouse' && ( - modifierKeyNotPressed(panModifierKey, srcEvent) || - modifierKeyPressed(zoomModifierKey, srcEvent)) + keyNotPressed(getModifierKey(panOptions), srcEvent) || keyPressed(getModifierKey(zoomOptions), srcEvent)) ) { call(panOptions.onPanRejected, [{chart, event}]); return false; diff --git a/src/handlers.js b/src/handlers.js index 30ee5fc5..b1fea2f2 100644 --- a/src/handlers.js +++ b/src/handlers.js @@ -1,11 +1,8 @@ -import {directionEnabled, debounce} from './utils'; +import {directionEnabled, debounce, keyNotPressed, getModifierKey, keyPressed} from './utils'; import {zoom, zoomRect} from './core'; import {callback as call} from 'chart.js/helpers'; import {getState} from './state'; -export const modifierKeyPressed = (key, event) => key && event[key + 'Key']; -export const modifierKeyNotPressed = (key, event) => key && !event[key + 'Key']; - function removeHandler(chart, type) { const {handlers} = getState(chart); const handler = handlers[type]; @@ -49,10 +46,8 @@ function zoomStart(chart, event, zoomOptions) { export function mouseDown(chart, event) { const state = getState(chart); - const {pan: panOptions, zoom: zoomOptions} = state.options; - const panKey = panOptions && panOptions.enabled && panOptions.modifierKey; - const dragKey = zoomOptions && zoomOptions.drag && zoomOptions.drag.modifierKey; - if (modifierKeyPressed(panKey, event) || modifierKeyNotPressed(dragKey, event)) { + const {pan: panOptions, zoom: zoomOptions = {}} = state.options; + if (keyPressed(getModifierKey(panOptions), event) || keyNotPressed(getModifierKey(zoomOptions.drag), event)) { return call(zoomOptions.onZoomRejected, [{chart, event}]); } @@ -123,10 +118,9 @@ export function mouseUp(chart, event) { } function wheelPreconditions(chart, event, zoomOptions) { - const {wheel: wheelOptions, onZoomRejected} = zoomOptions; // Before preventDefault, check if the modifier key required and pressed - if (modifierKeyNotPressed(wheelOptions.modifierKey, event)) { - call(onZoomRejected, [{chart, event}]); + if (keyNotPressed(getModifierKey(zoomOptions.wheel), event)) { + call(zoomOptions.onZoomRejected, [{chart, event}]); return; } diff --git a/src/utils.js b/src/utils.js index ce4eb03f..cce065f7 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,5 +1,9 @@ import {each} from 'chart.js/helpers'; +export const getModifierKey = opts => opts && opts.enabled && opts.modifierKey; +export const keyPressed = (key, event) => key && event[key + 'Key']; +export const keyNotPressed = (key, event) => key && !event[key + 'Key']; + /** * @param {string|function} mode can be 'x', 'y' or 'xy' * @param {string} dir can be 'x' or 'y'