]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Add new hooks for plugins (#8103)
authorJukka Kurkela <jukka.kurkela@gmail.com>
Sat, 28 Nov 2020 20:57:45 +0000 (22:57 +0200)
committerGitHub <noreply@github.com>
Sat, 28 Nov 2020 20:57:45 +0000 (22:57 +0200)
* Notify beforeUpdate on disabled plugins

cc?

cc2

cc3

typo

* init, unInit, enabled, disabled

self review :)

update the new hook signatures to unified

merge error

* Review update

* start/stop, cc

* types, jsdoc

* stop between destroy and uninstall

docs/docs/getting-started/v3-migration.md
src/core/core.plugins.js
src/plugins/plugin.legend.js
src/plugins/plugin.title.js
test/specs/core.controller.tests.js
test/specs/plugin.legend.tests.js
test/specs/plugin.title.tests.js
types/core/index.d.ts

index ac35edfb689133f936a433ac758b102b00419c5b..80258d3f3e047582fdcf9726ae4cea098cc2bdaa 100644 (file)
@@ -499,3 +499,4 @@ All helpers are now exposed in a flat hierarchy, e.g., `Chart.helpers.canvas.cli
 * `afterDatasetsUpdate`, `afterUpdate`, `beforeDatasetsUpdate`, and `beforeUpdate` now receive `args` object as second argument. `options` argument is always the last and thus was moved from 2nd to 3rd place.
 * `afterEvent` and `beforeEvent` now receive a wrapped `event` as the `event` property of the second argument. The native event is available via `args.event.native`.
 * Initial `resize` is no longer silent. Meaning that `resize` event can fire between `beforeInit` and `afterInit`
+* New hooks: `install`, `start`, `stop`, and `uninstall`
index 5950d342f2aaf9ea56124d8d5536eca8c92f4123..f859734d44a0e83c79457eb82d4589e9bd96a5ce 100644 (file)
@@ -1,6 +1,6 @@
 import defaults from './core.defaults';
 import registry from './core.registry';
-import {mergeIf, valueOrDefault} from '../helpers/helpers.core';
+import {callback as callCallback, mergeIf, valueOrDefault} from '../helpers/helpers.core';
 
 /**
  * @typedef { import("./core.controller").default } Chart
@@ -9,6 +9,10 @@ import {mergeIf, valueOrDefault} from '../helpers/helpers.core';
  */
 
 export default class PluginService {
+       constructor() {
+               this._init = [];
+       }
+
        /**
         * Calls enabled plugins for `chart` on the specified hook and with the given args.
         * This method immediately returns as soon as a plugin explicitly returns false. The
@@ -19,18 +23,34 @@ export default class PluginService {
         * @returns {boolean} false if any of the plugins return false, else returns true.
         */
        notify(chart, hook, args) {
-               args = args || {};
-               const descriptors = this._descriptors(chart);
+               const me = this;
 
-               for (let i = 0; i < descriptors.length; ++i) {
-                       const descriptor = descriptors[i];
+               if (hook === 'beforeInit') {
+                       me._init = me._createDescriptors(chart, true);
+                       me._notify(me._init, chart, 'install');
+               }
+
+               const descriptors = me._descriptors(chart);
+               const result = me._notify(descriptors, chart, hook, args);
+
+               if (hook === 'destroy') {
+                       me._notify(descriptors, chart, 'stop');
+                       me._notify(me._init, chart, 'uninstall');
+               }
+               return result;
+       }
+
+       /**
+        * @private
+        */
+       _notify(descriptors, chart, hook, args) {
+               args = args || {};
+               for (const descriptor of descriptors) {
                        const plugin = descriptor.plugin;
                        const method = plugin[hook];
-                       if (typeof method === 'function') {
-                               const params = [chart, args, descriptor.options];
-                               if (method.apply(plugin, params) === false) {
-                                       return false;
-                               }
+                       const params = [chart, args, descriptor.options];
+                       if (callCallback(method, params, plugin) === false) {
+                               return false;
                        }
                }
 
@@ -38,6 +58,7 @@ export default class PluginService {
        }
 
        invalidate() {
+               this._oldCache = this._cache;
                this._cache = undefined;
        }
 
@@ -50,15 +71,31 @@ export default class PluginService {
                        return this._cache;
                }
 
+               const descriptors = this._cache = this._createDescriptors(chart);
+
+               this._notifyStateChanges(chart);
+
+               return descriptors;
+       }
+
+       _createDescriptors(chart, all) {
                const config = chart && chart.config;
                const options = valueOrDefault(config.options && config.options.plugins, {});
                const plugins = allPlugins(config);
                // options === false => all plugins are disabled
-               const descriptors = options === false ? [] : createDescriptors(plugins, options);
-
-               this._cache = descriptors;
+               return options === false && !all ? [] : createDescriptors(plugins, options, all);
+       }
 
-               return descriptors;
+       /**
+        * @param {Chart} chart
+        * @private
+        */
+       _notifyStateChanges(chart) {
+               const previousDescriptors = this._oldCache || [];
+               const descriptors = this._cache;
+               const diff = (a, b) => a.filter(x => !b.some(y => x.plugin.id === y.plugin.id));
+               this._notify(diff(previousDescriptors, descriptors), chart, 'stop');
+               this._notify(diff(descriptors, previousDescriptors), chart, 'start');
        }
 }
 
@@ -84,20 +121,26 @@ function allPlugins(config) {
        return plugins;
 }
 
-function createDescriptors(plugins, options) {
+function getOpts(options, all) {
+       if (!all && options === false) {
+               return null;
+       }
+       if (options === true) {
+               return {};
+       }
+       return options;
+}
+
+function createDescriptors(plugins, options, all) {
        const result = [];
 
        for (let i = 0; i < plugins.length; i++) {
                const plugin = plugins[i];
                const id = plugin.id;
-
-               let opts = options[id];
-               if (opts === false) {
+               const opts = getOpts(options[id], all);
+               if (opts === null) {
                        continue;
                }
-               if (opts === true) {
-                       opts = {};
-               }
                result.push({
                        plugin,
                        options: mergeIf({}, [opts, defaults.plugins[id]])
@@ -113,6 +156,30 @@ function createDescriptors(plugins, options) {
  * @typedef {object} IPlugin
  * @since 2.1.0
  */
+/**
+ * @method IPlugin#install
+ * @desc Called when plugin is installed for this chart instance. This hook is called on disabled plugins (options === false).
+ * @param {Chart} chart - The chart instance.
+ * @param {object} args - The call arguments.
+ * @param {object} options - The plugin options.
+ * @since 3.0.0
+ */
+/**
+ * @method IPlugin#start
+ * @desc Called when a plugin is starting. This happens when chart is created or plugin is enabled.
+ * @param {Chart} chart - The chart instance.
+ * @param {object} args - The call arguments.
+ * @param {object} options - The plugin options.
+ * @since 3.0.0
+ */
+/**
+ * @method IPlugin#stop
+ * @desc Called when a plugin stopping. This happens when chart is destroyed or plugin is disabled.
+ * @param {Chart} chart - The chart instance.
+ * @param {object} args - The call arguments.
+ * @param {object} options - The plugin options.
+ * @since 3.0.0
+ */
 /**
  * @method IPlugin#beforeInit
  * @desc Called before initializing `chart`.
@@ -338,8 +405,16 @@ function createDescriptors(plugins, options) {
  */
 /**
  * @method IPlugin#destroy
- * @desc Called after the chart as been destroyed.
+ * @desc Called after the chart has been destroyed.
+ * @param {Chart} chart - The chart instance.
+ * @param {object} args - The call arguments.
+ * @param {object} options - The plugin options.
+ */
+/**
+ * @method IPlugin#uninstall
+ * @desc Called after chart is destroyed on all plugins that were installed for that chart. This hook is called on disabled plugins (options === false).
  * @param {Chart} chart - The chart instance.
  * @param {object} args - The call arguments.
  * @param {object} options - The plugin options.
+ * @since 3.0.0
  */
index 133df9da3e8eb34d5a71e7527f1c47a25e8b9144..ef33ae0da34dfbd423387cc58056e5e6f7119012 100644 (file)
@@ -653,12 +653,14 @@ export default {
         */
        _element: Legend,
 
-       beforeInit(chart) {
+       start(chart) {
                const legendOpts = resolveOptions(chart.options.plugins.legend);
+               createNewLegendAndAttach(chart, legendOpts);
+       },
 
-               if (legendOpts) {
-                       createNewLegendAndAttach(chart, legendOpts);
-               }
+       stop(chart) {
+               layouts.removeBox(chart, chart.legend);
+               delete chart.legend;
        },
 
        // During the beforeUpdate step, the layout configuration needs to run
index 850ba89bf1d73a0918e05801fafbd85c94f5c467..04e59629b90aca56a6f20db59ba3f18581801337 100644 (file)
@@ -184,11 +184,17 @@ export default {
         */
        _element: Title,
 
-       beforeInit(chart, options) {
+       start(chart, _args, options) {
                createTitle(chart, options);
        },
 
-       beforeUpdate(chart, args, options) {
+       stop(chart) {
+               const titleBlock = chart.titleBlock;
+               layouts.removeBox(chart, titleBlock);
+               delete chart.titleBlock;
+       },
+
+       beforeUpdate(chart, _args, options) {
                if (options === false) {
                        removeTitle(chart);
                } else {
index 2dcb43385a0888324aafbee81e61f1ffe6c10312..23d47a2b2da7ad9e3b59c3396ae78ce7982c8149 100644 (file)
@@ -1383,44 +1383,49 @@ describe('Chart', function() {
        });
 
        describe('plugin.extensions', function() {
+               var hooks = {
+                       install: ['install'],
+                       uninstall: ['uninstall'],
+                       init: [
+                               'beforeInit',
+                               'resize',
+                               'afterInit'
+                       ],
+                       start: ['start'],
+                       stop: ['stop'],
+                       update: [
+                               'beforeUpdate',
+                               'beforeLayout',
+                               'afterLayout',
+                               'beforeDatasetsUpdate',
+                               'beforeDatasetUpdate',
+                               'afterDatasetUpdate',
+                               'afterDatasetsUpdate',
+                               'afterUpdate',
+                       ],
+                       render: [
+                               'beforeRender',
+                               'beforeDraw',
+                               'beforeDatasetsDraw',
+                               'beforeDatasetDraw',
+                               'afterDatasetDraw',
+                               'afterDatasetsDraw',
+                               'beforeTooltipDraw',
+                               'afterTooltipDraw',
+                               'afterDraw',
+                               'afterRender',
+                       ],
+                       resize: [
+                               'resize'
+                       ],
+                       destroy: [
+                               'destroy'
+                       ]
+               };
+
                it ('should notify plugin in correct order', function(done) {
                        var plugin = this.plugin = {};
                        var sequence = [];
-                       var hooks = {
-                               init: [
-                                       'beforeInit',
-                                       'resize',
-                                       'afterInit'
-                               ],
-                               update: [
-                                       'beforeUpdate',
-                                       'beforeLayout',
-                                       'afterLayout',
-                                       'beforeDatasetsUpdate',
-                                       'beforeDatasetUpdate',
-                                       'afterDatasetUpdate',
-                                       'afterDatasetsUpdate',
-                                       'afterUpdate',
-                               ],
-                               render: [
-                                       'beforeRender',
-                                       'beforeDraw',
-                                       'beforeDatasetsDraw',
-                                       'beforeDatasetDraw',
-                                       'afterDatasetDraw',
-                                       'afterDatasetsDraw',
-                                       'beforeTooltipDraw',
-                                       'afterTooltipDraw',
-                                       'afterDraw',
-                                       'afterRender',
-                               ],
-                               resize: [
-                                       'resize'
-                               ],
-                               destroy: [
-                                       'destroy'
-                               ]
-                       };
 
                        Object.keys(hooks).forEach(function(group) {
                                hooks[group].forEach(function(name) {
@@ -1447,13 +1452,17 @@ describe('Chart', function() {
                                chart.destroy();
 
                                expect(sequence).toEqual([].concat(
+                                       hooks.install,
+                                       hooks.start,
                                        hooks.init,
                                        hooks.update,
                                        hooks.render,
                                        hooks.resize,
                                        hooks.update,
                                        hooks.render,
-                                       hooks.destroy
+                                       hooks.destroy,
+                                       hooks.stop,
+                                       hooks.uninstall
                                ));
 
                                done();
@@ -1461,6 +1470,55 @@ describe('Chart', function() {
                        chart.canvas.parentNode.style.width = '400px';
                });
 
+               it ('should notify initially disabled plugin in correct order', function() {
+                       var plugin = this.plugin = {id: 'plugin'};
+                       var sequence = [];
+
+                       Object.keys(hooks).forEach(function(group) {
+                               hooks[group].forEach(function(name) {
+                                       plugin[name] = function() {
+                                               sequence.push(name);
+                                       };
+                               });
+                       });
+
+                       var chart = window.acquireChart({
+                               type: 'line',
+                               data: {datasets: [{}]},
+                               plugins: [plugin],
+                               options: {
+                                       plugins: {
+                                               plugin: false
+                                       }
+                               }
+                       });
+
+                       expect(sequence).toEqual([].concat(
+                               hooks.install
+                       ));
+
+                       sequence = [];
+                       chart.options.plugins.plugin = true;
+                       chart.update();
+
+                       expect(sequence).toEqual([].concat(
+                               hooks.start,
+                               hooks.update,
+                               hooks.render
+                       ));
+
+                       sequence = [];
+                       chart.options.plugins.plugin = false;
+                       chart.update();
+
+                       expect(sequence).toEqual(hooks.stop);
+
+                       sequence = [];
+                       chart.destroy();
+
+                       expect(sequence).toEqual(hooks.uninstall);
+               });
+
                it('should not notify before/afterDatasetDraw if dataset is hidden', function() {
                        var sequence = [];
                        var plugin = this.plugin = {
index e924212ddf108e0eaf36a069e754c8e01ecb34c5..36a3439f4749e8ab5774dbfa7db53cd918aa9fef 100644 (file)
@@ -760,7 +760,7 @@ describe('Legend block tests', function() {
                        expect(chart.legend.weight).toBe(42);
                });
 
-               xit ('should remove the legend if the new options are false', function() {
+               it ('should remove the legend if the new options are false', function() {
                        var chart = acquireChart({
                                type: 'line',
                                data: {
index ac339d09525ef2e77d926562492241c2e206b288..9a794f65a8fdabb4e108b2da16476930aae008f7 100644 (file)
@@ -295,7 +295,7 @@ describe('Title block tests', function() {
                        expect(chart.titleBlock.weight).toBe(42);
                });
 
-               xit ('should remove the title if the new options are false', function() {
+               it ('should remove the title if the new options are false', function() {
                        var chart = acquireChart({
                                type: 'line',
                                data: {
index d31dbe853321e471c730166fef6847b4a33049ec..1c9641a26238ed67d5974174e9ec5f996dd01ef6 100644 (file)
@@ -567,6 +567,30 @@ export const layouts: {
 export interface Plugin<O = {}> {
        id: string;
 
+       /**
+        * @desc Called when plugin is installed for this chart instance. This hook is also invoked for disabled plugins (options === false).
+        * @param {Chart} chart - The chart instance.
+        * @param {object} args - The call arguments.
+        * @param {object} options - The plugin options.
+        * @since 3.0.0
+        */
+       install?(chart: Chart, args: {}, options: O): void;
+       /**
+        * @desc Called when a plugin is starting. This happens when chart is created or plugin is enabled.
+        * @param {Chart} chart - The chart instance.
+        * @param {object} args - The call arguments.
+        * @param {object} options - The plugin options.
+        * @since 3.0.0
+        */
+       start?(chart: Chart, args: {}, options: O): void;
+       /**
+        * @desc Called when a plugin stopping. This happens when chart is destroyed or plugin is disabled.
+        * @param {Chart} chart - The chart instance.
+        * @param {object} args - The call arguments.
+        * @param {object} options - The plugin options.
+        * @since 3.0.0
+        */
+       stop?(chart: Chart, args: {}, options: O): void;
        /**
         * @desc Called before initializing `chart`.
         * @param {Chart} chart - The chart instance.
@@ -772,11 +796,20 @@ export interface Plugin<O = {}> {
         */
        resize?(chart: Chart, args: { size: { width: number, height: number } }, options: O): boolean | void;
        /**
-        * Called after the chart as been destroyed.
+        * Called after the chart has been destroyed.
+        * @param {Chart} chart - The chart instance.
+        * @param {object} args - The call arguments.
+        * @param {object} options - The plugin options.
+        */
+       destroy?(chart: Chart, args: {}, options: O): boolean | void;
+       /**
+        * Called after chart is destroyed on all plugins that were installed for that chart. This hook is also invoked for disabled plugins (options === false).
         * @param {Chart} chart - The chart instance.
+        * @param {object} args - The call arguments.
         * @param {object} options - The plugin options.
+        * @since 3.0.0
         */
-       destroy?(chart: Chart, options: O): boolean | void;
+       uninstall?(chart: Chart, args: {}, options: O): void;
 }
 
 export declare type ChartComponentLike = ChartComponent | ChartComponent[] | { [key: string]: ChartComponent };