From f775202cc149d4de4cd804ae846a0c0802f3a806 Mon Sep 17 00:00:00 2001 From: Jukka Kurkela Date: Sat, 10 Apr 2021 21:46:56 +0300 Subject: [PATCH] Enable event filtering per plugin --- docs/configuration/interactions.md | 30 ++++++++++++++++++++----- src/core/core.controller.js | 10 +++++---- src/core/core.plugins.js | 15 +++++++++++-- test/specs/core.plugin.tests.js | 27 ++++++++++++++++++++++ test/specs/plugin.tooltip.tests.js | 36 ++++++++++++++++++++++++++++-- 5 files changed, 105 insertions(+), 13 deletions(-) diff --git a/docs/configuration/interactions.md b/docs/configuration/interactions.md index 32824099b98..2b7a44f9ac0 100644 --- a/docs/configuration/interactions.md +++ b/docs/configuration/interactions.md @@ -17,7 +17,7 @@ Namespace: `options` | Name | Type | Default | Description | ---- | ---- | ------- | ----------- -| `events` | `string[]` | `['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove']` | The `events` option defines the browser events that the chart should listen to for tooltips and hovering. [more...](#event-option) +| `events` | `string[]` | `['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove']` | The `events` option defines the browser events that the chart should listen to for. Each of these events trigger hover and are passed to plugins. [more...](#event-option) | `onHover` | `function` | `null` | Called when any of the events fire. Passed the event, an array of active elements (bars, points, etc), and the chart. | `onClick` | `function` | `null` | Called if the event is of type `'mouseup'` or `'click'`. Passed the event, an array of active elements, and the chart. @@ -27,12 +27,32 @@ For example, to have the chart only respond to click events, you could do: ```javascript var chart = new Chart(ctx, { - type: 'line', - data: data, - options: { - // This chart will not respond to mousemove, etc + type: 'line', + data: data, + options: { + // This chart will not respond to mousemove, etc + events: ['click'] + } +}); +``` + +Events for each plugin can be further limited by defining (allowed) events array in plugin options: + +```javascript +var chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + // All of these (default) events trigger a hover and are passed to all plugins, + // unless limited at plugin options + events: ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove'], + plugins: { + tooltip: { + // Tooltip will only receive click events events: ['click'] + } } + } }); ``` diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 6e6bfec13e1..34b342014d8 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -1018,10 +1018,11 @@ class Chart { * returned value can be used, for instance, to interrupt the current action. * @param {string} hook - The name of the plugin method to call (e.g. 'beforeUpdate'). * @param {Object} [args] - Extra arguments to apply to the hook call. + * @param {import('./core.plugins').filterCallback} [filter] - Filtering function for limiting which plugins are notified * @returns {boolean} false if any of the plugins return false, else returns true. */ - notifyPlugins(hook, args) { - return this._plugins.notify(this, hook, args); + notifyPlugins(hook, args, filter) { + return this._plugins.notify(this, hook, args, filter); } /** @@ -1049,15 +1050,16 @@ class Chart { _eventHandler(e, replay) { const me = this; const args = {event: e, replay, cancelable: true}; + const eventFilter = (plugin) => (plugin.options.events || this.options.events).includes(e.type); - if (me.notifyPlugins('beforeEvent', args) === false) { + if (me.notifyPlugins('beforeEvent', args, eventFilter) === false) { return; } const changed = me._handleEvent(e, replay); args.cancelable = false; - me.notifyPlugins('afterEvent', args); + me.notifyPlugins('afterEvent', args, eventFilter); if (changed || args.changed) { me.render(); diff --git a/src/core/core.plugins.js b/src/core/core.plugins.js index c4c6da25c28..29f98cf291f 100644 --- a/src/core/core.plugins.js +++ b/src/core/core.plugins.js @@ -7,6 +7,16 @@ import {callback as callCallback, isNullOrUndef, valueOrDefault} from '../helper * @typedef { import("../plugins/plugin.tooltip").default } Tooltip */ +/** + * @callback filterCallback + * @param {{plugin: object, options: object}} value + * @param {number} [index] + * @param {array} [array] + * @param {object} [thisArg] + * @return {boolean} + */ + + export default class PluginService { constructor() { this._init = []; @@ -19,9 +29,10 @@ export default class PluginService { * @param {Chart} chart - The chart instance for which plugins should be called. * @param {string} hook - The name of the plugin method to call (e.g. 'beforeUpdate'). * @param {object} [args] - Extra arguments to apply to the hook call. + * @param {filterCallback} [filter] - Filtering function for limiting which plugins are notified * @returns {boolean} false if any of the plugins return false, else returns true. */ - notify(chart, hook, args) { + notify(chart, hook, args, filter) { const me = this; if (hook === 'beforeInit') { @@ -29,7 +40,7 @@ export default class PluginService { me._notify(me._init, chart, 'install'); } - const descriptors = me._descriptors(chart); + const descriptors = filter ? me._descriptors(chart).filter(filter) : me._descriptors(chart); const result = me._notify(descriptors, chart, hook, args); if (hook === 'destroy') { diff --git a/test/specs/core.plugin.tests.js b/test/specs/core.plugin.tests.js index b699a87754c..5c1c9cccb8d 100644 --- a/test/specs/core.plugin.tests.js +++ b/test/specs/core.plugin.tests.js @@ -393,5 +393,32 @@ describe('Chart.plugins', function() { plugins: [plugin] }); }); + + it('should filter event callbacks by plugin events array', async function() { + const results = []; + const chart = window.acquireChart({ + options: { + events: ['mousemove', 'test', 'test2'], + plugins: { + testPlugin: { + events: ['test'] + } + } + }, + plugins: [{ + id: 'testPlugin', + beforeEvent: function(_chart, args) { + results.push('before' + args.event.type); + }, + afterEvent: function(_chart, args) { + results.push('after' + args.event.type); + } + }] + }); + await jasmine.triggerMouseEvent(chart, 'mousemove', {x: 0, y: 0}); + await jasmine.triggerMouseEvent(chart, 'test', {x: 0, y: 0}); + await jasmine.triggerMouseEvent(chart, 'test2', {x: 0, y: 0}); + expect(results).toEqual(['beforetest', 'aftertest']); + }); }); }); diff --git a/test/specs/plugin.tooltip.tests.js b/test/specs/plugin.tooltip.tests.js index 8060514bcfc..d7bc06682df 100644 --- a/test/specs/plugin.tooltip.tests.js +++ b/test/specs/plugin.tooltip.tests.js @@ -1536,8 +1536,8 @@ describe('Plugin.Tooltip', function() { }); }); - describe('active events', function() { - it('should set the active events', function() { + describe('active elements', function() { + it('should set the active elements', function() { var chart = window.acquireChart({ type: 'line', data: { @@ -1556,4 +1556,36 @@ describe('Plugin.Tooltip', function() { expect(chart.tooltip.getActiveElements()[0].element).toBe(meta.data[0]); }); }); + + describe('events', function() { + it('should not be called on events not in plugin events array', async function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + plugins: { + tooltip: { + events: ['click'] + } + } + } + }); + + const meta = chart.getDatasetMeta(0); + const point = meta.data[1]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + expect(chart.tooltip.opacity).toEqual(0); + await jasmine.triggerMouseEvent(chart, 'click', point); + expect(chart.tooltip.opacity).toEqual(1); + }); + }); });